20559 lines
683 KiB
JavaScript
20559 lines
683 KiB
JavaScript
"use strict";
|
|
|
|
// ./modules/shared/lib/defines.js
|
|
|
|
/**
|
|
* @fileoverview Compile-time definitions used by C1Pjs and PCjs.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2014-May-08
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/**
|
|
* @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-2016 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
|
|
|
|
/*
|
|
* NODE should be true if we're running under NodeJS (eg, command-line), false if not (eg, web browser)
|
|
*/
|
|
var NODE = false;
|
|
|
|
|
|
// ./modules/shared/lib/dumpapi.js
|
|
|
|
/**
|
|
* @fileoverview Disk APIs, as defined by diskdump.js and consumed by disk.js
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2014-May-08
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* 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
|
|
BYTES: "bytes", // displays data as hex bytes; normally used only when comments are enabled
|
|
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];
|
|
|
|
|
|
// ./modules/shared/lib/reportapi.js
|
|
|
|
/**
|
|
* @fileoverview Report API, as defined by httpapi.js
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2014-May-13
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
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"
|
|
}
|
|
};
|
|
|
|
|
|
// ./modules/shared/lib/userapi.js
|
|
|
|
/**
|
|
* @fileoverview User API, as defined by httpapi.js
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2014-May-13
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* 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"
|
|
}
|
|
};
|
|
|
|
|
|
// ./modules/shared/lib/strlib.js
|
|
|
|
/**
|
|
* @fileoverview String-related helper functions
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
|
|
* @version 1.0
|
|
* Created 2014-03-09
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
var 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 use this function to validate the entire string.
|
|
*
|
|
* @param {string} s is the string representation of some number
|
|
* @param {number} [base] is the radix of the number represented above (only 2, 10 and 16 are supported)
|
|
* @return {boolean} true if valid, false if invalid (or the specified base isn't supported)
|
|
*/
|
|
str.isValidInt = function(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 == 2) return s.match(/^[01]+$/i) !== null;
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* parseInt(s, base)
|
|
*
|
|
* This is a wrapper around the built-in parseInt() function, which recognizes certain prefixes (eg,
|
|
* '$' or "0x" for hex) and suffixes (eg, 'h' for hex, or '.' for decimal), and then calls isValidInt()
|
|
* to ensure we don't convert strings that contain partial values (see isValidInt() for details).
|
|
*
|
|
* We don't support multiple prefix/suffix combinations, nor do we support the "0b" prefix (or "b" suffix)
|
|
* for binary, because 1) it's not commonly used, and 2) it conflicts with valid hex sequences.
|
|
*
|
|
* @param {string} s is the string representation of some number
|
|
* @param {number} [base] is the default radix to use (default is 16); can be overridden by prefixes/suffixes
|
|
* @return {number|undefined} corresponding value, or undefined if invalid
|
|
*/
|
|
str.parseInt = function(s, base)
|
|
{
|
|
var value;
|
|
if (s) {
|
|
if (!base) base = 16;
|
|
if (s.charAt(0) == '$') {
|
|
base = 16;
|
|
s = s.substr(1);
|
|
} else if (s.substr(0, 2) == "0x") {
|
|
base = 16;
|
|
s = s.substr(2);
|
|
} else {
|
|
var chSuffix = s.charAt(s.length-1).toLowerCase();
|
|
if (chSuffix == 'h') {
|
|
base = 16;
|
|
chSuffix = null;
|
|
}
|
|
else if (chSuffix == '.') {
|
|
base = 10;
|
|
chSuffix = null;
|
|
}
|
|
if (chSuffix == null) s = s.substr(0, s.length-1);
|
|
}
|
|
var v;
|
|
if (str.isValidInt(s, base) && !isNaN(v = parseInt(s, base))) {
|
|
value = v|0;
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* toBin(n, cch)
|
|
*
|
|
* Converts an integer to binary, with the specified number of digits (up to the default of 32).
|
|
*
|
|
* @param {number|null|undefined} n is a 32-bit value
|
|
* @param {number} [cch] is the desired number of binary digits (32 is both the default and the maximum)
|
|
* @return {string} the binary representation of n
|
|
*/
|
|
str.toBin = function(n, cch)
|
|
{
|
|
var s = "";
|
|
if (cch === undefined) {
|
|
cch = 32;
|
|
} else {
|
|
if (cch > 32) cch = 32;
|
|
}
|
|
/*
|
|
* 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.
|
|
*/
|
|
if (n == null || isNaN(n)) {
|
|
while (cch-- > 0) s = '?' + s;
|
|
} else {
|
|
while (cch-- > 0) {
|
|
s = ((n & 0x1)? '1' : '0') + s;
|
|
n >>= 1;
|
|
}
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* toBinBytes(n, cb)
|
|
*
|
|
* Converts an integer to binary, with the specified number of bytes (up to the default of 4).
|
|
*
|
|
* @param {number|null|undefined} n is a 32-bit value
|
|
* @param {number} [cb] is the desired number of binary bytes (4 is both the default and the maximum)
|
|
* @return {string} the binary representation of n
|
|
*/
|
|
str.toBinBytes = function(n, cb)
|
|
{
|
|
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) + 'b' + s;
|
|
n >>= 8;
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* toHex(n, cch)
|
|
*
|
|
* Converts an integer to hex, with the specified number of digits (up to the default of 8).
|
|
*
|
|
* 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 is a 32-bit value
|
|
* @param {number} [cch] is the desired number of hex digits (8 is both the default and the maximum)
|
|
* @return {string} the hex representation of n
|
|
*/
|
|
str.toHex = function(n, cch)
|
|
{
|
|
var s = "";
|
|
if (cch === undefined) {
|
|
cch = 8;
|
|
} else {
|
|
if (cch > 8) cch = 8;
|
|
}
|
|
/*
|
|
* 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.
|
|
*/
|
|
if (n == null || isNaN(n)) {
|
|
while (cch-- > 0) s = '?' + s;
|
|
} else {
|
|
while (cch-- > 0) {
|
|
var d = n & 0xf;
|
|
d += (d >= 0 && d <= 9? 0x30 : 0x41 - 10);
|
|
s = String.fromCharCode(d) + s;
|
|
n >>= 4;
|
|
}
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* toHexByte(b)
|
|
*
|
|
* Alias for "0x" + str.toHex(b, 2)
|
|
*
|
|
* @param {number|null|undefined} b is a byte value
|
|
* @return {string} the hex representation of b
|
|
*/
|
|
str.toHexByte = function(b)
|
|
{
|
|
return "0x" + str.toHex(b, 2);
|
|
};
|
|
|
|
/**
|
|
* toHexWord(w)
|
|
*
|
|
* Alias for "0x" + str.toHex(w, 4)
|
|
*
|
|
* @param {number|null|undefined} w is a word (16-bit) value
|
|
* @return {string} the hex representation of w
|
|
*/
|
|
str.toHexWord = function(w)
|
|
{
|
|
return "0x" + str.toHex(w, 4);
|
|
};
|
|
|
|
/**
|
|
* toHexLong(l)
|
|
*
|
|
* Alias for "0x" + toHex(l)
|
|
*
|
|
* @param {number|null|undefined} l is a dword (32-bit) value
|
|
* @return {string} the hex representation of w
|
|
*/
|
|
str.toHexLong = function(l)
|
|
{
|
|
return "0x" + str.toHex(l);
|
|
};
|
|
|
|
/**
|
|
* 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}
|
|
*/
|
|
str.getBaseName = function(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
|
|
*/
|
|
str.getExtension = function(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
|
|
*/
|
|
str.endsWith = function(s, sSuffix)
|
|
{
|
|
return s.indexOf(sSuffix, s.length - sSuffix.length) !== -1;
|
|
};
|
|
|
|
str.aHTMLEscapeMap = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
|
|
/**
|
|
* escapeHTML(sHTML)
|
|
*
|
|
* @param {string} sHTML
|
|
* @return {string} with HTML entities "escaped", similar to PHP's htmlspecialchars()
|
|
*/
|
|
str.escapeHTML = function(sHTML)
|
|
{
|
|
return sHTML.replace(/[&<>"']/g, function(m) {
|
|
return str.aHTMLEscapeMap[m];
|
|
});
|
|
};
|
|
|
|
/**
|
|
* replaceAll(sFind, sReplace, s)
|
|
*
|
|
* @param {string} sFind
|
|
* @param {string} sReplace
|
|
* @param {string} s
|
|
* @return {string}
|
|
*/
|
|
str.replaceAll = function(sFind, sReplace, s)
|
|
{
|
|
var a = {};
|
|
a[sFind] = sReplace;
|
|
return str.replaceArray(a, s);
|
|
};
|
|
|
|
/**
|
|
* replaceArray(a, s)
|
|
*
|
|
* @param {Object} a
|
|
* @param {string} s
|
|
* @return {string}
|
|
*/
|
|
str.replaceArray = function(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)
|
|
* @returns {string} the original string (s) with spaces padding it to the specified length
|
|
*/
|
|
str.pad = function(s, cch, fPadLeft)
|
|
{
|
|
var sPadding = " ";
|
|
return fPadLeft? (sPadding + s).slice(-cch) : (s + sPadding).slice(0, cch);
|
|
};
|
|
|
|
/**
|
|
* trim(s)
|
|
*
|
|
* @param {string} s
|
|
* @returns {string}
|
|
*/
|
|
str.trim = function(s)
|
|
{
|
|
if (String.prototype.trim) {
|
|
return s.trim();
|
|
}
|
|
return s.replace(/^\s+|\s+$/g, "");
|
|
};
|
|
|
|
|
|
// ./modules/shared/lib/usrlib.js
|
|
|
|
/**
|
|
* @fileoverview Assorted helper functions
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
|
|
* @version 1.0
|
|
* Created 2014-03-09
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
var 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
|
|
*/
|
|
usr.binarySearch = function(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]
|
|
*/
|
|
usr.binaryInsert = function(a, v, fnCompare) {
|
|
var index = usr.binarySearch(a, v, fnCompare);
|
|
if (index < 0) {
|
|
a.splice(-(index + 1), 0, v);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getTime()
|
|
*
|
|
* @return {number} the current time, in milliseconds
|
|
*/
|
|
usr.getTime = Date.now || function() { return +new Date(); };
|
|
|
|
/**
|
|
* getTimestamp()
|
|
*
|
|
* @return {string} timestamp containing the current date and time ("yyyy-mm-dd hh:mm:ss")
|
|
*/
|
|
usr.getTimestamp = function() {
|
|
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
|
|
*/
|
|
usr.getMonthDays = function(nMonth, nYear)
|
|
{
|
|
var nDays = usr.aMonthDays[nMonth - 1];
|
|
if (nDays == 28) {
|
|
if ((nYear % 4) === 0 && ((nYear % 100) || (nYear % 400) === 0)) {
|
|
nDays++;
|
|
}
|
|
}
|
|
return nDays;
|
|
};
|
|
|
|
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];
|
|
|
|
/**
|
|
* 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,...,31)
|
|
* g: hour in 12-hour format, without leading zeros (1,...,12)
|
|
* i: minutes, with leading zeros (00,...,59)
|
|
* j: day of the month, without leading zeros (1,...,31)
|
|
* l: day of the week ("Sunday",...,"Saturday")
|
|
* m: month, with leading zeros (01,...,12)
|
|
* s: seconds, with leading zeros (00,...,59)
|
|
* F: month ("January",...,"December")
|
|
* H: hour in 24-hour format, with leading zeros (00,...,23)
|
|
* Y: year (eg, 2014)
|
|
*
|
|
* For more inspiration, see: http://php.net/manual/en/function.date.php
|
|
*/
|
|
usr.formatDate = function(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 'g':
|
|
sDate += (!iHour? 12 : (iHour > 12? iHour - 12 : iHour));
|
|
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 's':
|
|
sDate += ('0' + date.getSeconds()).slice(-2);
|
|
break;
|
|
case 'F':
|
|
sDate += usr.asMonths[iMonth - 1];
|
|
break;
|
|
case 'H':
|
|
sDate += ('0' + iHour).slice(-2);
|
|
break;
|
|
case 'Y':
|
|
sDate += date.getFullYear();
|
|
break;
|
|
default:
|
|
sDate += ch;
|
|
break;
|
|
}
|
|
}
|
|
return sDate;
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* mask: number,
|
|
* shift: number
|
|
* }}
|
|
*/
|
|
var BitField;
|
|
|
|
/**
|
|
* @typedef {Object.<BitField>}
|
|
*/
|
|
var BitFields;
|
|
|
|
/**
|
|
* 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 containg 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}
|
|
*/
|
|
usr.defineBitFields = function(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
|
|
*/
|
|
usr.initBitFields = function(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
|
|
*/
|
|
usr.getBitField = function(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
|
|
*/
|
|
usr.setBitField = function(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}
|
|
*/
|
|
usr.indexOf = function(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;
|
|
};
|
|
|
|
|
|
// ./modules/shared/lib/weblib.js
|
|
|
|
/**
|
|
* @fileoverview Browser-related helper functions
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a> (@jeffpar)
|
|
* @version 1.0
|
|
* Created 2014-05-08
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* 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
|
|
*/
|
|
|
|
/* global window: true, setTimeout: false, clearTimeout: false, SITEHOST: false */
|
|
|
|
|
|
var web = {};
|
|
|
|
/*
|
|
* We must defer loading the Component module until the function(s) requiring it are
|
|
* called; otherwise, we create an initialization cycle in which Component requires weblib
|
|
* and weblib requires Component.
|
|
*
|
|
* In an ideal world, weblib would not be dependent on Component, but we really want to use
|
|
* its I/O functions. The simplest solution is to create wrapper functions.
|
|
*/
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
web.log = function(s, type)
|
|
{
|
|
Component.log(s, type);
|
|
};
|
|
|
|
/**
|
|
* notice(s, fPrintOnly, id)
|
|
*
|
|
* If Component.notice() calls web.alertUser(), it will fall back to web.log() if all else fails.
|
|
*
|
|
* @param {string} s is the message text
|
|
* @param {boolean} [fPrintOnly]
|
|
* @param {string} [id] is the caller's ID, if any
|
|
*/
|
|
web.notice = function(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().
|
|
*
|
|
* Also, 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
|
|
*/
|
|
web.getResource = function(sURL, dataPost, fAsync, 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 that we put on archive.pcjs.org should also be available locally...
|
|
*/
|
|
sURL = sURL.replace("http://archive.pcjs.org", "");
|
|
}
|
|
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
web.sendReport = function(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}
|
|
*/
|
|
web.getHost = function()
|
|
{
|
|
return ("http://" + (window? window.location.host : SITEHOST));
|
|
};
|
|
|
|
/**
|
|
* getHostURL()
|
|
*
|
|
* @return {string|null}
|
|
*/
|
|
web.getHostURL = function()
|
|
{
|
|
return (window? window.location.href : null);
|
|
};
|
|
|
|
/**
|
|
* getHostProtocol()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
web.getHostProtocol = function()
|
|
{
|
|
return (window? window.location.protocol : "file:");
|
|
};
|
|
|
|
/**
|
|
* getUserAgent()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
web.getUserAgent = function()
|
|
{
|
|
return (window? window.navigator.userAgent : "");
|
|
};
|
|
|
|
/**
|
|
* alertUser(sMessage)
|
|
*
|
|
* @param {string} sMessage
|
|
*/
|
|
web.alertUser = function(sMessage)
|
|
{
|
|
if (window) window.alert(sMessage); else web.log(sMessage);
|
|
};
|
|
|
|
/**
|
|
* confirmUser(sPrompt)
|
|
*
|
|
* @param {string} sPrompt
|
|
* @returns {boolean} true if the user clicked OK, false if Cancel/Close
|
|
*/
|
|
web.confirmUser = function(sPrompt)
|
|
{
|
|
var fResponse = false;
|
|
if (window) {
|
|
fResponse = window.confirm(sPrompt);
|
|
}
|
|
return fResponse;
|
|
};
|
|
|
|
/**
|
|
* promptUser()
|
|
*
|
|
* @param {string} sPrompt
|
|
* @param {string} [sDefault]
|
|
* @returns {string|null}
|
|
*/
|
|
web.promptUser = function(sPrompt, sDefault)
|
|
{
|
|
var sResponse = null;
|
|
if (window) {
|
|
sResponse = window.prompt(sPrompt, sDefault === undefined? "" : sDefault);
|
|
}
|
|
return sResponse;
|
|
};
|
|
|
|
/**
|
|
* 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";
|
|
|
|
/**
|
|
* hasLocalStorage
|
|
*
|
|
* true if localStorage support exists, is enabled, and works; false otherwise
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
web.hasLocalStorage = function() {
|
|
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
|
|
*/
|
|
web.logLocalStorageError = function(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
|
|
*/
|
|
web.getLocalStorageItem = function(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
|
|
*/
|
|
web.setLocalStorageItem = function(sKey, sValue)
|
|
{
|
|
try {
|
|
window.localStorage.setItem(sKey, sValue);
|
|
return true;
|
|
} catch(e) {
|
|
web.logLocalStorageError(e);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* removeLocalStorageItem(sKey)
|
|
*
|
|
* @param {string} sKey
|
|
*/
|
|
web.removeLocalStorageItem = function(sKey)
|
|
{
|
|
try {
|
|
window.localStorage.removeItem(sKey);
|
|
} catch(e) {
|
|
web.logLocalStorageError(e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getLocalStorageKeys()
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
web.getLocalStorageKeys = function()
|
|
{
|
|
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()
|
|
*/
|
|
web.reloadPage = function()
|
|
{
|
|
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
|
|
*/
|
|
web.isUserAgent = function(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))? true : false;
|
|
}
|
|
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
|
|
*/
|
|
web.isMobile = function()
|
|
{
|
|
return web.isUserAgent("Mobi");
|
|
};
|
|
|
|
/**
|
|
* getURLParameters(sParms)
|
|
*
|
|
* @param {string} [sParms] containing the parameter portion of a URL (ie, after the '?')
|
|
* @return {Object} containing properties for each parameter found
|
|
*/
|
|
web.getURLParameters = function(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]
|
|
*/
|
|
web.downloadFile = function(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]
|
|
*/
|
|
web.onCountRepeat = function(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
|
|
*/
|
|
web.onClickRepeat = function(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;
|
|
};
|
|
};
|
|
|
|
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.fPageReady = false; // set once the browser's first page initialization has occurred
|
|
web.fPageEventsEnabled = true;
|
|
|
|
/**
|
|
* onPageEvent(sName, fn)
|
|
*
|
|
* @param {string} sFunc
|
|
* @param {function()} fn
|
|
*
|
|
* Use this instead of setting window['onload'], window['onunload'], etc.
|
|
* Allows multiple JavaScript modules to define a handler for the same event.
|
|
*
|
|
* Moreover, 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).
|
|
*/
|
|
web.onPageEvent = function(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)
|
|
*
|
|
* @param {function()} fn
|
|
*
|
|
* Use this instead of setting window.onload. Allows multiple JavaScript modules to define their own 'onload' event handler.
|
|
*/
|
|
web.onInit = function(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.
|
|
*/
|
|
web.onShow = function(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.
|
|
*/
|
|
web.onExit = function(fn)
|
|
{
|
|
web.aPageEventHandlers['exit'].push(fn);
|
|
};
|
|
|
|
/**
|
|
* doPageEvent(afn)
|
|
*
|
|
* @param {Array.<function()>} afn
|
|
*/
|
|
web.doPageEvent = function(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)
|
|
*/
|
|
web.enablePageEvents = function(fEnable)
|
|
{
|
|
if (!web.fPageEventsEnabled && fEnable) {
|
|
web.fPageEventsEnabled = true;
|
|
if (web.fPageReady) web.sendPageEvent('init');
|
|
return;
|
|
}
|
|
web.fPageEventsEnabled = fEnable;
|
|
};
|
|
|
|
/**
|
|
* sendPageEvent(sEvent)
|
|
*
|
|
* This allows us to manually trigger page events.
|
|
*
|
|
* @param {string} sEvent (one of 'init', 'show' or 'exit')
|
|
*/
|
|
web.sendPageEvent = function(sEvent)
|
|
{
|
|
if (web.aPageEventHandlers[sEvent]) {
|
|
web.doPageEvent(web.aPageEventHandlers[sEvent]);
|
|
}
|
|
};
|
|
|
|
web.onPageEvent('onload', function onPageLoad() { web.fPageReady = true; web.doPageEvent(web.aPageEventHandlers['init']); });
|
|
web.onPageEvent('onpageshow', function onPageShow() { web.doPageEvent(web.aPageEventHandlers['show']); });
|
|
web.onPageEvent(web.isUserAgent("Opera") || web.isUserAgent("iOS")? 'onunload' : 'onbeforeunload', function onPageUnload() { web.doPageEvent(web.aPageEventHandlers['exit']); });
|
|
|
|
|
|
// ./modules/shared/lib/component.js
|
|
|
|
/**
|
|
* @fileoverview The Component class used by C1Pjs and PCx86.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2012-May-14
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* All the C1Pjs and PCjs components now use JSDoc types, primarily so that Google's Closure Compiler
|
|
* will compile everything with ZERO warnings. 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 use JSLint, but it's excessively strict for my taste, so this is the only file
|
|
* I tried massaging for JSLint's sake. I gave up when it complained about my use of "while (true)";
|
|
* replacing "true" with an assignment expression didn't make it any happier.
|
|
*
|
|
* I wasn't thrilled about replacing all "++" and "--" operators with "+= 1" and "-= 1", nor about using
|
|
* "(s || '')" instead of "(s? s : '')", because while the former may seem simpler, it is NOT more portable.
|
|
* It's not that I'm trying to write "portable JavaScript", but some of this code was ported from C code I'd
|
|
* written about 14 years earlier, and portability is good, so I'm not going to rewrite if there's no need.
|
|
*
|
|
* UPDATE: I've since switched to JSHint, which seems to have more reasonable defaults.
|
|
*/
|
|
|
|
/* global window: true, DEBUG: true */
|
|
|
|
|
|
/**
|
|
* Component(type, parms, constructor, 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)
|
|
*
|
|
* Subclasses that use Component.subclass() to extend Component will likely have additional (parms) properties.
|
|
*
|
|
* @constructor
|
|
* @param {string} type
|
|
* @param {Object} [parms]
|
|
* @param {Object} [constructor]
|
|
* @param {number} [bitsMessage] selects message(s) that the component wants to enable (default is 0)
|
|
*/
|
|
function Component(type, parms, constructor, bitsMessage)
|
|
{
|
|
this.type = type;
|
|
|
|
if (!parms) parms = {'id': "", 'name': ""};
|
|
|
|
this.id = parms['id'];
|
|
this.name = parms['name'];
|
|
this.comment = parms['comment'];
|
|
this.parms = parms;
|
|
if (this.id === undefined) this.id = "";
|
|
|
|
var i = this.id.indexOf('.');
|
|
if (i > 0) {
|
|
this.idMachine = this.id.substr(0, i);
|
|
this.idComponent = this.id.substr(i + 1);
|
|
} else {
|
|
this.idComponent = this.id;
|
|
}
|
|
|
|
/*
|
|
* Recording the constructor is really just a debugging aid, because many of our constructors
|
|
* have class constants, but they're hard to find when the constructors are buried among all the
|
|
* other globals.
|
|
*/
|
|
this[type] = constructor;
|
|
|
|
/*
|
|
* 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 = {
|
|
fReady: false,
|
|
fBusy: false,
|
|
fBusyCancel: false,
|
|
fPowered: false,
|
|
fError: false
|
|
};
|
|
|
|
this.fnReady = null;
|
|
this.clearError();
|
|
this.bindings = {};
|
|
this.dbg = null; // by default, no connection to a Debugger
|
|
this.bitsMessage = bitsMessage || 0;
|
|
|
|
/*
|
|
* 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 (well, OK, an FPU is also supported, but that's considered
|
|
* a different component).
|
|
*
|
|
* 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.parmsURL
|
|
*
|
|
* Initialized to the set of URL parameters, if any, for the current web page.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
Component.parmsURL = web.getURLParameters();
|
|
|
|
/**
|
|
* Component.inherit(p)
|
|
*
|
|
* Returns a newly created object that inherits properties from the prototype object p.
|
|
* It uses the ECMAScript 5 function Object.create() if it is defined, and otherwise falls back to an older technique.
|
|
*
|
|
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-1)
|
|
*
|
|
* @param {Object} p
|
|
*/
|
|
Component.inherit = function(p)
|
|
{
|
|
if (window) {
|
|
if (!p) throw new TypeError();
|
|
if (Object.create) {
|
|
return Object.create(p);
|
|
}
|
|
var t = typeof p;
|
|
if (t !== "object" && t !== "function") throw new TypeError();
|
|
}
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function F() {}
|
|
F.prototype = p;
|
|
return new F();
|
|
};
|
|
|
|
/**
|
|
* Component.extend(o, p)
|
|
*
|
|
* Copies the enumerable properties of p to o and returns o.
|
|
* If o and p have a property by the same name, o's property is overwritten.
|
|
*
|
|
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 6-2)
|
|
*
|
|
* @param {Object} o
|
|
* @param {Object} p
|
|
*/
|
|
Component.extend = function(o, p)
|
|
{
|
|
for (var prop in p) {
|
|
o[prop] = p[prop];
|
|
}
|
|
return o;
|
|
};
|
|
|
|
/**
|
|
* Component.subclass(subclass, superclass, methods, statics)
|
|
*
|
|
* See: Flanagan, David (2011-04-18). JavaScript: The Definitive Guide: The Definitive Guide (Kindle Locations 9854-9903). OReilly Media - A. Kindle Edition (Example 9-11)
|
|
*
|
|
* @param {Object} subclass is the constructor for the new subclass
|
|
* @param {Object} [superclass] is the constructor of the superclass (default is Component)
|
|
* @param {Object} [methods] contains all instance methods
|
|
* @param {Object} [statics] contains all class properties and methods
|
|
*/
|
|
Component.subclass = function(subclass, superclass, methods, statics)
|
|
{
|
|
if (!superclass) superclass = Component;
|
|
subclass.prototype = Component.inherit(superclass.prototype);
|
|
subclass.prototype.constructor = subclass;
|
|
subclass.prototype.parent = superclass.prototype;
|
|
if (methods) {
|
|
Component.extend(subclass.prototype, methods);
|
|
}
|
|
if (statics) {
|
|
Component.extend(subclass, statics);
|
|
}
|
|
return subclass;
|
|
};
|
|
|
|
/*
|
|
* Every component created on the current page is recorded in this array (see Component.add()).
|
|
*
|
|
* This enables any component to locate another component by ID (see Component.getComponentByID())
|
|
* or by type (see Component.getComponentByType()).
|
|
*/
|
|
Component.components = [];
|
|
|
|
/**
|
|
* Component.add(component)
|
|
*
|
|
* @param {Component} component
|
|
*/
|
|
Component.add = function(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);
|
|
};
|
|
|
|
/*
|
|
* Every machine on the page are now recorded as well, by their machine ID. We then record the various resources
|
|
* used by that machine.
|
|
*/
|
|
Component.machines = {};
|
|
|
|
/**
|
|
* Component.addMachine(idMachine)
|
|
*
|
|
* @param {string} idMachine
|
|
*/
|
|
Component.addMachine = function(idMachine)
|
|
{
|
|
Component.machines[idMachine] = {};
|
|
};
|
|
|
|
/**
|
|
* Component.addMachineResource(idMachine, sName, data)
|
|
*
|
|
* @param {string} idMachine
|
|
* @param {string|null} sName (name of the resource)
|
|
* @param {*} data
|
|
*/
|
|
Component.addMachineResource = function(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}
|
|
*/
|
|
Component.getMachineResources = function(idMachine)
|
|
{
|
|
return Component.machines[idMachine];
|
|
};
|
|
|
|
/**
|
|
* Component.log(s, type)
|
|
*
|
|
* For diagnostic output only.
|
|
*
|
|
* @param {string} [s] is the message text
|
|
* @param {string} [type] is the message type
|
|
*/
|
|
Component.log = function(s, type)
|
|
{
|
|
if (DEBUG) {
|
|
if (s) {
|
|
var sElapsed = "", sMsg = (type? (type + ": ") : "") + s;
|
|
if (typeof usr != "undefined") {
|
|
if (Component.msStart === undefined) {
|
|
Component.msStart = usr.getTime();
|
|
}
|
|
sElapsed = (usr.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
|
|
*/
|
|
Component.assert = function(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
|
|
*/
|
|
Component.println = function(s, type, id)
|
|
{
|
|
if (DEBUG) {
|
|
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
|
|
*/
|
|
Component.notice = function(s, fPrintOnly, id)
|
|
{
|
|
if (DEBUG) {
|
|
Component.println(s, "notice", id);
|
|
}
|
|
if (!fPrintOnly) web.alertUser(s);
|
|
};
|
|
|
|
/**
|
|
* Component.warning(s)
|
|
*
|
|
* @param {string} s describes the warning
|
|
*/
|
|
Component.warning = function(s)
|
|
{
|
|
if (DEBUG) {
|
|
Component.println(s, "warning");
|
|
}
|
|
web.alertUser(s);
|
|
};
|
|
|
|
/**
|
|
* Component.error(s)
|
|
*
|
|
* @param {string} s describes the error; an alert() is displayed as well
|
|
*/
|
|
Component.error = function(s)
|
|
{
|
|
if (DEBUG) {
|
|
Component.println(s, "error");
|
|
}
|
|
web.alertUser(s);
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
Component.getComponents = function(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}
|
|
*/
|
|
Component.getComponentByID = function(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}
|
|
*/
|
|
Component.getComponentByType = function(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
|
|
*/
|
|
Component.getComponentParms = function(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
|
|
*/
|
|
Component.bindExternalControl = function(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
|
|
*/
|
|
Component.bindComponentControls = function(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}
|
|
*/
|
|
Component.getElementsByClass = function(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.prototype = {
|
|
constructor: Component,
|
|
parent: null,
|
|
/**
|
|
* toString()
|
|
*
|
|
* @this {Component}
|
|
* @return {string}
|
|
*/
|
|
toString: function() {
|
|
return (this.name? this.name : (this.id || this.type));
|
|
},
|
|
/**
|
|
* getMachineNum()
|
|
*
|
|
* @this {Component}
|
|
* @return {number} unique machine number
|
|
*/
|
|
getMachineNum: function() {
|
|
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: function(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 (DEBUG && window && window.console) console.log(s);
|
|
};
|
|
}(control));
|
|
/**
|
|
* Override this.notice() with a replacement function that eliminates the web.alertUser() call
|
|
*
|
|
* @this {Component}
|
|
* @param {string} s
|
|
* @param {boolean} [fPrintOnly]
|
|
* @param {string} [id]
|
|
*/
|
|
this.notice = function noticePanel(s, fPrintOnly, id) {
|
|
this.println(s, "notice", id);
|
|
};
|
|
}
|
|
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: function(s, type) {
|
|
if (DEBUG) {
|
|
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} f is the expression we are asserting to be true
|
|
* @param {string} [s] is description of the assertion on failure
|
|
*/
|
|
assert: function(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: function(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: function(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
|
|
*/
|
|
notice: function(s, fPrintOnly, id) {
|
|
Component.notice(s, fPrintOnly, id || this.id);
|
|
},
|
|
/**
|
|
* setError(s)
|
|
*
|
|
* Set a fatal error condition
|
|
*
|
|
* @this {Component}
|
|
* @param {string} s describes a fatal error condition
|
|
*/
|
|
setError: function(s) {
|
|
this.flags.fError = 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: function() {
|
|
this.flags.fError = false;
|
|
},
|
|
/**
|
|
* isError()
|
|
*
|
|
* Report any fatal error condition
|
|
*
|
|
* @this {Component}
|
|
* @return {boolean} true if a fatal error condition exists, false if not
|
|
*/
|
|
isError: function() {
|
|
if (this.flags.fError) {
|
|
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: function(fnReady) {
|
|
if (fnReady) {
|
|
if (this.flags.fReady) {
|
|
fnReady();
|
|
} else {
|
|
if (MAXDEBUG) this.log("NOT ready");
|
|
this.fnReady = fnReady;
|
|
}
|
|
}
|
|
return this.flags.fReady;
|
|
},
|
|
/**
|
|
* 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: function(fReady) {
|
|
if (!this.flags.fError) {
|
|
this.flags.fReady = (fReady !== false);
|
|
if (this.flags.fReady) {
|
|
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: function(fCancel) {
|
|
if (this.flags.fBusy) {
|
|
if (fCancel) {
|
|
this.flags.fBusyCancel = true;
|
|
} else if (fCancel === undefined) {
|
|
this.println(this.toString() + " busy");
|
|
}
|
|
}
|
|
return this.flags.fBusy;
|
|
},
|
|
/**
|
|
* setBusy(fBusy)
|
|
*
|
|
* Update the current busy state; if an fCancel request is pending, it will be honored now.
|
|
*
|
|
* @this {Component}
|
|
* @param {boolean} fBusy
|
|
* @return {boolean}
|
|
*/
|
|
setBusy: function(fBusy) {
|
|
if (this.flags.fBusyCancel) {
|
|
if (this.flags.fBusy) {
|
|
this.flags.fBusy = false;
|
|
}
|
|
this.flags.fBusyCancel = false;
|
|
return false;
|
|
}
|
|
if (this.flags.fError) {
|
|
this.println(this.toString() + " error");
|
|
return false;
|
|
}
|
|
this.flags.fBusy = fBusy;
|
|
return this.flags.fBusy;
|
|
},
|
|
/**
|
|
* 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: function(data, fRepower) {
|
|
this.flags.fPowered = 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: function(fSave, fShutdown) {
|
|
if (fShutdown) this.flags.fPowered = 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: function(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: function(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: function(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);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// ./modules/pc8080/lib/defines.js
|
|
|
|
/**
|
|
* @fileoverview PC8080-specific compile-time definitions.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/**
|
|
* @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 Memory.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
|
|
}
|
|
|
|
|
|
// ./modules/pc8080/lib/cpudef.js
|
|
|
|
/**
|
|
* @fileoverview Defines PC8080 CPU constants.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
var CPUDef = {
|
|
/*
|
|
* 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; for internal use only)
|
|
},
|
|
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: 0x00,
|
|
INTL: 0x07, // last interrupt level requested
|
|
INTR: 0x08, // set if interrupt has been requested
|
|
HALT: 0x10 // 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.
|
|
*/
|
|
CPUDef.PS.INTERNAL = (CPUDef.PS.IF);
|
|
|
|
/*
|
|
* PS "arithmetic" flags are NOT stored in regPS; they are maintained across separate result registers,
|
|
* hence the RESULT designation.
|
|
*/
|
|
CPUDef.PS.RESULT = (CPUDef.PS.CF | CPUDef.PS.PF | CPUDef.PS.AF | CPUDef.PS.ZF | CPUDef.PS.SF);
|
|
|
|
/*
|
|
* These are the "always set" PS bits for the 8080.
|
|
*/
|
|
CPUDef.PS.SET = (CPUDef.PS.BIT1);
|
|
|
|
|
|
// ./modules/pc8080/lib/messages.js
|
|
|
|
/**
|
|
* @fileoverview Defines message categories.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
var Messages = {
|
|
CPU: 0x00000001,
|
|
BUS: 0x00000040,
|
|
MEM: 0x00000080,
|
|
PORT: 0x00000100,
|
|
CHIPSET: 0x00008000,
|
|
KEYBOARD: 0x00010000,
|
|
KEYS: 0x00020000,
|
|
VIDEO: 0x00040000,
|
|
FDC: 0x00080000,
|
|
DISK: 0x00200000,
|
|
SERIAL: 0x00800000,
|
|
SPEAKER: 0x02000000,
|
|
COMPUTER: 0x04000000,
|
|
LOG: 0x10000000,
|
|
WARN: 0x20000000,
|
|
BUFFER: 0x40000000,
|
|
HALT: 0x80000000|0
|
|
};
|
|
|
|
|
|
// ./modules/pc8080/lib/panel.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Panel component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-19
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Panel(parmsPanel)
|
|
*
|
|
* The Panel component has no required (parmsPanel) properties.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsPanel
|
|
*/
|
|
function Panel(parmsPanel)
|
|
{
|
|
Component.call(this, "Panel", parmsPanel, Panel);
|
|
}
|
|
|
|
Component.subclass(Panel);
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* Most panel layouts don't have bindings of their own, so we pass along all binding requests to the
|
|
* Computer, CPU, Keyboard and Debugger components first. The order shouldn't matter, since any component
|
|
* that doesn't recognize the specified binding should simply ignore it.
|
|
*
|
|
* @this {Panel}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Panel.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
if (this.cmp && this.cmp.setBinding(sHTMLType, sBinding, control, sValue)) return true;
|
|
if (this.cpu && this.cpu.setBinding(sHTMLType, sBinding, control, sValue)) return true;
|
|
if (this.kbd && this.kbd.setBinding(sHTMLType, sBinding, control, sValue)) return true;
|
|
if (DEBUGGER && this.dbg && this.dbg.setBinding(sHTMLType, sBinding, control, sValue)) return true;
|
|
return this.parent.setBinding.call(this, sHTMLType, sBinding, control, sValue);
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {Panel}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
Panel.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.cmp = cmp;
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.kbd = cmp.getMachineComponent("Keyboard");
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {Panel}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Panel.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) Panel.init();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {Panel}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
Panel.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* updateStatus(fForce)
|
|
*
|
|
* Update function for Panels containing elements with high-frequency display requirements.
|
|
*
|
|
* For older (and slower) DOM-based display elements, those are sill being managed by the CPUState component,
|
|
* so it has its own updateStatus() handler.
|
|
*
|
|
* The Computer's updateStatus() handler is currently responsible for calling both our handler and the CPU's handler.
|
|
*
|
|
* @this {Panel}
|
|
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
|
|
*/
|
|
Panel.prototype.updateStatus = function(fForce)
|
|
{
|
|
};
|
|
|
|
/**
|
|
* Panel.init()
|
|
*
|
|
* This function operates on every HTML element of class "panel", extracting the
|
|
* JSON-encoded parameters for the Panel constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a Panel component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*
|
|
* NOTE: Unlike most other component init() functions, this one is designed to be
|
|
* called multiple times: once at load time, so that we can bind our print()
|
|
* function to the panel's output control ASAP, and again when the Computer component
|
|
* is verifying that all components are ready and invoking their powerUp() functions.
|
|
*
|
|
* Our powerUp() method gives us a second opportunity to notify any components that
|
|
* that might care (eg, CPU, Keyboard, and Debugger) that we have some controls they
|
|
* might want to use.
|
|
*/
|
|
Panel.init = function()
|
|
{
|
|
var fReady = false;
|
|
var aePanels = Component.getElementsByClass(document, 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 Panel(parmsPanel);
|
|
}
|
|
Component.bindComponentControls(panel, ePanel, PC8080.APPCLASS);
|
|
if (fReady) panel.setReady();
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Panel module on the page.
|
|
*/
|
|
web.onInit(Panel.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/bus.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Bus component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Bus(cpu, dbg)
|
|
*
|
|
* The Bus component manages physical memory and I/O address spaces.
|
|
*
|
|
* The Bus component has no UI elements, so it does not require an init() handler,
|
|
* but it still inherits from the Component class and must be allocated like any
|
|
* other device component. It's currently allocated by the Computer's init() handler,
|
|
* which then calls the initBus() method of all the other components.
|
|
*
|
|
* For memory beyond the simple needs of the ROM and RAM components (ie, memory-mapped
|
|
* devices), the address space must still be allocated through the Bus component via
|
|
* addMemory(). If the component needs something more than simple read/write storage,
|
|
* it must provide a custom controller.
|
|
*
|
|
* All port (I/O) operations are defined by external handlers; they register with us,
|
|
* and we manage those registrations and provide support for I/O breakpoints, but the
|
|
* only default I/O behavior we provide is ignoring writes to any unregistered output
|
|
* ports and returning 0xff from any unregistered input ports.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsBus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
function Bus(parmsBus, cpu, dbg)
|
|
{
|
|
Component.call(this, "Bus", parmsBus, Bus);
|
|
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
|
|
this.nBusWidth = parmsBus['busWidth'] || 16;
|
|
|
|
/*
|
|
* Compute all Bus memory block parameters, based on the width of the bus.
|
|
*
|
|
* Regarding blockTotal, we want to avoid using block overflow expressions like:
|
|
*
|
|
* iBlock < this.nBlockTotal? iBlock : 0
|
|
*
|
|
* As long as we know that blockTotal is a power of two (eg, 256 or 0x100, in the case of
|
|
* nBusWidth == 20 and blockSize == 4096), we can define blockMask as (blockTotal - 1) and
|
|
* rewrite the previous expression as:
|
|
*
|
|
* iBlock & this.nBlockMask
|
|
*
|
|
* Bus Property Old hard-coded values (when nBusWidth was always 20)
|
|
* ------------ ----------------------------------------------------
|
|
* this.nBusLimit 0xfffff
|
|
* this.nBusMask [same as busLimit]
|
|
* this.nBlockSize 4096
|
|
* this.nBlockLen (this.nBlockSize >> 2)
|
|
* this.nBlockShift 12
|
|
* this.nBlockLimit 0xfff
|
|
* this.nBlockTotal ((this.nBusLimit + this.nBlockSize) / this.nBlockSize) | 0
|
|
* this.nBlockMask (this.nBlockTotal - 1) [ie, 0xff]
|
|
*
|
|
* Note that we choose a nBlockShift value (and thus a physical memory block size) based on "buswidth":
|
|
*
|
|
* Bus Width Block Shift Block Size
|
|
* --------- ----------- ----------
|
|
* 16 bits (64Kb address space): 10 1Kb (64 maximum blocks)
|
|
* 20 bits (1Mb address space): 12 4Kb (256 maximum blocks)
|
|
* 24 bits (16Mb address space): 14 16Kb (1K maximum blocks)
|
|
* 32 bits (4Gb address space); 15 32Kb (128K maximum blocks)
|
|
*
|
|
* The coarser block granularities (ie, 16Kb and 32Kb) may cause problems for certain RAM and/or ROM
|
|
* allocations that are contiguous but are allocated out of order, or that have different controller
|
|
* requirements. Your choices, for the moment, are either to ensure the allocations are performed in
|
|
* order, or to choose smaller nBlockShift values (at the expense of a generating a larger block array).
|
|
*/
|
|
this.addrTotal = Math.pow(2, this.nBusWidth);
|
|
this.nBusLimit = this.nBusMask = (this.addrTotal - 1) | 0;
|
|
this.nBlockShift = (this.nBusWidth <= 16)? 10 : ((this.nBusWidth <= 20)? 12 : (this.nBusWidth <= 24? 14 : 15));
|
|
this.nBlockSize = 1 << this.nBlockShift;
|
|
this.nBlockLen = this.nBlockSize >> 2;
|
|
this.nBlockLimit = this.nBlockSize - 1;
|
|
this.nBlockTotal = (this.addrTotal / this.nBlockSize) | 0;
|
|
this.nBlockMask = this.nBlockTotal - 1;
|
|
|
|
|
|
/*
|
|
* Lists of I/O notification functions: aPortInputNotify and aPortOutputNotify are arrays, indexed by
|
|
* port, of sub-arrays which contain:
|
|
*
|
|
* [0]: registered function to call for every I/O access
|
|
*
|
|
* The registered function is called with the port address, and if the access was triggered by the CPU,
|
|
* the instruction pointer (IP) at the point of access.
|
|
*
|
|
* WARNING: Unlike the (old) read and write memory notification functions, these support only one
|
|
* pair of input/output functions per port. A more sophisticated architecture could support a list
|
|
* of chained functions across multiple components, but I doubt that will be necessary here.
|
|
*
|
|
* UPDATE: The Debugger now piggy-backs on these arrays to indicate ports for which it wants notification
|
|
* of I/O. In those cases, the registered component/function elements may or may not be set, but the
|
|
* following additional element will be set:
|
|
*
|
|
* [1]: true to break on I/O, false to ignore I/O
|
|
*
|
|
* The false case is important if fPortInputBreakAll and/or fPortOutputBreakAll is set, because it allows the
|
|
* Debugger to selectively ignore specific ports.
|
|
*/
|
|
this.aPortInputNotify = [];
|
|
this.aPortOutputNotify = [];
|
|
this.fPortInputBreakAll = this.fPortOutputBreakAll = false;
|
|
|
|
/*
|
|
* By default, all I/O ports are 1 byte wide; ports that are wider must add themselves to one or both of
|
|
* these lists, using addPortInputWidth() and/or addPortOutputWidth().
|
|
*/
|
|
this.aPortInputWidth = [];
|
|
this.aPortOutputWidth = [];
|
|
|
|
/*
|
|
* Allocate empty Memory blocks to span the entire physical address space.
|
|
*/
|
|
this.initMemory();
|
|
|
|
this.setReady();
|
|
}
|
|
|
|
Component.subclass(Bus);
|
|
|
|
Bus.ERROR = {
|
|
ADD_MEM_INUSE: 1,
|
|
ADD_MEM_BADRANGE: 2,
|
|
SET_MEM_BADRANGE: 4,
|
|
REM_MEM_BADRANGE: 5
|
|
};
|
|
|
|
/**
|
|
* @typedef {number}
|
|
*/
|
|
var BlockInfo;
|
|
|
|
/**
|
|
* This defines the BlockInfo bit fields used by scanMemory() when it creates the aBlocks array.
|
|
*
|
|
* @typedef {{
|
|
* num: BitField,
|
|
* count: BitField,
|
|
* btmod: BitField,
|
|
* type: BitField
|
|
* }}
|
|
*/
|
|
Bus.BlockInfo = usr.defineBitFields({num:20, count:8, btmod:1, type:3});
|
|
|
|
/**
|
|
* BusInfo object definition (returned by scanMemory())
|
|
*
|
|
* cbTotal: total bytes allocated
|
|
* cBlocks: total Memory blocks allocated
|
|
* aBlocks: array of allocated Memory block numbers
|
|
*
|
|
* @typedef {{
|
|
* cbTotal: number,
|
|
* cBlocks: number,
|
|
* aBlocks: Array.<BlockInfo>
|
|
* }}
|
|
*/
|
|
var BusInfo;
|
|
|
|
/**
|
|
* initMemory()
|
|
*
|
|
* Allocate enough (empty) Memory blocks to span the entire physical address space.
|
|
*
|
|
* @this {Bus}
|
|
*/
|
|
Bus.prototype.initMemory = function()
|
|
{
|
|
var block = new Memory();
|
|
block.copyBreakpoints(this.dbg);
|
|
this.aMemBlocks = new Array(this.nBlockTotal);
|
|
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
|
|
this.aMemBlocks[iBlock] = block;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {Bus}
|
|
*/
|
|
Bus.prototype.reset = function()
|
|
{
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* We don't need a powerDown() handler, because for largely historical reasons, our state is saved by saveMemory(),
|
|
* which called by the CPU.
|
|
*
|
|
* However, we do need a powerUp() handler, because on resumable machines, the Computer's onReset() function calls
|
|
* everyone's powerUp() handler rather than their reset() handler.
|
|
*
|
|
* TODO: Perhaps Computer should be smarter: if there's no powerUp() handler, then fallback to the reset() handler.
|
|
* In that case, however, we'd either need to remove the powerUp() stub in Component, or detect the existence of the stub.
|
|
*
|
|
* @this {Bus}
|
|
* @param {Object|null} data (always null because we supply no powerDown() handler)
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Bus.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) this.reset();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* addMemory(addr, size, type)
|
|
*
|
|
* Adds new Memory blocks to the specified address range. Any Memory blocks previously
|
|
* added to that range must first be removed via removeMemory(); otherwise, you'll get
|
|
* an allocation conflict error. This helps prevent address calculation errors, redundant
|
|
* allocations, etc.
|
|
*
|
|
* We've relaxed some of the original requirements (ie, that addresses must start at a
|
|
* block-granular address, or that sizes must be equal to exactly one or more blocks),
|
|
* because machines with large block sizes can make it impossible to load certain ROMs at
|
|
* their required addresses. Every allocation still allocates a whole number of blocks.
|
|
*
|
|
* Even so, Bus memory management does NOT provide a general-purpose heap. Most memory
|
|
* allocations occur during machine initialization and never change. In particular, there
|
|
* is NO support for removing partial-block allocations.
|
|
*
|
|
* Each Memory block keeps track of a start address (addr) and length (used), indicating
|
|
* the used space within the block; any free space that precedes or follows that used space
|
|
* can be allocated later, by simply extending the beginning or ending of the previously used
|
|
* space. However, any holes that might have existed between the original allocation and an
|
|
* extension are subsumed by the extension.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is the starting physical address of the request
|
|
* @param {number} size of the request, in bytes
|
|
* @param {number} type is one of the Memory.TYPE constants
|
|
* @return {boolean} true if successful, false if not
|
|
*/
|
|
Bus.prototype.addMemory = function(addr, size, type)
|
|
{
|
|
var addrNext = addr;
|
|
var sizeLeft = size;
|
|
var iBlock = addrNext >>> this.nBlockShift;
|
|
|
|
while (sizeLeft > 0 && iBlock < this.aMemBlocks.length) {
|
|
|
|
var block = this.aMemBlocks[iBlock];
|
|
var addrBlock = iBlock * this.nBlockSize;
|
|
var sizeBlock = this.nBlockSize - (addrNext - addrBlock);
|
|
if (sizeBlock > sizeLeft) sizeBlock = sizeLeft;
|
|
|
|
if (block && block.size) {
|
|
if (block.type == type) {
|
|
/*
|
|
* Where there is already a similar block with a non-zero size, we allow the allocation only if:
|
|
*
|
|
* 1) addrNext + sizeLeft <= block.addr (the request precedes the used portion of the current block), or
|
|
* 2) addrNext >= block.addr + block.used (the request follows the used portion of the current block)
|
|
*/
|
|
if (addrNext + sizeLeft <= block.addr) {
|
|
block.used += (block.addr - addrNext);
|
|
block.addr = addrNext;
|
|
return true;
|
|
}
|
|
if (addrNext >= block.addr + block.used) {
|
|
var sizeAvail = block.size - (addrNext - addrBlock);
|
|
if (sizeAvail > sizeLeft) sizeAvail = sizeLeft;
|
|
block.used = addrNext - block.addr + sizeAvail;
|
|
addrNext = addrBlock + this.nBlockSize;
|
|
sizeLeft -= sizeAvail;
|
|
iBlock++;
|
|
continue;
|
|
}
|
|
}
|
|
return this.reportError(Bus.ERROR.ADD_MEM_INUSE, addrNext, sizeLeft);
|
|
}
|
|
|
|
var blockNew = new Memory(addrNext, sizeBlock, this.nBlockSize, type);
|
|
blockNew.copyBreakpoints(this.dbg, block);
|
|
this.aMemBlocks[iBlock++] = blockNew;
|
|
|
|
addrNext = addrBlock + this.nBlockSize;
|
|
sizeLeft -= sizeBlock;
|
|
}
|
|
|
|
if (sizeLeft <= 0) {
|
|
this.status(Math.floor(size / 1024) + "Kb " + Memory.TYPE.NAMES[type] + " at " + str.toHexWord(addr));
|
|
return true;
|
|
}
|
|
|
|
return this.reportError(Bus.ERROR.ADD_MEM_BADRANGE, addr, size);
|
|
};
|
|
|
|
/**
|
|
* cleanMemory(addr, size)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr
|
|
* @param {number} size
|
|
* @return {boolean} true if all blocks were clean, false if dirty; all blocks are cleaned in the process
|
|
*/
|
|
Bus.prototype.cleanMemory = function(addr, size)
|
|
{
|
|
var fClean = true;
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
while (size > 0 && iBlock < this.aMemBlocks.length) {
|
|
if (this.aMemBlocks[iBlock].fDirty) {
|
|
this.aMemBlocks[iBlock].fDirty = fClean = false;
|
|
this.aMemBlocks[iBlock].fDirtyEver = true;
|
|
}
|
|
size -= this.nBlockSize;
|
|
iBlock++;
|
|
}
|
|
return fClean;
|
|
};
|
|
|
|
/**
|
|
* scanMemory(info, addr, size)
|
|
*
|
|
* Returns a BusInfo object for the specified address range.
|
|
*
|
|
* @this {Bus}
|
|
* @param {Object} [info] previous BusInfo, if any
|
|
* @param {number} [addr] starting address of range (0 if none provided)
|
|
* @param {number} [size] size of range, in bytes (up to end of address space if none provided)
|
|
* @return {Object} updated info (or new info if no previous info provided)
|
|
*/
|
|
Bus.prototype.scanMemory = function(info, addr, size)
|
|
{
|
|
if (addr == null) addr = 0;
|
|
if (size == null) size = (this.addrTotal - addr) | 0;
|
|
if (info == null) info = {cbTotal: 0, cBlocks: 0, aBlocks: []};
|
|
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
var iBlockMax = ((addr + size - 1) >>> this.nBlockShift);
|
|
|
|
info.cbTotal = 0;
|
|
info.cBlocks = 0;
|
|
while (iBlock <= iBlockMax) {
|
|
var block = this.aMemBlocks[iBlock];
|
|
info.cbTotal += block.size;
|
|
if (block.size) {
|
|
info.aBlocks.push(usr.initBitFields(Bus.BlockInfo, iBlock, 0, 0, block.type));
|
|
info.cBlocks++
|
|
}
|
|
iBlock++;
|
|
}
|
|
return info;
|
|
};
|
|
|
|
/**
|
|
* getWidth()
|
|
*
|
|
* @this {Bus}
|
|
* @return {number}
|
|
*/
|
|
Bus.prototype.getWidth = function()
|
|
{
|
|
return this.nBusWidth;
|
|
};
|
|
|
|
/**
|
|
* removeMemory(addr, size)
|
|
*
|
|
* Replaces every block in the specified address range with empty Memory blocks that ignore all reads/writes.
|
|
*
|
|
* TODO: Update the removeMemory() interface to reflect the relaxed requirements of the addMemory() interface.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr
|
|
* @param {number} size
|
|
* @return {boolean} true if successful, false if not
|
|
*/
|
|
Bus.prototype.removeMemory = function(addr, size)
|
|
{
|
|
if (!(addr & this.nBlockLimit) && size && !(size & this.nBlockLimit)) {
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
while (size > 0) {
|
|
var blockOld = this.aMemBlocks[iBlock];
|
|
var blockNew = new Memory(addr);
|
|
blockNew.copyBreakpoints(this.dbg, blockOld);
|
|
this.aMemBlocks[iBlock++] = blockNew;
|
|
addr = iBlock * this.nBlockSize;
|
|
size -= this.nBlockSize;
|
|
}
|
|
return true;
|
|
}
|
|
return this.reportError(Bus.ERROR.REM_MEM_BADRANGE, addr, size);
|
|
};
|
|
|
|
/**
|
|
* getMemoryBlocks(addr, size)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is the starting physical address
|
|
* @param {number} size of the request, in bytes
|
|
* @return {Array} of Memory blocks
|
|
*/
|
|
Bus.prototype.getMemoryBlocks = function(addr, size)
|
|
{
|
|
var aBlocks = [];
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
while (size > 0 && iBlock < this.aMemBlocks.length) {
|
|
aBlocks.push(this.aMemBlocks[iBlock++]);
|
|
size -= this.nBlockSize;
|
|
}
|
|
return aBlocks;
|
|
};
|
|
|
|
/**
|
|
* setMemoryBlocks(addr, size, aBlocks, type)
|
|
*
|
|
* If no type is specified, then specified address range uses all the provided blocks as-is;
|
|
* this form of setMemoryBlocks() is used for complete physical aliases.
|
|
*
|
|
* Otherwise, new blocks are allocated with the specified type; the underlying memory from the
|
|
* provided blocks is still used, but the new blocks may have different access to that memory.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is the starting physical address
|
|
* @param {number} size of the request, in bytes
|
|
* @param {Array} aBlocks as returned by getMemoryBlocks()
|
|
* @param {number} [type] is one of the Memory.TYPE constants
|
|
*/
|
|
Bus.prototype.setMemoryBlocks = function(addr, size, aBlocks, type)
|
|
{
|
|
var i = 0;
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
while (size > 0 && iBlock < this.aMemBlocks.length) {
|
|
var block = aBlocks[i++];
|
|
|
|
if (!block) break;
|
|
if (type !== undefined) {
|
|
var blockNew = new Memory(addr);
|
|
blockNew.clone(block, type, this.dbg);
|
|
block = blockNew;
|
|
}
|
|
this.aMemBlocks[iBlock++] = block;
|
|
size -= this.nBlockSize;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getByte(addr)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @return {number} byte (8-bit) value at that address
|
|
*/
|
|
Bus.prototype.getByte = function(addr)
|
|
{
|
|
return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByte(addr & this.nBlockLimit, addr);
|
|
};
|
|
|
|
/**
|
|
* getByteDirect(addr)
|
|
*
|
|
* This is useful for the Debugger and other components that want to bypass getByte() breakpoint detection.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @return {number} byte (8-bit) value at that address
|
|
*/
|
|
Bus.prototype.getByteDirect = function(addr)
|
|
{
|
|
return this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].readByteDirect(addr & this.nBlockLimit, addr);
|
|
};
|
|
|
|
/**
|
|
* getShort(addr)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @return {number} word (16-bit) value at that address
|
|
*/
|
|
Bus.prototype.getShort = function(addr)
|
|
{
|
|
var off = addr & this.nBlockLimit;
|
|
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
|
|
if (off != this.nBlockLimit) {
|
|
return this.aMemBlocks[iBlock].readShort(off, addr);
|
|
}
|
|
return this.aMemBlocks[iBlock++].readByte(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByte(0, addr + 1) << 8);
|
|
};
|
|
|
|
/**
|
|
* getShortDirect(addr)
|
|
*
|
|
* This is useful for the Debugger and other components that want to bypass getShort() breakpoint detection.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @return {number} word (16-bit) value at that address
|
|
*/
|
|
Bus.prototype.getShortDirect = function(addr)
|
|
{
|
|
var off = addr & this.nBlockLimit;
|
|
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
|
|
if (off != this.nBlockLimit) {
|
|
return this.aMemBlocks[iBlock].readShortDirect(off, addr);
|
|
}
|
|
return this.aMemBlocks[iBlock++].readByteDirect(off, addr) | (this.aMemBlocks[iBlock & this.nBlockMask].readByteDirect(0, addr + 1) << 8);
|
|
};
|
|
|
|
/**
|
|
* setByte(addr, b)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe)
|
|
*/
|
|
Bus.prototype.setByte = function(addr, b)
|
|
{
|
|
this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByte(addr & this.nBlockLimit, b & 0xff, addr);
|
|
};
|
|
|
|
/**
|
|
* setByteDirect(addr, b)
|
|
*
|
|
* This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only
|
|
* memory protection (for example, this is an interface the ROM component could use to initialize ROM contents).
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @param {number} b is the byte (8-bit) value to write (we truncate it to 8 bits to be safe)
|
|
*/
|
|
Bus.prototype.setByteDirect = function(addr, b)
|
|
{
|
|
this.aMemBlocks[(addr & this.nBusMask) >>> this.nBlockShift].writeByteDirect(addr & this.nBlockLimit, b & 0xff, addr);
|
|
};
|
|
|
|
/**
|
|
* setShort(addr, w)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe)
|
|
*/
|
|
Bus.prototype.setShort = function(addr, w)
|
|
{
|
|
var off = addr & this.nBlockLimit;
|
|
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
|
|
if (off != this.nBlockLimit) {
|
|
this.aMemBlocks[iBlock].writeShort(off, w & 0xffff, addr);
|
|
return;
|
|
}
|
|
this.aMemBlocks[iBlock++].writeByte(off, w & 0xff, addr);
|
|
this.aMemBlocks[iBlock & this.nBlockMask].writeByte(0, (w >> 8) & 0xff, addr + 1);
|
|
};
|
|
|
|
/**
|
|
* setShortDirect(addr, w)
|
|
*
|
|
* This is useful for the Debugger and other components that want to bypass breakpoint detection AND read-only
|
|
* memory protection (for example, this is an interface the ROM component could use to initialize ROM contents).
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr is a physical address
|
|
* @param {number} w is the word (16-bit) value to write (we truncate it to 16 bits to be safe)
|
|
*/
|
|
Bus.prototype.setShortDirect = function(addr, w)
|
|
{
|
|
var off = addr & this.nBlockLimit;
|
|
var iBlock = (addr & this.nBusMask) >>> this.nBlockShift;
|
|
if (off != this.nBlockLimit) {
|
|
this.aMemBlocks[iBlock].writeShortDirect(off, w & 0xffff, addr);
|
|
return;
|
|
}
|
|
this.aMemBlocks[iBlock++].writeByteDirect(off, w & 0xff, addr);
|
|
this.aMemBlocks[iBlock & this.nBlockMask].writeByteDirect(0, (w >> 8) & 0xff, addr + 1);
|
|
};
|
|
|
|
/**
|
|
* addMemBreak(addr, fWrite)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr
|
|
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
|
|
*/
|
|
Bus.prototype.addMemBreak = function(addr, fWrite)
|
|
{
|
|
if (DEBUGGER) {
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
this.aMemBlocks[iBlock].addBreakpoint(addr & this.nBlockLimit, fWrite);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* removeMemBreak(addr, fWrite)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} addr
|
|
* @param {boolean} fWrite is true for a memory write breakpoint, false for a memory read breakpoint
|
|
*/
|
|
Bus.prototype.removeMemBreak = function(addr, fWrite)
|
|
{
|
|
if (DEBUGGER) {
|
|
var iBlock = addr >>> this.nBlockShift;
|
|
this.aMemBlocks[iBlock].removeBreakpoint(addr & this.nBlockLimit, fWrite);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* saveMemory(fAll)
|
|
*
|
|
* The only memory blocks we save are those marked as dirty, but most likely all of RAM will have been marked dirty,
|
|
* and even if our dirty-memory flags were as smart as our dirty-sector flags (ie, were set only when a write changed
|
|
* what was already there), it's unlikely that would reduce the number of RAM blocks we must save/restore. At least
|
|
* all the ROM blocks should be clean (except in the unlikely event that the Debugger was used to modify them).
|
|
*
|
|
* All dirty blocks will be stored in a single array, as pairs of block numbers and data arrays, like so:
|
|
*
|
|
* [iBlock0, [dw0, dw1, ...], iBlock1, [dw0, dw1, ...], ...]
|
|
*
|
|
* In a normal 4Kb block, there will be 1K DWORD values in the data array. Remember that each DWORD is a signed 32-bit
|
|
* integer (because they are formed using bit-wise operator rather than floating-point math operators), so don't be
|
|
* surprised to see negative numbers in the data.
|
|
*
|
|
* The above example assumes "uncompressed" data arrays. If we choose to use "compressed" data arrays, the data arrays
|
|
* will look like:
|
|
*
|
|
* [count0, dw0, count1, dw1, ...]
|
|
*
|
|
* where each count indicates how many times the following DWORD value occurs. A data array length less than 1K indicates
|
|
* that it's compressed, since we'll only store them in compressed form if they actually shrank, and we'll use State
|
|
* helper methods compress() and decompress() to create and expand the compressed data arrays.
|
|
*
|
|
* @this {Bus}
|
|
* @param {boolean} [fAll] (true to save all non-ROM memory blocks, regardless of their dirty flags)
|
|
* @return {Array} a
|
|
*/
|
|
Bus.prototype.saveMemory = function(fAll)
|
|
{
|
|
var i = 0;
|
|
var a = [];
|
|
|
|
for (var iBlock = 0; iBlock < this.nBlockTotal; iBlock++) {
|
|
var block = this.aMemBlocks[iBlock];
|
|
/*
|
|
* We have to check both fDirty and fDirtyEver, because we may have called cleanMemory() on some of
|
|
* the memory blocks (eg, video memory), and while cleanMemory() will clear a dirty block's fDirty flag,
|
|
* it also sets the dirty block's fDirtyEver flag, which is left set for the lifetime of the machine.
|
|
*/
|
|
if (fAll && block.type != Memory.TYPE.ROM || block.fDirty || block.fDirtyEver) {
|
|
a[i++] = iBlock;
|
|
a[i++] = State.compress(block.save());
|
|
}
|
|
}
|
|
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* restoreMemory(a)
|
|
*
|
|
* This restores the contents of all Memory blocks; called by CPUState.restore().
|
|
*
|
|
* In theory, we ONLY have to save/restore block contents. Other block attributes,
|
|
* like the type, the memory controller (if any), and the active memory access functions,
|
|
* should already be restored, since every component (re)allocates all the memory blocks
|
|
* it was using when it's restored. And since the CPU is guaranteed to be the last
|
|
* component to be restored, all those blocks (and their attributes) should be in place now.
|
|
*
|
|
* See saveMemory() for more information on how the memory block contents are saved.
|
|
*
|
|
* @this {Bus}
|
|
* @param {Array} a
|
|
* @return {boolean} true if successful, false if not
|
|
*/
|
|
Bus.prototype.restoreMemory = function(a)
|
|
{
|
|
var i;
|
|
for (i = 0; i < a.length - 1; i += 2) {
|
|
var iBlock = a[i];
|
|
var adw = a[i+1];
|
|
if (adw && adw.length < this.nBlockLen) {
|
|
adw = State.decompress(adw, this.nBlockLen);
|
|
}
|
|
var block = this.aMemBlocks[iBlock];
|
|
if (!block || !block.restore(adw)) {
|
|
/*
|
|
* Either the block to restore hasn't been allocated, indicating a change in the machine
|
|
* configuration since it was last saved (the most likely explanation) or there's some internal
|
|
* inconsistency (eg, the block size is wrong).
|
|
*/
|
|
Component.error("Unable to restore memory block " + iBlock);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* addPortInputBreak(port)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} [port]
|
|
* @return {boolean} true if break on port input enabled, false if disabled
|
|
*/
|
|
Bus.prototype.addPortInputBreak = function(port)
|
|
{
|
|
if (port === undefined) {
|
|
this.fPortInputBreakAll = !this.fPortInputBreakAll;
|
|
return this.fPortInputBreakAll;
|
|
}
|
|
if (this.aPortInputNotify[port] === undefined) {
|
|
this.aPortInputNotify[port] = [null, false];
|
|
}
|
|
this.aPortInputNotify[port][1] = !this.aPortInputNotify[port][1];
|
|
return this.aPortInputNotify[port][1];
|
|
};
|
|
|
|
/**
|
|
* addPortInputNotify(start, end, fn)
|
|
*
|
|
* Add a port input-notification handler to the list of such handlers.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} start port address
|
|
* @param {number} end port address
|
|
* @param {function(number,number)} fn is called with the port and IP values at the time of the input
|
|
*/
|
|
Bus.prototype.addPortInputNotify = function(start, end, fn)
|
|
{
|
|
if (fn !== undefined) {
|
|
for (var port = start; port <= end; port++) {
|
|
if (this.aPortInputNotify[port] !== undefined) {
|
|
Component.warning("Input port " + str.toHexWord(port) + " already registered");
|
|
continue;
|
|
}
|
|
this.aPortInputNotify[port] = [fn, false];
|
|
if (MAXDEBUG) this.log("addPortInputNotify(" + str.toHexWord(port) + ")");
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* addPortInputTable(component, table, offset)
|
|
*
|
|
* Add port input-notification handlers from the specified table (a batch version of addPortInputNotify)
|
|
*
|
|
* @this {Bus}
|
|
* @param {Component} component
|
|
* @param {Object} table
|
|
* @param {number} [offset] is an optional port offset
|
|
*/
|
|
Bus.prototype.addPortInputTable = function(component, table, offset)
|
|
{
|
|
if (offset === undefined) offset = 0;
|
|
for (var port in table) {
|
|
this.addPortInputNotify(+port + offset, +port + offset, table[port].bind(component));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* addPortInputWidth(port, size)
|
|
*
|
|
* By default, all input ports are 1 byte wide; ports that are wider must call this function.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} port
|
|
* @param {number} size (1, 2 or 4)
|
|
*/
|
|
Bus.prototype.addPortInputWidth = function(port, size)
|
|
{
|
|
this.aPortInputWidth[port] = size;
|
|
};
|
|
|
|
/**
|
|
* checkPortInputNotify(port, size, addrIP)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} port
|
|
* @param {number} size (1, 2 or 4)
|
|
* @param {number} [addrIP] is the IP value at the time of the input
|
|
* @return {number} simulated port data
|
|
*
|
|
* NOTE: It seems that parts of the ROM BIOS (like the RS-232 probes around F000:E5D7 in the 5150 BIOS)
|
|
* assume that ports for non-existent hardware return 0xff rather than 0x00, hence my new default (0xff) below.
|
|
*/
|
|
Bus.prototype.checkPortInputNotify = function(port, size, addrIP)
|
|
{
|
|
var data = 0, shift = 0;
|
|
|
|
while (size > 0) {
|
|
|
|
var aNotify = this.aPortInputNotify[port];
|
|
var sizePort = this.aPortInputWidth[port] || 1;
|
|
var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1));
|
|
var dataPort = maskPort;
|
|
|
|
/*
|
|
* TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O
|
|
* to a 32-bit port). We probably should pass the size through to the aNotify[0] handler,
|
|
* and let it decide what to do, but I don't feel like changing all the I/O handlers right now.
|
|
* The good news, at least, is that the 8-bit handlers would not have to do anything special.
|
|
* This assert will warn us if this is a pressing need.
|
|
*/
|
|
|
|
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* removePortInputNotify(start, end)
|
|
*
|
|
* Remove port input-notification handler(s) (to be ENABLED later if needed)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} start address
|
|
* @param {number} end address
|
|
*
|
|
Bus.prototype.removePortInputNotify = function(start, end)
|
|
{
|
|
for (var port = start; port < end; port++) {
|
|
if (this.aPortInputNotify[port]) {
|
|
delete this.aPortInputNotify[port];
|
|
}
|
|
}
|
|
};
|
|
*/
|
|
|
|
/**
|
|
* addPortOutputBreak(port)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} [port]
|
|
* @return {boolean} true if break on port output enabled, false if disabled
|
|
*/
|
|
Bus.prototype.addPortOutputBreak = function(port)
|
|
{
|
|
if (port === undefined) {
|
|
this.fPortOutputBreakAll = !this.fPortOutputBreakAll;
|
|
return this.fPortOutputBreakAll;
|
|
}
|
|
if (this.aPortOutputNotify[port] === undefined) {
|
|
this.aPortOutputNotify[port] = [null, false];
|
|
}
|
|
this.aPortOutputNotify[port][1] = !this.aPortOutputNotify[port][1];
|
|
return this.aPortOutputNotify[port][1];
|
|
};
|
|
|
|
/**
|
|
* addPortOutputNotify(start, end, fn)
|
|
*
|
|
* Add a port output-notification handler to the list of such handlers.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} start port address
|
|
* @param {number} end port address
|
|
* @param {function(number,number)} fn is called with the port and IP values at the time of the output
|
|
*/
|
|
Bus.prototype.addPortOutputNotify = function(start, end, fn)
|
|
{
|
|
if (fn !== undefined) {
|
|
for (var port = start; port <= end; port++) {
|
|
if (this.aPortOutputNotify[port] !== undefined) {
|
|
Component.warning("Output port " + str.toHexWord(port) + " already registered");
|
|
continue;
|
|
}
|
|
this.aPortOutputNotify[port] = [fn, false];
|
|
if (MAXDEBUG) this.log("addPortOutputNotify(" + str.toHexWord(port) + ")");
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* addPortOutputTable(component, table, offset)
|
|
*
|
|
* Add port output-notification handlers from the specified table (a batch version of addPortOutputNotify)
|
|
*
|
|
* @this {Bus}
|
|
* @param {Component} component
|
|
* @param {Object} table
|
|
* @param {number} [offset] is an optional port offset
|
|
*/
|
|
Bus.prototype.addPortOutputTable = function(component, table, offset)
|
|
{
|
|
if (offset === undefined) offset = 0;
|
|
for (var port in table) {
|
|
this.addPortOutputNotify(+port + offset, +port + offset, table[port].bind(component));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* addPortOutputWidth(port, size)
|
|
*
|
|
* By default, all output ports are 1 byte wide; ports that are wider must call this function.
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} port
|
|
* @param {number} size (1, 2 or 4)
|
|
*/
|
|
Bus.prototype.addPortOutputWidth = function(port, size)
|
|
{
|
|
this.aPortOutputWidth[port] = size;
|
|
};
|
|
|
|
/**
|
|
* checkPortOutputNotify(port, size, data, addrIP)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} port
|
|
* @param {number} size
|
|
* @param {number} data
|
|
* @param {number} [addrIP] is the IP value at the time of the output
|
|
*/
|
|
Bus.prototype.checkPortOutputNotify = function(port, size, data, addrIP)
|
|
{
|
|
var shift = 0;
|
|
|
|
while (size > 0) {
|
|
|
|
var aNotify = this.aPortOutputNotify[port];
|
|
var sizePort = this.aPortOutputWidth[port] || 1;
|
|
var maskPort = (sizePort == 1? 0xff : (sizePort == 2? 0xffff : -1));
|
|
var dataPort = (data >>>= shift) & maskPort;
|
|
|
|
/*
|
|
* TODO: We need to decide what to do about 8-bit I/O to a 16-bit port (ditto for 16-bit I/O
|
|
* to a 32-bit port). We probably should pass the size through to the aNotify[0] handler,
|
|
* and let it decide what to do, but I don't feel like changing all the I/O handlers right now.
|
|
* The good news, at least, is that the 8-bit handlers would not have to do anything special.
|
|
* This assert will warn us if this is a pressing need.
|
|
*/
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* removePortOutputNotify(start, end)
|
|
*
|
|
* Remove port output-notification handler(s) (to be ENABLED later if needed)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} start address
|
|
* @param {number} end address
|
|
*
|
|
Bus.prototype.removePortOutputNotify = function(start, end)
|
|
{
|
|
for (var port = start; port < end; port++) {
|
|
if (this.aPortOutputNotify[port]) {
|
|
delete this.aPortOutputNotify[port];
|
|
}
|
|
}
|
|
};
|
|
*/
|
|
|
|
/**
|
|
* reportError(op, addr, size, fQuiet)
|
|
*
|
|
* @this {Bus}
|
|
* @param {number} op
|
|
* @param {number} addr
|
|
* @param {number} size
|
|
* @param {boolean} [fQuiet] (true if any error should be quietly logged)
|
|
* @return {boolean} false
|
|
*/
|
|
Bus.prototype.reportError = function(op, addr, size, fQuiet)
|
|
{
|
|
var sError = "Memory block error (" + op + ": " + str.toHex(addr) + "," + str.toHex(size) + ")";
|
|
if (fQuiet) {
|
|
if (this.dbg) {
|
|
this.dbg.message(sError);
|
|
} else {
|
|
this.log(sError);
|
|
}
|
|
} else {
|
|
Component.error(sError);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
// ./modules/pc8080/lib/memory.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Memory component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @class DataView
|
|
* @property {function(number,boolean):number} getUint8
|
|
* @property {function(number,number,boolean)} setUint8
|
|
* @property {function(number,boolean):number} getUint16
|
|
* @property {function(number,number,boolean)} setUint16
|
|
* @property {function(number,boolean):number} getInt32
|
|
* @property {function(number,number,boolean)} setInt32
|
|
*/
|
|
|
|
var littleEndian = (TYPEDARRAYS? (function() {
|
|
var buffer = new ArrayBuffer(2);
|
|
new DataView(buffer).setUint16(0, 256, true);
|
|
return new Uint16Array(buffer)[0] === 256;
|
|
})() : false);
|
|
|
|
/**
|
|
* Memory(addr, used, size, type)
|
|
*
|
|
* The Bus component allocates Memory objects so that each has a memory buffer with a
|
|
* block-granular starting address and an address range equal to bus.nBlockSize; however,
|
|
* the size of any given Memory object's underlying buffer can be either zero or bus.nBlockSize;
|
|
* memory read/write functions for empty (buffer-less) blocks are mapped to readNone/writeNone.
|
|
*
|
|
* The Bus allocates empty blocks for the entire address space during initialization, so that
|
|
* any reads/writes to undefined addresses will have no effect. Later, the ROM and RAM
|
|
* components will ask the Bus to allocate memory for specific ranges, and the Bus will allocate
|
|
* as many new blockSize Memory objects as the ranges require. Partial Memory blocks could
|
|
* also be supported in theory, but in practice, they're not.
|
|
*
|
|
* Because Memory blocks now allow us to have a "sparse" address space, we could choose to
|
|
* take the memory hit of allocating 4K arrays per block, where each element stores only one byte,
|
|
* instead of the more frugal but slightly slower approach of allocating arrays of 32-bit dwords
|
|
* (LONGARRAYS) and shifting/masking bytes/words to/from dwords; in theory, byte accesses would
|
|
* be faster and word accesses somewhat less faster.
|
|
*
|
|
* However, preliminary testing of that feature (BYTEARRAYS) did not yield significantly faster
|
|
* performance, so it is OFF by default to minimize our memory consumption. Using TYPEDARRAYS
|
|
* would seem best, but as discussed in defines.js, it's off by default, because it doesn't perform
|
|
* as well as LONGARRAYS; the other advantage of TYPEDARRAYS is that it should theoretically use
|
|
* about 1/2 the memory of LONGARRAYS (32-bit elements vs 64-bit numbers), but I value speed over
|
|
* size at this point. Also, not all JavaScript implementations support TYPEDARRAYS (IE9 is probably
|
|
* the only real outlier: it lacks typed arrays but otherwise has all the necessary HTML5 support).
|
|
*
|
|
* WARNING: Since Memory blocks are low-level objects that have no UI requirements, they
|
|
* do not inherit from the Component class, so if you want to use any Component class methods,
|
|
* such as Component.assert(), use the corresponding Debugger methods instead (assuming a debugger
|
|
* is available).
|
|
*
|
|
* @constructor
|
|
* @param {number|null} [addr] of lowest used address in block
|
|
* @param {number} [used] portion of block in bytes (0 for none); must be a multiple of 4
|
|
* @param {number} [size] of block's buffer in bytes (0 for none); must be a multiple of 4
|
|
* @param {number} [type] is one of the Memory.TYPE constants (default is Memory.TYPE.NONE)
|
|
*/
|
|
function Memory(addr, used, size, type)
|
|
{
|
|
var i;
|
|
this.id = (Memory.idBlock += 2);
|
|
this.adw = null;
|
|
this.offset = 0;
|
|
this.addr = addr;
|
|
this.used = used;
|
|
this.size = size || 0;
|
|
this.type = type || Memory.TYPE.NONE;
|
|
this.fReadOnly = (type == Memory.TYPE.ROM);
|
|
this.copyBreakpoints(); // initialize the block's Debugger info; the caller will reinitialize
|
|
|
|
/*
|
|
* TODO: Study the impact of dirty block tracking. The original purposes were to allow saveMemory()
|
|
* to save only dirty blocks, and to enable the Video component to quickly detect changes to the video buffer.
|
|
* But the benefit to saveMemory() is minimal, and the Video component has other options; for example, it now
|
|
* uses a custom memory controller for all EGA/VGA video modes, which performs its own dirty block tracking,
|
|
* and that could easily be extended to the older MDA/CGA video modes, which still use conventional memory blocks.
|
|
* Alternatively, we could restrict the use of dirty block tracking to certain memory types (eg, VIDEO memory).
|
|
*
|
|
* However, a quick test with dirty block tracking disabled didn't yield a noticeable improvement in performance,
|
|
* so I think the overhead of our block-based architecture is swamping the impact of these micro-updates.
|
|
*/
|
|
this.fDirty = this.fDirtyEver = false;
|
|
|
|
/*
|
|
* For empty memory blocks, all we need to do is ensure all access functions are mapped to "none" handlers.
|
|
*/
|
|
if (!size) {
|
|
this.setAccess();
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* This is the normal case: allocate a buffer that provides 8 bits of data per address;
|
|
* no controller is required because our default memory access functions (see afnMemory)
|
|
* know how to deal with this simple 1-1 mapping of addresses to bytes and words.
|
|
*
|
|
* TODO: Consider initializing the memory array to random (or pseudo-random) values in DEBUG
|
|
* mode; pseudo-random might be best, to help make any bugs reproducible.
|
|
*/
|
|
if (TYPEDARRAYS) {
|
|
this.buffer = new ArrayBuffer(size);
|
|
this.dv = new DataView(this.buffer, 0, size);
|
|
/*
|
|
* If littleEndian is true, we can use ab[], aw[] and adw[] directly; well, we can use them
|
|
* whenever the offset is a multiple of 1, 2 or 4, respectively. Otherwise, we must fallback to
|
|
* dv.getUint8()/dv.setUint8(), dv.getUint16()/dv.setUint16() and dv.getInt32()/dv.setInt32().
|
|
*/
|
|
this.ab = new Uint8Array(this.buffer, 0, size);
|
|
this.aw = new Uint16Array(this.buffer, 0, size >> 1);
|
|
this.adw = new Int32Array(this.buffer, 0, size >> 2);
|
|
this.setAccess(littleEndian? Memory.afnArrayLE : Memory.afnArrayBE);
|
|
} else {
|
|
if (BYTEARRAYS) {
|
|
this.ab = new Array(size);
|
|
} else {
|
|
/*
|
|
* NOTE: This is the default mode of operation (!TYPEDARRAYS && !BYTEARRAYS), because it
|
|
* seems to provide the best performance; and although in theory, that performance might
|
|
* come at twice the overhead of TYPEDARRAYS, it's increasingly likely that the JavaScript
|
|
* runtime will notice that all we ever store are 32-bit values, and optimize accordingly.
|
|
*/
|
|
this.adw = new Array(size >> 2);
|
|
for (i = 0; i < this.adw.length; i++) this.adw[i] = 0;
|
|
}
|
|
this.setAccess(Memory.afnMemory);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Basic memory types
|
|
*
|
|
* RAM is the most conventional memory type, providing full read/write capability to x86-compatible (ie,
|
|
* 'little endian") storage. ROM is equally conventional, except that the fReadOnly property is set,
|
|
* disabling writes. VIDEO is treated exactly like RAM, unless a controller is provided. Both RAM and
|
|
* VIDEO memory are always considered writable, and even ROM can be written using the Bus setByteDirect()
|
|
* interface (which in turn uses the Memory writeByteDirect() interface), allowing the ROM component to
|
|
* initialize its own memory. The CTRL type is used to identify memory-mapped devices that do not need
|
|
* any default storage and always provide their own controller.
|
|
*
|
|
* Unallocated regions of the address space contain a special memory block of type NONE that contains
|
|
* no storage. Mapping every addressible location to a memory block allows all accesses to be routed in
|
|
* exactly the same manner, without resorting to any range or processor checks.
|
|
*
|
|
* These types are not mutually exclusive. For example, VIDEO memory could be allocated as RAM, with or
|
|
* without a custom controller (the original Monochrome and CGA video cards used read/write storage that
|
|
* was indistinguishable from RAM), and CTRL memory could be allocated as an empty block of any type, with
|
|
* a custom controller. A few types are required for certain features (eg, ROM is required if you want
|
|
* read-only memory), but the larger purpose of these types is to help document the caller's intent and to
|
|
* provide the Control Panel with the ability to highlight memory regions accordingly.
|
|
*/
|
|
Memory.TYPE = {
|
|
NONE: 0,
|
|
RAM: 1,
|
|
ROM: 2,
|
|
VIDEO: 3,
|
|
CTRL: 4,
|
|
COLORS: ["black", "blue", "green", "cyan"],
|
|
NAMES: ["NONE", "RAM", "ROM", "VID", "H/W"]
|
|
};
|
|
|
|
/*
|
|
* Last used block ID (used for debugging only)
|
|
*/
|
|
Memory.idBlock = 0;
|
|
|
|
/**
|
|
* adjustEndian(dw)
|
|
*
|
|
* @param {number} dw
|
|
* @return {number}
|
|
*/
|
|
Memory.adjustEndian = function(dw) {
|
|
if (TYPEDARRAYS && !littleEndian) {
|
|
dw = (dw << 24) | ((dw << 8) & 0x00ff0000) | ((dw >> 8) & 0x0000ff00) | (dw >>> 24);
|
|
}
|
|
return dw;
|
|
};
|
|
|
|
Memory.prototype = {
|
|
constructor: Memory,
|
|
parent: null,
|
|
/**
|
|
* init(addr)
|
|
*
|
|
* Quick reinitializer when reusing a Memory block.
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} addr
|
|
*/
|
|
init: function(addr) {
|
|
this.addr = addr;
|
|
},
|
|
/**
|
|
* clone(mem, type)
|
|
*
|
|
* Converts the current Memory block (this) into a clone of the given Memory block (mem),
|
|
* and optionally overrides the current block's type with the specified type.
|
|
*
|
|
* @this {Memory}
|
|
* @param {Memory} mem
|
|
* @param {number} [type]
|
|
* @param {Debugger} [dbg]
|
|
*/
|
|
clone: function(mem, type, dbg) {
|
|
/*
|
|
* Original memory block IDs are even; cloned memory block IDs are odd;
|
|
* the original ID of the current block is lost, but that's OK, since it was presumably
|
|
* produced merely to become a clone.
|
|
*/
|
|
this.id = mem.id | 0x1;
|
|
this.used = mem.used;
|
|
this.size = mem.size;
|
|
if (type) {
|
|
this.type = type;
|
|
this.fReadOnly = (type == Memory.TYPE.ROM);
|
|
}
|
|
if (TYPEDARRAYS) {
|
|
this.buffer = mem.buffer;
|
|
this.dv = mem.dv;
|
|
this.ab = mem.ab;
|
|
this.aw = mem.aw;
|
|
this.adw = mem.adw;
|
|
this.setAccess(littleEndian? Memory.afnArrayLE : Memory.afnArrayBE);
|
|
} else {
|
|
if (BYTEARRAYS) {
|
|
this.ab = mem.ab;
|
|
} else {
|
|
this.adw = mem.adw;
|
|
}
|
|
this.setAccess(Memory.afnMemory);
|
|
}
|
|
this.copyBreakpoints(dbg, mem);
|
|
},
|
|
/**
|
|
* save()
|
|
*
|
|
* This gets the contents of a Memory block as an array of 32-bit values; used by Bus.saveMemory(),
|
|
* which in turn is called by CPUState.save().
|
|
*
|
|
* Memory blocks with custom memory controllers do NOT save their contents; that's the responsibility
|
|
* of the controller component.
|
|
*
|
|
* @this {Memory}
|
|
* @return {Array|Int32Array|null}
|
|
*/
|
|
save: function() {
|
|
var adw, i;
|
|
if (BYTEARRAYS) {
|
|
adw = new Array(this.size >> 2);
|
|
var off = 0;
|
|
for (i = 0; i < adw.length; i++) {
|
|
adw[i] = this.ab[off] | (this.ab[off + 1] << 8) | (this.ab[off + 2] << 16) | (this.ab[off + 3] << 24);
|
|
off += 4;
|
|
}
|
|
}
|
|
else if (TYPEDARRAYS) {
|
|
/*
|
|
* It might be tempting to just return a copy of Int32Array(this.buffer, 0, this.size >> 2),
|
|
* but we can't be sure of the "endianness" of an Int32Array -- which would be OK if the array
|
|
* was always saved/restored on the same machine, but there's no guarantee of that, either.
|
|
* So we use getInt32() and require little-endian values.
|
|
*
|
|
* Moreover, an Int32Array isn't treated by JSON.stringify() and JSON.parse() exactly like
|
|
* a normal array; it's serialized as an Object rather than an Array, so it lacks a "length"
|
|
* property and causes problems for State.store() and State.parse().
|
|
*/
|
|
adw = new Array(this.size >> 2);
|
|
for (i = 0; i < adw.length; i++) {
|
|
adw[i] = this.dv.getInt32(i << 2, true);
|
|
}
|
|
}
|
|
else {
|
|
adw = this.adw;
|
|
}
|
|
return adw;
|
|
},
|
|
/**
|
|
* restore(adw)
|
|
*
|
|
* This restores the contents of a Memory block from an array of 32-bit values;
|
|
* used by Bus.restoreMemory(), which is called by CPUState.restore(), after all other
|
|
* components have been restored and thus all Memory blocks have been allocated
|
|
* by their respective components.
|
|
*
|
|
* @this {Memory}
|
|
* @param {Array|null} adw
|
|
* @return {boolean} true if successful, false if block size mismatch
|
|
*/
|
|
restore: function(adw) {
|
|
/*
|
|
* At this point, it's a consistency error for adw to be null; it's happened once already,
|
|
* when there was a restore bug in the Video component that added the frame buffer at the video
|
|
* card's "spec'ed" address instead of the programmed address, so there were no controller-owned
|
|
* memory blocks installed at the programmed address, and so we arrived here at a block with
|
|
* no controller AND no data.
|
|
*/
|
|
|
|
|
|
if (adw && this.size == adw.length << 2) {
|
|
var i;
|
|
if (BYTEARRAYS) {
|
|
var off = 0;
|
|
for (i = 0; i < adw.length; i++) {
|
|
this.ab[off] = adw[i] & 0xff;
|
|
this.ab[off + 1] = (adw[i] >> 8) & 0xff;
|
|
this.ab[off + 2] = (adw[i] >> 16) & 0xff;
|
|
this.ab[off + 3] = (adw[i] >> 24) & 0xff;
|
|
off += 4;
|
|
}
|
|
} else if (TYPEDARRAYS) {
|
|
for (i = 0; i < adw.length; i++) {
|
|
this.dv.setInt32(i << 2, adw[i], true);
|
|
}
|
|
} else {
|
|
this.adw = adw;
|
|
}
|
|
this.fDirty = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
/**
|
|
* setAccess(afn, fDirect)
|
|
*
|
|
* If no function table is specified, a default is selected based on the Memory type.
|
|
*
|
|
* @this {Memory}
|
|
* @param {Array.<function()>} [afn] function table
|
|
* @param {boolean} [fDirect] (true to update direct access functions as well; default is true)
|
|
*/
|
|
setAccess: function(afn, fDirect) {
|
|
if (!afn) {
|
|
|
|
afn = Memory.afnNone;
|
|
}
|
|
this.setReadAccess(afn, fDirect);
|
|
this.setWriteAccess(afn, fDirect);
|
|
},
|
|
/**
|
|
* setReadAccess(afn, fDirect)
|
|
*
|
|
* @this {Memory}
|
|
* @param {Array.<function()>} afn
|
|
* @param {boolean} [fDirect]
|
|
*/
|
|
setReadAccess: function(afn, fDirect) {
|
|
if (!fDirect || !this.cReadBreakpoints) {
|
|
this.readByte = afn[0] || this.readNone;
|
|
this.readShort = afn[1] || this.readShortDefault;
|
|
}
|
|
if (fDirect || fDirect === undefined) {
|
|
this.readByteDirect = afn[0] || this.readNone;
|
|
this.readShortDirect = afn[1] || this.readShortDefault;
|
|
}
|
|
},
|
|
/**
|
|
* setWriteAccess(afn, fDirect)
|
|
*
|
|
* @this {Memory}
|
|
* @param {Array.<function()>} afn
|
|
* @param {boolean} [fDirect]
|
|
*/
|
|
setWriteAccess: function(afn, fDirect) {
|
|
if (!fDirect || !this.cWriteBreakpoints) {
|
|
this.writeByte = !this.fReadOnly && afn[2] || this.writeNone;
|
|
this.writeShort = !this.fReadOnly && afn[3] || this.writeShortDefault;
|
|
}
|
|
if (fDirect || fDirect === undefined) {
|
|
this.writeByteDirect = afn[2] || this.writeNone;
|
|
this.writeShortDirect = afn[3] || this.writeShortDefault;
|
|
}
|
|
},
|
|
/**
|
|
* resetReadAccess()
|
|
*
|
|
* @this {Memory}
|
|
*/
|
|
resetReadAccess: function() {
|
|
this.readByte = this.readByteDirect;
|
|
this.readShort = this.readShortDirect;
|
|
},
|
|
/**
|
|
* resetWriteAccess()
|
|
*
|
|
* @this {Memory}
|
|
*/
|
|
resetWriteAccess: function() {
|
|
this.writeByte = this.fReadOnly? this.writeNone : this.writeByteDirect;
|
|
this.writeShort = this.fReadOnly? this.writeShortDefault : this.writeShortDirect;
|
|
},
|
|
/**
|
|
* printAddr(sMessage)
|
|
*
|
|
* @this {Memory}
|
|
* @param {string} sMessage
|
|
*/
|
|
printAddr: function(sMessage) {
|
|
if (DEBUG && this.dbg && this.dbg.messageEnabled(Messages.MEM)) {
|
|
this.dbg.printMessage(sMessage + ' ' + (this.addr != null? ('%' + str.toHex(this.addr)) : '#' + this.id), true);
|
|
}
|
|
},
|
|
/**
|
|
* addBreakpoint(off, fWrite)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {boolean} fWrite
|
|
*/
|
|
addBreakpoint: function(off, fWrite) {
|
|
if (!fWrite) {
|
|
if (this.cReadBreakpoints++ === 0) {
|
|
this.setReadAccess(Memory.afnChecked, false);
|
|
}
|
|
if (DEBUG) this.printAddr("read breakpoint added to memory block");
|
|
}
|
|
else {
|
|
if (this.cWriteBreakpoints++ === 0) {
|
|
this.setWriteAccess(Memory.afnChecked, false);
|
|
}
|
|
if (DEBUG) this.printAddr("write breakpoint added to memory block");
|
|
}
|
|
},
|
|
/**
|
|
* removeBreakpoint(off, fWrite)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {boolean} fWrite
|
|
*/
|
|
removeBreakpoint: function(off, fWrite) {
|
|
if (!fWrite) {
|
|
if (--this.cReadBreakpoints === 0) {
|
|
this.resetReadAccess();
|
|
if (DEBUG) this.printAddr("all read breakpoints removed from memory block");
|
|
}
|
|
|
|
}
|
|
else {
|
|
if (--this.cWriteBreakpoints === 0) {
|
|
this.resetWriteAccess();
|
|
if (DEBUG) this.printAddr("all write breakpoints removed from memory block");
|
|
}
|
|
|
|
}
|
|
},
|
|
/**
|
|
* copyBreakpoints(dbg, mem)
|
|
*
|
|
* @this {Memory}
|
|
* @param {Debugger} [dbg]
|
|
* @param {Memory} [mem] (outgoing Memory block to copy breakpoints from, if any)
|
|
*/
|
|
copyBreakpoints: function(dbg, mem) {
|
|
this.dbg = dbg;
|
|
this.cReadBreakpoints = this.cWriteBreakpoints = 0;
|
|
if (mem) {
|
|
if ((this.cReadBreakpoints = mem.cReadBreakpoints)) {
|
|
this.setReadAccess(Memory.afnChecked, false);
|
|
}
|
|
if ((this.cWriteBreakpoints = mem.cWriteBreakpoints)) {
|
|
this.setWriteAccess(Memory.afnChecked, false);
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* readNone(off)
|
|
*
|
|
* Previously, this always returned 0x00, but the initial memory probe by the COMPAQ DeskPro 386 ROM BIOS
|
|
* writes 0x0000 to the first word of every 64Kb block in the nearly 16Mb address space it supports, and
|
|
* if it reads back 0x0000, it will initially think that LOTS of RAM exists, only to be disappointed later
|
|
* when it performs a more exhaustive memory test, generating unwanted error messages in the process.
|
|
*
|
|
* TODO: Determine if we should have separate readByteNone(), readShortNone() and readLongNone() functions
|
|
* to return 0xff, 0xffff and 0xffffffff|0, respectively. This seems sufficient for now, as it seems unlikely
|
|
* that a system would require nonexistent memory locations to return ALL bits set.
|
|
*
|
|
* Also, I'm reluctant to address that potential issue by simply returning -1, because to date, the above
|
|
* Memory interfaces have always returned values that are properly masked to 8, 16 or 32 bits, respectively.
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readNone: function readNone(off, addr) {
|
|
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages.CPU | Messages.MEM) /* && !off */) {
|
|
this.dbg.message("attempt to read invalid block %" + str.toHex(this.addr), true);
|
|
}
|
|
return 0xff;
|
|
},
|
|
/**
|
|
* writeNone(off, v, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} v (could be either a byte or word value, since we use the same handler for both kinds of accesses)
|
|
* @param {number} addr
|
|
*/
|
|
writeNone: function writeNone(off, v, addr) {
|
|
if (DEBUGGER && this.dbg && this.dbg.messageEnabled(Messages.CPU | Messages.MEM) /* && !off */) {
|
|
this.dbg.message("attempt to write " + str.toHexWord(v) + " to invalid block %" + str.toHex(this.addr), true);
|
|
}
|
|
},
|
|
/**
|
|
* readShortDefault(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readShortDefault: function readShortDefault(off, addr) {
|
|
return this.readByte(off++, addr++) | (this.readByte(off, addr) << 8);
|
|
},
|
|
/**
|
|
* writeShortDefault(off, w, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} w
|
|
* @param {number} addr
|
|
*/
|
|
writeShortDefault: function writeShortDefault(off, w, addr) {
|
|
this.writeByte(off++, w & 0xff, addr++);
|
|
this.writeByte(off, w >> 8, addr);
|
|
},
|
|
/**
|
|
* readByteMemory(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readByteMemory: function readByteMemory(off, addr) {
|
|
if (BYTEARRAYS) {
|
|
return this.ab[off];
|
|
}
|
|
return ((this.adw[off >> 2] >>> ((off & 0x3) << 3)) & 0xff);
|
|
},
|
|
/**
|
|
* readShortMemory(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readShortMemory: function readShortMemory(off, addr) {
|
|
if (BYTEARRAYS) {
|
|
return this.ab[off] | (this.ab[off + 1] << 8);
|
|
}
|
|
var w;
|
|
var idw = off >> 2;
|
|
var nShift = (off & 0x3) << 3;
|
|
var dw = (this.adw[idw] >> nShift);
|
|
if (nShift < 24) {
|
|
w = dw & 0xffff;
|
|
} else {
|
|
w = (dw & 0xff) | ((this.adw[idw + 1] & 0xff) << 8);
|
|
}
|
|
return w;
|
|
},
|
|
/**
|
|
* writeByteMemory(off, b, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b
|
|
* @param {number} addr
|
|
*/
|
|
writeByteMemory: function writeByteMemory(off, b, addr) {
|
|
if (BYTEARRAYS) {
|
|
this.ab[off] = b;
|
|
} else {
|
|
var idw = off >> 2;
|
|
var nShift = (off & 0x3) << 3;
|
|
this.adw[idw] = (this.adw[idw] & ~(0xff << nShift)) | (b << nShift);
|
|
}
|
|
this.fDirty = true;
|
|
},
|
|
/**
|
|
* writeShortMemory(off, w, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} w
|
|
* @param {number} addr
|
|
*/
|
|
writeShortMemory: function writeShortMemory(off, w, addr) {
|
|
if (BYTEARRAYS) {
|
|
this.ab[off] = (w & 0xff);
|
|
this.ab[off + 1] = (w >> 8);
|
|
} else {
|
|
var idw = off >> 2;
|
|
var nShift = (off & 0x3) << 3;
|
|
if (nShift < 24) {
|
|
this.adw[idw] = (this.adw[idw] & ~(0xffff << nShift)) | (w << nShift);
|
|
} else {
|
|
this.adw[idw] = (this.adw[idw] & 0x00ffffff) | (w << 24);
|
|
idw++;
|
|
this.adw[idw] = (this.adw[idw] & (0xffffff00|0)) | (w >> 8);
|
|
}
|
|
}
|
|
this.fDirty = true;
|
|
},
|
|
/**
|
|
* readByteChecked(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readByteChecked: function readByteChecked(off, addr) {
|
|
if (DEBUGGER && this.dbg && this.addr != null) {
|
|
this.dbg.checkMemoryRead(this.addr + off);
|
|
}
|
|
return this.readByteDirect(off, addr);
|
|
},
|
|
/**
|
|
* readShortChecked(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readShortChecked: function readShortChecked(off, addr) {
|
|
if (DEBUGGER && this.dbg && this.addr != null) {
|
|
this.dbg.checkMemoryRead(this.addr + off, 2);
|
|
}
|
|
return this.readShortDirect(off, addr);
|
|
},
|
|
/**
|
|
* writeByteChecked(off, b, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @param {number} b
|
|
*/
|
|
writeByteChecked: function writeByteChecked(off, b, addr) {
|
|
if (DEBUGGER && this.dbg && this.addr != null) {
|
|
this.dbg.checkMemoryWrite(this.addr + off);
|
|
}
|
|
if (this.fReadOnly) this.writeNone(off, b, addr); else this.writeByteDirect(off, b, addr);
|
|
},
|
|
/**
|
|
* writeShortChecked(off, w, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @param {number} w
|
|
*/
|
|
writeShortChecked: function writeShortChecked(off, w, addr) {
|
|
if (DEBUGGER && this.dbg && this.addr != null) {
|
|
this.dbg.checkMemoryWrite(this.addr + off, 2)
|
|
}
|
|
if (this.fReadOnly) this.writeNone(off, w, addr); else this.writeShortDirect(off, w, addr);
|
|
},
|
|
/**
|
|
* readByteBE(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readByteBE: function readByteBE(off, addr) {
|
|
return this.ab[off];
|
|
},
|
|
/**
|
|
* readByteLE(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readByteLE: function readByteLE(off, addr) {
|
|
return this.ab[off];
|
|
},
|
|
/**
|
|
* readShortBE(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readShortBE: function readShortBE(off, addr) {
|
|
return this.dv.getUint16(off, true);
|
|
},
|
|
/**
|
|
* readShortLE(off, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @return {number}
|
|
*/
|
|
readShortLE: function readShortLE(off, addr) {
|
|
/*
|
|
* TODO: It remains to be seen if there's any advantage to checking the offset for an aligned read
|
|
* vs. always reading the bytes separately; it seems a safe bet for longs, but it's less clear for shorts.
|
|
*/
|
|
return (off & 0x1)? (this.ab[off] | (this.ab[off+1] << 8)) : this.aw[off >> 1];
|
|
},
|
|
/**
|
|
* writeByteBE(off, b, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b
|
|
* @param {number} addr
|
|
*/
|
|
writeByteBE: function writeByteBE(off, b, addr) {
|
|
this.ab[off] = b;
|
|
this.fDirty = true;
|
|
},
|
|
/**
|
|
* writeByteLE(off, b, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @param {number} b
|
|
*/
|
|
writeByteLE: function writeByteLE(off, b, addr) {
|
|
this.ab[off] = b;
|
|
this.fDirty = true;
|
|
},
|
|
/**
|
|
* writeShortBE(off, w, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @param {number} w
|
|
*/
|
|
writeShortBE: function writeShortBE(off, w, addr) {
|
|
this.dv.setUint16(off, w, true);
|
|
this.fDirty = true;
|
|
},
|
|
/**
|
|
* writeShortLE(off, w, addr)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} addr
|
|
* @param {number} w
|
|
*/
|
|
writeShortLE: function writeShortLE(off, w, addr) {
|
|
/*
|
|
* TODO: It remains to be seen if there's any advantage to checking the offset for an aligned write
|
|
* vs. always writing the bytes separately; it seems a safe bet for longs, but it's less clear for shorts.
|
|
*/
|
|
if (off & 0x1) {
|
|
this.ab[off] = w;
|
|
this.ab[off+1] = w >> 8;
|
|
} else {
|
|
this.aw[off >> 1] = w;
|
|
}
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* This is the effective definition of afnNone, but we need not fully define it, because setAccess()
|
|
* uses these defaults when any of the 6 handlers (ie, 3 read handlers and 3 write handlers) are undefined.
|
|
*
|
|
Memory.afnNone = [Memory.prototype.readNone, Memory.prototype.readShortDefault, Memory.prototype.writeNone, Memory.prototype.writeShortDefault];
|
|
*/
|
|
|
|
Memory.afnNone = [];
|
|
Memory.afnMemory = [Memory.prototype.readByteMemory, Memory.prototype.readShortMemory, Memory.prototype.writeByteMemory, Memory.prototype.writeShortMemory];
|
|
Memory.afnChecked = [Memory.prototype.readByteChecked, Memory.prototype.readShortChecked, Memory.prototype.writeByteChecked, Memory.prototype.writeShortChecked];
|
|
|
|
if (TYPEDARRAYS) {
|
|
Memory.afnArrayBE = [Memory.prototype.readByteBE, Memory.prototype.readShortBE, Memory.prototype.writeByteBE, Memory.prototype.writeShortBE];
|
|
Memory.afnArrayLE = [Memory.prototype.readByteLE, Memory.prototype.readShortLE, Memory.prototype.writeByteLE, Memory.prototype.writeShortLE];
|
|
}
|
|
|
|
|
|
// ./modules/pc8080/lib/cpu.js
|
|
|
|
/**
|
|
* @fileoverview Controls the PC8080 CPU component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* CPU(parmsCPU, nCyclesDefault)
|
|
*
|
|
* The CPU class supports the following (parmsCPU) properties:
|
|
*
|
|
* cycles: the machine's base cycles per second; the CPUState constructor will
|
|
* provide us with a default (based on the CPU model) to use as a fallback.
|
|
*
|
|
* multiplier: base cycle multiplier; default is 1.
|
|
*
|
|
* autoStart: true to automatically start, false to not, or null if "it depends";
|
|
* null is the default, which means do not autostart UNLESS there is no Debugger
|
|
* and no "Run" button (ie, no way to manually start the machine).
|
|
*
|
|
* csStart: the number of cycles that runCPU() must wait before generating
|
|
* checksum records; -1 if disabled. checksum records are a diagnostic aid
|
|
* used to help compare one CPU run to another.
|
|
*
|
|
* csInterval: the number of cycles that runCPU() must execute before
|
|
* generating a checksum record; -1 if disabled.
|
|
*
|
|
* csStop: the number of cycles to stop generating checksum records.
|
|
*
|
|
* This component is primarily responsible for interfacing the CPU with the outside
|
|
* world (eg, Panel and Debugger components), and managing overall CPU operation.
|
|
*
|
|
* It is extended by the CPUState component, where the simulation control logic resides.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsCPU
|
|
* @param {number} nCyclesDefault
|
|
*/
|
|
function CPU(parmsCPU, nCyclesDefault)
|
|
{
|
|
Component.call(this, "CPU", parmsCPU, CPU, Messages.CPU);
|
|
|
|
var nCycles = parmsCPU['cycles'] || nCyclesDefault;
|
|
|
|
var nMultiplier = parmsCPU['multiplier'] || 1;
|
|
|
|
this.aCounts = {};
|
|
this.aCounts.nCyclesPerSecond = nCycles;
|
|
this.aCounts.nVideoUpdates = 0;
|
|
|
|
/*
|
|
* nCyclesMultiplier replaces the old "speed" variable (0, 1, 2) and eliminates the need for
|
|
* the constants (SPEED_SLOW, SPEED_FAST and SPEED_MAX). The UI simply doubles the multiplier
|
|
* until we've exceeded the host's speed limit and then starts the multiplier over at 1.
|
|
*/
|
|
this.aCounts.nCyclesMultiplier = nMultiplier;
|
|
this.aCounts.mhzDefault = Math.round(this.aCounts.nCyclesPerSecond / 10000) / 100;
|
|
/*
|
|
* TODO: Take care of this with an initial setSpeed() call instead?
|
|
*/
|
|
this.aCounts.mhzTarget = this.aCounts.mhzDefault * this.aCounts.nCyclesMultiplier;
|
|
|
|
/*
|
|
* We add a number of flags to the set initialized by Component
|
|
*/
|
|
this.flags.fRunning = false;
|
|
this.flags.fStarting = false;
|
|
this.flags.fAutoStart = parmsCPU['autoStart'];
|
|
|
|
/*
|
|
* TODO: Add some UI for fDisplayLiveRegs (either an XML property, or a UI checkbox, or both)
|
|
*/
|
|
this.flags.fDisplayLiveRegs = false;
|
|
|
|
/*
|
|
* Get checksum parameters, if any. runCPU() behavior is not affected until fChecksum
|
|
* is true, which won't happen until resetChecksum() is called with nCyclesChecksumInterval
|
|
* ("csInterval") set to a positive value.
|
|
*
|
|
* As above, any of these parameters can also be set with the Debugger's execution options
|
|
* command ("x"); for example, "x cs int 5000" will set nCyclesChecksumInterval to 5000
|
|
* and call resetChecksum().
|
|
*/
|
|
this.flags.fChecksum = false;
|
|
this.aCounts.nChecksum = this.aCounts.nCyclesChecksumNext = 0;
|
|
this.aCounts.nCyclesChecksumStart = parmsCPU["csStart"];
|
|
this.aCounts.nCyclesChecksumInterval = parmsCPU["csInterval"];
|
|
this.aCounts.nCyclesChecksumStop = parmsCPU["csStop"];
|
|
|
|
this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); };
|
|
|
|
this.setReady();
|
|
}
|
|
|
|
Component.subclass(CPU);
|
|
|
|
/*
|
|
* Constants that control the frequency at which various updates should occur.
|
|
*
|
|
* These values do NOT control the simulation directly. Instead, they are used by
|
|
* calcCycles(), which uses the nCyclesPerSecond passed to the constructor as a starting
|
|
* point and computes the following variables:
|
|
*
|
|
* this.aCounts.nCyclesPerYield (this.aCounts.nCyclesPerSecond / CPU.YIELDS_PER_SECOND)
|
|
* this.aCounts.nCyclesPerVideoUpdate (this.aCounts.nCyclesPerSecond / CPU.VIDEO_UPDATES_PER_SECOND)
|
|
* this.aCounts.nCyclesPerStatusUpdate (this.aCounts.nCyclesPerSecond / CPU.STATUS_UPDATES_PER_SECOND)
|
|
*
|
|
* The above variables are also multiplied by any cycle multiplier in effect, via setSpeed(),
|
|
* and then they're used to initialize another set of variables for each runCPU() iteration:
|
|
*
|
|
* this.aCounts.nCyclesNextYield <= this.aCounts.nCyclesPerYield
|
|
* this.aCounts.nCyclesNextVideoUpdate <= this.aCounts.nCyclesPerVideoUpdate
|
|
* this.aCounts.nCyclesNextStatusUpdate <= this.aCounts.nCyclesPerStatusUpdate
|
|
*/
|
|
CPU.YIELDS_PER_SECOND = 30;
|
|
CPU.VIDEO_UPDATES_PER_SECOND = 60;
|
|
CPU.STATUS_UPDATES_PER_SECOND = 2;
|
|
|
|
CPU.BUTTONS = ["power", "reset"];
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {CPU}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPU} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
CPU.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.cmp = cmp;
|
|
this.bus = bus;
|
|
this.dbg = dbg;
|
|
|
|
for (var i = 0; i < CPU.BUTTONS.length; i++) {
|
|
var control = this.bindings[CPU.BUTTONS[i]];
|
|
if (control) this.cmp.setBinding(null, CPU.BUTTONS[i], control);
|
|
}
|
|
|
|
/*
|
|
* We need to know the refresh rate (and corresponding interrupt rate, if any) of the Video component.
|
|
*/
|
|
var video = cmp.getMachineComponent("Video");
|
|
this.refreshRate = video && video.getRefreshRate() || CPU.VIDEO_UPDATES_PER_SECOND;
|
|
|
|
/*
|
|
* Attach the ChipSet component to the CPU so that it can be notified whenever the CPU stops and starts.
|
|
*/
|
|
this.chipset = cmp.getMachineComponent("ChipSet");
|
|
|
|
/*
|
|
* We've already saved the parmsCPU 'autoStart' setting, but there may be a machine (or URL) override.
|
|
*/
|
|
var sAutoStart = cmp.getMachineParm('autoStart');
|
|
if (sAutoStart != null) {
|
|
this.flags.fAutoStart = (sAutoStart == "true"? true : (sAutoStart == "false"? false : !!sAutoStart));
|
|
}
|
|
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {CPU}
|
|
*/
|
|
CPU.prototype.reset = function()
|
|
{
|
|
this.aCounts.nVideoUpdates = 0;
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This is a placeholder for save support (overridden by the CPUState component).
|
|
*
|
|
* @this {CPU}
|
|
* @return {Object|null}
|
|
*/
|
|
CPU.prototype.save = function()
|
|
{
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This is a placeholder for restore support (overridden by the CPUState component).
|
|
*
|
|
* @this {CPU}
|
|
* @param {Object} data
|
|
* @return {boolean} true if restore successful, false if not
|
|
*/
|
|
CPU.prototype.restore = function(data)
|
|
{
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {CPU}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
CPU.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) {
|
|
if (!data || !this.restore) {
|
|
this.reset();
|
|
} else {
|
|
this.resetCycles();
|
|
if (!this.restore(data)) return false;
|
|
this.resetChecksum();
|
|
}
|
|
/*
|
|
* Give the Debugger a chance to do/print something once we've powered up
|
|
*/
|
|
if (DEBUGGER && this.dbg) {
|
|
this.dbg.init();
|
|
} else {
|
|
/*
|
|
* The Computer (this.cmp) knows if there's a Control Panel (this.cmp.panel), and the Control Panel
|
|
* knows if there's a "print" control (this.cmp.panel.controlPrint), and if there IS a "print" control
|
|
* but no debugger, the machine is probably misconfigured (most likely, the page simply neglected to
|
|
* load the Debugger component).
|
|
*
|
|
* However, we don't actually need to check all that; it's always safe use println(), regardless whether
|
|
* a Control Panel with a "print" control is present or not.
|
|
*/
|
|
this.println("No debugger detected");
|
|
}
|
|
}
|
|
/*
|
|
* The Computer component (which is responsible for all powerDown and powerUp notifications)
|
|
* is now responsible for managing a component's fPowered flag, not us.
|
|
*
|
|
* this.flags.fPowered = true;
|
|
*/
|
|
this.updateCPU();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
CPU.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
/*
|
|
* The Computer component (which is responsible for all powerDown and powerUp notifications)
|
|
* is now responsible for managing a component's fPowered flag, not us.
|
|
*
|
|
* this.flags.fPowered = false;
|
|
*/
|
|
return fSave? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* autoStart()
|
|
*
|
|
* @this {CPU}
|
|
* @return {boolean} true if started, false if not
|
|
*/
|
|
CPU.prototype.autoStart = function()
|
|
{
|
|
/*
|
|
* Start running automatically on power-up, assuming there's no Debugger and no "Run" button
|
|
*/
|
|
if (this.flags.fAutoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) {
|
|
/*
|
|
* Now we ALSO set fUpdateFocus when calling runCPU(), on the assumption that in the "auto-starting" context,
|
|
* a machine without focus is like a day without sunshine.
|
|
*/
|
|
this.runCPU(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* isPowered()
|
|
*
|
|
* @this {CPU}
|
|
* @return {boolean}
|
|
*/
|
|
CPU.prototype.isPowered = function()
|
|
{
|
|
if (!this.flags.fPowered) {
|
|
this.println(this.toString() + " not powered");
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* isRunning()
|
|
*
|
|
* @this {CPU}
|
|
* @return {boolean}
|
|
*/
|
|
CPU.prototype.isRunning = function()
|
|
{
|
|
return this.flags.fRunning;
|
|
};
|
|
|
|
/**
|
|
* getChecksum()
|
|
*
|
|
* This will be implemented by the CPUState component.
|
|
*
|
|
* @this {CPU}
|
|
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
|
|
*/
|
|
CPU.prototype.getChecksum = function()
|
|
{
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* resetChecksum()
|
|
*
|
|
* If checksum generation is enabled (fChecksum is true), this resets the running 32-bit checksum and the
|
|
* cycle counter that will trigger the next displayChecksum(); called by resetCycles(), which is called whenever
|
|
* the CPU is reset or restored.
|
|
*
|
|
* @this {CPU}
|
|
* @return {boolean} true if checksum generation enabled, false if not
|
|
*/
|
|
CPU.prototype.resetChecksum = function()
|
|
{
|
|
if (this.aCounts.nCyclesChecksumStart === undefined) this.aCounts.nCyclesChecksumStart = 0;
|
|
if (this.aCounts.nCyclesChecksumInterval === undefined) this.aCounts.nCyclesChecksumInterval = -1;
|
|
if (this.aCounts.nCyclesChecksumStop === undefined) this.aCounts.nCyclesChecksumStop = -1;
|
|
this.flags.fChecksum = (this.aCounts.nCyclesChecksumStart >= 0 && this.aCounts.nCyclesChecksumInterval > 0);
|
|
if (this.flags.fChecksum) {
|
|
this.aCounts.nChecksum = 0;
|
|
this.aCounts.nCyclesChecksumNext = this.aCounts.nCyclesChecksumStart - this.nTotalCycles;
|
|
/*
|
|
* this.aCounts.nCyclesChecksumNext = this.aCounts.nCyclesChecksumStart + this.aCounts.nCyclesChecksumInterval -
|
|
* (this.nTotalCycles % this.aCounts.nCyclesChecksumInterval);
|
|
*/
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* updateChecksum(nCycles)
|
|
*
|
|
* When checksum generation is enabled (fChecksum is true), runCPU() asks stepCPU() to execute a minimum
|
|
* number of cycles (1), effectively limiting execution to a single instruction, and then we're called with
|
|
* the exact number cycles that were actually executed. This should give us instruction-granular checksums
|
|
* at precise intervals that are 100% repeatable.
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} nCycles
|
|
*/
|
|
CPU.prototype.updateChecksum = function(nCycles)
|
|
{
|
|
if (this.flags.fChecksum) {
|
|
/*
|
|
* Get a 32-bit summation of the current CPU state and add it to our running 32-bit checksum
|
|
*/
|
|
var fDisplay = false;
|
|
this.aCounts.nChecksum = (this.aCounts.nChecksum + this.getChecksum())|0;
|
|
this.aCounts.nCyclesChecksumNext -= nCycles;
|
|
if (this.aCounts.nCyclesChecksumNext <= 0) {
|
|
this.aCounts.nCyclesChecksumNext += this.aCounts.nCyclesChecksumInterval;
|
|
fDisplay = true;
|
|
}
|
|
if (this.aCounts.nCyclesChecksumStop >= 0) {
|
|
if (this.aCounts.nCyclesChecksumStop <= this.getCycles()) {
|
|
this.aCounts.nCyclesChecksumInterval = this.aCounts.nCyclesChecksumStop = -1;
|
|
this.resetChecksum();
|
|
this.stopCPU();
|
|
fDisplay = true;
|
|
}
|
|
}
|
|
if (fDisplay) this.displayChecksum();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* displayChecksum()
|
|
*
|
|
* When checksum generation is enabled (fChecksum is true), this is called to provide a crude log of all
|
|
* checksums generated at the specified cycle intervals, as specified by the "csStart" and "csInterval" parmsCPU
|
|
* properties).
|
|
*
|
|
* @this {CPU}
|
|
*/
|
|
CPU.prototype.displayChecksum = function()
|
|
{
|
|
this.println(this.getCycles() + " cycles: " + "checksum=" + str.toHex(this.aCounts.nChecksum));
|
|
};
|
|
|
|
/**
|
|
* displayValue(sLabel, nValue, cch)
|
|
*
|
|
* This is principally for displaying register values, but in reality, it can be used to display any
|
|
* numeric (hex) value bound to the given label.
|
|
*
|
|
* @this {CPU}
|
|
* @param {string} sLabel
|
|
* @param {number} nValue
|
|
* @param {number} cch
|
|
*/
|
|
CPU.prototype.displayValue = function(sLabel, nValue, cch)
|
|
{
|
|
if (this.bindings[sLabel]) {
|
|
if (nValue === undefined) {
|
|
this.setError("Value for " + sLabel + " is invalid");
|
|
this.stopCPU();
|
|
}
|
|
var sVal;
|
|
if (!this.flags.fRunning || this.flags.fDisplayLiveRegs) {
|
|
sVal = str.toHex(nValue, cch);
|
|
} else {
|
|
sVal = "--------".substr(0, cch);
|
|
}
|
|
/*
|
|
* TODO: Determine if this test actually avoids any redrawing when a register hasn't changed, and/or if
|
|
* we should maintain our own (numeric) cache of displayed register values (to avoid creating these temporary
|
|
* string values that will have to garbage-collected), and/or if this is actually slower, and/or if I'm being
|
|
* too obsessive.
|
|
*/
|
|
if (this.bindings[sLabel].textContent != sVal) this.bindings[sLabel].textContent = sVal;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {CPU}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "run")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
CPU.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var cpu = this;
|
|
var fBound = false;
|
|
|
|
switch (sBinding) {
|
|
case "power":
|
|
case "reset":
|
|
/*
|
|
* The "power" and "reset" buttons are functions of the entire computer, not just the CPU,
|
|
* but it's not always convenient to stick a power button in the Computer component definition,
|
|
* so we record those bindings here and pass them on to the Computer component in initBus().
|
|
*/
|
|
this.bindings[sBinding] = control;
|
|
fBound = true;
|
|
break;
|
|
|
|
case "run":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickRun() {
|
|
if (!cpu.cmp || !cpu.cmp.checkPower()) return;
|
|
if (!cpu.flags.fRunning)
|
|
cpu.runCPU(true);
|
|
else
|
|
cpu.stopCPU(true);
|
|
};
|
|
fBound = true;
|
|
break;
|
|
|
|
case "speed":
|
|
this.bindings[sBinding] = control;
|
|
fBound = true;
|
|
break;
|
|
|
|
case "setSpeed":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickSetSpeed() {
|
|
cpu.setSpeed(cpu.aCounts.nCyclesMultiplier << 1, true);
|
|
};
|
|
control.textContent = this.getSpeedTarget();
|
|
fBound = true;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return fBound;
|
|
};
|
|
|
|
/**
|
|
* setBurstCycles(nCycles)
|
|
*
|
|
* This function is used by the ChipSet component whenever a very low timer count is set,
|
|
* in anticipation of the timer requiring an update sooner than the normal nCyclesPerYield
|
|
* period in runCPU() would normally provide.
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} nCycles is the target number of cycles to drop the current burst to
|
|
* @return {boolean}
|
|
*/
|
|
CPU.prototype.setBurstCycles = function(nCycles)
|
|
{
|
|
if (this.flags.fRunning) {
|
|
var nDelta = this.nStepCycles - nCycles;
|
|
/*
|
|
* NOTE: If nDelta is negative, we will actually be increasing nStepCycles and nBurstCycles.
|
|
* Which is OK, but if we're also taking snapshots of the cycle counts, to make sure that instruction
|
|
* costs are being properly assessed, then we need to update nSnapCycles as well.
|
|
*
|
|
* TODO: If the delta is negative, we could simply ignore the request, but we must first carefully
|
|
* consider the impact on the ChipSet timers, if any.
|
|
*/
|
|
// if (DEBUG) this.nSnapCycles -= nDelta;
|
|
this.nStepCycles -= nDelta;
|
|
this.nBurstCycles -= nDelta;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* addCycles(nCycles, fEndStep)
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} nCycles
|
|
* @param {boolean} [fEndStep]
|
|
*/
|
|
CPU.prototype.addCycles = function(nCycles, fEndStep)
|
|
{
|
|
this.nTotalCycles += nCycles;
|
|
if (fEndStep) {
|
|
this.nBurstCycles = this.nStepCycles = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* calcCycles(fRecalc)
|
|
*
|
|
* Calculate the number of cycles to process for each "burst" of CPU activity. The size of a burst
|
|
* is driven by the following values:
|
|
*
|
|
* CPU.YIELDS_PER_SECOND (eg, 30)
|
|
* CPU.VIDEO_UPDATES_PER_SECOND (eg, 60)
|
|
* CPU.STATUS_UPDATES_PER_SECOND (eg, 5)
|
|
*
|
|
* The largest of the above values forces the size of the burst to its smallest value. Let's say that
|
|
* largest value is 30. Assuming nCyclesPerSecond is 1,000,000, that results in bursts of 33,333 cycles.
|
|
*
|
|
* At the end of each burst, we subtract burst cycles from yield, video, and status cycle "threshold"
|
|
* counters. Whenever the "next yield" cycle counter goes to (or below) zero, we compare elapsed time
|
|
* to the time we expected the virtual hardware to take (eg, 1000ms/50 or 20ms), and if we still have time
|
|
* remaining, we sleep the remaining time (or 0ms if there's no remaining time), and then restart runCPU().
|
|
*
|
|
* Similarly, whenever the "next video update" cycle counter goes to (or below) zero, we call updateVideo(),
|
|
* and whenever the "next status update" cycle counter goes to (or below) zero, we call updateStatus().
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fRecalc] is true if the caller wants to recalculate thresholds based on the most recent
|
|
* speed calculation (see calcSpeed).
|
|
*/
|
|
CPU.prototype.calcCycles = function(fRecalc)
|
|
{
|
|
/*
|
|
* Calculate the most cycles we're allowed to execute in a single "burst"
|
|
*/
|
|
var nMostUpdatesPerSecond = CPU.YIELDS_PER_SECOND;
|
|
if (nMostUpdatesPerSecond < this.refreshRate) nMostUpdatesPerSecond = this.refreshRate;
|
|
if (nMostUpdatesPerSecond < CPU.STATUS_UPDATES_PER_SECOND) nMostUpdatesPerSecond = CPU.STATUS_UPDATES_PER_SECOND;
|
|
|
|
/*
|
|
* Calculate cycle "per" values for the yield, video update, and status update cycle counters
|
|
*/
|
|
var vMultiplier = 1;
|
|
if (fRecalc) {
|
|
if (this.aCounts.nCyclesMultiplier > 1 && this.aCounts.mhz) {
|
|
vMultiplier = (this.aCounts.mhz / this.aCounts.mhzDefault);
|
|
}
|
|
}
|
|
|
|
this.aCounts.msPerYield = Math.round(1000 / CPU.YIELDS_PER_SECOND);
|
|
this.aCounts.nCyclesPerBurst = Math.floor(this.aCounts.nCyclesPerSecond / nMostUpdatesPerSecond * vMultiplier);
|
|
this.aCounts.nCyclesPerYield = Math.floor(this.aCounts.nCyclesPerSecond / CPU.YIELDS_PER_SECOND * vMultiplier);
|
|
this.aCounts.nCyclesPerVideoUpdate = Math.floor(this.aCounts.nCyclesPerSecond / this.refreshRate * vMultiplier);
|
|
this.aCounts.nCyclesPerStatusUpdate = Math.floor(this.aCounts.nCyclesPerSecond / CPU.STATUS_UPDATES_PER_SECOND * vMultiplier);
|
|
|
|
/*
|
|
* And initialize "next" yield, video update, and status update cycle "threshold" counters to those "per" values
|
|
*/
|
|
if (!fRecalc) {
|
|
this.aCounts.nCyclesNextYield = this.aCounts.nCyclesPerYield;
|
|
this.aCounts.nCyclesNextVideoUpdate = this.aCounts.nCyclesPerVideoUpdate;
|
|
this.aCounts.nCyclesNextStatusUpdate = this.aCounts.nCyclesPerStatusUpdate;
|
|
}
|
|
this.aCounts.nCyclesRecalc = 0;
|
|
};
|
|
|
|
/**
|
|
* getCycles(fScaled)
|
|
*
|
|
* getCycles() returns the number of cycles executed so far. Note that we can be called after
|
|
* runCPU() OR during runCPU(), perhaps from a handler triggered during the current run's stepCPU(),
|
|
* so nRunCycles must always be adjusted by number of cycles stepCPU() was asked to run (nBurstCycles),
|
|
* less the number of cycles it has yet to run (nStepCycles).
|
|
*
|
|
* nRunCycles is zeroed whenever the CPU is halted or the CPU speed is changed, which is why we also
|
|
* have nTotalCycles, which accumulates all nRunCycles before we zero it. However, nRunCycles and
|
|
* nTotalCycles eventually get reset by calcSpeed(), to avoid overflow, so components that rely on
|
|
* getCycles() returning steadily increasing values should also be prepared for a reset at any time.
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fScaled] is true if the caller wants a cycle count relative to a multiplier of 1
|
|
* @return {number}
|
|
*/
|
|
CPU.prototype.getCycles = function(fScaled)
|
|
{
|
|
var nCycles = this.nTotalCycles + this.nRunCycles + this.nBurstCycles - this.nStepCycles;
|
|
if (fScaled && this.aCounts.nCyclesMultiplier > 1 && this.aCounts.mhz > this.aCounts.mhzDefault) {
|
|
/*
|
|
* We could scale the current cycle count by the current effective speed (this.aCounts.mhz); eg:
|
|
*
|
|
* nCycles = Math.round(nCycles / (this.aCounts.mhz / this.aCounts.mhzDefault));
|
|
*
|
|
* but that speed will fluctuate somewhat: large fluctuations at first, but increasingly smaller
|
|
* fluctuations after each burst of instructions that runCPU() executes.
|
|
*
|
|
* Alternatively, we can scale the cycle count by the multiplier, which is good in that the
|
|
* multiplier doesn't vary once the user changes it, but a potential downside is that the
|
|
* multiplier might be set too high, resulting in a target speed that's higher than the effective
|
|
* speed is able to reach.
|
|
*
|
|
* Also, if multipliers were always limited to a power-of-two, then this could be calculated
|
|
* with a simple shift. However, only the "setSpeed" UI binding limits it that way; the Debugger
|
|
* interface allows any value, as does the CPU "multiplier" parmsCPU property (from the machine's
|
|
* XML file).
|
|
*/
|
|
nCycles = Math.round(nCycles / this.aCounts.nCyclesMultiplier);
|
|
}
|
|
return nCycles;
|
|
};
|
|
|
|
/**
|
|
* getCyclesPerSecond()
|
|
*
|
|
* This returns the CPU's "base" speed (ie, the original cycles per second defined for the machine)
|
|
*
|
|
* @this {CPU}
|
|
* @return {number}
|
|
*/
|
|
CPU.prototype.getCyclesPerSecond = function()
|
|
{
|
|
return this.aCounts.nCyclesPerSecond;
|
|
};
|
|
|
|
/**
|
|
* resetCycles()
|
|
*
|
|
* Resets speed and cycle information as part of any reset() or restore(); this typically occurs during powerUp().
|
|
* It's important that this be called BEFORE the actual restore() call, because restore() may want to call setSpeed(),
|
|
* which in turn assumes that all the cycle counts have been initialized to sensible values.
|
|
*
|
|
* @this {CPU}
|
|
*/
|
|
CPU.prototype.resetCycles = function()
|
|
{
|
|
this.aCounts.mhz = 0;
|
|
this.nTotalCycles = this.nRunCycles = this.nBurstCycles = this.nStepCycles = 0;
|
|
this.resetChecksum();
|
|
this.setSpeed(1);
|
|
};
|
|
|
|
/**
|
|
* getSpeed()
|
|
*
|
|
* @this {CPU}
|
|
* @return {number} the current speed multiplier
|
|
*/
|
|
CPU.prototype.getSpeed = function()
|
|
{
|
|
return this.aCounts.nCyclesMultiplier;
|
|
};
|
|
|
|
/**
|
|
* getSpeedCurrent()
|
|
*
|
|
* @this {CPU}
|
|
* @return {string} the current speed, in mhz, as a string formatted to two decimal places
|
|
*/
|
|
CPU.prototype.getSpeedCurrent = function()
|
|
{
|
|
/*
|
|
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
|
|
*/
|
|
return ((this.flags.fRunning && this.aCounts.mhz)? (this.aCounts.mhz.toFixed(2) + "Mhz") : "Stopped");
|
|
};
|
|
|
|
/**
|
|
* getSpeedTarget()
|
|
*
|
|
* @this {CPU}
|
|
* @return {string} the target speed, in mhz, as a string formatted to two decimal places
|
|
*/
|
|
CPU.prototype.getSpeedTarget = function()
|
|
{
|
|
/*
|
|
* TODO: Has toFixed() been "fixed" in all browsers (eg, IE) to return a rounded value now?
|
|
*/
|
|
return this.aCounts.mhzTarget.toFixed(2) + "Mhz";
|
|
};
|
|
|
|
/**
|
|
* setSpeed(nMultiplier, fUpdateFocus)
|
|
*
|
|
* NOTE: This used to return the target speed, in mhz, but no callers appear to care at this point.
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} [nMultiplier] is the new proposed multiplier (reverts to 1 if the target was too high)
|
|
* @param {boolean} [fUpdateFocus] is true to update Computer focus
|
|
* @return {boolean} true if successful, false if not
|
|
*
|
|
* @desc Whenever the speed is changed, the running cycle count and corresponding start time must be reset,
|
|
* so that the next effective speed calculation obtains sensible results. In fact, when runCPU() initially calls
|
|
* setSpeed() with no parameters, that's all this function does (it doesn't change the current speed setting).
|
|
*/
|
|
CPU.prototype.setSpeed = function(nMultiplier, fUpdateFocus)
|
|
{
|
|
var fSuccess = false;
|
|
if (nMultiplier !== undefined) {
|
|
/*
|
|
* If we haven't reached 80% (0.8) of the current target speed, revert to a multiplier of one (1).
|
|
*/
|
|
if (this.aCounts.mhz / this.aCounts.mhzTarget < 0.8) {
|
|
nMultiplier = 1;
|
|
} else {
|
|
fSuccess = true;
|
|
}
|
|
this.aCounts.nCyclesMultiplier = nMultiplier;
|
|
var mhz = this.aCounts.mhzDefault * this.aCounts.nCyclesMultiplier;
|
|
if (this.aCounts.mhzTarget != mhz) {
|
|
this.aCounts.mhzTarget = mhz;
|
|
var sSpeed = this.getSpeedTarget();
|
|
var controlSpeed = this.bindings["setSpeed"];
|
|
if (controlSpeed) controlSpeed.textContent = sSpeed;
|
|
this.println("target speed: " + sSpeed);
|
|
}
|
|
if (fUpdateFocus && this.cmp) this.cmp.updateFocus();
|
|
}
|
|
this.addCycles(this.nRunCycles);
|
|
this.nRunCycles = 0;
|
|
this.aCounts.msStartRun = usr.getTime();
|
|
this.aCounts.msEndThisRun = 0;
|
|
this.calcCycles();
|
|
return fSuccess;
|
|
};
|
|
|
|
/**
|
|
* calcSpeed(nCycles, msElapsed)
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} nCycles
|
|
* @param {number} msElapsed
|
|
*/
|
|
CPU.prototype.calcSpeed = function(nCycles, msElapsed)
|
|
{
|
|
if (msElapsed) {
|
|
this.aCounts.mhz = Math.round(nCycles / (msElapsed * 10)) / 100;
|
|
if (msElapsed >= 86400000) {
|
|
this.nTotalCycles = 0;
|
|
this.setSpeed(); // reset all counters once per day so that we never have to worry about overflow
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* calcStartTime()
|
|
*
|
|
* @this {CPU}
|
|
*/
|
|
CPU.prototype.calcStartTime = function()
|
|
{
|
|
if (this.aCounts.nCyclesRecalc >= this.aCounts.nCyclesPerSecond) {
|
|
this.calcCycles(true);
|
|
}
|
|
this.aCounts.nCyclesThisRun = 0;
|
|
this.aCounts.msStartThisRun = usr.getTime();
|
|
|
|
/*
|
|
* Try to detect situations where the browser may have throttled us, such as when the user switches
|
|
* to a different tab; in those situations, Chrome and Safari may restrict setTimeout() callbacks
|
|
* to roughly one per second.
|
|
*
|
|
* Another scenario: the user resizes the browser window. setTimeout() callbacks are not throttled,
|
|
* but there can still be enough of a lag between the callbacks that CPU speed will be noticeably
|
|
* erratic if we don't compensate for it here.
|
|
*
|
|
* We can detect throttling/lagging by verifying that msEndThisRun (which was set at the end of the
|
|
* previous run and includes any requested sleep time) is comparable to the current msStartThisRun;
|
|
* if the delta is significant, we compensate by bumping msStartRun forward by that delta.
|
|
*
|
|
* This shouldn't be triggered when the Debugger halts the CPU, because setSpeed() -- which is called
|
|
* whenever the CPU starts running again -- zeroes msEndThisRun.
|
|
*
|
|
* This also won't do anything about other internal delays; for example, Debugger message() calls.
|
|
* By the time the message() function has called yieldCPU(), the cost of the message has already been
|
|
* incurred, so it will be end up being charged against the instruction(s) that triggered it.
|
|
*
|
|
* TODO: Consider calling yieldCPU() sooner from message(), so that it can arrange for the msEndThisRun
|
|
* "snapshot" to occur sooner; it's unclear, however, whether that will really improve the CPU's ability
|
|
* to hit its target speed, since you would expect any instruction that displays a message to be an
|
|
* EXTREMELY slow instruction.
|
|
*/
|
|
if (this.aCounts.msEndThisRun) {
|
|
var msDelta = this.aCounts.msStartThisRun - this.aCounts.msEndThisRun;
|
|
if (msDelta > this.aCounts.msPerYield) {
|
|
if (MAXDEBUG) this.println("large time delay: " + msDelta + "ms");
|
|
this.aCounts.msStartRun += msDelta;
|
|
/*
|
|
* Bumping msStartRun forward should NEVER cause it to exceed msStartThisRun; however, just
|
|
* in case, I make absolutely sure it cannot happen, since doing so could result in negative
|
|
* speed calculations.
|
|
*/
|
|
|
|
if (this.aCounts.msStartRun > this.aCounts.msStartThisRun) {
|
|
this.aCounts.msStartRun = this.aCounts.msStartThisRun;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* calcRemainingTime()
|
|
*
|
|
* @this {CPU}
|
|
* @return {number}
|
|
*/
|
|
CPU.prototype.calcRemainingTime = function()
|
|
{
|
|
this.aCounts.msEndThisRun = usr.getTime();
|
|
|
|
var msYield = this.aCounts.msPerYield;
|
|
if (this.aCounts.nCyclesThisRun) {
|
|
/*
|
|
* Normally, we would assume we executed a full quota of work over msPerYield, but since the CPU
|
|
* now has the option of calling yieldCPU(), that might not be true. If nCyclesThisRun is correct, then
|
|
* the ratio of nCyclesThisRun/nCyclesPerYield should represent the percentage of work we performed,
|
|
* and so applying that percentage to msPerYield should give us a better estimate of work vs. time.
|
|
*/
|
|
msYield = Math.round(msYield * this.aCounts.nCyclesThisRun / this.aCounts.nCyclesPerYield);
|
|
}
|
|
|
|
var msElapsedThisRun = this.aCounts.msEndThisRun - this.aCounts.msStartThisRun;
|
|
var msRemainsThisRun = msYield - msElapsedThisRun;
|
|
|
|
/*
|
|
* We could pass only "this run" results to calcSpeed():
|
|
*
|
|
* nCycles = this.aCounts.nCyclesThisRun;
|
|
* msElapsed = msElapsedThisRun;
|
|
*
|
|
* but it seems preferable to use longer time periods and hopefully get a more accurate speed.
|
|
*
|
|
* Also, if msRemainsThisRun >= 0 && this.aCounts.nCyclesMultiplier == 1, we could pass these results instead:
|
|
*
|
|
* nCycles = this.aCounts.nCyclesThisRun;
|
|
* msElapsed = this.aCounts.msPerYield;
|
|
*
|
|
* to insure that we display a smooth, constant N Mhz. But for now, I prefer seeing any fluctuations.
|
|
*/
|
|
var nCycles = this.nRunCycles;
|
|
var msElapsed = this.aCounts.msEndThisRun - this.aCounts.msStartRun;
|
|
|
|
if (MAXDEBUG && msRemainsThisRun < 0 && this.aCounts.nCyclesMultiplier > 1) {
|
|
this.println("warning: updates @" + msElapsedThisRun + "ms (prefer " + Math.round(msYield) + "ms)");
|
|
}
|
|
|
|
this.calcSpeed(nCycles, msElapsed);
|
|
|
|
if (msRemainsThisRun < 0 || this.aCounts.mhz < this.aCounts.mhzTarget) {
|
|
/*
|
|
* If the last burst took MORE time than we allotted (ie, it's taking more than 1 second to simulate
|
|
* nCyclesPerSecond), all we can do is yield for as little time as possible (ie, 0ms) and hope that the
|
|
* simulation is at least usable.
|
|
*/
|
|
msRemainsThisRun = 0;
|
|
}
|
|
|
|
/*
|
|
* Last but not least, update nCyclesRecalc, so that when runCPU() starts up again and calls calcStartTime(),
|
|
* it'll be ready to decide if calcCycles() should be called again.
|
|
*/
|
|
this.aCounts.nCyclesRecalc += this.aCounts.nCyclesThisRun;
|
|
|
|
if (DEBUG && this.messageEnabled(Messages.LOG) && msRemainsThisRun) {
|
|
this.log("calcRemainingTime: " + msRemainsThisRun + "ms to sleep after " + this.aCounts.msEndThisRun + "ms");
|
|
}
|
|
|
|
this.aCounts.msEndThisRun += msRemainsThisRun;
|
|
return msRemainsThisRun;
|
|
};
|
|
|
|
/**
|
|
* runCPU(fUpdateFocus)
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fUpdateFocus] is true to update Computer focus
|
|
*/
|
|
CPU.prototype.runCPU = function(fUpdateFocus)
|
|
{
|
|
if (!this.setBusy(true)) {
|
|
this.updateCPU();
|
|
if (this.cmp) this.cmp.stop(usr.getTime(), this.getCycles());
|
|
return;
|
|
}
|
|
|
|
this.startCPU(fUpdateFocus);
|
|
|
|
/*
|
|
* calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally
|
|
* recalculates the the maximum number of cycles for each burst if the nCyclesRecalc threshold has been reached.
|
|
*/
|
|
this.calcStartTime();
|
|
try {
|
|
do {
|
|
var nCyclesPerBurst = (this.flags.fChecksum? 1 : this.aCounts.nCyclesPerBurst);
|
|
|
|
/*
|
|
* nCyclesPerBurst is how many cycles we WANT to run on each iteration of stepCPU(), but it may run
|
|
* significantly less (or slightly more, since we can't execute partial instructions).
|
|
*/
|
|
this.stepCPU(nCyclesPerBurst);
|
|
|
|
/*
|
|
* nBurstCycles, less any remaining nStepCycles, is how many cycles stepCPU() ACTUALLY ran (nCycles).
|
|
* We add that to nCyclesThisRun, as well as nRunCycles, which is the cycle count since the CPU first
|
|
* started running.
|
|
*/
|
|
var nCycles = this.nBurstCycles - this.nStepCycles;
|
|
this.nRunCycles += nCycles;
|
|
this.aCounts.nCyclesThisRun += nCycles;
|
|
this.addCycles(0, true);
|
|
this.updateChecksum(nCycles);
|
|
|
|
this.aCounts.nCyclesNextVideoUpdate -= nCycles;
|
|
if (this.aCounts.nCyclesNextVideoUpdate <= 0) {
|
|
this.aCounts.nCyclesNextVideoUpdate += this.aCounts.nCyclesPerVideoUpdate;
|
|
if (this.cmp) this.cmp.updateVideo(this.aCounts.nVideoUpdates++);
|
|
if (this.aCounts.nVideoUpdates > this.refreshRate) this.aCounts.nVideoUpdates = 0;
|
|
}
|
|
|
|
this.aCounts.nCyclesNextStatusUpdate -= nCycles;
|
|
if (this.aCounts.nCyclesNextStatusUpdate <= 0) {
|
|
this.aCounts.nCyclesNextStatusUpdate += this.aCounts.nCyclesPerStatusUpdate;
|
|
if (this.cmp) this.cmp.updateStatus();
|
|
}
|
|
|
|
this.aCounts.nCyclesNextYield -= nCycles;
|
|
if (this.aCounts.nCyclesNextYield <= 0) {
|
|
this.aCounts.nCyclesNextYield += this.aCounts.nCyclesPerYield;
|
|
break;
|
|
}
|
|
} while (this.flags.fRunning);
|
|
}
|
|
catch (e) {
|
|
this.stopCPU();
|
|
this.updateCPU();
|
|
if (this.cmp) this.cmp.stop(usr.getTime(), this.getCycles());
|
|
this.setBusy(false);
|
|
this.setError(e.stack || e.message);
|
|
return;
|
|
}
|
|
setTimeout(this.onRunTimeout, this.calcRemainingTime());
|
|
};
|
|
|
|
/**
|
|
* startCPU(fUpdateFocus)
|
|
*
|
|
* WARNING: Other components must use runCPU() to get the CPU running; this is a runCPU() helper function only.
|
|
*
|
|
* @param {boolean} [fUpdateFocus]
|
|
*/
|
|
CPU.prototype.startCPU = function(fUpdateFocus)
|
|
{
|
|
if (!this.flags.fRunning) {
|
|
/*
|
|
* setSpeed() without a speed parameter leaves the selected speed in place, but also resets the
|
|
* cycle counter and timestamp for the current series of runCPU() calls, calculates the maximum number
|
|
* of cycles for each burst based on the last known effective CPU speed, and resets the nCyclesRecalc
|
|
* threshold counter.
|
|
*/
|
|
this.setSpeed();
|
|
if (this.cmp) this.cmp.start(this.aCounts.msStartRun, this.getCycles());
|
|
this.flags.fRunning = true;
|
|
this.flags.fStarting = true;
|
|
if (this.chipset) this.chipset.start();
|
|
var controlRun = this.bindings["run"];
|
|
if (controlRun) controlRun.textContent = "Halt";
|
|
if (this.cmp) {
|
|
this.cmp.updateStatus(true);
|
|
if (fUpdateFocus) this.cmp.updateFocus(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* stepCPU(nMinCycles)
|
|
*
|
|
* This will be implemented by the CPUState component.
|
|
*
|
|
* @this {CPU}
|
|
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
|
|
* @return {number} of cycles executed; 0 indicates that the last instruction was not executed
|
|
*/
|
|
CPU.prototype.stepCPU = function(nMinCycles)
|
|
{
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* stopCPU(fComplete)
|
|
*
|
|
* For use by any component that wants to stop the CPU.
|
|
*
|
|
* This similar to yieldCPU(), but it doesn't need to zero nCyclesNextYield to break out of runCPU();
|
|
* it simply needs to clear fRunning (well, "simply" may be oversimplifying a bit....)
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fComplete]
|
|
*/
|
|
CPU.prototype.stopCPU = function(fComplete)
|
|
{
|
|
this.isBusy(true);
|
|
this.nBurstCycles -= this.nStepCycles;
|
|
this.nStepCycles = 0;
|
|
this.addCycles(this.nRunCycles);
|
|
this.nRunCycles = 0;
|
|
if (this.flags.fRunning) {
|
|
this.flags.fRunning = false;
|
|
if (this.chipset) this.chipset.stop();
|
|
var controlRun = this.bindings["run"];
|
|
if (controlRun) controlRun.textContent = "Run";
|
|
}
|
|
this.flags.fComplete = fComplete;
|
|
};
|
|
|
|
/**
|
|
* updateCPU(fForce)
|
|
*
|
|
* This used to be performed at the end of every stepCPU(), but runCPU() -- which relies upon
|
|
* stepCPU() -- needed to have more control over when these updates are performed. However, for
|
|
* other callers of stepCPU(), such as the Debugger, the combination of stepCPU() + updateCPU()
|
|
* provides the old behavior.
|
|
*
|
|
* @this {CPU}
|
|
* @param {boolean} [fForce] (true to force a video update; used by the Debugger)
|
|
*/
|
|
CPU.prototype.updateCPU = function(fForce)
|
|
{
|
|
if (this.cmp) {
|
|
this.cmp.updateVideo(-1);
|
|
this.cmp.updateStatus(fForce);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* yieldCPU()
|
|
*
|
|
* Similar to stopCPU() with regard to how it resets various cycle countdown values, but the CPU
|
|
* remains in a "running" state.
|
|
*
|
|
* @this {CPU}
|
|
*/
|
|
CPU.prototype.yieldCPU = function()
|
|
{
|
|
this.aCounts.nCyclesNextYield = 0; // this will break us out of runCPU(), once we break out of stepCPU()
|
|
this.nBurstCycles -= this.nStepCycles;
|
|
this.nStepCycles = 0; // this will break us out of stepCPU()
|
|
// if (DEBUG) this.nSnapCycles = this.nBurstCycles;
|
|
/*
|
|
* The Debugger calls yieldCPU() after every message() to ensure browser responsiveness, but it looks
|
|
* odd for those messages to show CPU state changes but for the CPU's own status display to not (ditto
|
|
* for the Video display), so I've added this call to try to keep things looking synchronized.
|
|
*/
|
|
this.updateCPU();
|
|
};
|
|
|
|
|
|
// ./modules/pc8080/lib/cpustate.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 CPU component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* CPUState(parmsCPU)
|
|
*
|
|
* The CPUState class uses the following (parmsCPU) properties:
|
|
*
|
|
* model: a number (eg, 8080) that should match one of the CPUDef.MODEL_* values
|
|
*
|
|
* This extends the CPU class and passes any remaining parmsCPU properties to the CPU class
|
|
* constructor, along with a default speed (cycles per second) based on the specified (or default)
|
|
* CPU model number.
|
|
*
|
|
* The CPUState class was initially written to simulate a 8080 microprocessor, although over time
|
|
* it may evolved to support other microprocessors (eg, the Zilog Z80).
|
|
*
|
|
* @constructor
|
|
* @extends CPU
|
|
* @param {Object} parmsCPU
|
|
*/
|
|
function CPUState(parmsCPU)
|
|
{
|
|
this.model = +parmsCPU['model'] || CPUDef.MODEL_8080;
|
|
|
|
var nCyclesDefault = 0;
|
|
switch(this.model) {
|
|
case CPUDef.MODEL_8080:
|
|
default:
|
|
nCyclesDefault = 1000000;
|
|
break;
|
|
}
|
|
|
|
CPU.call(this, parmsCPU, nCyclesDefault);
|
|
|
|
/*
|
|
* Initialize processor operation to match the requested model
|
|
*/
|
|
this.initProcessor();
|
|
|
|
/*
|
|
* A variety of stepCPU() state variables that don't strictly need to be initialized before the first
|
|
* stepCPU() call, but it's good form to do so.
|
|
*/
|
|
this.resetCycles();
|
|
this.flags.fComplete = this.flags.fDebugCheck = false;
|
|
|
|
/*
|
|
* If there are no live registers to display, then updateStatus() can skip a bit....
|
|
*/
|
|
this.cLiveRegs = 0;
|
|
|
|
/*
|
|
* Array of halt handlers, if any (see addHaltCheck)
|
|
*/
|
|
this.afnHalt = [];
|
|
this.addrReset = 0x0000;
|
|
|
|
/*
|
|
* This initial resetRegs() call is important to create all the registers, so that if/when we call restore(),
|
|
* it will have something to fill in.
|
|
*/
|
|
this.resetRegs();
|
|
}
|
|
|
|
Component.subclass(CPUState, CPU);
|
|
|
|
/**
|
|
* addHaltCheck(fn)
|
|
*
|
|
* Records a function that will be called during HLT opcode processing.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {function(number)} fn
|
|
*/
|
|
CPUState.prototype.addHaltCheck = function(fn)
|
|
{
|
|
this.afnHalt.push(fn);
|
|
};
|
|
|
|
/**
|
|
* initProcessor()
|
|
*
|
|
* Interestingly, if I dynamically generate aOps as an array of functions bound to "this", using the bind()
|
|
* method, overall performance is worse. You would think that eliminating the need to use the call() method
|
|
* on every opcode function invocation would be helpful, but it's not. I'm not sure exactly why yet; perhaps
|
|
* a Closure Compiler optimization is defeated when generating the function array at run-time instead of at
|
|
* compile-time.
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.initProcessor = function()
|
|
{
|
|
this.aOps = CPUDef.aOps8080;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.reset = function()
|
|
{
|
|
if (this.flags.fRunning) this.stopCPU();
|
|
this.resetRegs();
|
|
this.resetCycles();
|
|
this.clearError(); // clear any fatal error/exception that setError() may have flagged
|
|
this.parent.reset.call(this);
|
|
};
|
|
|
|
/**
|
|
* resetRegs()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.resetRegs = function()
|
|
{
|
|
this.regA = 0;
|
|
this.regB = 0;
|
|
this.regC = 0;
|
|
this.regD = 0;
|
|
this.regE = 0;
|
|
this.regH = 0;
|
|
this.regL = 0;
|
|
this.setSP(0);
|
|
this.setPC(this.addrReset);
|
|
|
|
/*
|
|
* This resets the Processor Status flags (regPS), along with all the internal "result registers".
|
|
*/
|
|
this.setPS(0);
|
|
|
|
/*
|
|
* intFlags contains some internal states we use to indicate whether a hardware interrupt (INTFLAG.INTR) or
|
|
* Trap software interrupt (INTR.TRAP) has been requested, as well as when we're in a "HLT" state (INTFLAG.HALT)
|
|
* that requires us to wait for a hardware interrupt (INTFLAG.INTR) before continuing execution.
|
|
*/
|
|
this.intFlags = CPUDef.INTFLAG.NONE;
|
|
};
|
|
|
|
/**
|
|
* setReset(addr)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} addr
|
|
*/
|
|
CPUState.prototype.setReset = function(addr)
|
|
{
|
|
this.addrReset = addr;
|
|
this.setPC(addr);
|
|
};
|
|
|
|
/**
|
|
* getChecksum()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} a 32-bit summation of key elements of the current CPU state (used by the CPU checksum code)
|
|
*/
|
|
CPUState.prototype.getChecksum = function()
|
|
{
|
|
var sum = (this.regA + this.regB + this.regC + this.regD + this.regE + this.regH + this.regL)|0;
|
|
sum = (sum + this.getSP() + this.getPC() + this.getPS())|0;
|
|
return sum;
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements save support for the CPUState component.
|
|
*
|
|
* @this {CPUState}
|
|
* @return {Object|null}
|
|
*/
|
|
CPUState.prototype.save = function()
|
|
{
|
|
var state = new State(this);
|
|
state.set(0, [this.regA, this.regB, this.regC, this.regD, this.regE, this.regH, this.regL, this.getSP(), this.getPC(), this.getPS()]);
|
|
state.set(1, [this.intFlags, this.nTotalCycles, this.getSpeed()]);
|
|
state.set(2, this.bus.saveMemory());
|
|
return state.data();
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements restore support for the CPUState component.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {Object} data
|
|
* @return {boolean} true if restore successful, false if not
|
|
*/
|
|
CPUState.prototype.restore = function(data)
|
|
{
|
|
var a = data[0];
|
|
this.regA = a[0];
|
|
this.regB = a[1];
|
|
this.regC = a[2];
|
|
this.regD = a[3];
|
|
this.regE = a[4];
|
|
this.regH = a[5];
|
|
this.regL = a[6];
|
|
this.setSP(a[7]);
|
|
this.setPC(a[8]);
|
|
this.setPS(a[9]);
|
|
a = data[1];
|
|
this.intFlags = a[0];
|
|
this.nTotalCycles = a[1];
|
|
this.setSpeed(a[3]);
|
|
return this.bus.restoreMemory(data[2]);
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "AX")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
CPUState.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var fBound = false;
|
|
switch (sBinding) {
|
|
case "A":
|
|
case "B":
|
|
case "C":
|
|
case "BC":
|
|
case "D":
|
|
case "E":
|
|
case "DE":
|
|
case "H":
|
|
case "L":
|
|
case "HL":
|
|
case "SP":
|
|
case "PC":
|
|
case "PS":
|
|
case "IF":
|
|
case "SF":
|
|
case "ZF":
|
|
case "AF":
|
|
case "PF":
|
|
case "CF":
|
|
this.bindings[sBinding] = control;
|
|
this.cLiveRegs++;
|
|
fBound = true;
|
|
break;
|
|
default:
|
|
fBound = this.parent.setBinding.call(this, sHTMLType, sBinding, control);
|
|
break;
|
|
}
|
|
return fBound;
|
|
};
|
|
|
|
/**
|
|
* getBC()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getBC = function()
|
|
{
|
|
return (this.regB << 8) | this.regC;
|
|
};
|
|
|
|
/**
|
|
* setBC(w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} w
|
|
*/
|
|
CPUState.prototype.setBC = function(w)
|
|
{
|
|
this.regB = (w >> 8) & 0xff;
|
|
this.regC = w & 0xff;
|
|
};
|
|
|
|
/**
|
|
* getDE()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getDE = function()
|
|
{
|
|
return (this.regD << 8) | this.regE;
|
|
};
|
|
|
|
/**
|
|
* setDE(w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} w
|
|
*/
|
|
CPUState.prototype.setDE = function(w)
|
|
{
|
|
this.regD = (w >> 8) & 0xff;
|
|
this.regE = w & 0xff;
|
|
};
|
|
|
|
/**
|
|
* getHL()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getHL = function()
|
|
{
|
|
return (this.regH << 8) | this.regL;
|
|
};
|
|
|
|
/**
|
|
* setHL(w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} w
|
|
*/
|
|
CPUState.prototype.setHL = function(w)
|
|
{
|
|
this.regH = (w >> 8) & 0xff;
|
|
this.regL = w & 0xff;
|
|
};
|
|
|
|
/**
|
|
* getSP()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getSP = function()
|
|
{
|
|
return this.regSP;
|
|
};
|
|
|
|
/**
|
|
* setSP(off)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} off
|
|
*/
|
|
CPUState.prototype.setSP = function(off)
|
|
{
|
|
this.regSP = off & 0xffff;
|
|
};
|
|
|
|
/**
|
|
* getPC()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getPC = function()
|
|
{
|
|
return this.regPC;
|
|
};
|
|
|
|
/**
|
|
* offPC()
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} off
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.offPC = function(off)
|
|
{
|
|
return (this.regPC + off) & 0xffff;
|
|
};
|
|
|
|
/**
|
|
* setPC(off)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} off
|
|
*/
|
|
CPUState.prototype.setPC = function(off)
|
|
{
|
|
this.regPC = off & 0xffff;
|
|
};
|
|
|
|
/**
|
|
* clearCF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearCF = function()
|
|
{
|
|
this.resultZeroCarry &= 0xff;
|
|
};
|
|
|
|
/**
|
|
* getCF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or 1 (CPUDef.PS.CF)
|
|
*/
|
|
CPUState.prototype.getCF = function()
|
|
{
|
|
return (this.resultZeroCarry & 0x100)? CPUDef.PS.CF : 0;
|
|
};
|
|
|
|
/**
|
|
* setCF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setCF = function()
|
|
{
|
|
this.resultZeroCarry |= 0x100;
|
|
};
|
|
|
|
/**
|
|
* updateCF(CF)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} CF (0x000 or 0x100)
|
|
*/
|
|
CPUState.prototype.updateCF = function(CF)
|
|
{
|
|
this.resultZeroCarry = (this.resultZeroCarry & 0xff) | CF;
|
|
};
|
|
|
|
/**
|
|
* clearPF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearPF = function()
|
|
{
|
|
if (this.getPF()) this.resultParitySign ^= 0x1;
|
|
};
|
|
|
|
/**
|
|
* getPF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or CPUDef.PS.PF
|
|
*/
|
|
CPUState.prototype.getPF = function()
|
|
{
|
|
return (CPUDef.PARITY[this.resultParitySign & 0xff])? CPUDef.PS.PF : 0;
|
|
};
|
|
|
|
/**
|
|
* setPF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setPF = function()
|
|
{
|
|
if (!this.getPF()) this.resultParitySign ^= 0x1;
|
|
};
|
|
|
|
/**
|
|
* clearAF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearAF = function()
|
|
{
|
|
this.resultAuxOverflow = (this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10);
|
|
};
|
|
|
|
/**
|
|
* getAF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or CPUDef.PS.AF
|
|
*/
|
|
CPUState.prototype.getAF = function()
|
|
{
|
|
return ((this.resultParitySign ^ this.resultAuxOverflow) & 0x10)? CPUDef.PS.AF : 0;
|
|
};
|
|
|
|
/**
|
|
* setAF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setAF = function()
|
|
{
|
|
this.resultAuxOverflow = (~this.resultParitySign & 0x10) | (this.resultAuxOverflow & ~0x10);
|
|
};
|
|
|
|
/**
|
|
* clearZF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearZF = function()
|
|
{
|
|
this.resultZeroCarry |= 0xff;
|
|
};
|
|
|
|
/**
|
|
* getZF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or CPUDef.PS.ZF
|
|
*/
|
|
CPUState.prototype.getZF = function()
|
|
{
|
|
return (this.resultZeroCarry & 0xff)? 0 : CPUDef.PS.ZF;
|
|
};
|
|
|
|
/**
|
|
* setZF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setZF = function()
|
|
{
|
|
this.resultZeroCarry &= ~0xff;
|
|
};
|
|
|
|
/**
|
|
* clearSF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearSF = function()
|
|
{
|
|
if (this.getSF()) this.resultParitySign ^= 0xc0;
|
|
};
|
|
|
|
/**
|
|
* getSF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or CPUDef.PS.SF
|
|
*/
|
|
CPUState.prototype.getSF = function()
|
|
{
|
|
return (this.resultParitySign & 0x80)? CPUDef.PS.SF : 0;
|
|
};
|
|
|
|
/**
|
|
* setSF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setSF = function()
|
|
{
|
|
if (!this.getSF()) this.resultParitySign ^= 0xc0;
|
|
};
|
|
|
|
/**
|
|
* clearIF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearIF = function()
|
|
{
|
|
this.regPS &= ~CPUDef.PS.IF;
|
|
};
|
|
|
|
/**
|
|
* getIF()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} 0 or CPUDef.PS.IF
|
|
*/
|
|
CPUState.prototype.getIF = function()
|
|
{
|
|
return (this.regPS & CPUDef.PS.IF);
|
|
};
|
|
|
|
/**
|
|
* setIF()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.setIF = function()
|
|
{
|
|
this.regPS |= CPUDef.PS.IF;
|
|
};
|
|
|
|
/**
|
|
* getPS()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getPS = function()
|
|
{
|
|
return (this.regPS & ~CPUDef.PS.RESULT) | (this.getSF() | this.getZF() | this.getAF() | this.getPF() | this.getCF());
|
|
};
|
|
|
|
/**
|
|
* setPS(regPS)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} regPS
|
|
*/
|
|
CPUState.prototype.setPS = function(regPS)
|
|
{
|
|
this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = 0;
|
|
if (regPS & CPUDef.PS.CF) this.resultZeroCarry |= 0x100;
|
|
if (!(regPS & CPUDef.PS.PF)) this.resultParitySign |= 0x01;
|
|
if (regPS & CPUDef.PS.AF) this.resultAuxOverflow |= 0x10;
|
|
if (!(regPS & CPUDef.PS.ZF)) this.resultZeroCarry |= 0xff;
|
|
if (regPS & CPUDef.PS.SF) this.resultParitySign ^= 0xc0;
|
|
this.regPS = (this.regPS & ~(CPUDef.PS.RESULT | CPUDef.PS.INTERNAL)) | (regPS & CPUDef.PS.INTERNAL) | CPUDef.PS.SET;
|
|
|
|
};
|
|
|
|
/**
|
|
* getPSW()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.getPSW = function()
|
|
{
|
|
return (this.getPS() & CPUDef.PS.MASK) | (this.regA << 8);
|
|
};
|
|
|
|
/**
|
|
* setPSW(w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} w
|
|
*/
|
|
CPUState.prototype.setPSW = function(w)
|
|
{
|
|
this.setPS((w & CPUDef.PS.MASK) | (this.regPS & ~CPUDef.PS.MASK));
|
|
this.regA = w >> 8;
|
|
};
|
|
|
|
/**
|
|
* addByte(src)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA + src
|
|
*/
|
|
CPUState.prototype.addByte = function(src)
|
|
{
|
|
this.resultAuxOverflow = this.regA ^ src;
|
|
return this.resultParitySign = (this.resultZeroCarry = this.regA + src) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* addByteCarry(src)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA + src + carry
|
|
*/
|
|
CPUState.prototype.addByteCarry = function(src)
|
|
{
|
|
this.resultAuxOverflow = this.regA ^ src;
|
|
return this.resultParitySign = (this.resultZeroCarry = this.regA + src + ((this.resultZeroCarry & 0x100)? 1 : 0)) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* andByte(src)
|
|
*
|
|
* Ordinarily, one would expect the Auxiliary Carry flag (AF) to be clear after this operation,
|
|
* but apparently the 8080 will set AF if bit 3 in either operand is set.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA & src
|
|
*/
|
|
CPUState.prototype.andByte = function(src)
|
|
{
|
|
this.resultZeroCarry = this.resultParitySign = this.resultAuxOverflow = this.regA & src;
|
|
if ((this.regA | src) & 0x8) this.resultAuxOverflow ^= 0x10; // set AF by inverting bit 4 in resultAuxOverflow
|
|
return this.resultZeroCarry;
|
|
};
|
|
|
|
/**
|
|
* decByte(b)
|
|
*
|
|
* We perform this operation using 8-bit two's complement arithmetic, by negating and then adding
|
|
* the implied src of 1. This appears to mimic how the 8080 manages the Auxiliary Carry flag (AF).
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} b
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.decByte = function(b)
|
|
{
|
|
this.resultAuxOverflow = b ^ 0xff;
|
|
b = this.resultParitySign = (b + 0xff) & 0xff;
|
|
this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b;
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* incByte(b)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} b
|
|
* @return {number}
|
|
*/
|
|
CPUState.prototype.incByte = function(b)
|
|
{
|
|
this.resultAuxOverflow = b;
|
|
b = this.resultParitySign = (b + 1) & 0xff;
|
|
this.resultZeroCarry = (this.resultZeroCarry & ~0xff) | b;
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* orByte(src)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA | src
|
|
*/
|
|
CPUState.prototype.orByte = function(src)
|
|
{
|
|
return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA | src;
|
|
};
|
|
|
|
/**
|
|
* subByte(src)
|
|
*
|
|
* We perform this operation using 8-bit two's complement arithmetic, by inverting src, adding
|
|
* src + 1, and then inverting the resulting carry (resultZeroCarry ^ 0x100). This appears to mimic
|
|
* how the 8080 manages the Auxiliary Carry flag (AF).
|
|
*
|
|
* This function is also used as a cmpByte() function; compare instructions simply ignore the
|
|
* return value.
|
|
*
|
|
* Example: A=66, SUI $10
|
|
*
|
|
* If we created the two's complement of 0x10 by negating it, there would just be one addition:
|
|
*
|
|
* 0110 0110 (0x66)
|
|
* + 1111 0000 (0xF0) (ie, -0x10)
|
|
* ---------
|
|
* 1 0101 0110 (0x56)
|
|
*
|
|
* But in order to mimic the 8080's AF flag, we must perform the two's complement of src in two steps,
|
|
* inverting it before the add, and then incrementing after the add; eg:
|
|
*
|
|
* 0110 0110 (0x66)
|
|
* + 1110 1111 (0xEF) (ie, ~0x10)
|
|
* ---------
|
|
* 1 0101 0101 (0x55)
|
|
* + 0000 0001 (0x01)
|
|
* ---------
|
|
* 1 0101 0110 (0x56)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA - src
|
|
*/
|
|
CPUState.prototype.subByte = function(src)
|
|
{
|
|
src ^= 0xff;
|
|
this.resultAuxOverflow = this.regA ^ src;
|
|
return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + 1) ^ 0x100) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* subByteBorrow(src)
|
|
*
|
|
* We perform this operation using 8-bit two's complement arithmetic, using logic similar to subByte(),
|
|
* but changing the final increment to a conditional increment, because if the Carry flag (CF) is set, then
|
|
* we don't need to perform the increment at all.
|
|
*
|
|
* This mimics the behavior of subByte() when the Carry flag (CF) is clear, and hopefully also mimics how the
|
|
* 8080 manages the Auxiliary Carry flag (AF) when the Carry flag (CF) is set.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA - src - carry
|
|
*/
|
|
CPUState.prototype.subByteBorrow = function(src)
|
|
{
|
|
src ^= 0xff;
|
|
this.resultAuxOverflow = this.regA ^ src;
|
|
return this.resultParitySign = (this.resultZeroCarry = (this.regA + src + ((this.resultZeroCarry & 0x100)? 0 : 1)) ^ 0x100) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* xorByte(src)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} src
|
|
* @return {number} regA ^ src
|
|
*/
|
|
CPUState.prototype.xorByte = function(src)
|
|
{
|
|
return this.resultParitySign = this.resultZeroCarry = this.resultAuxOverflow = this.regA ^ src;
|
|
};
|
|
|
|
/**
|
|
* getByte(addr)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} addr is a linear address
|
|
* @return {number} byte (8-bit) value at that address
|
|
*/
|
|
CPUState.prototype.getByte = function(addr)
|
|
{
|
|
return this.bus.getByte(addr);
|
|
};
|
|
|
|
/**
|
|
* getWord(addr)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} addr is a linear address
|
|
* @return {number} word (16-bit) value at that address
|
|
*/
|
|
CPUState.prototype.getWord = function(addr)
|
|
{
|
|
return this.bus.getShort(addr);
|
|
};
|
|
|
|
/**
|
|
* setByte(addr, b)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} addr is a linear address
|
|
* @param {number} b is the byte (8-bit) value to write (which we truncate to 8 bits; required by opSTOSb)
|
|
*/
|
|
CPUState.prototype.setByte = function(addr, b)
|
|
{
|
|
this.bus.setByte(addr, b);
|
|
};
|
|
|
|
/**
|
|
* setWord(addr, w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} addr is a linear address
|
|
* @param {number} w is the word (16-bit) value to write (which we truncate to 16 bits to be safe)
|
|
*/
|
|
CPUState.prototype.setWord = function(addr, w)
|
|
{
|
|
this.bus.setShort(addr, w);
|
|
};
|
|
|
|
/**
|
|
* getPCByte()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} byte at the current PC; PC advanced by 1
|
|
*/
|
|
CPUState.prototype.getPCByte = function()
|
|
{
|
|
var b = this.getByte(this.regPC);
|
|
this.setPC(this.regPC + 1);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* getPCWord()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} word at the current PC; PC advanced by 2
|
|
*/
|
|
CPUState.prototype.getPCWord = function()
|
|
{
|
|
var w = this.getWord(this.regPC);
|
|
this.setPC(this.regPC + 2);
|
|
return w;
|
|
};
|
|
|
|
/**
|
|
* popWord()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {number} word popped from the current SP; SP increased by 2
|
|
*/
|
|
CPUState.prototype.popWord = function()
|
|
{
|
|
var w = this.getWord(this.regSP);
|
|
this.setSP(this.regSP + 2);
|
|
return w;
|
|
};
|
|
|
|
/**
|
|
* pushWord(w)
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} w is the word (16-bit) value to push at current SP; SP decreased by 2
|
|
*/
|
|
CPUState.prototype.pushWord = function(w)
|
|
{
|
|
this.setSP(this.regSP - 2);
|
|
this.setWord(this.regSP, w);
|
|
};
|
|
|
|
/**
|
|
* checkINTR()
|
|
*
|
|
* @this {CPUState}
|
|
* @return {boolean} true if h/w interrupt has just been acknowledged, false if not
|
|
*/
|
|
CPUState.prototype.checkINTR = function()
|
|
{
|
|
if ((this.intFlags & CPUDef.INTFLAG.INTR) && this.getIF()) {
|
|
var bRST = CPUDef.OPCODE.RST0 | ((this.intFlags & CPUDef.INTFLAG.INTL) << 3);
|
|
this.clearINTR();
|
|
this.clearIF();
|
|
this.aOps[bRST].call(this);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* clearINTR()
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUState.prototype.clearINTR = function()
|
|
{
|
|
this.intFlags &= ~(CPUDef.INTFLAG.INTL | CPUDef.INTFLAG.INTR);
|
|
};
|
|
|
|
/**
|
|
* requestINTR(nLevel)
|
|
*
|
|
* This is called by any component that wants to request a h/w interrupt.
|
|
*
|
|
* NOTE: We allow INTR to be set regardless of the current state of interrupt flag (IF), on the theory
|
|
* that if/when the CPU briefly turns interrupts off, it shouldn't lose the last h/w interrupt requested.
|
|
* So instead of ignoring INTR here, checkINTR() ignores INTR as long as the interrupt flag (IF) is clear.
|
|
*
|
|
* The downside is that, as long as the CPU has interrupts disabled, an active INTR state will slow stepCPU()
|
|
* down slightly. We could avoid that by introducing a two-stage interrupt tracking system, where a separate
|
|
* variable keeps track of the last interrupt requested whenever the interrupt flag (IF) is clear, and when
|
|
* setIF() finally occurs, that interrupt is propagated to intFlags. But for now, we're going to assume that
|
|
* scenario is rare.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} nLevel (0-7)
|
|
*/
|
|
CPUState.prototype.requestINTR = function(nLevel)
|
|
{
|
|
this.intFlags = (this.intFlags & ~CPUDef.INTFLAG.INTL) | nLevel | CPUDef.INTFLAG.INTR;
|
|
};
|
|
|
|
/**
|
|
* updateReg(sReg, nValue, cch)
|
|
*
|
|
* This function helps updateStatus() by massaging the register names and values according to
|
|
* CPU type before passing the call to displayValue(); in the "old days", updateStatus() called
|
|
* displayValue() directly (although then it was called displayReg()).
|
|
*
|
|
* @this {CPUState}
|
|
* @param {string} sReg
|
|
* @param {number} nValue
|
|
* @param {number} [cch] (default is 2 hex digits)
|
|
*/
|
|
CPUState.prototype.updateReg = function(sReg, nValue, cch)
|
|
{
|
|
this.displayValue(sReg, nValue, cch || 2);
|
|
};
|
|
|
|
/**
|
|
* updateStatus(fForce)
|
|
*
|
|
* This provides periodic Control Panel updates (eg, a few times per second; see STATUS_UPDATES_PER_SECOND).
|
|
* this is where we take care of any DOM updates (eg, register values) while the CPU is running.
|
|
*
|
|
* Any high-frequency updates should be performed in updateVideo(), which should avoid DOM updates, since updateVideo()
|
|
* can be called up to 60 times per second (see VIDEO_UPDATES_PER_SECOND).
|
|
*
|
|
* @this {CPUState}
|
|
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
|
|
*/
|
|
CPUState.prototype.updateStatus = function(fForce)
|
|
{
|
|
if (this.cLiveRegs) {
|
|
if (fForce || !this.flags.fRunning || this.flags.fDisplayLiveRegs) {
|
|
this.updateReg("A", this.regA);
|
|
this.updateReg("B", this.regB);
|
|
this.updateReg("C", this.regC);
|
|
this.updateReg("BC", this.getBC(), 4);
|
|
this.updateReg("D", this.regD);
|
|
this.updateReg("E", this.regE);
|
|
this.updateReg("DE", this.getDE(), 4);
|
|
this.updateReg("H", this.regH);
|
|
this.updateReg("L", this.regL);
|
|
this.updateReg("HL", this.getHL(), 4);
|
|
this.updateReg("SP", this.getSP(), 4);
|
|
this.updateReg("PC", this.getPC(), 4);
|
|
var regPS = this.getPS();
|
|
this.updateReg("PS", regPS, 4);
|
|
this.updateReg("IF", (regPS & CPUDef.PS.IF)? 1 : 0, 1);
|
|
this.updateReg("SF", (regPS & CPUDef.PS.SF)? 1 : 0, 1);
|
|
this.updateReg("ZF", (regPS & CPUDef.PS.ZF)? 1 : 0, 1);
|
|
this.updateReg("AF", (regPS & CPUDef.PS.AF)? 1 : 0, 1);
|
|
this.updateReg("PF", (regPS & CPUDef.PS.PF)? 1 : 0, 1);
|
|
this.updateReg("CF", (regPS & CPUDef.PS.CF)? 1 : 0, 1);
|
|
}
|
|
}
|
|
var controlSpeed = this.bindings["speed"];
|
|
if (controlSpeed) controlSpeed.textContent = this.getSpeedCurrent();
|
|
};
|
|
|
|
/**
|
|
* stepCPU(nMinCycles)
|
|
*
|
|
* NOTE: Single-stepping should not be confused with the Trap flag; single-stepping is a Debugger
|
|
* operation that's completely independent of Trap status. The CPU can go in and out of Trap mode,
|
|
* in and out of h/w interrupt service routines (ISRs), etc, but from the Debugger's perspective,
|
|
* they're all one continuous stream of instructions that can be stepped or run at will. Moreover,
|
|
* stepping vs. running should never change the behavior of the simulation.
|
|
*
|
|
* As a result, the Debugger's complete independence means you can run other 8086/8088 debuggers
|
|
* (eg, DEBUG) inside the simulation without interference; you can even "debug" them with the Debugger.
|
|
*
|
|
* @this {CPUState}
|
|
* @param {number} nMinCycles (0 implies a single-step, and therefore breakpoints should be ignored)
|
|
* @return {number} of cycles executed; 0 indicates a pre-execution condition (ie, an execution breakpoint
|
|
* was hit), -1 indicates a post-execution condition (eg, a read or write breakpoint was hit), and a positive
|
|
* number indicates successful completion of that many cycles (which should always be >= nMinCycles).
|
|
*/
|
|
CPUState.prototype.stepCPU = function(nMinCycles)
|
|
{
|
|
/*
|
|
* The Debugger uses fComplete to determine if the instruction completed (true) or was interrupted
|
|
* by a breakpoint or some other exceptional condition (false). NOTE: this does NOT include JavaScript
|
|
* exceptions, which stepCPU() expects the caller to catch using its own exception handler.
|
|
*
|
|
* The CPU relies on the use of stopCPU() rather than fComplete, because the CPU never single-steps
|
|
* (ie, nMinCycles is always some large number), whereas the Debugger does. And conversely, when the
|
|
* Debugger is single-stepping (even when performing multiple single-steps), fRunning is never set,
|
|
* so stopCPU() would have no effect as far as the Debugger is concerned.
|
|
*/
|
|
this.flags.fComplete = true;
|
|
|
|
/*
|
|
* fDebugCheck is true if we need to "check" every instruction with the Debugger.
|
|
*/
|
|
var fDebugCheck = this.flags.fDebugCheck = (DEBUGGER && this.dbg && this.dbg.checksEnabled());
|
|
|
|
/*
|
|
* nDebugState is checked only when fDebugCheck is true, and its sole purpose is to tell the first call
|
|
* to checkInstruction() that it can skip breakpoint checks, and that will be true ONLY when fStarting is
|
|
* true OR nMinCycles is zero (the latter means the Debugger is single-stepping).
|
|
*
|
|
* Once we snap fStarting, we clear it, because technically, we've moved beyond "starting" and have
|
|
* officially "started" now.
|
|
*/
|
|
var nDebugState = (!nMinCycles)? -1 : (this.flags.fStarting? 0 : 1);
|
|
this.flags.fStarting = false;
|
|
|
|
/*
|
|
* We move the minimum cycle count to nStepCycles (the number of cycles left to step), so that other
|
|
* functions have the ability to force that number to zero (eg, stopCPU()), and thus we don't have to check
|
|
* any other criteria to determine whether we should continue stepping or not.
|
|
*/
|
|
this.nBurstCycles = this.nStepCycles = nMinCycles;
|
|
|
|
do {
|
|
if (this.intFlags) {
|
|
if (this.checkINTR()) {
|
|
if (!nMinCycles) {
|
|
|
|
if (DEBUGGER) {
|
|
this.println("interrupt dispatched");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (this.intFlags & CPUDef.INTFLAG.HALT) {
|
|
/*
|
|
* As discussed in opHLT(), the CPU is never REALLY halted by a HLT instruction; instead,
|
|
* opHLT() sets CPUDef.INTFLAG.HALT, signalling to us that we're free to end the current burst
|
|
* AND that we should not execute any more instructions until checkINTR() indicates a hardware
|
|
* interrupt has been requested.
|
|
*
|
|
* One downside to this approach is that it *might* appear to the careful observer that we
|
|
* executed a full complement of instructions during bursts where CPUDef.INTFLAG.HALT was set,
|
|
* when in fact we did not. However, the steady advance of the overall cycle count, and thus
|
|
* the steady series calls to stepCPU(), is needed to ensure that timer updates, video updates,
|
|
* etc, all continue to occur at the expected rates.
|
|
*
|
|
* If necessary, we can add another bookkeeping cycle counter (eg, one that keeps tracks of the
|
|
* number of cycles during which we did not actually execute any instructions).
|
|
*/
|
|
this.nStepCycles = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (DEBUGGER && fDebugCheck) {
|
|
if (this.dbg.checkInstruction(this.regPC, nDebugState)) {
|
|
this.stopCPU();
|
|
break;
|
|
}
|
|
nDebugState = 1;
|
|
}
|
|
|
|
this.aOps[this.getPCByte()].call(this);
|
|
|
|
} while (this.nStepCycles > 0);
|
|
|
|
return (this.flags.fComplete? this.nBurstCycles - this.nStepCycles : (this.flags.fComplete === undefined? 0 : -1));
|
|
};
|
|
|
|
/**
|
|
* CPUState.init()
|
|
*
|
|
* This function operates on every HTML element of class "cpu", extracting the
|
|
* JSON-encoded parameters for the CPUState constructor from the element's "data-value"
|
|
* attribute, invoking the constructor (which in turn invokes the CPU constructor)
|
|
* to create a CPUState component, and then binding any associated HTML controls to the
|
|
* new component.
|
|
*/
|
|
CPUState.init = function()
|
|
{
|
|
var aeCPUs = Component.getElementsByClass(document, PC8080.APPCLASS, "cpu");
|
|
for (var iCPU = 0; iCPU < aeCPUs.length; iCPU++) {
|
|
var eCPU = aeCPUs[iCPU];
|
|
var parmsCPU = Component.getComponentParms(eCPU);
|
|
var cpu = new CPUState(parmsCPU);
|
|
Component.bindComponentControls(cpu, eCPU, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every CPU module on the page
|
|
*/
|
|
web.onInit(CPUState.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/cpuops.js
|
|
|
|
/**
|
|
* @fileoverview Implements PC8080 opcode handlers.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-20
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* op=0x00 (NOP)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opNOP = function()
|
|
{
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x01 (LXI B,d16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLXIB = function()
|
|
{
|
|
this.setBC(this.getPCWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x02 (STAX B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSTAXB = function()
|
|
{
|
|
this.setByte(this.getBC(), this.regA);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x03 (INX B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINXB = function()
|
|
{
|
|
this.setBC(this.getBC() + 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x04 (INR B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRB = function()
|
|
{
|
|
this.regB = this.incByte(this.regB);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x05 (DCR B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRB = function()
|
|
{
|
|
this.regB = this.decByte(this.regB);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x06 (MVI B,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIB = function()
|
|
{
|
|
this.regB = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x07 (RLC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opDADB = function()
|
|
{
|
|
var w;
|
|
this.setHL(w = this.getHL() + this.getBC());
|
|
this.updateCF((w >> 8) & 0x100);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x0A (LDAX B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLDAXB = function()
|
|
{
|
|
this.regA = this.getByte(this.getBC());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x0B (DCX B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCXB = function()
|
|
{
|
|
this.setBC(this.getBC() - 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x0C (INR C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRC = function()
|
|
{
|
|
this.regC = this.incByte(this.regC);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x0D (DCR C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRC = function()
|
|
{
|
|
this.regC = this.decByte(this.regC);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x0E (MVI C,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIC = function()
|
|
{
|
|
this.regC = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x0F (RRC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opLXID = function()
|
|
{
|
|
this.setDE(this.getPCWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x12 (STAX D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSTAXD = function()
|
|
{
|
|
this.setByte(this.getDE(), this.regA);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x13 (INX D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINXD = function()
|
|
{
|
|
this.setDE(this.getDE() + 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x14 (INR D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRD = function()
|
|
{
|
|
this.regD = this.incByte(this.regD);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x15 (DCR D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRD = function()
|
|
{
|
|
this.regD = this.decByte(this.regD);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x16 (MVI D,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVID = function()
|
|
{
|
|
this.regD = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x17 (RAL)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opDADD = function()
|
|
{
|
|
var w;
|
|
this.setHL(w = this.getHL() + this.getDE());
|
|
this.updateCF((w >> 8) & 0x100);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x1A (LDAX D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLDAXD = function()
|
|
{
|
|
this.regA = this.getByte(this.getDE());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x1B (DCX D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCXD = function()
|
|
{
|
|
this.setDE(this.getDE() - 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x1C (INR E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRE = function()
|
|
{
|
|
this.regE = this.incByte(this.regE);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x1D (DCR E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRE = function()
|
|
{
|
|
this.regE = this.decByte(this.regE);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x1E (MVI E,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIE = function()
|
|
{
|
|
this.regE = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x1F (RAR)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opLXIH = function()
|
|
{
|
|
this.setHL(this.getPCWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x22 (SHLD a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSHLD = function()
|
|
{
|
|
this.setWord(this.getPCWord(), this.getHL());
|
|
this.nStepCycles -= 16;
|
|
};
|
|
|
|
/**
|
|
* op=0x23 (INX H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINXH = function()
|
|
{
|
|
this.setHL(this.getHL() + 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x24 (INR H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRH = function()
|
|
{
|
|
this.regH = this.incByte(this.regH);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x25 (DCR H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRH = function()
|
|
{
|
|
this.regH = this.decByte(this.regH);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x26 (MVI H,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIH = function()
|
|
{
|
|
this.regH = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x27 (DAA)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 = CPUDef.PS.CF;
|
|
}
|
|
this.regA = this.addByte(src);
|
|
this.updateCF(CF? 0x100 : 0);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x29 (DAD H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDADH = function()
|
|
{
|
|
var w;
|
|
this.setHL(w = this.getHL() + this.getHL());
|
|
this.updateCF((w >> 8) & 0x100);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x2A (LHLD a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLHLD = function()
|
|
{
|
|
this.setHL(this.getWord(this.getPCWord()));
|
|
this.nStepCycles -= 16;
|
|
};
|
|
|
|
/**
|
|
* op=0x2B (DCX H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCXH = function()
|
|
{
|
|
this.setHL(this.getHL() - 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x2C (INR L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRL = function()
|
|
{
|
|
this.regL = this.incByte(this.regL);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x2D (DCR L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRL = function()
|
|
{
|
|
this.regL = this.decByte(this.regL);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x2E (MVI L,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIL = function()
|
|
{
|
|
this.regL = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x2F (CMA)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMA = function()
|
|
{
|
|
this.regA = ~this.regA & 0xff;
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x31 (LXI SP,d16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLXISP = function()
|
|
{
|
|
this.setSP(this.getPCWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x32 (STA a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSTA = function()
|
|
{
|
|
this.setByte(this.getPCWord(), this.regA);
|
|
this.nStepCycles -= 13;
|
|
};
|
|
|
|
/**
|
|
* op=0x33 (INX SP)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINXSP = function()
|
|
{
|
|
this.setSP(this.getSP() + 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x34 (INR M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRM = function()
|
|
{
|
|
var addr = this.getHL();
|
|
this.setByte(addr, this.incByte(this.getByte(addr)));
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x35 (DCR M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRM = function()
|
|
{
|
|
var addr = this.getHL();
|
|
this.setByte(addr, this.decByte(this.getByte(addr)));
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x36 (MVI M,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIM = function()
|
|
{
|
|
this.setByte(this.getHL(), this.getPCByte());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x37 (STC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSTC = function()
|
|
{
|
|
this.setCF();
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x39 (DAD SP)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDADSP = function()
|
|
{
|
|
var w;
|
|
this.setHL(w = this.getHL() + this.getSP());
|
|
this.updateCF((w >> 8) & 0x100);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0x3A (LDA a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opLDA = function()
|
|
{
|
|
this.regA = this.getByte(this.getPCWord());
|
|
this.nStepCycles -= 13;
|
|
};
|
|
|
|
/**
|
|
* op=0x3B (DCX SP)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCXSP = function()
|
|
{
|
|
this.setSP(this.getSP() - 1);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x3C (INR A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opINRA = function()
|
|
{
|
|
this.regA = this.incByte(this.regA);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x3D (DCR A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDCRA = function()
|
|
{
|
|
this.regA = this.decByte(this.regA);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x3E (MVI A,d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMVIA = function()
|
|
{
|
|
this.regA = this.getPCByte();
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x3F (CMC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMC = function()
|
|
{
|
|
this.updateCF(this.getCF()? 0 : 0x100);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x40 (MOV B,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBB = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x41 (MOV B,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBC = function()
|
|
{
|
|
this.regB = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x42 (MOV B,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBD = function()
|
|
{
|
|
this.regB = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x43 (MOV B,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBE = function()
|
|
{
|
|
this.regB = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x44 (MOV B,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBH = function()
|
|
{
|
|
this.regB = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x45 (MOV B,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBL = function()
|
|
{
|
|
this.regB = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x46 (MOV B,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBM = function()
|
|
{
|
|
this.regB = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x47 (MOV B,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVBA = function()
|
|
{
|
|
this.regB = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x48 (MOV C,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCB = function()
|
|
{
|
|
this.regC = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x49 (MOV C,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCC = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x4A (MOV C,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCD = function()
|
|
{
|
|
this.regC = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x4B (MOV C,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCE = function()
|
|
{
|
|
this.regC = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x4C (MOV C,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCH = function()
|
|
{
|
|
this.regC = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x4D (MOV C,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCL = function()
|
|
{
|
|
this.regC = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x4E (MOV C,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCM = function()
|
|
{
|
|
this.regC = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x4F (MOV C,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVCA = function()
|
|
{
|
|
this.regC = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x50 (MOV D,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDB = function()
|
|
{
|
|
this.regD = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x51 (MOV D,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDC = function()
|
|
{
|
|
this.regD = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x52 (MOV D,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDD = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x53 (MOV D,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDE = function()
|
|
{
|
|
this.regD = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x54 (MOV D,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDH = function()
|
|
{
|
|
this.regD = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x55 (MOV D,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDL = function()
|
|
{
|
|
this.regD = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x56 (MOV D,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDM = function()
|
|
{
|
|
this.regD = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x57 (MOV D,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVDA = function()
|
|
{
|
|
this.regD = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x58 (MOV E,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEB = function()
|
|
{
|
|
this.regE = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x59 (MOV E,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEC = function()
|
|
{
|
|
this.regE = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x5A (MOV E,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVED = function()
|
|
{
|
|
this.regE = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x5B (MOV E,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEE = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x5C (MOV E,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEH = function()
|
|
{
|
|
this.regE = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x5D (MOV E,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEL = function()
|
|
{
|
|
this.regE = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x5E (MOV E,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEM = function()
|
|
{
|
|
this.regE = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x5F (MOV E,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVEA = function()
|
|
{
|
|
this.regE = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x60 (MOV H,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHB = function()
|
|
{
|
|
this.regH = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x61 (MOV H,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHC = function()
|
|
{
|
|
this.regH = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x62 (MOV H,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHD = function()
|
|
{
|
|
this.regH = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x63 (MOV H,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHE = function()
|
|
{
|
|
this.regH = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x64 (MOV H,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHH = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x65 (MOV H,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHL = function()
|
|
{
|
|
this.regH = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x66 (MOV H,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHM = function()
|
|
{
|
|
this.regH = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x67 (MOV H,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVHA = function()
|
|
{
|
|
this.regH = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x68 (MOV L,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLB = function()
|
|
{
|
|
this.regL = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x69 (MOV L,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLC = function()
|
|
{
|
|
this.regL = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x6A (MOV L,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLD = function()
|
|
{
|
|
this.regL = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x6B (MOV L,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLE = function()
|
|
{
|
|
this.regL = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x6C (MOV L,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLH = function()
|
|
{
|
|
this.regL = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x6D (MOV L,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLL = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x6E (MOV L,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLM = function()
|
|
{
|
|
this.regL = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x6F (MOV L,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVLA = function()
|
|
{
|
|
this.regL = this.regA;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x70 (MOV M,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVMB = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regB);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x71 (MOV M,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVMC = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regC);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x72 (MOV M,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVMD = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regD);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x73 (MOV M,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVME = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regE);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x74 (MOV M,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVMH = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regH);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x75 (MOV M,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVML = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regL);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x76 (HLT)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opHLT = function()
|
|
{
|
|
/*
|
|
* The CPU is never REALLY halted by a HLT instruction; instead, by setting X86.INTFLAG.HALT,
|
|
* we are signalling to stepCPU() that it's free to end the current burst AND that it should not
|
|
* execute any more instructions until checkINTR() indicates a hardware interrupt is requested.
|
|
*/
|
|
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.intFlags |= CPUDef.INTFLAG.HALT;
|
|
this.nStepCycles -= 7;
|
|
|
|
/*
|
|
* 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(Messages.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 {CPUState}
|
|
*/
|
|
CPUDef.opMOVMA = function()
|
|
{
|
|
this.setByte(this.getHL(), this.regA);
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x78 (MOV A,B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAB = function()
|
|
{
|
|
this.regA = this.regB;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x79 (MOV A,C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAC = function()
|
|
{
|
|
this.regA = this.regC;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x7A (MOV A,D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAD = function()
|
|
{
|
|
this.regA = this.regD;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x7B (MOV A,E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAE = function()
|
|
{
|
|
this.regA = this.regE;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x7C (MOV A,H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAH = function()
|
|
{
|
|
this.regA = this.regH;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x7D (MOV A,L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAL = function()
|
|
{
|
|
this.regA = this.regL;
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x7E (MOV A,M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAM = function()
|
|
{
|
|
this.regA = this.getByte(this.getHL());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x7F (MOV A,A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opMOVAA = function()
|
|
{
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0x80 (ADD B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDB = function()
|
|
{
|
|
this.regA = this.addByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x81 (ADD C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDC = function()
|
|
{
|
|
this.regA = this.addByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x82 (ADD D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDD = function()
|
|
{
|
|
this.regA = this.addByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x83 (ADD E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDE = function()
|
|
{
|
|
this.regA = this.addByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x84 (ADD H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDH = function()
|
|
{
|
|
this.regA = this.addByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x85 (ADD L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDL = function()
|
|
{
|
|
this.regA = this.addByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x86 (ADD M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDM = function()
|
|
{
|
|
this.regA = this.addByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x87 (ADD A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADDA = function()
|
|
{
|
|
this.regA = this.addByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x88 (ADC B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCB = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x89 (ADC C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCC = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x8A (ADC D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCD = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x8B (ADC E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCE = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x8C (ADC H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCH = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x8D (ADC L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCL = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x8E (ADC M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCM = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x8F (ADC A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADCA = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x90 (SUB B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBB = function()
|
|
{
|
|
this.regA = this.subByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x91 (SUB C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBC = function()
|
|
{
|
|
this.regA = this.subByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x92 (SUB D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBD = function()
|
|
{
|
|
this.regA = this.subByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x93 (SUB E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBE = function()
|
|
{
|
|
this.regA = this.subByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x94 (SUB H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBH = function()
|
|
{
|
|
this.regA = this.subByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x95 (SUB L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBL = function()
|
|
{
|
|
this.regA = this.subByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x96 (SUB M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBM = function()
|
|
{
|
|
this.regA = this.subByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x97 (SUB A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUBA = function()
|
|
{
|
|
this.regA = this.subByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x98 (SBB B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBB = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x99 (SBB C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBC = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x9A (SBB D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBD = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x9B (SBB E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBE = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x9C (SBB H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBH = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x9D (SBB L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBL = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0x9E (SBB M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBM = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0x9F (SBB A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSBBA = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA0 (ANA B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAB = function()
|
|
{
|
|
this.regA = this.andByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA1 (ANA C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAC = function()
|
|
{
|
|
this.regA = this.andByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA2 (ANA D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAD = function()
|
|
{
|
|
this.regA = this.andByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA3 (ANA E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAE = function()
|
|
{
|
|
this.regA = this.andByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA4 (ANA H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAH = function()
|
|
{
|
|
this.regA = this.andByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA5 (ANA L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAL = function()
|
|
{
|
|
this.regA = this.andByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA6 (ANA M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAM = function()
|
|
{
|
|
this.regA = this.andByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xA7 (ANA A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANAA = function()
|
|
{
|
|
this.regA = this.andByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA8 (XRA B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAB = function()
|
|
{
|
|
this.regA = this.xorByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xA9 (XRA C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAC = function()
|
|
{
|
|
this.regA = this.xorByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xAA (XRA D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAD = function()
|
|
{
|
|
this.regA = this.xorByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xAB (XRA E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAE = function()
|
|
{
|
|
this.regA = this.xorByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xAC (XRA H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAH = function()
|
|
{
|
|
this.regA = this.xorByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xAD (XRA L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAL = function()
|
|
{
|
|
this.regA = this.xorByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xAE (XRA M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAM = function()
|
|
{
|
|
this.regA = this.xorByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xAF (XRA A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXRAA = function()
|
|
{
|
|
this.regA = this.xorByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB0 (ORA B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAB = function()
|
|
{
|
|
this.regA = this.orByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB1 (ORA C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAC = function()
|
|
{
|
|
this.regA = this.orByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB2 (ORA D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAD = function()
|
|
{
|
|
this.regA = this.orByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB3 (ORA E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAE = function()
|
|
{
|
|
this.regA = this.orByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB4 (ORA H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAH = function()
|
|
{
|
|
this.regA = this.orByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB5 (ORA L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAL = function()
|
|
{
|
|
this.regA = this.orByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB6 (ORA M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAM = function()
|
|
{
|
|
this.regA = this.orByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xB7 (ORA A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORAA = function()
|
|
{
|
|
this.regA = this.orByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB8 (CMP B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPB = function()
|
|
{
|
|
this.subByte(this.regB);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xB9 (CMP C)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPC = function()
|
|
{
|
|
this.subByte(this.regC);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xBA (CMP D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPD = function()
|
|
{
|
|
this.subByte(this.regD);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xBB (CMP E)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPE = function()
|
|
{
|
|
this.subByte(this.regE);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xBC (CMP H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPH = function()
|
|
{
|
|
this.subByte(this.regH);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xBD (CMP L)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPL = function()
|
|
{
|
|
this.subByte(this.regL);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xBE (CMP M)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPM = function()
|
|
{
|
|
this.subByte(this.getByte(this.getHL()));
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xBF (CMP A)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opCMPA = function()
|
|
{
|
|
this.subByte(this.regA);
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xC0 (RNZ)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRNZ = function()
|
|
{
|
|
if (!this.getZF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xC1 (POP B)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opPOPB = function()
|
|
{
|
|
this.setBC(this.popWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xC2 (JNZ a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJNZ = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (!this.getZF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xC3 (JMP a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJMP = function()
|
|
{
|
|
this.setPC(this.getPCWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xC4 (CNZ a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opPUSHB = function()
|
|
{
|
|
this.pushWord(this.getBC());
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xC6 (ADI d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opADI = function()
|
|
{
|
|
this.regA = this.addByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xC7 (RST 0)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST0 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xC8 (RZ)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRZ = function()
|
|
{
|
|
if (this.getZF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xC9 (RET)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRET = function()
|
|
{
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xCA (JZ a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJZ = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (this.getZF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xCC (CZ a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opCALL = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
this.pushWord(this.getPC());
|
|
this.setPC(w);
|
|
this.nStepCycles -= 17;
|
|
};
|
|
|
|
/**
|
|
* op=0xCE (ACI d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opACI = function()
|
|
{
|
|
this.regA = this.addByteCarry(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xCF (RST 1)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST1 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x08);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xD0 (RNC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRNC = function()
|
|
{
|
|
if (!this.getCF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xD1 (POP D)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opPOPD = function()
|
|
{
|
|
this.setDE(this.popWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xD2 (JNC a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJNC = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (!this.getCF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xD3 (OUT d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opOUT = function()
|
|
{
|
|
var port = this.getPCByte();
|
|
this.bus.checkPortOutputNotify(port, 1, this.regA, this.offPC(-2));
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xD4 (CNC a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opPUSHD = function()
|
|
{
|
|
this.pushWord(this.getDE());
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xD6 (SUI d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSUI = function()
|
|
{
|
|
this.regA = this.subByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xD7 (RST 2)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST2 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x10);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xD8 (RC)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRC = function()
|
|
{
|
|
if (this.getCF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xDA (JC a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJC = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (this.getCF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xDB (IN d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opSBI = function()
|
|
{
|
|
this.regA = this.subByteBorrow(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xDF (RST 3)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST3 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x18);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xE0 (RPO)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRPO = function()
|
|
{
|
|
if (!this.getPF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xE1 (POP H)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opPOPH = function()
|
|
{
|
|
this.setHL(this.popWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xE2 (JPO a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJPO = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (!this.getPF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xE3 (XTHL)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXTHL = function()
|
|
{
|
|
var w = this.popWord();
|
|
this.pushWord(this.getHL());
|
|
this.setHL(w);
|
|
this.nStepCycles -= 18;
|
|
};
|
|
|
|
/**
|
|
* op=0xE4 (CPO a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opPUSHH = function()
|
|
{
|
|
this.pushWord(this.getHL());
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xE6 (ANI d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opANI = function()
|
|
{
|
|
this.regA = this.andByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xE7 (RST 4)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST4 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x20);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xE8 (RPE)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRPE = function()
|
|
{
|
|
if (this.getPF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xE9 (PCHL)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opPCHL = function()
|
|
{
|
|
this.setPC(this.getHL());
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xEA (JPE a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJPE = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (this.getPF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xEB (XCHG)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opXCHG = function()
|
|
{
|
|
var w = this.getHL();
|
|
this.setHL(this.getDE());
|
|
this.setDE(w);
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xEC (CPE a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opXRI = function()
|
|
{
|
|
this.regA = this.xorByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xEF (RST 5)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST5 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x28);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xF0 (RP)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRP = function()
|
|
{
|
|
if (!this.getSF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xF1 (POP PSW)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opPOPSW = function()
|
|
{
|
|
this.setPSW(this.popWord());
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xF2 (JP a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJP = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (!this.getSF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xF3 (DI)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opDI = function()
|
|
{
|
|
this.clearIF();
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xF4 (CP a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opPUPSW = function()
|
|
{
|
|
this.pushWord(this.getPSW());
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xF6 (ORI d8)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opORI = function()
|
|
{
|
|
this.regA = this.orByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xF7 (RST 6)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRST6 = function()
|
|
{
|
|
this.pushWord(this.getPC());
|
|
this.setPC(0x30);
|
|
this.nStepCycles -= 11;
|
|
};
|
|
|
|
/**
|
|
* op=0xF8 (RM)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opRM = function()
|
|
{
|
|
if (this.getSF()) {
|
|
this.setPC(this.popWord());
|
|
this.nStepCycles -= 6;
|
|
}
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xF9 (SPHL)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opSPHL = function()
|
|
{
|
|
this.setSP(this.getHL());
|
|
this.nStepCycles -= 5;
|
|
};
|
|
|
|
/**
|
|
* op=0xFA (JM a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opJM = function()
|
|
{
|
|
var w = this.getPCWord();
|
|
if (this.getSF()) this.setPC(w);
|
|
this.nStepCycles -= 10;
|
|
};
|
|
|
|
/**
|
|
* op=0xFB (EI)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.opEI = function()
|
|
{
|
|
this.setIF();var w = this.getHL();
|
|
this.nStepCycles -= 4;
|
|
};
|
|
|
|
/**
|
|
* op=0xFC (CM a16)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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 {CPUState}
|
|
*/
|
|
CPUDef.opCPI = function()
|
|
{
|
|
this.subByte(this.getPCByte());
|
|
this.nStepCycles -= 7;
|
|
};
|
|
|
|
/**
|
|
* op=0xFF (RST 7)
|
|
*
|
|
* @this {CPUState}
|
|
*/
|
|
CPUDef.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.
|
|
*/
|
|
CPUDef.aOps8080 = [
|
|
/* 0x00-0x03 */ CPUDef.opNOP, CPUDef.opLXIB, CPUDef.opSTAXB, CPUDef.opINXB,
|
|
/* 0x04-0x07 */ CPUDef.opINRB, CPUDef.opDCRB, CPUDef.opMVIB, CPUDef.opRLC,
|
|
/* 0x08-0x0B */ CPUDef.opNOP, CPUDef.opDADB, CPUDef.opLDAXB, CPUDef.opDCXB,
|
|
/* 0x0C-0x0F */ CPUDef.opINRC, CPUDef.opDCRC, CPUDef.opMVIC, CPUDef.opRRC,
|
|
/* 0x10-0x13 */ CPUDef.opNOP, CPUDef.opLXID, CPUDef.opSTAXD, CPUDef.opINXD,
|
|
/* 0x14-0x17 */ CPUDef.opINRD, CPUDef.opDCRD, CPUDef.opMVID, CPUDef.opRAL,
|
|
/* 0x18-0x1B */ CPUDef.opNOP, CPUDef.opDADD, CPUDef.opLDAXD, CPUDef.opDCXD,
|
|
/* 0x1C-0x1F */ CPUDef.opINRE, CPUDef.opDCRE, CPUDef.opMVIE, CPUDef.opRAR,
|
|
/* 0x20-0x23 */ CPUDef.opNOP, CPUDef.opLXIH, CPUDef.opSHLD, CPUDef.opINXH,
|
|
/* 0x24-0x27 */ CPUDef.opINRH, CPUDef.opDCRH, CPUDef.opMVIH, CPUDef.opDAA,
|
|
/* 0x28-0x2B */ CPUDef.opNOP, CPUDef.opDADH, CPUDef.opLHLD, CPUDef.opDCXH,
|
|
/* 0x2C-0x2F */ CPUDef.opINRL, CPUDef.opDCRL, CPUDef.opMVIL, CPUDef.opCMA,
|
|
/* 0x30-0x33 */ CPUDef.opNOP, CPUDef.opLXISP, CPUDef.opSTA, CPUDef.opINXSP,
|
|
/* 0x34-0x37 */ CPUDef.opINRM, CPUDef.opDCRM, CPUDef.opMVIM, CPUDef.opSTC,
|
|
/* 0x38-0x3B */ CPUDef.opNOP, CPUDef.opDADSP, CPUDef.opLDA, CPUDef.opDCXSP,
|
|
/* 0x3C-0x3F */ CPUDef.opINRA, CPUDef.opDCRA, CPUDef.opMVIA, CPUDef.opCMC,
|
|
/* 0x40-0x43 */ CPUDef.opMOVBB, CPUDef.opMOVBC, CPUDef.opMOVBD, CPUDef.opMOVBE,
|
|
/* 0x44-0x47 */ CPUDef.opMOVBH, CPUDef.opMOVBL, CPUDef.opMOVBM, CPUDef.opMOVBA,
|
|
/* 0x48-0x4B */ CPUDef.opMOVCB, CPUDef.opMOVCC, CPUDef.opMOVCD, CPUDef.opMOVCE,
|
|
/* 0x4C-0x4F */ CPUDef.opMOVCH, CPUDef.opMOVCL, CPUDef.opMOVCM, CPUDef.opMOVCA,
|
|
/* 0x50-0x53 */ CPUDef.opMOVDB, CPUDef.opMOVDC, CPUDef.opMOVDD, CPUDef.opMOVDE,
|
|
/* 0x54-0x57 */ CPUDef.opMOVDH, CPUDef.opMOVDL, CPUDef.opMOVDM, CPUDef.opMOVDA,
|
|
/* 0x58-0x5B */ CPUDef.opMOVEB, CPUDef.opMOVEC, CPUDef.opMOVED, CPUDef.opMOVEE,
|
|
/* 0x5C-0x5F */ CPUDef.opMOVEH, CPUDef.opMOVEL, CPUDef.opMOVEM, CPUDef.opMOVEA,
|
|
/* 0x60-0x63 */ CPUDef.opMOVHB, CPUDef.opMOVHC, CPUDef.opMOVHD, CPUDef.opMOVHE,
|
|
/* 0x64-0x67 */ CPUDef.opMOVHH, CPUDef.opMOVHL, CPUDef.opMOVHM, CPUDef.opMOVHA,
|
|
/* 0x68-0x6B */ CPUDef.opMOVLB, CPUDef.opMOVLC, CPUDef.opMOVLD, CPUDef.opMOVLE,
|
|
/* 0x6C-0x6F */ CPUDef.opMOVLH, CPUDef.opMOVLL, CPUDef.opMOVLM, CPUDef.opMOVLA,
|
|
/* 0x70-0x73 */ CPUDef.opMOVMB, CPUDef.opMOVMC, CPUDef.opMOVMD, CPUDef.opMOVME,
|
|
/* 0x74-0x77 */ CPUDef.opMOVMH, CPUDef.opMOVML, CPUDef.opHLT, CPUDef.opMOVMA,
|
|
/* 0x78-0x7B */ CPUDef.opMOVAB, CPUDef.opMOVAC, CPUDef.opMOVAD, CPUDef.opMOVAE,
|
|
/* 0x7C-0x7F */ CPUDef.opMOVAH, CPUDef.opMOVAL, CPUDef.opMOVAM, CPUDef.opMOVAA,
|
|
/* 0x80-0x83 */ CPUDef.opADDB, CPUDef.opADDC, CPUDef.opADDD, CPUDef.opADDE,
|
|
/* 0x84-0x87 */ CPUDef.opADDH, CPUDef.opADDL, CPUDef.opADDM, CPUDef.opADDA,
|
|
/* 0x88-0x8B */ CPUDef.opADCB, CPUDef.opADCC, CPUDef.opADCD, CPUDef.opADCE,
|
|
/* 0x8C-0x8F */ CPUDef.opADCH, CPUDef.opADCL, CPUDef.opADCM, CPUDef.opADCA,
|
|
/* 0x90-0x93 */ CPUDef.opSUBB, CPUDef.opSUBC, CPUDef.opSUBD, CPUDef.opSUBE,
|
|
/* 0x94-0x97 */ CPUDef.opSUBH, CPUDef.opSUBL, CPUDef.opSUBM, CPUDef.opSUBA,
|
|
/* 0x98-0x9B */ CPUDef.opSBBB, CPUDef.opSBBC, CPUDef.opSBBD, CPUDef.opSBBE,
|
|
/* 0x9C-0x9F */ CPUDef.opSBBH, CPUDef.opSBBL, CPUDef.opSBBM, CPUDef.opSBBA,
|
|
/* 0xA0-0xA3 */ CPUDef.opANAB, CPUDef.opANAC, CPUDef.opANAD, CPUDef.opANAE,
|
|
/* 0xA4-0xA7 */ CPUDef.opANAH, CPUDef.opANAL, CPUDef.opANAM, CPUDef.opANAA,
|
|
/* 0xA8-0xAB */ CPUDef.opXRAB, CPUDef.opXRAC, CPUDef.opXRAD, CPUDef.opXRAE,
|
|
/* 0xAC-0xAF */ CPUDef.opXRAH, CPUDef.opXRAL, CPUDef.opXRAM, CPUDef.opXRAA,
|
|
/* 0xB0-0xB3 */ CPUDef.opORAB, CPUDef.opORAC, CPUDef.opORAD, CPUDef.opORAE,
|
|
/* 0xB4-0xB7 */ CPUDef.opORAH, CPUDef.opORAL, CPUDef.opORAM, CPUDef.opORAA,
|
|
/* 0xB8-0xBB */ CPUDef.opCMPB, CPUDef.opCMPC, CPUDef.opCMPD, CPUDef.opCMPE,
|
|
/* 0xBC-0xBF */ CPUDef.opCMPH, CPUDef.opCMPL, CPUDef.opCMPM, CPUDef.opCMPA,
|
|
/* 0xC0-0xC3 */ CPUDef.opRNZ, CPUDef.opPOPB, CPUDef.opJNZ, CPUDef.opJMP,
|
|
/* 0xC4-0xC7 */ CPUDef.opCNZ, CPUDef.opPUSHB, CPUDef.opADI, CPUDef.opRST0,
|
|
/* 0xC8-0xCB */ CPUDef.opRZ, CPUDef.opRET, CPUDef.opJZ, CPUDef.opJMP,
|
|
/* 0xCC-0xCF */ CPUDef.opCZ, CPUDef.opCALL, CPUDef.opACI, CPUDef.opRST1,
|
|
/* 0xD0-0xD3 */ CPUDef.opRNC, CPUDef.opPOPD, CPUDef.opJNC, CPUDef.opOUT,
|
|
/* 0xD4-0xD7 */ CPUDef.opCNC, CPUDef.opPUSHD, CPUDef.opSUI, CPUDef.opRST2,
|
|
/* 0xD8-0xDB */ CPUDef.opRC, CPUDef.opRET, CPUDef.opJC, CPUDef.opIN,
|
|
/* 0xDC-0xDF */ CPUDef.opCC, CPUDef.opCALL, CPUDef.opSBI, CPUDef.opRST3,
|
|
/* 0xE0-0xE3 */ CPUDef.opRPO, CPUDef.opPOPH, CPUDef.opJPO, CPUDef.opXTHL,
|
|
/* 0xE4-0xE7 */ CPUDef.opCPO, CPUDef.opPUSHH, CPUDef.opANI, CPUDef.opRST4,
|
|
/* 0xE8-0xEB */ CPUDef.opRPE, CPUDef.opPCHL, CPUDef.opJPE, CPUDef.opXCHG,
|
|
/* 0xEC-0xEF */ CPUDef.opCPE, CPUDef.opCALL, CPUDef.opXRI, CPUDef.opRST5,
|
|
/* 0xF0-0xF3 */ CPUDef.opRP, CPUDef.opPOPSW, CPUDef.opJP, CPUDef.opDI,
|
|
/* 0xF4-0xF7 */ CPUDef.opCP, CPUDef.opPUPSW, CPUDef.opORI, CPUDef.opRST6,
|
|
/* 0xF8-0xFB */ CPUDef.opRM, CPUDef.opSPHL, CPUDef.opJM, CPUDef.opEI,
|
|
/* 0xFC-0xFF */ CPUDef.opCM, CPUDef.opCALL, CPUDef.opCPI, CPUDef.opRST7
|
|
];
|
|
|
|
// ./modules/pc8080/lib/chipset.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 ChipSet component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-25
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* ChipSet(parmsChipSet)
|
|
*
|
|
* The ChipSet component has the following component-specific (parmsChipSet) properties:
|
|
*
|
|
* model: eg, "SI1978" (should be a member of ChipSet.MODELS)
|
|
* swDIP: eg, "00000000", where swDIP[0] is DIP0, swDIP[1] is DIP1, etc.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsChipSet
|
|
*/
|
|
function ChipSet(parmsChipSet)
|
|
{
|
|
Component.call(this, "ChipSet", parmsChipSet, ChipSet, Messages.CHIPSET);
|
|
|
|
var model = parmsChipSet['model'];
|
|
|
|
/*
|
|
* this.model is a numeric version of the 'model' string; when comparing this.model to "base"
|
|
* model numbers, you should generally compare (this.model|0) to the target value, which truncates it.
|
|
*/
|
|
if (model && !ChipSet.MODELS[model]) {
|
|
Component.notice("Unrecognized ChipSet model: " + model);
|
|
}
|
|
|
|
this.model = ChipSet.MODELS[model] || ChipSet.SI_1978.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();
|
|
}
|
|
|
|
Component.subclass(ChipSet);
|
|
|
|
ChipSet.SI_1978 = {
|
|
MODEL: 1978.1,
|
|
STATUS0: { // NOTE: STATUS0 not used by the SI_1978 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
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Supported model strings
|
|
*/
|
|
ChipSet.MODELS = {
|
|
"SI1978": ChipSet.SI_1978.MODEL
|
|
};
|
|
|
|
/**
|
|
* parseDIPSwitches(sBits, bDefault)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {string} sBits describing switch settings
|
|
* @param {number} [bDefault]
|
|
* @return {number|undefined}
|
|
*/
|
|
ChipSet.prototype.parseDIPSwitches = function(sBits, bDefault)
|
|
{
|
|
var b = bDefault;
|
|
if (sBits) {
|
|
/*
|
|
* NOTE: We can't use parseInt() with a base of 2, because both bit order and bit sense are reversed.
|
|
*/
|
|
b = 0;
|
|
var bit = 0x1;
|
|
for (var i = 0; i < sBits.length; i++) {
|
|
if (sBits.charAt(i) == "0") b |= bit;
|
|
bit <<= 1;
|
|
}
|
|
}
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {ChipSet}
|
|
* @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
|
|
*/
|
|
ChipSet.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
ChipSet.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.cmp = cmp;
|
|
if (this.model == ChipSet.SI_1978.MODEL) {
|
|
bus.addPortInputTable(this, ChipSet.aPortInput);
|
|
bus.addPortOutputTable(this, ChipSet.aPortOutput);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
ChipSet.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) {
|
|
if (!data) {
|
|
this.reset();
|
|
} else {
|
|
if (!this.restore(data)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
ChipSet.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
return fSave? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {ChipSet}
|
|
*/
|
|
ChipSet.prototype.reset = function()
|
|
{
|
|
this.bStatus0 = ChipSet.SI_1978.STATUS0.ALWAYS_SET;
|
|
this.bStatus1 = ChipSet.SI_1978.STATUS1.ALWAYS_SET;
|
|
this.bStatus2 = ChipSet.SI_1978.STATUS2.ALWAYS_SET;
|
|
this.wShiftData = 0;
|
|
this.bShiftCount = 0;
|
|
this.bSound1 = this.bSound2 = 0;
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements save support for the ChipSet component.
|
|
*
|
|
* @this {ChipSet}
|
|
* @return {Object}
|
|
*/
|
|
ChipSet.prototype.save = function()
|
|
{
|
|
var state = new State(this);
|
|
if (this.model == ChipSet.SI_1978.MODEL) {
|
|
state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.wShiftData, this.bShiftCount, this.bSound1, this.bSound2]);
|
|
}
|
|
return state.data();
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements restore support for the ChipSet component.
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {Object} data
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
ChipSet.prototype.restore = function(data)
|
|
{
|
|
var a, i;
|
|
a = data[0];
|
|
if (this.model == ChipSet.SI_1978.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;
|
|
};
|
|
|
|
/**
|
|
* start()
|
|
*
|
|
* Notification from the CPU that it's starting.
|
|
*
|
|
* @this {ChipSet}
|
|
*/
|
|
ChipSet.prototype.start = function()
|
|
{
|
|
/*
|
|
* Currently, all we (may) do with this notification is allow the speaker to make noise.
|
|
*/
|
|
};
|
|
|
|
/**
|
|
* stop()
|
|
*
|
|
* Notification from the CPU that it's stopping.
|
|
*
|
|
* @this {ChipSet}
|
|
*/
|
|
ChipSet.prototype.stop = function()
|
|
{
|
|
/*
|
|
* Currently, all we (may) do with this notification is prevent the speaker from making noise.
|
|
*/
|
|
};
|
|
|
|
/**
|
|
* updateStatus0(bit, fSet)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} bit
|
|
* @param {boolean} fSet
|
|
*/
|
|
ChipSet.prototype.updateStatus0 = function(bit, fSet)
|
|
{
|
|
this.bStatus0 &= ~bit;
|
|
if (fSet) this.bStatus0 |= bit;
|
|
};
|
|
|
|
/**
|
|
* updateStatus1(bit, fSet)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} bit
|
|
* @param {boolean} fSet
|
|
*/
|
|
ChipSet.prototype.updateStatus1 = function(bit, fSet)
|
|
{
|
|
this.bStatus1 &= ~bit;
|
|
if (fSet) this.bStatus1 |= bit;
|
|
};
|
|
|
|
/**
|
|
* updateStatus2(bit, fSet)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} bit
|
|
* @param {boolean} fSet
|
|
*/
|
|
ChipSet.prototype.updateStatus2 = function(bit, fSet)
|
|
{
|
|
this.bStatus2 &= ~bit;
|
|
if (fSet) this.bStatus2 |= bit;
|
|
};
|
|
|
|
/**
|
|
* inSIStatus0(port, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x00)
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
ChipSet.prototype.inSIStatus0 = function(port, addrFrom)
|
|
{
|
|
var b = this.bStatus0;
|
|
this.printMessageIO(port, null, addrFrom, "STATUS0", b, true);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* inSIStatus1(port, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x01)
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
ChipSet.prototype.inSIStatus1 = function(port, addrFrom)
|
|
{
|
|
var b = this.bStatus1;
|
|
this.printMessageIO(port, null, addrFrom, "STATUS1", b, true);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* inSIStatus2(port, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x02)
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
ChipSet.prototype.inSIStatus2 = function(port, addrFrom)
|
|
{
|
|
var b = this.bStatus2;
|
|
this.printMessageIO(port, null, addrFrom, "STATUS2", b, true);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* inSIShiftResult(port, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x03)
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
|
|
* @return {number} simulated port value
|
|
*/
|
|
ChipSet.prototype.inSIShiftResult = function(port, addrFrom)
|
|
{
|
|
var b = (this.wShiftData >> (8 - this.bShiftCount)) & 0xff;
|
|
this.printMessageIO(port, null, addrFrom, "SHIFT.RESULT", b, true);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outSIShiftCount(port, b, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x02)
|
|
* @param {number} b
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
|
*/
|
|
ChipSet.prototype.outSIShiftCount = function(port, b, addrFrom)
|
|
{
|
|
this.printMessageIO(port, b, addrFrom, "SHIFT.COUNT", null, true);
|
|
this.bShiftCount = b;
|
|
};
|
|
|
|
/**
|
|
* outSISound1(port, b, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x03)
|
|
* @param {number} b
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
|
*/
|
|
ChipSet.prototype.outSISound1 = function(port, b, addrFrom)
|
|
{
|
|
this.printMessageIO(port, b, addrFrom, "SOUND1", null, true);
|
|
this.bSound1 = b;
|
|
};
|
|
|
|
/**
|
|
* outSIShiftData(port, b, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x04)
|
|
* @param {number} b
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
|
*/
|
|
ChipSet.prototype.outSIShiftData = function(port, b, addrFrom)
|
|
{
|
|
this.printMessageIO(port, b, addrFrom, "SHIFT.DATA", null, true);
|
|
this.wShiftData = (b << 8) | (this.wShiftData >> 8);
|
|
};
|
|
|
|
/**
|
|
* outSISound2(port, b, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x05)
|
|
* @param {number} b
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
|
*/
|
|
ChipSet.prototype.outSISound2 = function(port, b, addrFrom)
|
|
{
|
|
this.printMessageIO(port, b, addrFrom, "SOUND2", null, true);
|
|
this.bSound2 = b;
|
|
};
|
|
|
|
/**
|
|
* outSIWatchdog(port, b, addrFrom)
|
|
*
|
|
* @this {ChipSet}
|
|
* @param {number} port (0x06)
|
|
* @param {number} b
|
|
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
|
*/
|
|
ChipSet.prototype.outSIWatchdog = function(port, b, addrFrom)
|
|
{
|
|
this.printMessageIO(port, b, addrFrom, "WATCHDOG", null, true);
|
|
};
|
|
|
|
/*
|
|
* Port input notification tables
|
|
*/
|
|
ChipSet.aPortInput = {
|
|
0x00: ChipSet.prototype.inSIStatus0,
|
|
0x01: ChipSet.prototype.inSIStatus1,
|
|
0x02: ChipSet.prototype.inSIStatus2,
|
|
0x03: ChipSet.prototype.inSIShiftResult
|
|
};
|
|
|
|
/*
|
|
* Port output notification tables
|
|
*/
|
|
ChipSet.aPortOutput = {
|
|
0x02: ChipSet.prototype.outSIShiftCount,
|
|
0x03: ChipSet.prototype.outSISound1,
|
|
0x04: ChipSet.prototype.outSIShiftData,
|
|
0x05: ChipSet.prototype.outSISound2,
|
|
0x06: ChipSet.prototype.outSIWatchdog
|
|
};
|
|
|
|
/**
|
|
* ChipSet.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.
|
|
*/
|
|
ChipSet.init = function()
|
|
{
|
|
var aeChipSet = Component.getElementsByClass(document, PC8080.APPCLASS, "chipset");
|
|
for (var iChip = 0; iChip < aeChipSet.length; iChip++) {
|
|
var eChipSet = aeChipSet[iChip];
|
|
var parmsChipSet = Component.getComponentParms(eChipSet);
|
|
var chipset = new ChipSet(parmsChipSet);
|
|
Component.bindComponentControls(chipset, eChipSet, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every ChipSet module on the page.
|
|
*/
|
|
web.onInit(ChipSet.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/rom.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 ROM component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-19
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* ROM(parmsROM)
|
|
*
|
|
* The ROM component expects the following (parmsROM) properties:
|
|
*
|
|
* addr: physical address of ROM
|
|
* size: amount of ROM, in bytes
|
|
* alias: physical alias address (null if none)
|
|
* file: name of ROM data file
|
|
* writable: true to make ROM writable (default is false)
|
|
*
|
|
* NOTE: The ROM data will not be copied into place until the Bus is ready (see initBus()) AND the
|
|
* ROM data file has finished loading (see doneLoad()).
|
|
*
|
|
* Also, while the size parameter may seem redundant, I consider it useful to confirm that the ROM you received
|
|
* is the ROM you expected.
|
|
*
|
|
* Finally, while making ROM "writable" may seem a contradiction in terms, I want to be able to load selected
|
|
* CP/M binary files into memory purely for testing purposes, and the RAM component has no "file" option, so the
|
|
* simplest solution was to add the option to load binary files into memory as "writable" ROMs.
|
|
*
|
|
* Moreover, if a "writable" ROM is installed at addr 0x100, that triggers our "Fake CP/M" support, providing
|
|
* a quick-and-dirty means of loading simple CP/M test binaries. See addROM() for details.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsROM
|
|
*/
|
|
function ROM(parmsROM)
|
|
{
|
|
Component.call(this, "ROM", parmsROM, ROM);
|
|
|
|
this.abROM = null;
|
|
this.addrROM = parmsROM['addr'];
|
|
this.sizeROM = parmsROM['size'];
|
|
this.fWritable = parmsROM['writable'];
|
|
|
|
/*
|
|
* The new 'alias' property can now be EITHER a single physical address (like 'addr') OR an array of
|
|
* physical addresses; eg:
|
|
*
|
|
* [0xf0000,0xffff0000,0xffff8000]
|
|
*
|
|
* We could have overloaded 'addr' to accomplish the same thing, but I think it's better to have any
|
|
* aliased locations listed under a separate property.
|
|
*
|
|
* Most ROMs are not aliased, in which case the 'alias' property should have the default value of null.
|
|
*/
|
|
this.addrAlias = parmsROM['alias'];
|
|
|
|
this.sFilePath = parmsROM['file'];
|
|
this.sFileName = str.getBaseName(this.sFilePath);
|
|
|
|
if (this.sFilePath) {
|
|
var sFileURL = this.sFilePath;
|
|
if (DEBUG) this.log('load("' + sFileURL + '")');
|
|
/*
|
|
* If the selected ROM file has a ".json" extension, then we assume it's pre-converted
|
|
* JSON-encoded ROM data, so we load it as-is; ditto for ROM files with a ".hex" extension.
|
|
* Otherwise, we ask our server-side ROM converter to return the file in a JSON-compatible format.
|
|
*/
|
|
var sFileExt = str.getExtension(this.sFileName);
|
|
if (sFileExt != DumpAPI.FORMAT.JSON && sFileExt != DumpAPI.FORMAT.HEX) {
|
|
sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFilePath + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES + '&' + DumpAPI.QUERY.DECIMAL + '=true';
|
|
}
|
|
var rom = this;
|
|
web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) {
|
|
rom.doneLoad(sURL, sResponse, nErrorCode);
|
|
});
|
|
}
|
|
}
|
|
|
|
Component.subclass(ROM);
|
|
|
|
ROM.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
|
|
}
|
|
}
|
|
};
|
|
|
|
ROM.CPM.VECTORS = [ROM.CPM.BIOS.VECTOR, ROM.CPM.BDOS.VECTOR];
|
|
|
|
/*
|
|
* NOTE: There's currently no need for this component to have a reset() function, since
|
|
* once the ROM data is loaded, it can't be changed, so there's nothing to reinitialize.
|
|
*
|
|
* OK, well, I take that back, because the Debugger, if installed, has the ability to modify
|
|
* ROM contents, so in that case, having a reset() function that restores the original ROM data
|
|
* might be useful; then again, it might not, depending on what you're trying to debug.
|
|
*
|
|
* If we do add reset(), then we'll want to change copyROM() to hang onto the original
|
|
* ROM data; currently, we release it after copying it into the read-only memory allocated
|
|
* via bus.addMemory().
|
|
*/
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {ROM}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
ROM.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.copyROM();
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {ROM}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
ROM.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (this.aSymbols) {
|
|
if (this.dbg) {
|
|
this.dbg.addSymbols(this.id, this.addrROM, this.sizeROM, this.aSymbols);
|
|
}
|
|
/*
|
|
* Our only role in the handling of symbols is to hand them off to the Debugger at our
|
|
* first opportunity. Now that we've done that, our copy of the symbols, if any, are toast.
|
|
*/
|
|
delete this.aSymbols;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* Since we have nothing to do on powerDown(), and no state to return, we could simply omit
|
|
* this function. But it doesn't hurt anything, and maybe we'll use our state to save something
|
|
* useful down the road, like user-defined symbols (ie, symbols that the Debugger may have
|
|
* created, above and beyond those symbols we automatically loaded, if any, along with the ROM).
|
|
*
|
|
* @this {ROM}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
ROM.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* doneLoad(sURL, sROMData, nErrorCode)
|
|
*
|
|
* @this {ROM}
|
|
* @param {string} sURL
|
|
* @param {string} sROMData
|
|
* @param {number} nErrorCode (response from server if anything other than 200)
|
|
*/
|
|
ROM.prototype.doneLoad = function(sURL, sROMData, nErrorCode)
|
|
{
|
|
if (nErrorCode) {
|
|
this.notice("Unable to load system ROM (error " + nErrorCode + ": " + sURL + ")");
|
|
return;
|
|
}
|
|
|
|
Component.addMachineResource(this.idMachine, sURL, sROMData);
|
|
|
|
if (sROMData.charAt(0) == "[" || sROMData.charAt(0) == "{") {
|
|
try {
|
|
/*
|
|
* The most likely source of any exception will be here: parsing the JSON-encoded ROM data.
|
|
*/
|
|
var rom = eval("(" + sROMData + ")");
|
|
var ab = rom['bytes'];
|
|
var adw = rom['data'];
|
|
|
|
if (ab) {
|
|
this.abROM = ab;
|
|
}
|
|
else if (adw) {
|
|
/*
|
|
* Convert all the DWORDs into BYTEs, so that subsequent code only has to deal with abROM.
|
|
*/
|
|
this.abROM = new Array(adw.length * 4);
|
|
for (var idw = 0, ib = 0; idw < adw.length; idw++) {
|
|
this.abROM[ib++] = adw[idw] & 0xff;
|
|
this.abROM[ib++] = (adw[idw] >> 8) & 0xff;
|
|
this.abROM[ib++] = (adw[idw] >> 16) & 0xff;
|
|
this.abROM[ib++] = (adw[idw] >> 24) & 0xff;
|
|
}
|
|
}
|
|
else {
|
|
this.abROM = rom;
|
|
}
|
|
|
|
this.aSymbols = rom['symbols'];
|
|
|
|
if (!this.abROM.length) {
|
|
Component.error("Empty ROM: " + sURL);
|
|
return;
|
|
}
|
|
else if (this.abROM.length == 1) {
|
|
Component.error(this.abROM[0]);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
this.notice("ROM data error: " + e.message);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
/*
|
|
* Parse the ROM data manually; we assume it's in "simplified" hex form (a series of hex byte-values
|
|
* separated by whitespace).
|
|
*/
|
|
var sHexData = sROMData.replace(/\n/gm, " ").replace(/ +$/, "");
|
|
var asHexData = sHexData.split(" ");
|
|
this.abROM = new Array(asHexData.length);
|
|
for (var i = 0; i < asHexData.length; i++) {
|
|
this.abROM[i] = str.parseInt(asHexData[i], 16);
|
|
}
|
|
}
|
|
this.copyROM();
|
|
};
|
|
|
|
/**
|
|
* copyROM()
|
|
*
|
|
* This function is called by both initBus() and doneLoad(), but it cannot copy the the ROM data into place
|
|
* until after initBus() has received the Bus component AND doneLoad() has received the abROM data. When both
|
|
* those criteria are satisfied, the component becomes "ready".
|
|
*
|
|
* @this {ROM}
|
|
*/
|
|
ROM.prototype.copyROM = function()
|
|
{
|
|
if (!this.isReady()) {
|
|
if (!this.sFilePath) {
|
|
this.setReady();
|
|
}
|
|
else if (this.abROM && this.bus) {
|
|
/*
|
|
* If no explicit size was specified, then use whatever the actual size is.
|
|
*/
|
|
if (!this.sizeROM) {
|
|
this.sizeROM = this.abROM.length;
|
|
}
|
|
if (this.abROM.length != this.sizeROM) {
|
|
/*
|
|
* Note that setError() sets the component's fError flag, which in turn prevents setReady() from
|
|
* marking the component ready. TODO: Revisit this decision. On the one hand, it sounds like a
|
|
* good idea to stop the machine in its tracks whenever a setError() occurs, but there may also be
|
|
* times when we'd like to forge ahead anyway.
|
|
*/
|
|
this.setError("ROM size (" + str.toHexLong(this.abROM.length) + ") does not match specified size (" + str.toHexLong(this.sizeROM) + ")");
|
|
}
|
|
else if (this.addROM(this.addrROM)) {
|
|
|
|
var aliases = [];
|
|
if (typeof this.addrAlias == "number") {
|
|
aliases.push(this.addrAlias);
|
|
} else if (this.addrAlias != null && this.addrAlias.length) {
|
|
aliases = this.addrAlias;
|
|
}
|
|
for (var i = 0; i < aliases.length; i++) {
|
|
this.cloneROM(aliases[i]);
|
|
}
|
|
/*
|
|
* We used to hang onto the original ROM data so that we could restore any bytes the CPU overwrote,
|
|
* using memory write-notification handlers, but with the introduction of read-only memory blocks, that's
|
|
* no longer necessary.
|
|
*
|
|
* TODO: Consider an option to retain the ROM data, and give the user some way of restoring ROMs.
|
|
* That may be useful for "resumable" machines that save/restore all dirty block of memory, regardless
|
|
* whether they're ROM or RAM. However, the only way to modify a machine's ROM is with the Debugger,
|
|
* and Debugger users should know better.
|
|
*/
|
|
delete this.abROM;
|
|
}
|
|
this.setReady();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* addROM(addr)
|
|
*
|
|
* @this {ROM}
|
|
* @param {number} addr
|
|
* @return {boolean}
|
|
*/
|
|
ROM.prototype.addROM = function(addr)
|
|
{
|
|
if (this.bus.addMemory(addr, this.sizeROM, this.fWritable? Memory.TYPE.RAM : Memory.TYPE.ROM)) {
|
|
if (DEBUG) this.log("addROM(): copying ROM to " + str.toHexLong(addr) + " (" + str.toHexLong(this.abROM.length) + " bytes)");
|
|
var i;
|
|
for (i = 0; i < this.abROM.length; i++) {
|
|
this.bus.setByteDirect(addr + i, this.abROM[i]);
|
|
}
|
|
if (this.fWritable && addr == 0x100) {
|
|
/*
|
|
* 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.
|
|
*/
|
|
for (i = 0; i < ROM.CPM.VECTORS.length; i++) {
|
|
this.bus.setByteDirect(ROM.CPM.VECTORS[i], CPUDef.OPCODE.HLT);
|
|
}
|
|
|
|
this.cpu.addHaltCheck(function(rom) {
|
|
return function(addr) {
|
|
return rom.checkCPMVector(addr)
|
|
};
|
|
}(this));
|
|
|
|
this.cpu.setReset(addr);
|
|
}
|
|
return true;
|
|
}
|
|
/*
|
|
* We don't need to report an error here, because addMemory() already takes care of that.
|
|
*/
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* checkCPMVector(addr)
|
|
*
|
|
* @this {ROM}
|
|
* @param {number} addr (of the HLT opcode)
|
|
* @return {boolean} true if special processing performed, false if not
|
|
*/
|
|
ROM.prototype.checkCPMVector = function(addr)
|
|
{
|
|
var i = ROM.CPM.VECTORS.indexOf(addr);
|
|
if (i >= 0) {
|
|
var fCPM = false;
|
|
var cpu = this.cpu;
|
|
var dbg = this.dbg;
|
|
if (addr == ROM.CPM.BDOS.VECTOR) {
|
|
fCPM = true;
|
|
switch(cpu.regC) {
|
|
case ROM.CPM.BDOS.FUNC.CON_WRITE:
|
|
this.writeCPMString(this.getCPMChar(cpu.regE));
|
|
break;
|
|
case ROM.CPM.BDOS.FUNC.STR_WRITE:
|
|
this.writeCPMString(this.getCPMString(cpu.getDE(), '$'));
|
|
break;
|
|
default:
|
|
fCPM = false;
|
|
break;
|
|
}
|
|
}
|
|
if (fCPM) {
|
|
CPUDef.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 {ROM}
|
|
* @param {number} ch
|
|
* @return {string}
|
|
*/
|
|
ROM.prototype.getCPMChar = function(ch)
|
|
{
|
|
return String.fromCharCode(ch);
|
|
};
|
|
|
|
/**
|
|
* getCPMString(addr, chEnd)
|
|
*
|
|
* @this {ROM}
|
|
* @param {number} addr (of a string)
|
|
* @param {string|number} [chEnd] (terminating character, default is 0)
|
|
* @return {string}
|
|
*/
|
|
ROM.prototype.getCPMString = function(addr, chEnd)
|
|
{
|
|
var s = "";
|
|
var cchMax = 255;
|
|
var bEnd = chEnd && chEnd.length && chEnd.charCodeAt(0) || chEnd || 0;
|
|
while (cchMax--) {
|
|
var b = this.cpu.getByte(addr++);
|
|
if (b == bEnd) break;
|
|
s += String.fromCharCode(b);
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* writeCPMString(s)
|
|
*
|
|
* @this {ROM}
|
|
* @param {string} s
|
|
*/
|
|
ROM.prototype.writeCPMString = function(s)
|
|
{
|
|
s = s.replace(/\r/g, '');
|
|
if (this.controlPrint) {
|
|
this.controlPrint.value += s;
|
|
this.controlPrint.scrollTop = this.controlPrint.scrollHeight;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* cloneROM(addr)
|
|
*
|
|
* For ROMs with one or more alias addresses, we used to call addROM() for each address. However,
|
|
* that obviously wasted memory, since each alias was an independent copy, and if you used the
|
|
* Debugger to edit the ROM in one location, the changes would not appear in the other location(s).
|
|
*
|
|
* Now that the Bus component provides low-level getMemoryBlocks() and setMemoryBlocks() methods
|
|
* to manually get and set the blocks of any memory range, it is now possible to create true aliases.
|
|
*
|
|
* @this {ROM}
|
|
* @param {number} addr
|
|
*/
|
|
ROM.prototype.cloneROM = function(addr)
|
|
{
|
|
var aBlocks = this.bus.getMemoryBlocks(this.addrROM, this.sizeROM);
|
|
this.bus.setMemoryBlocks(addr, this.sizeROM, aBlocks);
|
|
};
|
|
|
|
/**
|
|
* ROM.init()
|
|
*
|
|
* This function operates on every HTML element of class "rom", extracting the
|
|
* JSON-encoded parameters for the ROM constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a ROM component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
ROM.init = function()
|
|
{
|
|
var aeROM = Component.getElementsByClass(document, PC8080.APPCLASS, "rom");
|
|
for (var iROM = 0; iROM < aeROM.length; iROM++) {
|
|
var eROM = aeROM[iROM];
|
|
var parmsROM = Component.getComponentParms(eROM);
|
|
var rom = new ROM(parmsROM);
|
|
Component.bindComponentControls(rom, eROM, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize all the ROM modules on the page.
|
|
*/
|
|
web.onInit(ROM.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/ram.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 RAM component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-19
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* RAM(parmsRAM)
|
|
*
|
|
* The RAM component expects the following (parmsRAM) properties:
|
|
*
|
|
* addr: starting physical address of RAM (default is 0)
|
|
* size: amount of RAM, in bytes (default is 0, which means defer to motherboard switch settings)
|
|
*
|
|
* NOTE: We make a note of the specified size, but no memory is initially allocated for the RAM until the
|
|
* Computer component calls powerUp().
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsRAM
|
|
*/
|
|
function RAM(parmsRAM)
|
|
{
|
|
Component.call(this, "RAM", parmsRAM, RAM);
|
|
|
|
this.addrRAM = parmsRAM['addr'];
|
|
this.sizeRAM = parmsRAM['size'];
|
|
this.fInstalled = (!!this.sizeRAM); // 0 is the default value for 'size' when none is specified
|
|
this.fAllocated = false;
|
|
}
|
|
|
|
Component.subclass(RAM);
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {RAM}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
RAM.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {RAM}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
RAM.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) {
|
|
/*
|
|
* The Computer powers up the CPU last, at which point CPUState state is restored,
|
|
* which includes the Bus state, and since we use the Bus to allocate all our memory,
|
|
* memory contents are already restored for us, so we don't need the usual restore
|
|
* logic. We just need to call reset(), to allocate memory for the RAM.
|
|
*/
|
|
this.reset();
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {RAM}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
|
*/
|
|
RAM.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
/*
|
|
* The Computer powers down the CPU first, at which point CPUState state is saved,
|
|
* which includes the Bus state, and since we use the Bus component to allocate all
|
|
* our memory, memory contents are already saved for us, so we don't need the usual
|
|
* save logic.
|
|
*/
|
|
return (fSave)? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {RAM}
|
|
*/
|
|
RAM.prototype.reset = function()
|
|
{
|
|
if (!this.fAllocated && this.sizeRAM) {
|
|
if (this.bus.addMemory(this.addrRAM, this.sizeRAM, Memory.TYPE.RAM)) {
|
|
this.fAllocated = true;
|
|
}
|
|
}
|
|
if (!this.fAllocated) {
|
|
Component.error("No RAM allocated");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements save support for the RAM component.
|
|
*
|
|
* @this {RAM}
|
|
* @return {Object}
|
|
*/
|
|
RAM.prototype.save = function()
|
|
{
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements restore support for the RAM component.
|
|
*
|
|
* @this {RAM}
|
|
* @param {Object} data
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
RAM.prototype.restore = function(data)
|
|
{
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* RAM.init()
|
|
*
|
|
* This function operates on every HTML element of class "ram", extracting the
|
|
* JSON-encoded parameters for the RAM constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a RAM component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
RAM.init = function()
|
|
{
|
|
var aeRAM = Component.getElementsByClass(document, PC8080.APPCLASS, "ram");
|
|
for (var iRAM = 0; iRAM < aeRAM.length; iRAM++) {
|
|
var eRAM = aeRAM[iRAM];
|
|
var parmsRAM = Component.getComponentParms(eRAM);
|
|
var ram = new RAM(parmsRAM);
|
|
Component.bindComponentControls(ram, eRAM, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize all the RAM modules on the page.
|
|
*/
|
|
web.onInit(RAM.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/keyboard.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Keyboard component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-19
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Keyboard(parmsKbd)
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsKbd
|
|
*/
|
|
function Keyboard(parmsKbd)
|
|
{
|
|
Component.call(this, "Keyboard", parmsKbd, Keyboard, Messages.KEYBOARD);
|
|
|
|
this.reset();
|
|
|
|
this.setReady();
|
|
}
|
|
|
|
Component.subclass(Keyboard);
|
|
|
|
/**
|
|
* Alphanumeric and other common (printable) ASCII codes.
|
|
*
|
|
* TODO: Determine what we can do to get ALL constants like these inlined (enum doesn't seem to
|
|
* get the job done); the problem seems to be limited to property references that use quotes, which
|
|
* is why I've 'unquoted' as many of them as possible.
|
|
*
|
|
* @enum {number}
|
|
*/
|
|
Keyboard.ASCII = {
|
|
CTRL_A: 1, CTRL_C: 3, CTRL_Z: 26,
|
|
' ': 32, '!': 33, '"': 34, '#': 35, '$': 36, '%': 37, '&': 38, "'": 39,
|
|
'(': 40, ')': 41, '*': 42, '+': 43, ',': 44, '-': 45, '.': 46, '/': 47,
|
|
'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55,
|
|
'8': 56, '9': 57, ':': 58, ';': 59, '<': 60, '=': 61, '>': 62, '?': 63,
|
|
'@': 64, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71,
|
|
H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79,
|
|
P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87,
|
|
X: 88, Y: 89, Z: 90, '[': 91, '\\':92, ']': 93, '^': 94, '_': 95,
|
|
'`': 96, a: 97, b: 98, c: 99, d: 100, e: 101, f: 102, g: 103,
|
|
h: 104, i: 105, j: 106, k: 107, l: 108, m: 109, n: 110, o: 111,
|
|
p: 112, q: 113, r: 114, s: 115, t: 116, u: 117, v: 118, w: 119,
|
|
x: 120, y: 121, z: 122, '{':123, '|':124, '}':125, '~':126
|
|
};
|
|
|
|
/**
|
|
* Browser keyCodes we must pay particular attention to. For the most part, these are non-alphanumeric
|
|
* or function keys, some which may require special treatment (eg, preventDefault() if returning false on
|
|
* the initial keyDown event is insufficient).
|
|
*
|
|
* keyCodes for most common ASCII keys can simply use the appropriate ASCII code above.
|
|
*
|
|
* Most of these represent non-ASCII keys (eg, the LEFT arrow key), yet for some reason, browsers defined
|
|
* them using ASCII codes (eg, the LEFT arrow key uses the ASCII code for '%' or 37). This conflict is
|
|
* discussed further in the definition of CLICKCODE below.
|
|
*
|
|
* @enum {number}
|
|
*/
|
|
Keyboard.KEYCODE = {
|
|
/* 0x08 */ BS: 8,
|
|
/* 0x09 */ TAB: 9,
|
|
/* 0x0A */ LF: 10,
|
|
/* 0x0D */ CR: 13,
|
|
/* 0x10 */ SHIFT: 16,
|
|
/* 0x11 */ CTRL: 17,
|
|
/* 0x12 */ ALT: 18,
|
|
/* 0x13 */ PAUSE: 19, // PAUSE/BREAK
|
|
/* 0x14 */ CAPS_LOCK: 20,
|
|
/* 0x1B */ ESC: 27,
|
|
/* 0x20 */ SPACE: 32,
|
|
/* 0x21 */ PGUP: 33,
|
|
/* 0x22 */ PGDN: 34,
|
|
/* 0x23 */ END: 35,
|
|
/* 0x24 */ HOME: 36,
|
|
/* 0x25 */ LEFT: 37,
|
|
/* 0x26 */ UP: 38,
|
|
/* 0x27 */ RIGHT: 39,
|
|
/* 0x27 */ FF_QUOTE: 39,
|
|
/* 0x28 */ DOWN: 40,
|
|
/* 0x2C */ FF_COMMA: 44,
|
|
/* 0x2C */ PRTSC: 44,
|
|
/* 0x2D */ INS: 45,
|
|
/* 0x2E */ DEL: 46,
|
|
/* 0x2E */ FF_PERIOD: 46,
|
|
/* 0x2F */ FF_SLASH: 47,
|
|
/* 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_INS: 96, // 0
|
|
/* 0x60 */ FF_BQUOTE: 96,
|
|
/* 0x61 */ NUM_END: 97, // 1
|
|
/* 0x62 */ NUM_DOWN: 98, // 2
|
|
/* 0x63 */ NUM_PGDN: 99, // 3
|
|
/* 0x64 */ NUM_LEFT: 100, // 4
|
|
/* 0x65 */ NUM_CENTER: 101, // 5
|
|
/* 0x66 */ NUM_RIGHT: 102, // 6
|
|
/* 0x67 */ NUM_HOME: 103, // 7
|
|
/* 0x68 */ NUM_UP: 104, // 8
|
|
/* 0x69 */ NUM_PGUP: 105, // 9
|
|
/* 0x6A */ NUM_MUL: 106,
|
|
/* 0x6B */ NUM_ADD: 107,
|
|
/* 0x6D */ NUM_SUB: 109,
|
|
/* 0x6E */ NUM_DEL: 110, // .
|
|
/* 0x6F */ NUM_DIV: 111,
|
|
/* 0x70 */ F1: 112,
|
|
/* 0x71 */ F2: 113,
|
|
/* 0x72 */ F3: 114,
|
|
/* 0x73 */ F4: 115,
|
|
/* 0x74 */ F5: 116,
|
|
/* 0x75 */ F6: 117,
|
|
/* 0x76 */ F7: 118,
|
|
/* 0x77 */ F8: 119,
|
|
/* 0x78 */ F9: 120,
|
|
/* 0x79 */ F10: 121,
|
|
/* 0x7A */ F11: 122,
|
|
/* 0x7B */ F12: 123,
|
|
/* 0x90 */ NUM_LOCK: 144,
|
|
/* 0x91 */ SCROLL_LOCK: 145,
|
|
/* 0xAD */ FF_DASH: 173,
|
|
/* 0xBA */ SEMI: 186, // Firefox: 59
|
|
/* 0xBB */ EQUALS: 187, // Firefox: 61
|
|
/* 0xBC */ COMMA: 188, // Firefox: 44
|
|
/* 0xBD */ DASH: 189, // Firefox: 173
|
|
/* 0xBE */ PERIOD: 190, // Firefox: 46
|
|
/* 0xBF */ SLASH: 191, // Firefox: 47
|
|
/* 0xC0 */ BQUOTE: 192, // Firefox: 96
|
|
/* 0xDB */ LBRACK: 219, // Firefox: 91
|
|
/* 0xDC */ BSLASH: 220, // Firefox: 92
|
|
/* 0xDD */ RBRACK: 221, // Firefox: 93
|
|
/* 0xDE */ QUOTE: 222, // Firefox: 39
|
|
/* 0xE0 */ FF_CMD: 224, // Firefox only (used for both CMD and RCMD)
|
|
//
|
|
// The following biases use what I'll call Decimal Coded Binary or DCB (the opposite of BCD),
|
|
// where the thousands digit is used to store the sum of "binary" digits 1 and/or 2 and/or 4.
|
|
//
|
|
// Technically, that makes it DCO (Decimal Coded Octal), but then again, BCD should have really
|
|
// been called HCD (Hexadecimal Coded Decimal), so if "they" can take liberties, so can I.
|
|
//
|
|
// ONDOWN is a bias we add to browser keyCodes that we want to handle on "down" rather than on "press".
|
|
//
|
|
ONDOWN: 1000,
|
|
//
|
|
// ONRIGHT is a bias we add to browser keyCodes that need to check for a "right" location (default is "left")
|
|
//
|
|
ONRIGHT: 2000,
|
|
//
|
|
// FAKE is a bias we add to signal these are fake keyCodes corresponding to internal keystroke combinations.
|
|
// The actual values are for internal use only and merely need to be unique and used consistently.
|
|
//
|
|
FAKE: 4000
|
|
};
|
|
|
|
/*
|
|
* Maps "stupid" keyCodes to their "non-stupid" counterparts
|
|
*/
|
|
Keyboard.STUPID_KEYCODES = {};
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.SEMI] = Keyboard.ASCII[';']; // 186 -> 59
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.EQUALS] = Keyboard.ASCII['=']; // 187 -> 61
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.COMMA] = Keyboard.ASCII[',']; // 188 -> 44
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.DASH] = Keyboard.ASCII['-']; // 189 -> 45
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.PERIOD] = Keyboard.ASCII['.']; // 190 -> 46
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.SLASH] = Keyboard.ASCII['/']; // 191 -> 47
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.BQUOTE] = Keyboard.ASCII['`']; // 192 -> 96
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.LBRACK] = Keyboard.ASCII['[']; // 219 -> 91
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.BSLASH] = Keyboard.ASCII['\\']; // 220 -> 92
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.RBRACK] = Keyboard.ASCII[']']; // 221 -> 93
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.QUOTE] = Keyboard.ASCII["'"]; // 222 -> 39
|
|
Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.FF_DASH] = Keyboard.ASCII['-'];
|
|
|
|
/**
|
|
* Maps SOFTCODE (string) to KEYCODE (number).
|
|
*
|
|
* @enum {number}
|
|
*/
|
|
Keyboard.SOFTCODES = {
|
|
'1p': Keyboard.KEYCODE.ONE,
|
|
'2p': Keyboard.KEYCODE.TWO,
|
|
'coin': Keyboard.KEYCODE.THREE,
|
|
'left': Keyboard.KEYCODE.LEFT,
|
|
'right': Keyboard.KEYCODE.RIGHT,
|
|
'fire': Keyboard.KEYCODE.SPACE
|
|
};
|
|
|
|
/**
|
|
* Alternate keyCode mappings (to support the popular WASD directional 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.
|
|
*/
|
|
Keyboard.ALTCODES = {};
|
|
Keyboard.ALTCODES[Keyboard.ASCII.A] = Keyboard.KEYCODE.LEFT;
|
|
Keyboard.ALTCODES[Keyboard.ASCII.D] = Keyboard.KEYCODE.RIGHT;
|
|
Keyboard.ALTCODES[Keyboard.ASCII.L] = Keyboard.KEYCODE.SPACE;
|
|
|
|
/**
|
|
* getSoftCode(keyCode)
|
|
*
|
|
* @this {Keyboard}
|
|
* @return {string|null}
|
|
*/
|
|
Keyboard.prototype.getSoftCode = function(keyCode)
|
|
{
|
|
keyCode = Keyboard.ALTCODES[keyCode] || keyCode;
|
|
for (var sSoftCode in Keyboard.SOFTCODES) {
|
|
if (Keyboard.SOFTCODES[sSoftCode] === keyCode) {
|
|
return sSoftCode;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {Keyboard}
|
|
*/
|
|
Keyboard.prototype.reset = function()
|
|
{
|
|
/*
|
|
* As SOFTCODE keyDown events are encountered, a corresponding property is set to true in
|
|
* keysPressed, and as SOFTCODE keyUp events are encountered, the property is set to false.
|
|
*/
|
|
this.keysPressed = {};
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {Keyboard}
|
|
* @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
|
|
*/
|
|
Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
/*
|
|
* There's a special binding that the Video component uses ("kbd") to effectively bind its
|
|
* screen to the entire keyboard, in Video.powerUp(); ie:
|
|
*
|
|
* video.kbd.setBinding("canvas", "kbd", video.canvasScreen);
|
|
* or:
|
|
* video.kbd.setBinding("textarea", "kbd", 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">Kbd</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) {
|
|
switch (sBinding) {
|
|
case "kbd":
|
|
/*
|
|
* 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 "kbd", so that it doesn't matter which
|
|
* display the user gives focus to.
|
|
*
|
|
* this.bindings[id] = control;
|
|
*/
|
|
control.onkeydown = function onKeyDown(event) {
|
|
return kbd.onKeyDown(event, true);
|
|
};
|
|
control.onkeyup = function onKeyUp(event) {
|
|
return kbd.onKeyDown(event, false);
|
|
};
|
|
return true;
|
|
|
|
default:
|
|
if (Keyboard.SOFTCODES[sBinding] !== undefined) {
|
|
this.bindings[id] = control;
|
|
var fnDown = function(kbd, sSoftCode) {
|
|
return function onMouseOrTouchDownKeyboard(event) {
|
|
kbd.onSoftKeyDown(sSoftCode, true);
|
|
};
|
|
}(this, sBinding);
|
|
var fnUp = function (kbd, sSoftCode) {
|
|
return function onMouseOrTouchUpKeyboard(event) {
|
|
kbd.onSoftKeyDown(sSoftCode, false);
|
|
};
|
|
}(this, sBinding);
|
|
if ('ontouchstart' in window) {
|
|
control.ontouchstart = fnDown;
|
|
control.ontouchend = fnUp;
|
|
} else {
|
|
control.onmousedown = fnDown;
|
|
control.onmouseup = control.onmouseout = fnUp;
|
|
}
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {Keyboard}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
Keyboard.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
|
|
this.chipset = cmp.getMachineComponent("ChipSet");
|
|
};
|
|
|
|
/**
|
|
* onKeyDown(event, fDown)
|
|
*
|
|
* @this {Keyboard}
|
|
* @param {Object} event
|
|
* @param {boolean} fDown is true for a keyDown event, false for a keyUp event
|
|
* @return {boolean} true to pass the event along, false to consume it
|
|
*/
|
|
Keyboard.prototype.onKeyDown = function(event, fDown)
|
|
{
|
|
var fPass = true;
|
|
var keyCode = event.keyCode;
|
|
var sSoftCode = this.getSoftCode(keyCode);
|
|
|
|
if (sSoftCode) {
|
|
fPass = this.onSoftKeyDown(sSoftCode, fDown);
|
|
}
|
|
|
|
if (!fPass) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
if (!COMPILED && this.messageEnabled(Messages.KEYS)) {
|
|
this.printMessage("onKey" + (fDown? "Down" : "Up") + "(" + keyCode + "): " + (fPass? "true" : "false"), true);
|
|
}
|
|
|
|
return fPass;
|
|
};
|
|
|
|
/**
|
|
* onSoftKeyDown(sSoftCode, fDown)
|
|
*
|
|
* @this {Keyboard}
|
|
* @param {string} sSoftCode
|
|
* @param {boolean} fDown is true for a down event, false for an up event
|
|
* @return {boolean} true to pass the event along, false to consume it
|
|
*/
|
|
Keyboard.prototype.onSoftKeyDown = function(sSoftCode, fDown)
|
|
{
|
|
this.keysPressed[sSoftCode] = fDown;
|
|
|
|
if (this.chipset) {
|
|
switch(sSoftCode) {
|
|
case '1p':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1, fDown);
|
|
break;
|
|
|
|
case '2p':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P2, fDown);
|
|
break;
|
|
|
|
case 'coin':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.CREDIT, fDown);
|
|
break;
|
|
|
|
case 'left':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_LEFT, fDown);
|
|
break;
|
|
|
|
case 'right':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_RIGHT, fDown);
|
|
break;
|
|
|
|
case 'fire':
|
|
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_FIRE, fDown);
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Keyboard.init()
|
|
*
|
|
* This function operates on every HTML element of class "keyboard", extracting the
|
|
* JSON-encoded parameters for the Keyboard constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a Keyboard component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
Keyboard.init = function()
|
|
{
|
|
var aeKbd = Component.getElementsByClass(document, PC8080.APPCLASS, "keyboard");
|
|
for (var iKbd = 0; iKbd < aeKbd.length; iKbd++) {
|
|
var eKbd = aeKbd[iKbd];
|
|
var parmsKbd = Component.getComponentParms(eKbd);
|
|
var kbd = new Keyboard(parmsKbd);
|
|
Component.bindComponentControls(kbd, eKbd, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Keyboard module on the page.
|
|
*/
|
|
web.onInit(Keyboard.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/video.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Video component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-20
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Video(parmsVideo, canvas, context, textarea, container)
|
|
*
|
|
* The Video component can be configured with the following (parmsVideo) properties:
|
|
*
|
|
* screenWidth: width of the screen canvas, in pixels
|
|
* screenHeight: height of the screen canvas, in pixels
|
|
* screenColor: background color of the screen canvas (default is black)
|
|
* screenRotate: the amount of counter-clockwise screen rotation required (eg, -90 or 270)
|
|
* aspectRatio (eg, 1.33)
|
|
* bufferAddr: the starting address of the frame buffer (eg, 0x2400)
|
|
* bufferRAM: true to use existing RAM (default is false)
|
|
* bufferFormat: if defined, one of the recognized formats in Video.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)
|
|
*
|
|
* We record all the above values now, but we defer creation of the frame buffer until our initBus()
|
|
* handler is called. At that point, we will also compute the extent of the frame buffer, determine the
|
|
* appropriate "cell" size (ie, the number of pixels that updateScreen() will fetch and process at once),
|
|
* and then allocate our cell cache.
|
|
*
|
|
* Why interruptRate in addition to refreshRate? A higher interrupt rate is required for Space Invaders,
|
|
* because even though the CRT refreshes at 60Hz, the CRT controller interrupts the CPU *twice* per
|
|
* refresh (once after the top half of the screen has been redrawn, and again after the bottom half has
|
|
* been redrawn), so we need an interrupt rate of 120Hz. We pass the higher rate on to the CPU, so that
|
|
* it will call updateScreen() more frequently, but we still limit our screen updates to every *other* call.
|
|
*
|
|
* bufferRotate is an alternative to screenRotate; you may set one or the other (but not both) to -90 to
|
|
* enable different approaches to counter-clockwise 90-degree image rotation. screenRotate uses canvas
|
|
* transformation methods (translate(), rotate(), and scale()), while bufferRotate inverts the dimensions
|
|
* of the off-screen buffer and then relies on setPixel() to "rotate" the data into it.
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsVideo
|
|
* @param {Object} [canvas]
|
|
* @param {Object} [context]
|
|
* @param {Object} [textarea]
|
|
* @param {Object} [container]
|
|
*/
|
|
function Video(parmsVideo, canvas, context, textarea, container)
|
|
{
|
|
var video = this;
|
|
this.fGecko = web.isUserAgent("Gecko/");
|
|
var i, sEvent, asWebPrefixes = ['', 'moz', 'ms', 'webkit'];
|
|
|
|
Component.call(this, "Video", parmsVideo, Video, Messages.VIDEO);
|
|
|
|
this.cxScreen = parmsVideo['screenWidth'];
|
|
this.cyScreen = parmsVideo['screenHeight'];
|
|
|
|
this.addrBuffer = parmsVideo['bufferAddr'];
|
|
this.fUseRAM = parmsVideo['bufferRAM'];
|
|
|
|
var sFormat = parmsVideo['bufferFormat'];
|
|
this.nFormat = sFormat && Video.FORMATS[sFormat.toLowerCase()] || Video.FORMAT.UNKNOWN;
|
|
|
|
this.cxBuffer = parmsVideo['bufferCols'];
|
|
this.cyBuffer = parmsVideo['bufferRows'];
|
|
this.nBitsPerPixel = parmsVideo['bufferBits'] || 1;
|
|
this.iBitFirstPixel = parmsVideo['bufferLeft'] || 0;
|
|
this.rotateBuffer = parmsVideo['bufferRotate'];
|
|
if (this.rotateBuffer) {
|
|
this.rotateBuffer = this.rotateBuffer % 360;
|
|
if (this.rotateBuffer > 0) this.rotateBuffer -= 360;
|
|
if (this.rotateBuffer != -90) {
|
|
this.notice("unsupported buffer rotation: " + this.rotateBuffer);
|
|
this.rotateBuffer = 0;
|
|
}
|
|
}
|
|
|
|
this.interruptRate = parmsVideo['interruptRate'];
|
|
this.refreshRate = parmsVideo['refreshRate'] || 60;
|
|
|
|
this.canvasScreen = canvas;
|
|
this.contextScreen = context;
|
|
this.textareaScreen = textarea;
|
|
this.inputScreen = textarea || canvas || null;
|
|
|
|
/*
|
|
* Support for disabling (or, less commonly, enabling) image smoothing, which all browsers
|
|
* seem to support now (well, OK, I still have to test the latest MS Edge browser), despite
|
|
* it still being labelled "experimental technology". Let's hope the browsers standardize
|
|
* on this. I see other options emerging, like the CSS property "image-rendering: pixelated"
|
|
* that's apparently been added to Chrome. Sigh.
|
|
*/
|
|
var fSmoothing = parmsVideo['smoothing'];
|
|
var sSmoothing = Component.parmsURL['smoothing'];
|
|
if (sSmoothing) fSmoothing = (sSmoothing == "true");
|
|
if (fSmoothing != null) {
|
|
for (i = 0; i < asWebPrefixes.length; i++) {
|
|
sEvent = asWebPrefixes[i];
|
|
if (!sEvent) {
|
|
sEvent = 'imageSmoothingEnabled';
|
|
} else {
|
|
sEvent += 'ImageSmoothingEnabled';
|
|
}
|
|
if (this.contextScreen[sEvent] !== undefined) {
|
|
this.contextScreen[sEvent] = fSmoothing;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.rotateScreen = parmsVideo['screenRotate'];
|
|
if (this.rotateScreen) {
|
|
this.rotateScreen = this.rotateScreen % 360;
|
|
if (this.rotateScreen > 0) this.rotateScreen -= 360;
|
|
if (this.rotateScreen != -90) {
|
|
this.notice("unsupported screen rotation: " + this.rotateScreen);
|
|
this.rotateScreen = 0;
|
|
} else {
|
|
this.contextScreen.translate(0, this.cyScreen);
|
|
this.contextScreen.rotate((this.rotateScreen * Math.PI)/180);
|
|
this.contextScreen.scale(this.cyScreen/this.cxScreen, this.cxScreen/this.cyScreen);
|
|
}
|
|
}
|
|
|
|
this.initColors();
|
|
|
|
/*
|
|
* Allocate off-screen buffers.
|
|
*/
|
|
var cxBuffer = this.cxBuffer;
|
|
var cyBuffer = this.cyBuffer;
|
|
if (this.rotateBuffer) {
|
|
cxBuffer = this.cyBuffer;
|
|
cyBuffer = this.cxBuffer;
|
|
}
|
|
this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer);
|
|
this.canvasBuffer = document.createElement("canvas");
|
|
this.canvasBuffer.width = cxBuffer;
|
|
this.canvasBuffer.height = cyBuffer;
|
|
this.contextBuffer = this.canvasBuffer.getContext("2d");
|
|
|
|
/*
|
|
* Here's the gross code to handle full-screen support across all supported browsers. The lack of standards
|
|
* is exasperating; browsers can't agree on 'full' or 'Full, 'request' or 'Request', 'screen' or 'Screen', and
|
|
* while some browsers honor other browser prefixes, most browsers don't.
|
|
*/
|
|
this.container = container;
|
|
if (this.container) {
|
|
this.container.doFullScreen = container['requestFullscreen'] || container['msRequestFullscreen'] || container['mozRequestFullScreen'] || container['webkitRequestFullscreen'];
|
|
if (this.container.doFullScreen) {
|
|
for (i = 0; i < asWebPrefixes.length; i++) {
|
|
sEvent = asWebPrefixes[i] + 'fullscreenchange';
|
|
if ('on' + sEvent in document) {
|
|
var onFullScreenChange = function() {
|
|
var fFullScreen = (document['fullscreenElement'] || document['msFullscreenElement'] || document['mozFullScreenElement'] || document['webkitFullscreenElement']);
|
|
video.notifyFullScreen(fFullScreen? true : false);
|
|
};
|
|
document.addEventListener(sEvent, onFullScreenChange, false);
|
|
break;
|
|
}
|
|
}
|
|
for (i = 0; i < asWebPrefixes.length; i++) {
|
|
sEvent = asWebPrefixes[i] + 'fullscreenerror';
|
|
if ('on' + sEvent in document) {
|
|
var onFullScreenError = function() {
|
|
video.notifyFullScreen(null);
|
|
};
|
|
document.addEventListener(sEvent, onFullScreenError, false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DEBUG) this.nCyclesPrev = 0;
|
|
}
|
|
|
|
Component.subclass(Video);
|
|
|
|
Video.COLORS = {
|
|
OVERLAY_TOP: 0,
|
|
OVERLAY_BOTTOM: 1,
|
|
OVERLAY_TOTAL: 2
|
|
};
|
|
|
|
Video.FORMAT = {
|
|
UNKNOWN: 0,
|
|
SI1978: 1,
|
|
VT100: 2
|
|
};
|
|
|
|
Video.FORMATS = {
|
|
"vt100": Video.FORMAT.VT100
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* @this {Video}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
Video.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.cmp = cmp;
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
|
|
if (!this.nFormat) {
|
|
this.chipset = cmp.getMachineComponent("ChipSet");
|
|
if (this.chipset && this.chipset.model == ChipSet.SI_1978.MODEL) {
|
|
this.nFormat = Video.FORMAT.SI1978;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Compute the size of the frame buffer and allocate.
|
|
*/
|
|
if (!this.fUseRAM) {
|
|
this.sizeBuffer = ((this.cxBuffer * this.nBitsPerPixel) >> 3) * this.cyBuffer;
|
|
if (this.bus.addMemory(this.addrBuffer, this.sizeBuffer, Memory.TYPE.VIDEO)) {
|
|
/*
|
|
* Compute the number of cells and initialize the cell cache; note that sizeBuffer is a number of
|
|
* bytes, whereas nCellCache is a number of 16-bit words (aka shorts) because we fetch memory 16 bits
|
|
* at a time during screen updates.
|
|
*/
|
|
this.nCellCache = this.sizeBuffer >> 1;
|
|
this.nPixelsPerCell = (16 / this.nBitsPerPixel)|0;
|
|
this.initCache();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If we have an associated keyboard, then ensure that the keyboard will be notified whenever the canvas
|
|
* gets focus and receives input.
|
|
*/
|
|
this.kbd = cmp.getMachineComponent("Keyboard");
|
|
if (this.kbd && this.canvasScreen) {
|
|
this.kbd.setBinding(this.textareaScreen? "textarea" : "canvas", "kbd", this.inputScreen);
|
|
}
|
|
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {Video}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "refresh")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Video.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var video = this;
|
|
|
|
switch (sBinding) {
|
|
case "fullScreen":
|
|
this.bindings[sBinding] = control;
|
|
if (this.container && this.container.doFullScreen) {
|
|
control.onclick = function onClickFullScreen() {
|
|
if (DEBUG) video.printMessage("fullScreen()");
|
|
video.doFullScreen();
|
|
};
|
|
} else {
|
|
if (DEBUG) this.log("FullScreen API not available");
|
|
control.parentNode.removeChild(/** @type {Node} */ (control));
|
|
}
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* doFullScreen()
|
|
*
|
|
* @this {Video}
|
|
* @return {boolean} true if request successful, false if not (eg, failed OR not supported)
|
|
*/
|
|
Video.prototype.doFullScreen = function()
|
|
{
|
|
var fSuccess = false;
|
|
if (this.container) {
|
|
if (this.container.doFullScreen) {
|
|
/*
|
|
* Styling the container with a width of "100%" and a height of "auto" works great when the aspect ratio
|
|
* of our virtual screen is at least roughly equivalent to the physical screen's aspect ratio, but now that
|
|
* we support virtual VGA screens with an aspect ratio of 1.33, that's very much out of step with modern
|
|
* wide-screen monitors, which usually have an aspect ratio of 1.6 or greater.
|
|
*
|
|
* And unfortunately, none of the browsers I've tested appear to make any attempt to scale our container to
|
|
* the physical screen's dimensions, so the bottom of our screen gets clipped. To prevent that, I reduce
|
|
* the width from 100% to whatever percentage will accommodate the entire height of the virtual screen.
|
|
*
|
|
* NOTE: Mozilla recommends both a width and a height of "100%", but all my tests suggest that using "auto"
|
|
* for height works equally well, so I'm sticking with it, because "auto" is also consistent with how I've
|
|
* implemented a responsive canvas when the browser window is being resized.
|
|
*/
|
|
var sWidth = "100%";
|
|
var sHeight = "auto";
|
|
if (screen && screen.width && screen.height) {
|
|
var aspectPhys = screen.width / screen.height;
|
|
var aspectVirt = this.cxScreen / this.cyScreen;
|
|
if (aspectPhys > aspectVirt) {
|
|
sWidth = Math.round(aspectVirt / aspectPhys * 100) + '%';
|
|
}
|
|
// TODO: We may need to someday consider the case of a physical screen with an aspect ratio < 1.0....
|
|
}
|
|
if (!this.fGecko) {
|
|
this.container.style.width = sWidth;
|
|
this.container.style.height = sHeight;
|
|
} else {
|
|
/*
|
|
* Sadly, the above code doesn't work for Firefox, because as http://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode
|
|
* explains:
|
|
*
|
|
* 'It's worth noting a key difference here between the Gecko and WebKit implementations at this time:
|
|
* Gecko automatically adds CSS rules to the element to stretch it to fill the screen: "width: 100%; height: 100%".
|
|
*
|
|
* Which would be OK if Gecko did that BEFORE we're called, but apparently it does that AFTER, effectively
|
|
* overwriting our careful calculations. So we style the inner element (canvasScreen) instead, which
|
|
* requires even more work to ensure that the canvas is properly centered. FYI, this solution is consistent
|
|
* with Mozilla's recommendation for working around their automatic CSS rules:
|
|
*
|
|
* '[I]f you're trying to emulate WebKit's behavior on Gecko, you need to place the element you want
|
|
* to present inside another element, which you'll make fullscreen instead, and use CSS rules to adjust
|
|
* the inner element to match the appearance you want.'
|
|
*/
|
|
this.canvasScreen.style.width = sWidth;
|
|
this.canvasScreen.style.width = sWidth;
|
|
this.canvasScreen.style.display = "block";
|
|
this.canvasScreen.style.margin = "auto";
|
|
}
|
|
this.container.style.backgroundColor = "black";
|
|
this.container.doFullScreen();
|
|
fSuccess = true;
|
|
}
|
|
this.setFocus();
|
|
}
|
|
return fSuccess;
|
|
};
|
|
|
|
/**
|
|
* notifyFullScreen(fFullScreen)
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean|null} fFullScreen (null if there was a full-screen error)
|
|
*/
|
|
Video.prototype.notifyFullScreen = function(fFullScreen)
|
|
{
|
|
if (!fFullScreen && this.container) {
|
|
if (!this.fGecko) {
|
|
this.container.style.width = this.container.style.height = "";
|
|
} else {
|
|
this.canvasScreen.style.width = this.canvasScreen.style.height = "";
|
|
}
|
|
}
|
|
this.printMessage("notifyFullScreen(" + fFullScreen + ")", true);
|
|
};
|
|
|
|
/**
|
|
* setFocus()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.setFocus = function()
|
|
{
|
|
if (this.inputScreen) this.inputScreen.focus();
|
|
};
|
|
|
|
/**
|
|
* getRefreshRate()
|
|
*
|
|
* @this {Video}
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.getRefreshRate = function()
|
|
{
|
|
return Math.max(this.refreshRate, this.interruptRate);
|
|
};
|
|
|
|
/**
|
|
* initCache()
|
|
*
|
|
* Initializes the contents of our internal cell cache.
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.initCache = function()
|
|
{
|
|
this.fCellCacheValid = false;
|
|
if (this.aCellCache === undefined || this.aCellCache.length != this.nCellCache) {
|
|
this.aCellCache = new Array(this.nCellCache);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* initColors()
|
|
*
|
|
* This creates an array of nColors, with additional OVERLAY_TOTAL colors tacked on to the end of the array.
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.initColors = function()
|
|
{
|
|
var rgbBlack = [0x00, 0x00, 0x00, 0xff];
|
|
var rgbWhite = [0xff, 0xff, 0xff, 0xff];
|
|
this.nColors = (1 << this.nBitsPerPixel);
|
|
this.aRGB = new Array(this.nColors + Video.COLORS.OVERLAY_TOTAL);
|
|
this.aRGB[0] = rgbBlack;
|
|
this.aRGB[1] = rgbWhite;
|
|
if (this.nFormat == Video.FORMAT.SI1978) {
|
|
var rgbGreen = [0x00, 0xff, 0x00, 0xff];
|
|
var rgbYellow = [0xff, 0xff, 0x00, 0xff];
|
|
this.aRGB[this.nColors + Video.COLORS.OVERLAY_TOP] = rgbYellow;
|
|
this.aRGB[this.nColors + Video.COLORS.OVERLAY_BOTTOM] = rgbGreen;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setPixel(imageBuffer, x, y, bPixel)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} imageBuffer
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {number} bPixel (ie, an index into aRGB)
|
|
*/
|
|
Video.prototype.setPixel = function(imageBuffer, x, y, bPixel)
|
|
{
|
|
var index;
|
|
if (!this.rotateBuffer) {
|
|
index = (x + y * imageBuffer.width);
|
|
} else {
|
|
index = (imageBuffer.height - x - 1) * imageBuffer.width + y;
|
|
}
|
|
if (bPixel && this.nFormat == Video.FORMAT.SI1978) {
|
|
if (x >= 208 && x < 236) {
|
|
bPixel = this.nColors + Video.COLORS.OVERLAY_TOP;
|
|
}
|
|
else if (x >= 28 && x < 72) {
|
|
bPixel = this.nColors + Video.COLORS.OVERLAY_BOTTOM;
|
|
}
|
|
}
|
|
var rgb = this.aRGB[bPixel];
|
|
index *= rgb.length;
|
|
imageBuffer.data[index] = rgb[0];
|
|
imageBuffer.data[index+1] = rgb[1];
|
|
imageBuffer.data[index+2] = rgb[2];
|
|
imageBuffer.data[index+3] = rgb[3];
|
|
};
|
|
|
|
/**
|
|
* updateScreen(n)
|
|
*
|
|
* Propagates the video buffer to the cell cache and updates the screen with any changes. Forced updates
|
|
* are generally internal updates triggered by an I/O operation or other state change, while non-forced updates
|
|
* are the periodic updates coming from the CPU.
|
|
*
|
|
* For every cell in the video buffer, compare it to the cell stored in the cell cache, render if it differs,
|
|
* and then update the cell cache to match. Since initCache() sets every cell in the cell cache to an
|
|
* invalid value, we're assured that the next call to updateScreen() will redraw the entire (visible) video buffer.
|
|
*
|
|
* @this {Video}
|
|
* @param {number} n (where 0 <= n < getRefreshRate() for a normal update, or -1 for a forced update)
|
|
*/
|
|
Video.prototype.updateScreen = function(n)
|
|
{
|
|
var fClean;
|
|
var fUpdate = true;
|
|
|
|
if (n >= 0) {
|
|
if (!(n & 1)) {
|
|
/*
|
|
* On even updates, call cpu.requestINTR(1), and also update our copy of the screen.
|
|
*/
|
|
this.cpu.requestINTR(1);
|
|
} else {
|
|
/*
|
|
* On odd updates, call cpu.requestINTR(2), but do NOT update our copy of the screen, because
|
|
* the machine has presumably only updated the top half of the frame buffer at this point; it will
|
|
* update the bottom half of the frame buffer after acknowledging this interrupt.
|
|
*/
|
|
this.cpu.requestINTR(2);
|
|
fUpdate = false;
|
|
}
|
|
|
|
/*
|
|
* Since this is not a forced update, if our cell cache is valid AND the buffer is clean, then do nothing.
|
|
*/
|
|
if (fUpdate && this.fCellCacheValid) {
|
|
if ((fClean = this.bus.cleanMemory(this.addrBuffer, this.sizeBuffer))) {
|
|
fUpdate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (DEBUG) {
|
|
var nCycles = this.cpu.getCycles();
|
|
var nCyclesDelta = nCycles - this.nCyclesPrev;
|
|
this.nCyclesPrev = nCycles;
|
|
this.printMessage("updateScreen(" + n + "): clean=" + fClean + ", update=" + fUpdate + ", cycles=" + nCycles + ", delta=" + nCyclesDelta);
|
|
}
|
|
if (!fUpdate) return;
|
|
|
|
var addr = this.addrBuffer;
|
|
var addrLimit = addr + this.sizeBuffer;
|
|
|
|
var iCell = 0;
|
|
var nPixelShift = 1;
|
|
|
|
var xBuffer = 0, yBuffer = 0;
|
|
var xDirty = this.cxBuffer, xMaxDirty = 0, yDirty = this.cyBuffer, yMaxDirty = 0;
|
|
|
|
var nShiftInit = 0;
|
|
var nShiftPixel = this.nBitsPerPixel;
|
|
var nMask = (1 << nShiftPixel) - 1;
|
|
if (this.iBitFirstPixel) {
|
|
nShiftPixel = -nShiftPixel;
|
|
nShiftInit = 16 + nShiftPixel;
|
|
}
|
|
|
|
while (addr < addrLimit) {
|
|
var data = this.bus.getShortDirect(addr);
|
|
|
|
if (this.fCellCacheValid && data === this.aCellCache[iCell]) {
|
|
xBuffer += this.nPixelsPerCell;
|
|
} else {
|
|
this.aCellCache[iCell] = data;
|
|
var nShift = nShiftInit;
|
|
if (nShift) data = ((data >> 8) | ((data & 0xff) << 8));
|
|
if (xBuffer < xDirty) xDirty = xBuffer;
|
|
var cPixels = this.nPixelsPerCell;
|
|
while (cPixels--) {
|
|
var bPixel = (data >> nShift) & nMask;
|
|
this.setPixel(this.imageBuffer, xBuffer++, yBuffer, bPixel);
|
|
nShift += nShiftPixel;
|
|
}
|
|
if (xBuffer > xMaxDirty) xMaxDirty = xBuffer;
|
|
if (yBuffer < yDirty) yDirty = yBuffer;
|
|
if (yBuffer >= yMaxDirty) yMaxDirty = yBuffer + 1;
|
|
}
|
|
addr += 2; iCell++;
|
|
if (xBuffer >= this.cxBuffer) {
|
|
xBuffer = 0; yBuffer++;
|
|
if (yBuffer > this.cyBuffer) break;
|
|
}
|
|
}
|
|
|
|
this.fCellCacheValid = true;
|
|
|
|
/*
|
|
* Instead of blasting the ENTIRE imageBuffer into contextBuffer, and then blasting the ENTIRE
|
|
* canvasBuffer onto contextScreen, even for the smallest change, let's try to be a bit smarter about
|
|
* the update (well, to the extent that the canvas APIs permit).
|
|
*/
|
|
if (xDirty < this.cxBuffer) {
|
|
var cxDirty = xMaxDirty - xDirty;
|
|
var cyDirty = yMaxDirty - yDirty;
|
|
if (this.rotateBuffer) {
|
|
/*
|
|
* If rotateBuffer is set, then it must be -90, so we must "rotate" the dirty coordinates as well,
|
|
* because they are relative to the frame buffer, not the rotated image buffer. Alternatively, you
|
|
* can use the following call to blast the ENTIRE imageBuffer into contextBuffer instead:
|
|
*
|
|
* this.contextBuffer.putImageData(this.imageBuffer, 0, 0);
|
|
*/
|
|
var xDirtyOrig = xDirty, cxDirtyOrig = cxDirty;
|
|
//noinspection JSSuspiciousNameCombination
|
|
xDirty = yDirty;
|
|
cxDirty = cyDirty;
|
|
yDirty = this.cxBuffer - (xDirtyOrig + cxDirtyOrig);
|
|
cyDirty = cxDirtyOrig;
|
|
}
|
|
this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
|
|
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.canvasBuffer.width, this.canvasBuffer.height, 0, 0, this.cxScreen, this.cyScreen);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Video.init()
|
|
*
|
|
* This function operates on every HTML element of class "video", extracting the
|
|
* JSON-encoded parameters for the Video constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a Video component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
Video.init = function()
|
|
{
|
|
var aeVideo = Component.getElementsByClass(document, 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 <canvas> support. Please try a newer web browser.";
|
|
return;
|
|
}
|
|
|
|
eCanvas.setAttribute("class", "pcjs-canvas");
|
|
eCanvas.setAttribute("width", parmsVideo['screenWidth']);
|
|
eCanvas.setAttribute("height", parmsVideo['screenHeight']);
|
|
eCanvas.style.backgroundColor = parmsVideo['screenColor'];
|
|
|
|
/*
|
|
* The "contenteditable" attribute on a canvas element NOTICEABLY slows down canvas drawing on
|
|
* Safari as soon as you give the canvas focus (ie, click away from the canvas, and drawing speeds
|
|
* up; click on the canvas, and drawing slows down). So the "transparent textarea hack" that we
|
|
* once employed as only a work-around for Android devices is now our default.
|
|
*
|
|
* eCanvas.setAttribute("contenteditable", "true");
|
|
*
|
|
* HACK: A canvas style of "auto" provides for excellent responsive canvas scaling in EVERY browser
|
|
* except IE9/IE10, so I recalculate the appropriate CSS height every time the parent DIV is resized;
|
|
* IE11 works without this hack, so we take advantage of the fact that IE11 doesn't identify as "MSIE".
|
|
*
|
|
* The other reason it's good to keep this particular hack limited to IE9/IE10 is that most other
|
|
* browsers don't actually support an 'onresize' handler on anything but the window object.
|
|
*/
|
|
eCanvas.style.height = "auto";
|
|
if (web.getUserAgent().indexOf("MSIE") >= 0) {
|
|
eVideo.onresize = function(eParent, eChild, cx, cy) {
|
|
return function onResizeVideo() {
|
|
eChild.style.height = (((eParent.clientWidth * cy) / cx) | 0) + "px";
|
|
};
|
|
}(eVideo, eCanvas, parmsVideo['screenWidth'], parmsVideo['screenHeight']);
|
|
eVideo.onresize();
|
|
}
|
|
/*
|
|
* The following is a related hack that allows the user to force the screen to use a particular aspect
|
|
* ratio if an 'aspect' attribute or URL parameter is set. Initially, it's just for testing purposes
|
|
* until we figure out a better UI. And note that we use our web.onPageEvent() helper function to make
|
|
* sure we don't trample any other 'onresize' handler(s) attached to the window object.
|
|
*/
|
|
var aspect = +(parmsVideo['aspect'] || Component.parmsURL['aspect']);
|
|
/*
|
|
* No 'aspect' parameter yields NaN, which is falsey, and anything else must satisfy my arbitrary
|
|
* constraints of 0.3 <= aspect <= 3.33, to prevent any useless (or worse, browser-blowing) results.
|
|
*/
|
|
if (aspect && aspect >= 0.3 && aspect <= 3.33) {
|
|
web.onPageEvent('onresize', function(eParent, eChild, aspectRatio) {
|
|
return function onResizeWindow() {
|
|
/*
|
|
* Since aspectRatio is the target width/height, we have:
|
|
*
|
|
* eParent.clientWidth / eChild.style.height = aspectRatio
|
|
*
|
|
* which means that:
|
|
*
|
|
* eChild.style.height = eParent.clientWidth / aspectRatio
|
|
*
|
|
* so for example, if aspectRatio is 16:9, or 1.78, and clientWidth = 640,
|
|
* then the calculated height should approximately 360.
|
|
*/
|
|
eChild.style.height = ((eParent.clientWidth / aspectRatio)|0) + "px";
|
|
};
|
|
}(eVideo, eCanvas, aspect));
|
|
window['onresize']();
|
|
}
|
|
eVideo.appendChild(eCanvas);
|
|
|
|
/*
|
|
* HACK: Android-based browsers, like the Silk (Amazon) browser and Chrome for Android, don't honor the
|
|
* "contenteditable" attribute; that is, when the canvas receives focus, they don't activate the on-screen
|
|
* keyboard. So my fallback is to create a transparent textarea on top of the canvas.
|
|
*
|
|
* The parent DIV must have a style of "position:relative" (alternatively, a class of "pcjs-container"),
|
|
* so that we can position the textarea using absolute coordinates. Also, we don't want the textarea to be
|
|
* visible, but we must use "opacity:0" instead of "visibility:hidden", because the latter seems to prevent
|
|
* the element from receiving events. These styling requirements are taken care of in components.css
|
|
* (see references to the "pcjs-video-object" class).
|
|
*
|
|
* UPDATE: Unfortunately, Android keyboards like to compose whole words before transmitting any of the
|
|
* intervening characters; our textarea's keyDown/keyUp event handlers DO receive intervening key events,
|
|
* but their keyCode property is ZERO. Virtually the only usable key event we receive is the Enter key.
|
|
* Android users will have to use machines that include their own on-screen "soft keyboard", or use an
|
|
* external keyboard.
|
|
*
|
|
* The following attempt to use a password-enabled input field didn't work any better on Android. You could
|
|
* clearly see the overlaid semi-transparent input field, but none of the input characters were passed along,
|
|
* with the exception of the "Go" (Enter) key.
|
|
*
|
|
* var eInput = document.createElement("input");
|
|
* eInput.setAttribute("type", "password");
|
|
* eInput.setAttribute("style", "position:absolute; left:0; top:0; width:100%; height:100%; opacity:0.5");
|
|
* eVideo.appendChild(eInput);
|
|
*
|
|
* See this Chromium issue for more information: https://code.google.com/p/chromium/issues/detail?id=118639
|
|
*/
|
|
var eTextArea = document.createElement("textarea");
|
|
|
|
/*
|
|
* As noted in keyboard.js, the keyboard on an iOS device tends to pop up with the SHIFT key depressed,
|
|
* which is not the initial keyboard state that the Keyboard component expects, so hopefully turning off
|
|
* these "auto" attributes will help.
|
|
*/
|
|
if (web.isUserAgent("iOS")) {
|
|
eTextArea.setAttribute("autocapitalize", "off");
|
|
eTextArea.setAttribute("autocorrect", "off");
|
|
}
|
|
eVideo.appendChild(eTextArea);
|
|
|
|
/*
|
|
* Now we can create the Video object, record it, and wire it up to the associated document elements.
|
|
*/
|
|
var eContext = eCanvas.getContext("2d");
|
|
var video = new Video(parmsVideo, eCanvas, eContext, eTextArea /* || eInput */, eVideo);
|
|
|
|
/*
|
|
* Bind any video-specific controls (eg, the Refresh button). There are no essential controls, however;
|
|
* even the "Refresh" button is just a diagnostic tool, to ensure that the screen contents are up-to-date.
|
|
*/
|
|
Component.bindComponentControls(video, eVideo, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Video module on the page.
|
|
*/
|
|
web.onInit(Video.init);
|
|
|
|
|
|
// ./modules/pc8080/lib/debugger.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Debugger component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
if (DEBUGGER) {
|
|
}
|
|
|
|
/**
|
|
* Debugger Address Object
|
|
*
|
|
* addr address
|
|
* fTemporary true if this is a temporary breakpoint address
|
|
* sCmd set for breakpoint addresses if there's an associated command string
|
|
* aCmds preprocessed commands (from sCmd)
|
|
*
|
|
* @typedef {{
|
|
* addr:(number|undefined),
|
|
* fTemporary:(boolean|undefined),
|
|
* sCmd:(string|undefined),
|
|
* aCmds:(Array.<string>|undefined)
|
|
* }}
|
|
*/
|
|
var DbgAddr;
|
|
|
|
/**
|
|
* Debugger(parmsDbg)
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsDbg
|
|
*
|
|
* The Debugger component supports the following optional (parmsDbg) properties:
|
|
*
|
|
* commands: string containing zero or more commands, separated by ';'
|
|
*
|
|
* messages: string containing zero or more message categories to enable;
|
|
* multiple categories must be separated by '|' or ';'. Parsed by messageInit().
|
|
*
|
|
* The Debugger component is an optional component that implements a variety of user
|
|
* commands for controlling the CPU, dumping and editing memory, etc.
|
|
*/
|
|
function Debugger(parmsDbg)
|
|
{
|
|
if (DEBUGGER) {
|
|
|
|
Component.call(this, "Debugger", parmsDbg, Debugger);
|
|
|
|
this.style = Debugger.STYLE_8080;
|
|
|
|
/*
|
|
* These keep track of instruction activity, but only when tracing or when Debugger checks
|
|
* have been enabled (eg, one or more breakpoints have been set).
|
|
*
|
|
* They are zeroed by the reset() notification handler. cInstructions is advanced by
|
|
* stepCPU() and checkInstruction() calls. nCycles is updated by every stepCPU() or stop()
|
|
* call and simply represents the number of cycles performed by the last run of instructions.
|
|
*/
|
|
this.nCycles = 0;
|
|
this.cOpcodes = this.cOpcodesStart = 0;
|
|
|
|
/*
|
|
* Most commands that require an address call parseAddr(), which defaults to dbgAddrNextCode
|
|
* or dbgAddrNextData when no address has been given. doDump() and doUnassemble(), in turn,
|
|
* update dbgAddrNextData and dbgAddrNextCode, respectively, when they're done.
|
|
*
|
|
* For TEMPORARY breakpoint addresses, we set fTemporary to true, so that they can be automatically
|
|
* cleared when they're hit.
|
|
*/
|
|
this.dbgAddrNextCode = this.newAddr();
|
|
this.dbgAddrNextData = this.newAddr();
|
|
|
|
/*
|
|
* This maintains command history. New commands are inserted at index 0 of the array.
|
|
* When Enter is pressed on an empty input buffer, we default to the command at aPrevCmds[0].
|
|
*/
|
|
this.iPrevCmd = -1;
|
|
this.aPrevCmds = [];
|
|
|
|
/*
|
|
* fAssemble is true when "assemble mode" is active, false when not.
|
|
*/
|
|
this.fAssemble = false;
|
|
this.dbgAddrAssemble = this.newAddr();
|
|
|
|
/*
|
|
* aSymbolTable is an array of SymbolTable objects, one per ROM or other chunk of address space,
|
|
* where each object contains the following properties:
|
|
*
|
|
* sModule
|
|
* addr (physical address, if any; eg, symbols for a ROM)
|
|
* len
|
|
* aSymbols
|
|
* aOffsets
|
|
*
|
|
* See addSymbols() for more details, since that's how callers add sets of symbols to the table.
|
|
*/
|
|
this.aSymbolTable = [];
|
|
|
|
/*
|
|
* aVariables is an object with properties that grows as setVariable() assigns more variables;
|
|
* each property corresponds to one variable, where the property name is the variable name (ie,
|
|
* a string beginning with a letter or underscore, followed by zero or more additional letters,
|
|
* digits, or underscores) and the property value is the variable's numeric value. See doVar()
|
|
* and setVariable() for details.
|
|
*
|
|
* Note that parseValue(), through its reliance on str.parseInt(), assumes a default base of 16
|
|
* if no base is explicitly indicated (eg, a trailing decimal period), and if you define variable
|
|
* names containing exclusively hex alpha characters (a-f), those variables will take precedence
|
|
* over the corresponding hex values. In other words, if you define variables "a" and "b", you
|
|
* will no longer be able to simply type "a" or "b" to specify the decimal values 10 or 11.
|
|
*/
|
|
this.aVariables = {};
|
|
|
|
/*
|
|
* clearBreakpoints() initializes the breakpoints lists: aBreakExec is a list of addresses
|
|
* to halt on whenever attempting to execute an instruction at the corresponding address,
|
|
* and aBreakRead and aBreakWrite are lists of addresses to halt on whenever a read or write,
|
|
* respectively, occurs at the corresponding address.
|
|
*
|
|
* NOTE: Curiously, after upgrading the Google Closure Compiler from v20141215 to v20150609,
|
|
* the resulting compiled code would crash in clearBreakpoints(), because the (renamed) aBreakRead
|
|
* property was already defined. To eliminate whatever was confusing the Closure Compiler, I've
|
|
* explicitly initialized all the properties that clearBreakpoints() (re)initializes.
|
|
*/
|
|
this.aBreakExec = this.aBreakRead = this.aBreakWrite = [];
|
|
this.clearBreakpoints();
|
|
|
|
/*
|
|
* The new "bn" command allows you to specify a number of instructions to execute and then stop;
|
|
* "bn 0" disables any outstanding count.
|
|
*/
|
|
this.nBreakIns = 0;
|
|
|
|
/*
|
|
* Execution history is allocated by historyInit() whenever checksEnabled() conditions change.
|
|
* Execution history is updated whenever the CPU calls checkInstruction(), which will happen
|
|
* only when checksEnabled() returns true (eg, whenever one or more breakpoints have been set).
|
|
* This ensures that, by default, the CPU runs as fast as possible.
|
|
*/
|
|
this.historyInit();
|
|
|
|
/*
|
|
* Initialize Debugger message support
|
|
*/
|
|
this.afnDumpers = [];
|
|
this.messageInit(parmsDbg['messages']);
|
|
|
|
this.sInitCommands = parmsDbg['commands'];
|
|
|
|
/*
|
|
* Make it easier to access Debugger commands from an external REPL (eg, the WebStorm
|
|
* "live" console window); eg:
|
|
*
|
|
* $('r')
|
|
* $('dw 0:0')
|
|
* $('h')
|
|
* ...
|
|
*/
|
|
var dbg = this;
|
|
if (window) {
|
|
if (window['$'] === undefined) {
|
|
window['$'] = function(s) { return dbg.doCommands(s); };
|
|
}
|
|
} else {
|
|
if (global['$'] === undefined) {
|
|
global['$'] = function(s) { return dbg.doCommands(s); };
|
|
}
|
|
}
|
|
|
|
} // endif DEBUGGER
|
|
}
|
|
|
|
if (DEBUGGER) {
|
|
|
|
Component.subclass(Debugger);
|
|
|
|
/*
|
|
* NOTE: Every Debugger property from here to the first prototype function definition (initBus()) is a
|
|
* considered a "class constant"; most of them use our "all-caps" convention (and all of them SHOULD, but
|
|
* that wouldn't help us catch any bugs).
|
|
*
|
|
* Technically, all of them should ALSO be preceded by a "@const" annotation, but that's a lot of work and it
|
|
* really clutters the code. I wish the Closure Compiler had a way to annotate every definition with a given
|
|
* section with a single annotation....
|
|
*
|
|
* Bugs can slip through the cracks without those annotations; for example, I unthinkingly redefined TYPE_SI
|
|
* at one point, and if all the definitions had been preceded by an "@const", that mistake would have been
|
|
* caught at compile-time.
|
|
*/
|
|
|
|
Debugger.COMMANDS = {
|
|
'?': "help/print",
|
|
'a [#]': "assemble", // TODO: Implement this command someday
|
|
'b [#]': "breakpoint", // multiple variations (use b? to list them)
|
|
'c': "clear output",
|
|
'd [#]': "dump memory", // additional syntax: d [#] [l#], where l# is a number of bytes to dump
|
|
'e [#]': "edit memory",
|
|
'f': "frequencies",
|
|
'g [#]': "go [to #]",
|
|
'h': "halt",
|
|
'i [#]': "input port #",
|
|
'if': "eval expression",
|
|
'k': "stack trace",
|
|
"ln": "list nearest symbol(s)",
|
|
'm': "messages",
|
|
'o [#]': "output port #",
|
|
'p': "step over", // other variations: pr (step and dump registers)
|
|
'print': "print expression",
|
|
'r': "dump/set registers",
|
|
'reset': "reset machine",
|
|
's': "set options",
|
|
't [#]': "trace", // other variations: tr (trace and dump registers)
|
|
'u [#]': "unassemble",
|
|
'v': "print version",
|
|
'var': "assign variable"
|
|
};
|
|
|
|
Debugger.STYLE_8080 = 8080;
|
|
Debugger.STYLE_8086 = 8086;
|
|
|
|
/*
|
|
* CPU instruction ordinals
|
|
*/
|
|
Debugger.INS = {
|
|
NONE: 0, ACI: 1, ADC: 2, ADD: 3, ADI: 4, ANA: 5, ANI: 6, CALL: 7,
|
|
CC: 8, CM: 9, CNC: 10, CNZ: 11, CP: 12, CPE: 13, CPO: 14, CZ: 15,
|
|
CMA: 16, CMC: 17, CMP: 18, CPI: 19, DAA: 20, DAD: 21, DCR: 22, DCX: 23,
|
|
DI: 24, EI: 25, HLT: 26, IN: 27, INR: 28, INX: 29, JMP: 30, JC: 31,
|
|
JM: 32, JNC: 33, JNZ: 34, JP: 35, JPE: 36, JPO: 37, JZ: 38, LDA: 39,
|
|
LDAX: 40, LHLD: 41, LXI: 42, MOV: 43, MVI: 44, NOP: 45, ORA: 46, ORI: 47,
|
|
OUT: 48, PCHL: 49, POP: 50, PUSH: 51, RAL: 52, RAR: 53, RET: 54, RC: 55,
|
|
RM: 56, RNC: 57, RNZ: 58, RP: 59, RPE: 60, RPO: 61, RZ: 62, RLC: 63,
|
|
RRC: 64, RST: 65, SBB: 66, SBI: 67, SHLD: 68, SPHL: 69, STA: 70, STAX: 71,
|
|
STC: 72, SUB: 73, SUI: 74, XCHG: 75, XRA: 76, XRI: 77, XTHL: 78
|
|
};
|
|
|
|
/*
|
|
* CPU instruction names (mnemonics), indexed by CPU instruction ordinal (above)
|
|
*
|
|
* If you change the default style, using the "s" command (eg, "s 8086"), then the 8086 table
|
|
* will be used instead. TODO: Add a "s z80" command for Z80-style mnemonics.
|
|
*/
|
|
Debugger.INS_NAMES = [
|
|
"NONE", "ACI", "ADC", "ADD", "ADI", "ANA", "ANI", "CALL",
|
|
"CC", "CM", "CNC", "CNZ", "CP", "CPE", "CPO", "CZ",
|
|
"CMA", "CMC", "CMP", "CPI", "DAA", "DAD", "DCR", "DCX",
|
|
"DI", "EI", "HLT", "IN", "INR", "INX", "JMP", "JC",
|
|
"JM", "JNC", "JNZ", "JP", "JPE", "JPO", "JZ", "LDA",
|
|
"LDAX", "LHLD", "LXI", "MOV", "MVI", "NOP", "ORA", "ORI",
|
|
"OUT", "PCHL", "POP", "PUSH", "RAL", "RAR", "RET", "RC",
|
|
"RM", "RNC", "RNZ", "RP", "RPE", "RPO", "RZ", "RLC",
|
|
"RRC", "RST", "SBB", "SBI", "SHLD", "SPHL", "STA", "STAX",
|
|
"STC", "SUB", "SUI", "XCHG", "XRA", "XRI", "XTHL"
|
|
];
|
|
|
|
Debugger.INS_NAMES_8086 = [
|
|
"NONE", "ADC", "ADC", "ADD", "ADD", "AND", "AND", "CALL",
|
|
"CALLC", "CALLS", "CALLNC", "CALLNZ", "CALLNS", "CALLP", "CALLNP", "CALLZ",
|
|
"NOT", "CMC", "CMP", "CMP", "DAA", "ADD", "DEC", "DEC",
|
|
"CLI", "STI", "HLT", "IN", "INC", "INC", "JMP", "JC",
|
|
"JS", "JNC", "JNZ", "JNS", "JP", "JNP", "JZ", "MOV",
|
|
"MOV", "MOV", "MOV", "MOV", "MOV", "NOP", "OR", "OR",
|
|
"OUT", "JMP", "POP", "PUSH", "RCL", "RCR", "RET", "RETC",
|
|
"RETS", "RETNC", "RETNZ", "RETNS", "RETP", "RETNP", "RETZ", "ROL",
|
|
"ROR", "RST", "SBB", "SBB", "MOV", "MOV", "MOV", "MOV",
|
|
"STC", "SUB", "SUB", "XCHG", "XOR", "XOR", "XCHG"
|
|
];
|
|
|
|
Debugger.REG_B = 0x00;
|
|
Debugger.REG_C = 0x01;
|
|
Debugger.REG_D = 0x02;
|
|
Debugger.REG_E = 0x03;
|
|
Debugger.REG_H = 0x04;
|
|
Debugger.REG_L = 0x05;
|
|
Debugger.REG_M = 0x06;
|
|
Debugger.REG_A = 0x07;
|
|
Debugger.REG_BC = 0x08;
|
|
Debugger.REG_DE = 0x09;
|
|
Debugger.REG_HL = 0x0A;
|
|
Debugger.REG_SP = 0x0B;
|
|
Debugger.REG_PC = 0x0C;
|
|
Debugger.REG_PS = 0x0D;
|
|
Debugger.REG_PSW = 0x0E; // aka AF if Z80-style mnemonics
|
|
|
|
/*
|
|
* NOTE: "PS" is the complete processor status, which includes bits like the Interrupt flag (IF),
|
|
* which is NOT the same as "PSW", which is the low 8 bits of "PS" combined with "A" in the high byte.
|
|
*/
|
|
Debugger.REGS = [
|
|
"B", "C", "D", "E", "H", "L", "M", "A", "BC", "DE", "HL", "SP", "PC", "PS", "PSW"
|
|
];
|
|
|
|
/*
|
|
* Operand type descriptor masks and definitions
|
|
*/
|
|
Debugger.TYPE_SIZE = 0x000F; // size field
|
|
Debugger.TYPE_MODE = 0x00F0; // mode field
|
|
Debugger.TYPE_IREG = 0x0F00; // implied register field
|
|
Debugger.TYPE_OTHER = 0xF000; // "other" field
|
|
|
|
/*
|
|
* TYPE_SIZE values
|
|
*/
|
|
Debugger.TYPE_NONE = 0x0000; // (all other TYPE fields ignored)
|
|
Debugger.TYPE_BYTE = 0x0001; // byte, regardless of operand size
|
|
Debugger.TYPE_SBYTE = 0x0002; // byte sign-extended to word
|
|
Debugger.TYPE_WORD = 0x0003; // word (16-bit value)
|
|
|
|
/*
|
|
* TYPE_MODE values
|
|
*/
|
|
Debugger.TYPE_REG = 0x0010; // register
|
|
Debugger.TYPE_IMM = 0x0020; // immediate data
|
|
Debugger.TYPE_ADDR = 0x0033; // immediate (word) address
|
|
Debugger.TYPE_MEM = 0x0040; // memory reference
|
|
Debugger.TYPE_INT = 0x0080; // interrupt level encoded in instruction (bits 3-5)
|
|
|
|
/*
|
|
* TYPE_IREG values, based on the REG_* constants.
|
|
*
|
|
* NOte that TYPE_M isn't really a register, just an alternative form of TYPE_HL | TYPE_MEM.
|
|
*/
|
|
Debugger.TYPE_A = (Debugger.REG_A << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_B = (Debugger.REG_B << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_C = (Debugger.REG_C << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_D = (Debugger.REG_D << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_E = (Debugger.REG_E << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_H = (Debugger.REG_H << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_L = (Debugger.REG_L << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE);
|
|
Debugger.TYPE_M = (Debugger.REG_M << 8 | Debugger.TYPE_REG | Debugger.TYPE_BYTE | Debugger.TYPE_MEM);
|
|
Debugger.TYPE_BC = (Debugger.REG_BC << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
Debugger.TYPE_DE = (Debugger.REG_DE << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
Debugger.TYPE_HL = (Debugger.REG_HL << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
Debugger.TYPE_SP = (Debugger.REG_SP << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
Debugger.TYPE_PC = (Debugger.REG_PC << 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
Debugger.TYPE_PSW = (Debugger.REG_PSW<< 8 | Debugger.TYPE_REG | Debugger.TYPE_WORD);
|
|
|
|
/*
|
|
* TYPE_OTHER bit definitions
|
|
*/
|
|
Debugger.TYPE_IN = 0x1000; // operand is input
|
|
Debugger.TYPE_OUT = 0x2000; // operand is output
|
|
Debugger.TYPE_BOTH = (Debugger.TYPE_IN | Debugger.TYPE_OUT);
|
|
Debugger.TYPE_OPT = 0x4000; // optional operand (ie, normally omitted in 8080 assembly language)
|
|
Debugger.TYPE_UNDOC = 0x8000; // opcode is an undocumented alternative encoding
|
|
|
|
/*
|
|
* The aaOpDescs array is indexed by opcode, and each element is a sub-array (aOpDesc) that describes
|
|
* the corresponding opcode. The sub-elements are as follows:
|
|
*
|
|
* [0]: {number} of the opcode name (see INS.*)
|
|
* [1]: {number} containing the destination operand descriptor bit(s), if any
|
|
* [2]: {number} containing the source operand descriptor bit(s), if any
|
|
* [3]: {number} containing the occasional third operand descriptor bit(s), if any
|
|
*
|
|
* These sub-elements are all optional. If [0] is not present, the opcode is undefined; if [1] is not
|
|
* present (or contains zero), the opcode has no (or only implied) operands; if [2] is not present, the
|
|
* opcode has only a single operand. And so on.
|
|
*
|
|
* Additional default rules:
|
|
*
|
|
* 1) If no TYPE_OTHER bits are specified for the first (destination) operand, TYPE_OUT is assumed;
|
|
* 2) If no TYPE_OTHER bits are specified for the second (source) operand, TYPE_IN is assumed;
|
|
* 3) If no size is specified for the second operand, the size is assumed to match the first operand.
|
|
*/
|
|
Debugger.aaOpDescs = [
|
|
/* 0x00 */ [Debugger.INS.NOP],
|
|
/* 0x01 */ [Debugger.INS.LXI, Debugger.TYPE_BC, Debugger.TYPE_IMM],
|
|
/* 0x02 */ [Debugger.INS.STAX, Debugger.TYPE_BC | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT],
|
|
/* 0x03 */ [Debugger.INS.INX, Debugger.TYPE_BC],
|
|
/* 0x04 */ [Debugger.INS.INR, Debugger.TYPE_B],
|
|
/* 0x05 */ [Debugger.INS.DCR, Debugger.TYPE_B],
|
|
/* 0x06 */ [Debugger.INS.MVI, Debugger.TYPE_B, Debugger.TYPE_IMM],
|
|
/* 0x07 */ [Debugger.INS.RLC],
|
|
/* 0x08 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x09 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_BC],
|
|
/* 0x0A */ [Debugger.INS.LDAX, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_BC | Debugger.TYPE_MEM],
|
|
/* 0x0B */ [Debugger.INS.DCX, Debugger.TYPE_BC],
|
|
/* 0x0C */ [Debugger.INS.INR, Debugger.TYPE_C],
|
|
/* 0x0D */ [Debugger.INS.DCR, Debugger.TYPE_C],
|
|
/* 0x0E */ [Debugger.INS.MVI, Debugger.TYPE_C, Debugger.TYPE_IMM],
|
|
/* 0x0F */ [Debugger.INS.RRC],
|
|
/* 0x10 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x11 */ [Debugger.INS.LXI, Debugger.TYPE_DE, Debugger.TYPE_IMM],
|
|
/* 0x12 */ [Debugger.INS.STAX, Debugger.TYPE_DE | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT],
|
|
/* 0x13 */ [Debugger.INS.INX, Debugger.TYPE_DE],
|
|
/* 0x14 */ [Debugger.INS.INR, Debugger.TYPE_D],
|
|
/* 0x15 */ [Debugger.INS.DCR, Debugger.TYPE_D],
|
|
/* 0x16 */ [Debugger.INS.MVI, Debugger.TYPE_D, Debugger.TYPE_IMM],
|
|
/* 0x17 */ [Debugger.INS.RAL],
|
|
/* 0x18 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x19 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_DE],
|
|
/* 0x1A */ [Debugger.INS.LDAX, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_DE | Debugger.TYPE_MEM],
|
|
/* 0x1B */ [Debugger.INS.DCX, Debugger.TYPE_DE],
|
|
/* 0x1C */ [Debugger.INS.INR, Debugger.TYPE_E],
|
|
/* 0x1D */ [Debugger.INS.DCR, Debugger.TYPE_E],
|
|
/* 0x1E */ [Debugger.INS.MVI, Debugger.TYPE_E, Debugger.TYPE_IMM],
|
|
/* 0x1F */ [Debugger.INS.RAR],
|
|
/* 0x20 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x21 */ [Debugger.INS.LXI, Debugger.TYPE_HL, Debugger.TYPE_IMM],
|
|
/* 0x22 */ [Debugger.INS.SHLD, Debugger.TYPE_ADDR | Debugger.TYPE_MEM, Debugger.TYPE_HL | Debugger.TYPE_OPT],
|
|
/* 0x23 */ [Debugger.INS.INX, Debugger.TYPE_HL],
|
|
/* 0x24 */ [Debugger.INS.INR, Debugger.TYPE_H],
|
|
/* 0x25 */ [Debugger.INS.DCR, Debugger.TYPE_H],
|
|
/* 0x26 */ [Debugger.INS.MVI, Debugger.TYPE_H, Debugger.TYPE_IMM],
|
|
/* 0x27 */ [Debugger.INS.DAA],
|
|
/* 0x28 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x29 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_HL],
|
|
/* 0x2A */ [Debugger.INS.LHLD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_ADDR | Debugger.TYPE_MEM],
|
|
/* 0x2B */ [Debugger.INS.DCX, Debugger.TYPE_HL],
|
|
/* 0x2C */ [Debugger.INS.INR, Debugger.TYPE_L],
|
|
/* 0x2D */ [Debugger.INS.DCR, Debugger.TYPE_L],
|
|
/* 0x2E */ [Debugger.INS.MVI, Debugger.TYPE_L, Debugger.TYPE_IMM],
|
|
/* 0x2F */ [Debugger.INS.CMA, Debugger.TYPE_A | Debugger.TYPE_OPT],
|
|
/* 0x30 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x31 */ [Debugger.INS.LXI, Debugger.TYPE_SP, Debugger.TYPE_IMM],
|
|
/* 0x32 */ [Debugger.INS.STA, Debugger.TYPE_ADDR | Debugger.TYPE_MEM, Debugger.TYPE_A | Debugger.TYPE_OPT],
|
|
/* 0x33 */ [Debugger.INS.INX, Debugger.TYPE_SP],
|
|
/* 0x34 */ [Debugger.INS.INR, Debugger.TYPE_M],
|
|
/* 0x35 */ [Debugger.INS.DCR, Debugger.TYPE_M],
|
|
/* 0x36 */ [Debugger.INS.MVI, Debugger.TYPE_M, Debugger.TYPE_IMM],
|
|
/* 0x37 */ [Debugger.INS.STC],
|
|
/* 0x38 */ [Debugger.INS.NOP, Debugger.TYPE_UNDOC],
|
|
/* 0x39 */ [Debugger.INS.DAD, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_SP],
|
|
/* 0x3A */ [Debugger.INS.LDA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_ADDR | Debugger.TYPE_MEM],
|
|
/* 0x3B */ [Debugger.INS.DCX, Debugger.TYPE_SP],
|
|
/* 0x3C */ [Debugger.INS.INR, Debugger.TYPE_A],
|
|
/* 0x3D */ [Debugger.INS.DCR, Debugger.TYPE_A],
|
|
/* 0x3E */ [Debugger.INS.MVI, Debugger.TYPE_A, Debugger.TYPE_IMM],
|
|
/* 0x3F */ [Debugger.INS.CMC],
|
|
/* 0x40 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_B],
|
|
/* 0x41 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_C],
|
|
/* 0x42 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_D],
|
|
/* 0x43 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_E],
|
|
/* 0x44 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_H],
|
|
/* 0x45 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_L],
|
|
/* 0x46 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_M],
|
|
/* 0x47 */ [Debugger.INS.MOV, Debugger.TYPE_B, Debugger.TYPE_A],
|
|
/* 0x48 */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_B],
|
|
/* 0x49 */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_C],
|
|
/* 0x4A */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_D],
|
|
/* 0x4B */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_E],
|
|
/* 0x4C */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_H],
|
|
/* 0x4D */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_L],
|
|
/* 0x4E */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_M],
|
|
/* 0x4F */ [Debugger.INS.MOV, Debugger.TYPE_C, Debugger.TYPE_A],
|
|
/* 0x50 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_B],
|
|
/* 0x51 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_C],
|
|
/* 0x52 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_D],
|
|
/* 0x53 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_E],
|
|
/* 0x54 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_H],
|
|
/* 0x55 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_L],
|
|
/* 0x56 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_M],
|
|
/* 0x57 */ [Debugger.INS.MOV, Debugger.TYPE_D, Debugger.TYPE_A],
|
|
/* 0x58 */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_B],
|
|
/* 0x59 */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_C],
|
|
/* 0x5A */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_D],
|
|
/* 0x5B */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_E],
|
|
/* 0x5C */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_H],
|
|
/* 0x5D */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_L],
|
|
/* 0x5E */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_M],
|
|
/* 0x5F */ [Debugger.INS.MOV, Debugger.TYPE_E, Debugger.TYPE_A],
|
|
/* 0x60 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_B],
|
|
/* 0x61 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_C],
|
|
/* 0x62 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_D],
|
|
/* 0x63 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_E],
|
|
/* 0x64 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_H],
|
|
/* 0x65 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_L],
|
|
/* 0x66 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_M],
|
|
/* 0x67 */ [Debugger.INS.MOV, Debugger.TYPE_H, Debugger.TYPE_A],
|
|
/* 0x68 */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_B],
|
|
/* 0x69 */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_C],
|
|
/* 0x6A */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_D],
|
|
/* 0x6B */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_E],
|
|
/* 0x6C */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_H],
|
|
/* 0x6D */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_L],
|
|
/* 0x6E */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_M],
|
|
/* 0x6F */ [Debugger.INS.MOV, Debugger.TYPE_L, Debugger.TYPE_A],
|
|
/* 0x70 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_B],
|
|
/* 0x71 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_C],
|
|
/* 0x72 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_D],
|
|
/* 0x73 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_E],
|
|
/* 0x74 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_H],
|
|
/* 0x75 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_L],
|
|
/* 0x76 */ [Debugger.INS.HLT],
|
|
/* 0x77 */ [Debugger.INS.MOV, Debugger.TYPE_M, Debugger.TYPE_A],
|
|
/* 0x78 */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_B],
|
|
/* 0x79 */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_C],
|
|
/* 0x7A */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_D],
|
|
/* 0x7B */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_E],
|
|
/* 0x7C */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_H],
|
|
/* 0x7D */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_L],
|
|
/* 0x7E */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_M],
|
|
/* 0x7F */ [Debugger.INS.MOV, Debugger.TYPE_A, Debugger.TYPE_A],
|
|
/* 0x80 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0x81 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0x82 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0x83 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0x84 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0x85 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0x86 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0x87 */ [Debugger.INS.ADD, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0x88 */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0x89 */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0x8A */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0x8B */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0x8C */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0x8D */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0x8E */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0x8F */ [Debugger.INS.ADC, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0x90 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0x91 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0x92 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0x93 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0x94 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0x95 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0x96 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0x97 */ [Debugger.INS.SUB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0x98 */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0x99 */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0x9A */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0x9B */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0x9C */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0x9D */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0x9E */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0x9F */ [Debugger.INS.SBB, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0xA0 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0xA1 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0xA2 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0xA3 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0xA4 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0xA5 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0xA6 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0xA7 */ [Debugger.INS.ANA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0xA8 */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0xA9 */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0xAA */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0xAB */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0xAC */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0xAD */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0xAE */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0xAF */ [Debugger.INS.XRA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0xB0 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0xB1 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0xB2 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0xB3 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0xB4 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0xB5 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0xB6 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0xB7 */ [Debugger.INS.ORA, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0xB8 */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_B],
|
|
/* 0xB9 */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_C],
|
|
/* 0xBA */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_D],
|
|
/* 0xBB */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_E],
|
|
/* 0xBC */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_H],
|
|
/* 0xBD */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_L],
|
|
/* 0xBE */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_M],
|
|
/* 0xBF */ [Debugger.INS.CMP, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_A],
|
|
/* 0xC0 */ [Debugger.INS.RNZ],
|
|
/* 0xC1 */ [Debugger.INS.POP, Debugger.TYPE_BC],
|
|
/* 0xC2 */ [Debugger.INS.JNZ, Debugger.TYPE_ADDR],
|
|
/* 0xC3 */ [Debugger.INS.JMP, Debugger.TYPE_ADDR],
|
|
/* 0xC4 */ [Debugger.INS.CNZ, Debugger.TYPE_ADDR],
|
|
/* 0xC5 */ [Debugger.INS.PUSH, Debugger.TYPE_BC],
|
|
/* 0xC6 */ [Debugger.INS.ADI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xC7 */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xC8 */ [Debugger.INS.RZ],
|
|
/* 0xC9 */ [Debugger.INS.RET],
|
|
/* 0xCA */ [Debugger.INS.JZ, Debugger.TYPE_ADDR],
|
|
/* 0xCB */ [Debugger.INS.JMP, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC],
|
|
/* 0xCC */ [Debugger.INS.CZ, Debugger.TYPE_ADDR],
|
|
/* 0xCD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR],
|
|
/* 0xCE */ [Debugger.INS.ACI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xCF */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xD0 */ [Debugger.INS.RNC],
|
|
/* 0xD1 */ [Debugger.INS.POP, Debugger.TYPE_DE],
|
|
/* 0xD2 */ [Debugger.INS.JNC, Debugger.TYPE_ADDR],
|
|
/* 0xD3 */ [Debugger.INS.OUT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE,Debugger.TYPE_A | Debugger.TYPE_OPT],
|
|
/* 0xD4 */ [Debugger.INS.CNC, Debugger.TYPE_ADDR],
|
|
/* 0xD5 */ [Debugger.INS.PUSH, Debugger.TYPE_DE],
|
|
/* 0xD6 */ [Debugger.INS.SUI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xD7 */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xD8 */ [Debugger.INS.RC],
|
|
/* 0xD9 */ [Debugger.INS.RET, Debugger.TYPE_UNDOC],
|
|
/* 0xDA */ [Debugger.INS.JC, Debugger.TYPE_ADDR],
|
|
/* 0xDB */ [Debugger.INS.IN, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xDC */ [Debugger.INS.CC, Debugger.TYPE_ADDR],
|
|
/* 0xDD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC],
|
|
/* 0xDE */ [Debugger.INS.SBI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xDF */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xE0 */ [Debugger.INS.RPO],
|
|
/* 0xE1 */ [Debugger.INS.POP, Debugger.TYPE_HL],
|
|
/* 0xE2 */ [Debugger.INS.JPO, Debugger.TYPE_ADDR],
|
|
/* 0xE3 */ [Debugger.INS.XTHL, Debugger.TYPE_SP | Debugger.TYPE_MEM| Debugger.TYPE_OPT, Debugger.TYPE_HL | Debugger.TYPE_OPT],
|
|
/* 0xE4 */ [Debugger.INS.CPO, Debugger.TYPE_ADDR],
|
|
/* 0xE5 */ [Debugger.INS.PUSH, Debugger.TYPE_HL],
|
|
/* 0xE6 */ [Debugger.INS.ANI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xE7 */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xE8 */ [Debugger.INS.RPE],
|
|
/* 0xE9 */ [Debugger.INS.PCHL, Debugger.TYPE_HL],
|
|
/* 0xEA */ [Debugger.INS.JPE, Debugger.TYPE_ADDR],
|
|
/* 0xEB */ [Debugger.INS.XCHG, Debugger.TYPE_HL | Debugger.TYPE_OPT, Debugger.TYPE_DE | Debugger.TYPE_OPT],
|
|
/* 0xEC */ [Debugger.INS.CPE, Debugger.TYPE_ADDR],
|
|
/* 0xED */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC],
|
|
/* 0xEE */ [Debugger.INS.XRI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xEF */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xF0 */ [Debugger.INS.RP],
|
|
/* 0xF1 */ [Debugger.INS.POP, Debugger.TYPE_PSW],
|
|
/* 0xF2 */ [Debugger.INS.JP, Debugger.TYPE_ADDR],
|
|
/* 0xF3 */ [Debugger.INS.DI],
|
|
/* 0xF4 */ [Debugger.INS.CP, Debugger.TYPE_ADDR],
|
|
/* 0xF5 */ [Debugger.INS.PUSH, Debugger.TYPE_PSW],
|
|
/* 0xF6 */ [Debugger.INS.ORI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xF7 */ [Debugger.INS.RST, Debugger.TYPE_INT],
|
|
/* 0xF8 */ [Debugger.INS.RM],
|
|
/* 0xF9 */ [Debugger.INS.SPHL, Debugger.TYPE_SP | Debugger.TYPE_OPT, Debugger.TYPE_HL | Debugger.TYPE_OPT],
|
|
/* 0xFA */ [Debugger.INS.JM, Debugger.TYPE_ADDR],
|
|
/* 0xFB */ [Debugger.INS.EI],
|
|
/* 0xFC */ [Debugger.INS.CM, Debugger.TYPE_ADDR],
|
|
/* 0xFD */ [Debugger.INS.CALL, Debugger.TYPE_ADDR | Debugger.TYPE_UNDOC],
|
|
/* 0xFE */ [Debugger.INS.CPI, Debugger.TYPE_A | Debugger.TYPE_OPT, Debugger.TYPE_IMM | Debugger.TYPE_BYTE],
|
|
/* 0xFF */ [Debugger.INS.RST, Debugger.TYPE_INT]
|
|
];
|
|
|
|
/*
|
|
* Message categories supported by the messageEnabled() function and other assorted message
|
|
* functions. Each category has a corresponding bit value that can be combined (ie, OR'ed) as
|
|
* needed. The Debugger's message command ("m") is used to turn message categories on and off,
|
|
* like so:
|
|
*
|
|
* m port on
|
|
* m port off
|
|
* ...
|
|
*
|
|
* NOTE: The order of these categories can be rearranged, alphabetized, etc, as desired; just be
|
|
* aware that changing the bit values could break saved Debugger states (not a huge concern, just
|
|
* something to be aware of).
|
|
*/
|
|
Debugger.MESSAGES = {
|
|
"cpu": Messages.CPU,
|
|
"bus": Messages.BUS,
|
|
"mem": Messages.MEM,
|
|
"port": Messages.PORT,
|
|
"chipset": Messages.CHIPSET,
|
|
"keyboard": Messages.KEYBOARD, // "kbd" is also allowed as shorthand for "keyboard"; see doMessages()
|
|
"key": Messages.KEYS, // using "key" instead of "keys", since the latter is a method on JavasScript objects
|
|
"video": Messages.VIDEO,
|
|
"fdc": Messages.FDC,
|
|
"disk": Messages.DISK,
|
|
"serial": Messages.SERIAL,
|
|
"speaker": Messages.SPEAKER,
|
|
"computer": Messages.COMPUTER,
|
|
"log": Messages.LOG,
|
|
"warn": Messages.WARN,
|
|
/*
|
|
* Now we turn to message actions rather than message types; for example, setting "halt"
|
|
* on or off doesn't enable "halt" messages, but rather halts the CPU on any message above.
|
|
*
|
|
* Similarly, "m buffer on" turns on message buffering, defering the display of all messages
|
|
* until "m buffer off" is issued.
|
|
*/
|
|
"buffer": Messages.BUFFER,
|
|
"halt": Messages.HALT
|
|
};
|
|
|
|
Debugger.HISTORY_LIMIT = DEBUG? 100000 : 1000;
|
|
|
|
/**
|
|
* initBus(bus, cpu, dbg)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {CPUState} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
Debugger.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.cmp = cmp;
|
|
|
|
/*
|
|
* Re-initialize Debugger message support if necessary
|
|
*/
|
|
var sMessages = cmp.getMachineParm('messages');
|
|
if (sMessages) this.messageInit(sMessages);
|
|
|
|
this.aaOpDescs = Debugger.aaOpDescs;
|
|
|
|
this.messageDump(Messages.BUS, function onDumpBus(asArgs) { dbg.dumpBus(asArgs); });
|
|
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "debugInput")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Debugger.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var dbg = this;
|
|
switch (sBinding) {
|
|
|
|
case "debugInput":
|
|
this.bindings[sBinding] = control;
|
|
this.controlDebug = control;
|
|
/*
|
|
* For halted machines, this is fine, but for auto-start machines, it can be annoying.
|
|
*
|
|
* control.focus();
|
|
*/
|
|
control.onkeydown = function onKeyDownDebugInput(event) {
|
|
var sCmds;
|
|
if (event.keyCode == Keyboard.KEYCODE.CR) {
|
|
sCmds = control.value;
|
|
control.value = "";
|
|
dbg.doCommands(sCmds, true);
|
|
}
|
|
else if (event.keyCode == Keyboard.KEYCODE.ESC) {
|
|
control.value = sCmds = "";
|
|
}
|
|
else {
|
|
if (event.keyCode == Keyboard.KEYCODE.UP) {
|
|
if (dbg.iPrevCmd < dbg.aPrevCmds.length - 1) {
|
|
sCmds = dbg.aPrevCmds[++dbg.iPrevCmd];
|
|
}
|
|
}
|
|
else if (event.keyCode == Keyboard.KEYCODE.DOWN) {
|
|
if (dbg.iPrevCmd > 0) {
|
|
sCmds = dbg.aPrevCmds[--dbg.iPrevCmd];
|
|
} else {
|
|
sCmds = "";
|
|
dbg.iPrevCmd = -1;
|
|
}
|
|
}
|
|
if (sCmds != null) {
|
|
var cch = sCmds.length;
|
|
control.value = sCmds;
|
|
control.setSelectionRange(cch, cch);
|
|
}
|
|
}
|
|
if (sCmds != null && event.preventDefault) event.preventDefault();
|
|
};
|
|
return true;
|
|
|
|
case "debugEnter":
|
|
this.bindings[sBinding] = control;
|
|
web.onClickRepeat(
|
|
control,
|
|
500, 100,
|
|
function onClickDebugEnter(fRepeat) {
|
|
if (dbg.controlDebug) {
|
|
var sCmds = dbg.controlDebug.value;
|
|
dbg.controlDebug.value = "";
|
|
dbg.doCommands(sCmds, true);
|
|
return true;
|
|
}
|
|
if (DEBUG) dbg.log("no debugger input buffer");
|
|
return false;
|
|
}
|
|
);
|
|
return true;
|
|
|
|
case "step":
|
|
this.bindings[sBinding] = control;
|
|
web.onClickRepeat(
|
|
control,
|
|
500, 100,
|
|
function onClickStep(fRepeat) {
|
|
var fCompleted = false;
|
|
if (!dbg.isBusy(true)) {
|
|
dbg.setBusy(true);
|
|
fCompleted = dbg.stepCPU(fRepeat? 1 : 0);
|
|
dbg.setBusy(false);
|
|
}
|
|
return fCompleted;
|
|
}
|
|
);
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* updateFocus()
|
|
*
|
|
* @this {Debugger}
|
|
*/
|
|
Debugger.prototype.updateFocus = function()
|
|
{
|
|
if (this.controlDebug) this.controlDebug.focus();
|
|
};
|
|
|
|
/**
|
|
* getAddr(dbgAddr, fWrite, nb)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr|null|undefined} dbgAddr
|
|
* @param {boolean} [fWrite]
|
|
* @param {number} [nb] number of bytes to check (1 or 2); default is 1
|
|
* @return {number} is the corresponding linear address, or CPUDef.ADDR_INVALID
|
|
*/
|
|
Debugger.prototype.getAddr = function(dbgAddr, fWrite, nb)
|
|
{
|
|
var addr = dbgAddr && dbgAddr.addr;
|
|
if (addr == null) {
|
|
addr = CPUDef.ADDR_INVALID;
|
|
}
|
|
return addr;
|
|
};
|
|
|
|
/**
|
|
* getByte(dbgAddr, inc)
|
|
*
|
|
* We must route all our memory requests through the CPU now, in case paging is enabled.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} [inc]
|
|
* @return {number}
|
|
*/
|
|
Debugger.prototype.getByte = function(dbgAddr, inc)
|
|
{
|
|
var b = 0xff;
|
|
var addr = this.getAddr(dbgAddr, false, 1);
|
|
if (addr !== CPUDef.ADDR_INVALID) {
|
|
b = this.bus.getByteDirect(addr);
|
|
if (inc) this.incAddr(dbgAddr, inc);
|
|
}
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* getWord(dbgAddr, fAdvance)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {boolean} [fAdvance]
|
|
* @return {number}
|
|
*/
|
|
Debugger.prototype.getWord = function(dbgAddr, fAdvance)
|
|
{
|
|
return this.getShort(dbgAddr, fAdvance? 2 : 0);
|
|
};
|
|
|
|
/**
|
|
* getShort(dbgAddr, inc)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} [inc]
|
|
* @return {number}
|
|
*/
|
|
Debugger.prototype.getShort = function(dbgAddr, inc)
|
|
{
|
|
var w = 0xffff;
|
|
var addr = this.getAddr(dbgAddr, false, 2);
|
|
if (addr !== CPUDef.ADDR_INVALID) {
|
|
w = this.bus.getShortDirect(addr);
|
|
if (inc) this.incAddr(dbgAddr, inc);
|
|
}
|
|
return w;
|
|
};
|
|
|
|
/**
|
|
* setByte(dbgAddr, b, inc)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} b
|
|
* @param {number} [inc]
|
|
*/
|
|
Debugger.prototype.setByte = function(dbgAddr, b, inc)
|
|
{
|
|
var addr = this.getAddr(dbgAddr, true, 1);
|
|
if (addr !== CPUDef.ADDR_INVALID) {
|
|
this.bus.setByteDirect(addr, b);
|
|
if (inc) this.incAddr(dbgAddr, inc);
|
|
this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setShort(dbgAddr, w, inc)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} w
|
|
* @param {number} [inc]
|
|
*/
|
|
Debugger.prototype.setShort = function(dbgAddr, w, inc)
|
|
{
|
|
var addr = this.getAddr(dbgAddr, true, 2);
|
|
if (addr !== CPUDef.ADDR_INVALID) {
|
|
this.bus.setShortDirect(addr, w);
|
|
if (inc) this.incAddr(dbgAddr, inc);
|
|
this.cpu.updateCPU(true); // we set fForce to true in case video memory was the target
|
|
}
|
|
};
|
|
|
|
/**
|
|
* newAddr(addr)
|
|
*
|
|
* Returns a NEW DbgAddr object, initialized with specified values and/or defaults.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} [addr]
|
|
* @return {DbgAddr}
|
|
*/
|
|
Debugger.prototype.newAddr = function(addr)
|
|
{
|
|
return {addr: addr, fTemporary: false};
|
|
};
|
|
|
|
/**
|
|
* setAddr(dbgAddr, addr)
|
|
*
|
|
* Updates an EXISTING DbgAddr object, initialized with specified values and/or defaults.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} addr
|
|
* @return {DbgAddr}
|
|
*/
|
|
Debugger.prototype.setAddr = function(dbgAddr, addr)
|
|
{
|
|
dbgAddr.addr = addr;
|
|
dbgAddr.fTemporary = false;
|
|
return dbgAddr;
|
|
};
|
|
|
|
/**
|
|
* packAddr(dbgAddr)
|
|
*
|
|
* Packs a DbgAddr object into an Array suitable for saving in a machine state object.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @return {Array}
|
|
*/
|
|
Debugger.prototype.packAddr = function(dbgAddr)
|
|
{
|
|
return [dbgAddr.addr, dbgAddr.fTemporary];
|
|
};
|
|
|
|
/**
|
|
* unpackAddr(aAddr)
|
|
*
|
|
* Unpacks a DbgAddr object from an Array created by packAddr() and restored from a saved machine state.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aAddr
|
|
* @return {DbgAddr}
|
|
*/
|
|
Debugger.prototype.unpackAddr = function(aAddr)
|
|
{
|
|
return {addr: aAddr[0], fTemporary: aAddr[1]};
|
|
};
|
|
|
|
/**
|
|
* parseAddr(sAddr, fCode, fNoChecks, fPrint)
|
|
*
|
|
* Address evaluation and validation (eg, range checks) are no longer performed at this stage. That's
|
|
* done later, by getAddr(), which returns CPUDef.ADDR_INVALID for invalid segments, out-of-range offsets,
|
|
* etc. The Debugger's low-level get/set memory functions verify all getAddr() results, but even if an
|
|
* invalid address is passed through to the Bus memory interfaces, the address will simply be masked with
|
|
* Bus.nBusLimit; in the case of CPUDef.ADDR_INVALID, that will generally refer to the top of the physical
|
|
* address space.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sAddr
|
|
* @param {boolean} [fCode] (true if target is code, false if target is data)
|
|
* @param {boolean} [fNoChecks] (true when setting breakpoints that may not be valid now, but will be later)
|
|
* @param {boolean} [fPrint]
|
|
* @return {DbgAddr|null|undefined}
|
|
*/
|
|
Debugger.prototype.parseAddr = function(sAddr, fCode, fNoChecks, fPrint)
|
|
{
|
|
var dbgAddr;
|
|
var dbgAddrNext = (fCode? this.dbgAddrNextCode : this.dbgAddrNextData);
|
|
var addr = dbgAddrNext.addr;
|
|
if (sAddr !== undefined) {
|
|
sAddr = this.parseReference(sAddr);
|
|
dbgAddr = this.findSymbolAddr(sAddr);
|
|
if (dbgAddr) return dbgAddr;
|
|
addr = this.parseExpression(sAddr, fPrint);
|
|
}
|
|
if (addr != null) {
|
|
dbgAddr = this.newAddr(addr);
|
|
}
|
|
return dbgAddr;
|
|
};
|
|
|
|
/**
|
|
* parseAddrOptions(dbdAddr, sOptions)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {string} [sOptions]
|
|
*/
|
|
Debugger.prototype.parseAddrOptions = function(dbgAddr, sOptions)
|
|
{
|
|
if (sOptions) {
|
|
var a = sOptions.match(/(['"])(.*?)\1/);
|
|
if (a) {
|
|
dbgAddr.aCmds = this.parseCommand(dbgAddr.sCmd = a[2]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* incAddr(dbgAddr, inc)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} [inc] contains value to increment dbgAddr by (default is 1)
|
|
*/
|
|
Debugger.prototype.incAddr = function(dbgAddr, inc)
|
|
{
|
|
if (dbgAddr.addr != null) {
|
|
dbgAddr.addr += (inc || 1);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* toHexOffset(off)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number|null|undefined} [off]
|
|
* @return {string} the hex representation of off
|
|
*/
|
|
Debugger.prototype.toHexOffset = function(off)
|
|
{
|
|
return str.toHex(off, 4);
|
|
};
|
|
|
|
/**
|
|
* toHexAddr(dbgAddr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @return {string} the hex representation of the address
|
|
*/
|
|
Debugger.prototype.toHexAddr = function(dbgAddr)
|
|
{
|
|
return this.toHexOffset(dbgAddr.addr);
|
|
};
|
|
|
|
/**
|
|
* getSZ(dbgAddr, cchMax)
|
|
*
|
|
* Gets zero-terminated (aka "ASCIIZ") string from dbgAddr. It also stops at the first '$', in case this is
|
|
* a '$'-terminated string -- mainly because I'm lazy and didn't feel like writing a separate get() function.
|
|
* Yes, a zero-terminated string containing a '$' will be prematurely terminated, and no, I don't care.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {number} [cchMax] (default is 256)
|
|
* @return {string} (and dbgAddr advanced past the terminating zero)
|
|
*/
|
|
Debugger.prototype.getSZ = function(dbgAddr, cchMax)
|
|
{
|
|
var s = "";
|
|
cchMax = cchMax || 256;
|
|
while (s.length < cchMax) {
|
|
var b = this.getByte(dbgAddr, 1);
|
|
if (!b || b == 0x24 || b >= 127) break;
|
|
s += (b >= 32? String.fromCharCode(b) : '.');
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* dumpBlocks(aBlocks, sAddr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aBlocks
|
|
* @param {string} [sAddr] (optional block address)
|
|
*/
|
|
Debugger.prototype.dumpBlocks = function(aBlocks, sAddr)
|
|
{
|
|
var addr = 0, i = 0, n = aBlocks.length;
|
|
|
|
if (sAddr) {
|
|
addr = this.getAddr(this.parseAddr(sAddr));
|
|
if (addr === CPUDef.ADDR_INVALID) {
|
|
this.println("invalid address: " + sAddr);
|
|
return;
|
|
}
|
|
i = addr >>> this.bus.nBlockShift;
|
|
n = 1;
|
|
}
|
|
|
|
this.println("blockid physical blockaddr used size type");
|
|
this.println("-------- --------- ---------- ------ ------ ----");
|
|
|
|
var typePrev = -1, cPrev = 0;
|
|
while (n--) {
|
|
var block = aBlocks[i];
|
|
if (block.type == typePrev) {
|
|
if (!cPrev++) this.println("...");
|
|
} else {
|
|
typePrev = block.type;
|
|
var sType = Memory.TYPE.NAMES[typePrev];
|
|
if (block) {
|
|
this.println(str.toHex(block.id) + " %" + str.toHex(i << this.bus.nBlockShift) + " %%" + str.toHex(block.addr) + " " + str.toHexWord(block.used) + " " + str.toHexWord(block.size) + " " + sType);
|
|
}
|
|
if (typePrev != Memory.TYPE.NONE) typePrev = -1;
|
|
cPrev = 0;
|
|
}
|
|
addr += this.bus.nBlockSize;
|
|
i++;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* dumpBus(asArgs)
|
|
*
|
|
* Dumps Bus allocations.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs (asArgs[0] is an optional block address)
|
|
*/
|
|
Debugger.prototype.dumpBus = function(asArgs)
|
|
{
|
|
this.dumpBlocks(this.bus.aMemBlocks, asArgs[0]);
|
|
};
|
|
|
|
/**
|
|
* dumpHistory(sPrev, sLines)
|
|
*
|
|
* If sLines is not a number, it can be a instruction filter. However, for the moment, the only
|
|
* supported filter is "call", which filters the history buffer for all CALL and RET instructions
|
|
* from the specified previous point forward.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sPrev] is a (decimal) number of instructions to rewind to (default is 10)
|
|
* @param {string} [sLines] is a (decimal) number of instructions to print (default is, again, 10)
|
|
*/
|
|
Debugger.prototype.dumpHistory = function(sPrev, sLines)
|
|
{
|
|
var sMore = "";
|
|
var cHistory = 0;
|
|
var iHistory = this.iOpcodeHistory;
|
|
var aHistory = this.aOpcodeHistory;
|
|
|
|
if (aHistory.length) {
|
|
var nPrev = +sPrev || this.nextHistory;
|
|
var nLines = +sLines || 10;
|
|
|
|
if (isNaN(nPrev)) {
|
|
nPrev = nLines;
|
|
} else {
|
|
sMore = "more ";
|
|
}
|
|
|
|
if (nPrev > aHistory.length) {
|
|
this.println("note: only " + aHistory.length + " available");
|
|
nPrev = aHistory.length;
|
|
}
|
|
|
|
iHistory -= nPrev;
|
|
if (iHistory < 0) {
|
|
/*
|
|
* If the dbgAddr of the last aHistory element contains a valid selector, wrap around.
|
|
*/
|
|
if (aHistory[aHistory.length - 1].addr == null) {
|
|
nPrev = iHistory + nPrev;
|
|
iHistory = 0;
|
|
} else {
|
|
iHistory += aHistory.length;
|
|
}
|
|
}
|
|
|
|
var aFilters = [];
|
|
if (sLines == "call") {
|
|
nLines = 100000;
|
|
aFilters = ["CALL"];
|
|
}
|
|
|
|
if (sPrev !== undefined) {
|
|
this.println(nPrev + " instructions earlier:");
|
|
}
|
|
|
|
/*
|
|
* TODO: The following is necessary to prevent dumpHistory() from causing additional (or worse, recursive)
|
|
* faults due to segmented addresses that are no longer valid, but the only alternative is to dramatically
|
|
* increase the amount of memory used to store instruction history (eg, storing copies of all the instruction
|
|
* bytes alongside the execution addresses).
|
|
*
|
|
* For now, we're living dangerously, so that our history dumps actually work.
|
|
*
|
|
* this.nSuppressBreaks++;
|
|
*
|
|
* If you re-enable this protection, be sure to re-enable the decrement below, too.
|
|
*/
|
|
while (nLines > 0 && iHistory != this.iOpcodeHistory) {
|
|
|
|
var dbgAddr = aHistory[iHistory++];
|
|
if (dbgAddr.addr == null) break;
|
|
|
|
/*
|
|
* We must create a new dbgAddr from the address in aHistory, because dbgAddr was
|
|
* a reference, not a copy, and we don't want getInstruction() modifying the original.
|
|
*/
|
|
var dbgAddrNew = this.newAddr(dbgAddr.addr);
|
|
|
|
var sComment = "history";
|
|
var nSequence = nPrev--;
|
|
if (DEBUG && dbgAddr.cycleCount != null) {
|
|
sComment = "cycles";
|
|
nSequence = dbgAddr.cycleCount;
|
|
}
|
|
|
|
var sInstruction = this.getInstruction(dbgAddrNew, sComment, nSequence);
|
|
|
|
if (!aFilters.length || sInstruction.indexOf(aFilters[0]) >= 0) {
|
|
this.println(sInstruction);
|
|
}
|
|
|
|
/*
|
|
* If there were OPERAND or ADDRESS overrides on the previous instruction, getInstruction()
|
|
* will have automatically disassembled additional bytes, so skip additional history entries.
|
|
*/
|
|
if (dbgAddrNew.cOverrides) {
|
|
iHistory += dbgAddrNew.cOverrides; nLines -= dbgAddrNew.cOverrides; nPrev -= dbgAddrNew.cOverrides;
|
|
}
|
|
|
|
if (iHistory >= aHistory.length) iHistory = 0;
|
|
this.nextHistory = nPrev;
|
|
cHistory++;
|
|
nLines--;
|
|
}
|
|
/*
|
|
* See comments above.
|
|
*
|
|
* this.nSuppressBreaks--;
|
|
*/
|
|
}
|
|
|
|
if (!cHistory) {
|
|
this.println("no " + sMore + "history available");
|
|
this.nextHistory = undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* messageInit(sEnable)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sEnable contains zero or more message categories to enable, separated by '|'
|
|
*/
|
|
Debugger.prototype.messageInit = function(sEnable)
|
|
{
|
|
this.dbg = this;
|
|
this.bitsMessage = this.bitsWarning = Messages.WARN;
|
|
this.sMessagePrev = null;
|
|
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 Debugger.MESSAGES) {
|
|
if (usr.indexOf(aEnable, m) >= 0) {
|
|
this.bitsMessage |= Debugger.MESSAGES[m];
|
|
this.println(m + " messages enabled");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* messageDump(bitMessage, fnDumper)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} bitMessage is one Messages category flag
|
|
* @param {function(Array.<string>)} fnDumper is a function the Debugger can use to dump data for that category
|
|
* @return {boolean} true if successfully registered, false if not
|
|
*/
|
|
Debugger.prototype.messageDump = function(bitMessage, fnDumper)
|
|
{
|
|
for (var m in Debugger.MESSAGES) {
|
|
if (bitMessage == Debugger.MESSAGES[m]) {
|
|
this.afnDumpers[m] = fnDumper;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* getRegIndex(sReg, off)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sReg
|
|
* @param {number} [off] optional offset into sReg
|
|
* @return {number} register index, or -1 if not found
|
|
*/
|
|
Debugger.prototype.getRegIndex = function(sReg, off)
|
|
{
|
|
var i;
|
|
sReg = sReg.toUpperCase();
|
|
if (off == null) {
|
|
i = usr.indexOf(Debugger.REGS, sReg);
|
|
} else {
|
|
i = usr.indexOf(Debugger.REGS, sReg.substr(off, 2));
|
|
if (i < 0) i = usr.indexOf(Debugger.REGS, sReg.substr(off, 1));
|
|
}
|
|
return i;
|
|
};
|
|
|
|
/**
|
|
* getRegString(iReg)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} iReg
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.getRegString = function(iReg)
|
|
{
|
|
var cch = 0;
|
|
var n = this.getRegValue(iReg);
|
|
if (n !== undefined) {
|
|
switch(iReg) {
|
|
case Debugger.REG_A:
|
|
case Debugger.REG_B:
|
|
case Debugger.REG_C:
|
|
case Debugger.REG_D:
|
|
case Debugger.REG_E:
|
|
case Debugger.REG_H:
|
|
case Debugger.REG_L:
|
|
case Debugger.REG_M:
|
|
cch = 2;
|
|
break;
|
|
case Debugger.REG_BC:
|
|
case Debugger.REG_DE:
|
|
case Debugger.REG_HL:
|
|
case Debugger.REG_SP:
|
|
case Debugger.REG_PC:
|
|
case Debugger.REG_PS:
|
|
case Debugger.REG_PSW:
|
|
cch = 4;
|
|
break;
|
|
}
|
|
}
|
|
return cch? str.toHex(n, cch) : "??";
|
|
};
|
|
|
|
/**
|
|
* getRegValue(iReg)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} iReg
|
|
* @return {number|undefined}
|
|
*/
|
|
Debugger.prototype.getRegValue = function(iReg)
|
|
{
|
|
var n;
|
|
if (iReg >= 0) {
|
|
var cpu = this.cpu;
|
|
switch(iReg) {
|
|
case Debugger.REG_A:
|
|
n = cpu.regA;
|
|
break;
|
|
case Debugger.REG_B:
|
|
n = cpu.regB;
|
|
break;
|
|
case Debugger.REG_C:
|
|
n = cpu.regC;
|
|
break;
|
|
case Debugger.REG_BC:
|
|
n = cpu.getBC();
|
|
break;
|
|
case Debugger.REG_D:
|
|
n = cpu.regD;
|
|
break;
|
|
case Debugger.REG_E:
|
|
n = cpu.regE;
|
|
break;
|
|
case Debugger.REG_DE:
|
|
n = cpu.getDE();
|
|
break;
|
|
case Debugger.REG_H:
|
|
n = cpu.regH;
|
|
break;
|
|
case Debugger.REG_L:
|
|
n = cpu.regL;
|
|
break;
|
|
case Debugger.REG_HL:
|
|
n = cpu.getHL();
|
|
break;
|
|
case Debugger.REG_M:
|
|
n = cpu.getByte(cpu.getHL());
|
|
break;
|
|
case Debugger.REG_SP:
|
|
n = cpu.getSP();
|
|
break;
|
|
case Debugger.REG_PC:
|
|
n = cpu.getPC();
|
|
break;
|
|
case Debugger.REG_PS:
|
|
n = cpu.getPS();
|
|
break;
|
|
case Debugger.REG_PSW:
|
|
n = cpu.getPSW();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return n;
|
|
};
|
|
|
|
/**
|
|
* replaceRegs(s)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} s
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.replaceRegs = function(s)
|
|
{
|
|
/*
|
|
* Replace any references first; this means that register references inside the reference
|
|
* do NOT need to be prefixed with '@'.
|
|
*/
|
|
s = this.parseReference(s);
|
|
|
|
/*
|
|
* Replace every @XX (or @XXX), where XX (or XXX) is a register, with the register's value.
|
|
*/
|
|
var i = 0;
|
|
var b, sChar, sAddr, dbgAddr, sReplace;
|
|
while ((i = s.indexOf('@', i)) >= 0) {
|
|
var iReg = this.getRegIndex(s, i + 1);
|
|
if (iReg >= 0) {
|
|
s = s.substr(0, i) + this.getRegString(iReg) + s.substr(i + 1 + Debugger.REGS[iReg].length);
|
|
}
|
|
i++;
|
|
}
|
|
/*
|
|
* Replace every #XX, where XX is a hex byte value, with the corresponding ASCII character (if printable).
|
|
*/
|
|
i = 0;
|
|
while ((i = s.indexOf('#', i)) >= 0) {
|
|
sChar = s.substr(i+1, 2);
|
|
b = str.parseInt(sChar, 16);
|
|
if (b != null && b >= 32 && b < 128) {
|
|
sReplace = sChar + " '" + String.fromCharCode(b) + "'";
|
|
s = s.replace('#' + sChar, sReplace);
|
|
i += sReplace.length;
|
|
continue;
|
|
}
|
|
i++;
|
|
}
|
|
/*
|
|
* Replace every $XXXX:XXXX, where XXXX:XXXX is a segmented address, with the zero-terminated string at that address.
|
|
*/
|
|
i = 0;
|
|
while ((i = s.indexOf('$', i)) >= 0) {
|
|
sAddr = s.substr(i+1, 9);
|
|
dbgAddr = this.parseAddr(sAddr);
|
|
if (dbgAddr) {
|
|
sReplace = sAddr + ' "' + this.getSZ(dbgAddr) + '"';
|
|
s = s.replace('$' + sAddr, sReplace);
|
|
i += sReplace.length;
|
|
continue;
|
|
}
|
|
i++;
|
|
}
|
|
/*
|
|
* Replace every ^XXXX:XXXX, where XXXX:XXXX is a segmented address, with the FCB filename stored at that address.
|
|
*/
|
|
i = 0;
|
|
while ((i = s.indexOf('^', i)) >= 0) {
|
|
sAddr = s.substr(i+1, 9);
|
|
dbgAddr = this.parseAddr(sAddr);
|
|
if (dbgAddr) {
|
|
this.incAddr(dbgAddr);
|
|
sReplace = sAddr + ' "' + this.getSZ(dbgAddr, 11) + '"';
|
|
s = s.replace('^' + sAddr, sReplace);
|
|
i += sReplace.length;
|
|
continue;
|
|
}
|
|
i++;
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* message(sMessage, fAddress)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sMessage is any caller-defined message string
|
|
* @param {boolean} [fAddress] is true to display the current CS:IP
|
|
*/
|
|
Debugger.prototype.message = function(sMessage, fAddress)
|
|
{
|
|
if (fAddress) {
|
|
sMessage += " at " + this.toHexAddr(this.newAddr(this.cpu.getPC()));
|
|
}
|
|
|
|
if (this.bitsMessage & Messages.BUFFER) {
|
|
this.aMessageBuffer.push(sMessage);
|
|
return;
|
|
}
|
|
|
|
if (this.sMessagePrev && sMessage == this.sMessagePrev) return;
|
|
this.sMessagePrev = sMessage;
|
|
|
|
if (this.bitsMessage & Messages.HALT) {
|
|
this.stopCPU();
|
|
sMessage += " (cpu halted)";
|
|
}
|
|
|
|
this.println(sMessage); // + " (" + this.cpu.getCycles() + " cycles)"
|
|
|
|
/*
|
|
* We have no idea what the frequency of println() calls might be; all we know is that they easily
|
|
* screw up the CPU's careful assumptions about cycles per burst. So we call yieldCPU() after every
|
|
* message, to effectively end the current burst and start fresh.
|
|
*
|
|
* TODO: See CPU.calcStartTime() for a discussion of why we might want to call yieldCPU() *before*
|
|
* we display the message.
|
|
*/
|
|
if (this.cpu) this.cpu.yieldCPU();
|
|
};
|
|
|
|
/**
|
|
* messageIO(component, port, bOut, addrFrom, name, bIn, bitsMessage)
|
|
*
|
|
* Most (if not all) port handlers should provide a name for their respective ports, so if no name is provided,
|
|
* we assume this is an unknown port, and display a message by default.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Component} component
|
|
* @param {number} port
|
|
* @param {number|null} bOut if an output operation
|
|
* @param {number|null} [addrFrom]
|
|
* @param {string|null} [name] of the port, if any
|
|
* @param {number|null} [bIn] is the input value, if known, on an input operation
|
|
* @param {number} [bitsMessage] is one or more Messages category flag(s)
|
|
*/
|
|
Debugger.prototype.messageIO = function(component, port, bOut, addrFrom, name, bIn, bitsMessage)
|
|
{
|
|
bitsMessage |= Messages.PORT;
|
|
if (name == null || (this.bitsMessage & bitsMessage) == bitsMessage) {
|
|
this.message(component.idComponent + '.' + (bOut != null? "outPort" : "inPort") + '(' + str.toHexWord(port) + ',' + (name? name : "unknown") + (bOut != null? ',' + str.toHexByte(bOut) : "") + ')' + (bIn != null? (": " + str.toHexByte(bIn)) : "") + (addrFrom != null? (" at " + this.toHexOffset(addrFrom)) : ""));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* init()
|
|
*
|
|
* @this {Debugger}
|
|
*/
|
|
Debugger.prototype.init = function()
|
|
{
|
|
this.println("Type ? for help with 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 {Debugger}
|
|
* @param {boolean} [fQuiet]
|
|
*/
|
|
Debugger.prototype.historyInit = function(fQuiet)
|
|
{
|
|
var i;
|
|
if (!this.checksEnabled()) {
|
|
if (this.aOpcodeHistory && this.aOpcodeHistory.length && !fQuiet) {
|
|
this.println("instruction history buffer freed");
|
|
}
|
|
this.iOpcodeHistory = 0;
|
|
this.aOpcodeHistory = [];
|
|
this.aaOpcodeCounts = [];
|
|
return;
|
|
}
|
|
if (!this.aOpcodeHistory || !this.aOpcodeHistory.length) {
|
|
this.aOpcodeHistory = new Array(Debugger.HISTORY_LIMIT);
|
|
for (i = 0; i < this.aOpcodeHistory.length; i++) {
|
|
/*
|
|
* Preallocate dummy Addr (Array) objects in every history slot, so that
|
|
* checkInstruction() doesn't need to call newAddr() on every slot update.
|
|
*/
|
|
this.aOpcodeHistory[i] = this.newAddr();
|
|
}
|
|
this.iOpcodeHistory = 0;
|
|
if (!fQuiet) {
|
|
this.println("instruction history buffer allocated");
|
|
}
|
|
}
|
|
if (!this.aaOpcodeCounts || !this.aaOpcodeCounts.length) {
|
|
this.aaOpcodeCounts = new Array(256);
|
|
for (i = 0; i < this.aaOpcodeCounts.length; i++) {
|
|
this.aaOpcodeCounts[i] = [i, 0];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* runCPU(fUpdateFocus)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fUpdateFocus] is true to update focus
|
|
* @return {boolean} true if run request successful, false if not
|
|
*/
|
|
Debugger.prototype.runCPU = function(fUpdateFocus)
|
|
{
|
|
if (!this.isCPUAvail()) return false;
|
|
this.cpu.runCPU(fUpdateFocus);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* stepCPU(nCycles, fRegs, fUpdateCPU)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} nCycles (0 for one instruction without checking breakpoints)
|
|
* @param {boolean} [fRegs] is true to display registers after step (default is false)
|
|
* @param {boolean} [fUpdateCPU] is false to disable calls to updateCPU() (default is true)
|
|
* @return {boolean}
|
|
*/
|
|
Debugger.prototype.stepCPU = function(nCycles, fRegs, fUpdateCPU)
|
|
{
|
|
if (!this.isCPUAvail()) return false;
|
|
|
|
this.nCycles = 0;
|
|
|
|
if (!nCycles) {
|
|
/*
|
|
* When single-stepping, the CPU won't call checkInstruction(), which is good for
|
|
* avoiding breakpoints, but bad for instruction data collection if checks are enabled.
|
|
* So we call checkInstruction() ourselves.
|
|
*/
|
|
if (this.checksEnabled()) this.checkInstruction(this.cpu.getPC(), 0);
|
|
}
|
|
try {
|
|
var nCyclesStep = this.cpu.stepCPU(nCycles);
|
|
if (nCyclesStep > 0) {
|
|
this.nCycles += nCyclesStep;
|
|
this.cpu.addCycles(nCyclesStep, true);
|
|
this.cpu.updateChecksum(nCyclesStep);
|
|
this.cOpcodes++;
|
|
}
|
|
}
|
|
catch(exception) {
|
|
if (typeof exception != "number") {
|
|
var e = exception;
|
|
this.nCycles = 0;
|
|
this.cpu.setError(e.stack || e.message);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Because we called cpu.stepCPU() and not cpu.runCPU(), we must nudge the cpu's update code,
|
|
* and then update our own state. Normally, the only time fUpdateCPU will be false is when doTrace()
|
|
* is calling us in a loop, in which case it will perform its own updateCPU() when it's done.
|
|
*/
|
|
if (fUpdateCPU !== false) this.cpu.updateCPU();
|
|
|
|
this.updateStatus(fRegs || false);
|
|
return (this.nCycles > 0);
|
|
};
|
|
|
|
/**
|
|
* stopCPU()
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fComplete]
|
|
*/
|
|
Debugger.prototype.stopCPU = function(fComplete)
|
|
{
|
|
if (this.cpu) this.cpu.stopCPU(fComplete);
|
|
};
|
|
|
|
/**
|
|
* updateStatus(fRegs)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fRegs] (default is true)
|
|
*/
|
|
Debugger.prototype.updateStatus = function(fRegs)
|
|
{
|
|
if (fRegs === undefined) fRegs = true;
|
|
|
|
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
|
|
/*
|
|
* this.nStep used to be a simple boolean, but now it's 0 (or undefined)
|
|
* if inactive, 1 if stepping over an instruction without a register dump, or 2
|
|
* if stepping over an instruction with a register dump.
|
|
*/
|
|
if (!fRegs || this.nStep == 1)
|
|
this.doUnassemble();
|
|
else {
|
|
this.doRegisters();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* isCPUAvail()
|
|
*
|
|
* Make sure the CPU is ready (finished initializing), not busy (already running), and not in an error state.
|
|
*
|
|
* @this {Debugger}
|
|
* @return {boolean}
|
|
*/
|
|
Debugger.prototype.isCPUAvail = function()
|
|
{
|
|
if (!this.cpu)
|
|
return false;
|
|
if (!this.cpu.isReady())
|
|
return false;
|
|
if (!this.cpu.isPowered())
|
|
return false;
|
|
if (this.cpu.isBusy())
|
|
return false;
|
|
return !this.cpu.isError();
|
|
};
|
|
|
|
/**
|
|
* powerUp(data, fRepower)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Object|null} data
|
|
* @param {boolean} [fRepower]
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Debugger.prototype.powerUp = function(data, fRepower)
|
|
{
|
|
if (!fRepower) {
|
|
/*
|
|
* Because Debugger save/restore support is somewhat limited (and didn't always exist),
|
|
* we deviate from the typical save/restore design pattern: instead of reset OR restore,
|
|
* we always reset and then perform a (potentially limited) restore.
|
|
*/
|
|
this.reset(true);
|
|
|
|
// this.println(data? "resuming" : "powering up");
|
|
|
|
if (data && this.restore) {
|
|
if (!this.restore(data)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave, fShutdown)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fSave]
|
|
* @param {boolean} [fShutdown]
|
|
* @return {Object|boolean}
|
|
*/
|
|
Debugger.prototype.powerDown = function(fSave, fShutdown)
|
|
{
|
|
if (fShutdown) this.println(fSave? "suspending" : "shutting down");
|
|
return fSave? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* reset(fQuiet)
|
|
*
|
|
* This is a notification handler, called by the Computer, to inform us of a reset.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} fQuiet (true only when called from our own powerUp handler)
|
|
*/
|
|
Debugger.prototype.reset = function(fQuiet)
|
|
{
|
|
this.historyInit();
|
|
this.cOpcodes = this.cOpcodesStart = 0;
|
|
this.sMessagePrev = null;
|
|
this.nCycles = 0;
|
|
this.dbgAddrNextCode = this.newAddr(this.cpu.getPC());
|
|
/*
|
|
* fRunning is set by start() and cleared by stop(). In addition, we clear
|
|
* it here, so that if the CPU is reset while running, we can prevent stop()
|
|
* from unnecessarily dumping the CPU state.
|
|
*/
|
|
this.flags.fRunning = false;
|
|
this.clearTempBreakpoint();
|
|
if (!fQuiet) this.updateStatus();
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements (very rudimentary) save support for the Debugger component.
|
|
*
|
|
* @this {Debugger}
|
|
* @return {Object}
|
|
*/
|
|
Debugger.prototype.save = function()
|
|
{
|
|
var state = new State(this);
|
|
state.set(0, this.packAddr(this.dbgAddrNextCode));
|
|
state.set(1, this.packAddr(this.dbgAddrAssemble));
|
|
state.set(2, [this.aPrevCmds, this.fAssemble, this.bitsMessage]);
|
|
state.set(3, this.aSymbolTable);
|
|
return state.data();
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements (very rudimentary) restore support for the Debugger component.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Object} data
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Debugger.prototype.restore = function(data)
|
|
{
|
|
var i = 0;
|
|
if (data[2] !== undefined) {
|
|
this.dbgAddrNextCode = this.unpackAddr(data[i++]);
|
|
this.dbgAddrAssemble = this.unpackAddr(data[i++]);
|
|
this.aPrevCmds = data[i][0];
|
|
if (typeof this.aPrevCmds == "string") this.aPrevCmds = [this.aPrevCmds];
|
|
this.fAssemble = data[i][1];
|
|
this.bitsMessage |= data[i][2]; // keep our current message bits set, and simply "add" any extra bits defined by the saved state
|
|
}
|
|
if (data[3]) this.aSymbolTable = data[3];
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* start(ms, nCycles)
|
|
*
|
|
* This is a notification handler, called by the Computer, to inform us the CPU has started.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Debugger.prototype.start = function(ms, nCycles)
|
|
{
|
|
if (!this.nStep) this.println("running");
|
|
this.flags.fRunning = true;
|
|
this.msStart = ms;
|
|
this.nCyclesStart = nCycles;
|
|
};
|
|
|
|
/**
|
|
* stop(ms, nCycles)
|
|
*
|
|
* This is a notification handler, called by the Computer, to inform us the CPU has now stopped.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Debugger.prototype.stop = function(ms, nCycles)
|
|
{
|
|
if (this.flags.fRunning) {
|
|
this.flags.fRunning = false;
|
|
this.nCycles = nCycles - this.nCyclesStart;
|
|
if (!this.nStep) {
|
|
var sStopped = "stopped";
|
|
if (this.nCycles) {
|
|
var msTotal = ms - this.msStart;
|
|
var nCyclesPerSecond = (msTotal > 0? Math.round(this.nCycles * 1000 / msTotal) : 0);
|
|
sStopped += " (";
|
|
if (this.checksEnabled()) {
|
|
sStopped += this.cOpcodes + " opcodes, ";
|
|
/*
|
|
* $ops displays progress by calculating cOpcodes - cOpcodesStart, so before
|
|
* zeroing cOpcodes, we should subtract cOpcodes from cOpcodesStart (since we're
|
|
* effectively subtracting cOpcodes from cOpcodes as well).
|
|
*/
|
|
this.cOpcodesStart -= this.cOpcodes;
|
|
this.cOpcodes = 0;
|
|
}
|
|
sStopped += this.nCycles + " cycles, " + msTotal + " ms, " + nCyclesPerSecond + " hz)";
|
|
} else {
|
|
if (this.messageEnabled(Messages.HALT)) {
|
|
/*
|
|
* It's possible the user is trying to 'g' past a fault that was blocked by helpCheckFault()
|
|
* for the Debugger's benefit; if so, it will continue to be blocked, so try displaying a helpful
|
|
* message (another helpful tip would be to simply turn off the "halt" message category).
|
|
*/
|
|
sStopped += " (use the 't' command to execute blocked faults)";
|
|
}
|
|
}
|
|
this.println(sStopped);
|
|
}
|
|
this.updateStatus(true);
|
|
this.updateFocus();
|
|
this.clearTempBreakpoint(this.cpu.getPC());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* checksEnabled(fRelease)
|
|
*
|
|
* This "check" function is called by the CPU; we indicate whether or not every instruction needs to be checked.
|
|
*
|
|
* Originally, this returned true even when there were only read and/or write breakpoints, but those breakpoints
|
|
* no longer require the intervention of checkInstruction(); the Bus component automatically swaps in/out appropriate
|
|
* "checked" Memory access functions to deal with those breakpoints in the corresponding Memory blocks. So I've
|
|
* simplified the test below.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fRelease] is true for release criteria only; default is false (any criteria)
|
|
* @return {boolean} true if every instruction needs to pass through checkInstruction(), false if not
|
|
*/
|
|
Debugger.prototype.checksEnabled = function(fRelease)
|
|
{
|
|
return ((DEBUG && !fRelease)? true : (this.aBreakExec.length > 1 || !!this.nBreakIns));
|
|
};
|
|
|
|
/**
|
|
* checkInstruction(addr, nState)
|
|
*
|
|
* This "check" function is called by the CPU to inform us about the next instruction to be executed,
|
|
* giving us an opportunity to look for "exec" breakpoints and update opcode frequencies and instruction history.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} addr
|
|
* @param {number} nState is < 0 if stepping, 0 if starting, or > 0 if running
|
|
* @return {boolean} true if breakpoint hit, false if not
|
|
*/
|
|
Debugger.prototype.checkInstruction = function(addr, nState)
|
|
{
|
|
var cpu = this.cpu;
|
|
|
|
if (nState > 0) {
|
|
if (this.nBreakIns && !--this.nBreakIns) {
|
|
return true;
|
|
}
|
|
if (this.checkBreakpoint(addr, 1, this.aBreakExec)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The rest of the instruction tracking logic can only be performed if historyInit() has allocated the
|
|
* necessary data structures. Note that there is no explicit UI for enabling/disabling history, other than
|
|
* adding/removing breakpoints, simply because it's breakpoints that trigger the call to checkInstruction();
|
|
* well, OK, and a few other things now, like enabling Messages.INT messages.
|
|
*/
|
|
if (nState >= 0 && this.aaOpcodeCounts.length) {
|
|
this.cOpcodes++;
|
|
var bOpcode = this.bus.getByteDirect(addr);
|
|
if (bOpcode != null) {
|
|
this.aaOpcodeCounts[bOpcode][1]++;
|
|
var dbgAddr = this.aOpcodeHistory[this.iOpcodeHistory];
|
|
this.setAddr(dbgAddr, cpu.getPC());
|
|
if (DEBUG) dbgAddr.cycleCount = cpu.getCycles();
|
|
if (++this.iOpcodeHistory == this.aOpcodeHistory.length) this.iOpcodeHistory = 0;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* checkMemoryRead(addr, nb)
|
|
*
|
|
* This "check" function is called by a Memory block to inform us that a memory read occurred, giving us an
|
|
* opportunity to track the read if we want, and look for a matching "read" breakpoint, if any.
|
|
*
|
|
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
|
|
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" read.
|
|
*
|
|
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} addr
|
|
* @param {number} [nb] (# of bytes; default is 1)
|
|
* @return {boolean} true if breakpoint hit, false if not
|
|
*/
|
|
Debugger.prototype.checkMemoryRead = function(addr, nb)
|
|
{
|
|
if (this.checkBreakpoint(addr, nb || 1, this.aBreakRead)) {
|
|
this.stopCPU(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* checkMemoryWrite(addr, nb)
|
|
*
|
|
* This "check" function is called by a Memory block to inform us that a memory write occurred, giving us an
|
|
* opportunity to track the write if we want, and look for a matching "write" breakpoint, if any.
|
|
*
|
|
* In the "old days", it would be an error for this call to fail to find a matching Debugger breakpoint, but now
|
|
* Memory blocks have no idea whether the Debugger or the machine's Debug register(s) triggered this "checked" write.
|
|
*
|
|
* If we return true, we "trump" the machine's Debug register(s); false allows normal Debug register processing.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} addr
|
|
* @param {number} [nb] (# of bytes; default is 1)
|
|
* @return {boolean} true if breakpoint hit, false if not
|
|
*/
|
|
Debugger.prototype.checkMemoryWrite = function(addr, nb)
|
|
{
|
|
if (this.checkBreakpoint(addr, nb || 1, this.aBreakWrite)) {
|
|
this.stopCPU(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* checkPortInput(port, size, data)
|
|
*
|
|
* This "check" function is called by the Bus component to inform us that port input occurred.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} port
|
|
* @param {number} size
|
|
* @param {number} data
|
|
* @return {boolean} true if breakpoint hit, false if not
|
|
*/
|
|
Debugger.prototype.checkPortInput = function(port, size, data)
|
|
{
|
|
/*
|
|
* We trust that the Bus component won't call us unless we told it to, so we halt unconditionally
|
|
*/
|
|
this.println("break on input from port " + str.toHexWord(port) + ": " + str.toHex(data));
|
|
this.stopCPU(true);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* checkPortOutput(port, size, data)
|
|
*
|
|
* This "check" function is called by the Bus component to inform us that port output occurred.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} port
|
|
* @param {number} size
|
|
* @param {number} data
|
|
* @return {boolean} true if breakpoint hit, false if not
|
|
*/
|
|
Debugger.prototype.checkPortOutput = function(port, size, data)
|
|
{
|
|
/*
|
|
* We trust that the Bus component won't call us unless we told it to, so we halt unconditionally
|
|
*/
|
|
this.println("break on output to port " + str.toHexWord(port) + ": " + str.toHex(data));
|
|
this.stopCPU(true);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* clearBreakpoints()
|
|
*
|
|
* @this {Debugger}
|
|
*/
|
|
Debugger.prototype.clearBreakpoints = function()
|
|
{
|
|
var i, dbgAddr;
|
|
this.aBreakExec = ["bp"];
|
|
if (this.aBreakRead !== undefined) {
|
|
for (i = 1; i < this.aBreakRead.length; i++) {
|
|
dbgAddr = this.aBreakRead[i];
|
|
this.bus.removeMemBreak(this.getAddr(dbgAddr), false);
|
|
}
|
|
}
|
|
this.aBreakRead = ["br"];
|
|
if (this.aBreakWrite !== undefined) {
|
|
for (i = 1; i < this.aBreakWrite.length; i++) {
|
|
dbgAddr = this.aBreakWrite[i];
|
|
this.bus.removeMemBreak(this.getAddr(dbgAddr), true);
|
|
}
|
|
}
|
|
this.aBreakWrite = ["bw"];
|
|
/*
|
|
* nSuppressBreaks ensures we can't get into an infinite loop where a breakpoint lookup requires
|
|
* reading a segment descriptor via getSegment(), and that triggers more memory reads, which triggers
|
|
* more breakpoint checks.
|
|
*/
|
|
this.nSuppressBreaks = 0;
|
|
};
|
|
|
|
/**
|
|
* addBreakpoint(aBreak, dbgAddr, fTemporary)
|
|
*
|
|
* In case you haven't already figured this out, all our breakpoint commands use the address
|
|
* to identify a breakpoint, not an incrementally assigned breakpoint index like other debuggers;
|
|
* see doBreak() for details.
|
|
*
|
|
* This has a few implications, one being that you CANNOT set more than one kind of breakpoint
|
|
* on a single address. In practice, that's rarely a problem, because you can almost always set
|
|
* a different breakpoint on a neighboring address.
|
|
*
|
|
* Also, there is one exception to the "one address, one breakpoint" rule, and that involves
|
|
* temporary breakpoints (ie, one-time execution breakpoints that either a "p" or "g" command
|
|
* may create to step over a chunk of code). Those breakpoints automatically clear themselves,
|
|
* so there usually isn't any need to refer to them using breakpoint commands.
|
|
*
|
|
* TODO: Consider supporting the more "traditional" breakpoint index syntax; the current
|
|
* address-based syntax was implemented solely for expediency and consistency. At the same time,
|
|
* also consider a more WDEB386-like syntax, where "br" is used to set a variety of access-specific
|
|
* breakpoints, using modifiers like "r1", "r2", "w1", "w2, etc.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aBreak
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {boolean} [fTemporary]
|
|
* @return {boolean} true if breakpoint added, false if already exists
|
|
*/
|
|
Debugger.prototype.addBreakpoint = function(aBreak, dbgAddr, fTemporary)
|
|
{
|
|
var fSuccess = true;
|
|
|
|
// this.nSuppressBreaks++;
|
|
|
|
/*
|
|
* Instead of complaining that a breakpoint already exists (as we used to do), we now
|
|
* allow breakpoints to be re-set; this makes it easier to update any commands that may
|
|
* be associated with the breakpoint.
|
|
*
|
|
* The only exception: we DO allow a temporary breakpoint at an address where there may
|
|
* already be a breakpoint, so that you can easily step ("p" or "g") over such addresses.
|
|
*/
|
|
if (!fTemporary) {
|
|
this.findBreakpoint(aBreak, dbgAddr, true, false, true);
|
|
}
|
|
|
|
if (aBreak != this.aBreakExec) {
|
|
var addr = this.getAddr(dbgAddr);
|
|
if (addr === CPUDef.ADDR_INVALID) {
|
|
this.println("invalid address: " + this.toHexAddr(dbgAddr));
|
|
fSuccess = false;
|
|
} else {
|
|
this.bus.addMemBreak(addr, aBreak == this.aBreakWrite);
|
|
}
|
|
}
|
|
|
|
if (fSuccess) {
|
|
aBreak.push(dbgAddr);
|
|
if (fTemporary) {
|
|
dbgAddr.fTemporary = true;
|
|
}
|
|
else {
|
|
this.printBreakpoint(aBreak, aBreak.length-1, "set");
|
|
this.historyInit();
|
|
}
|
|
}
|
|
|
|
// this.nSuppressBreaks--;
|
|
|
|
return fSuccess;
|
|
};
|
|
|
|
/**
|
|
* findBreakpoint(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aBreak
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {boolean} [fRemove]
|
|
* @param {boolean} [fTemporary]
|
|
* @param {boolean} [fQuiet]
|
|
* @return {boolean} true if found, false if not
|
|
*/
|
|
Debugger.prototype.findBreakpoint = function(aBreak, dbgAddr, fRemove, fTemporary, fQuiet)
|
|
{
|
|
var fFound = false;
|
|
var addr = this.getAddr(dbgAddr);
|
|
for (var i = 1; i < aBreak.length; i++) {
|
|
var dbgAddrBreak = aBreak[i];
|
|
if (addr == this.getAddr(dbgAddrBreak)) {
|
|
if (!fTemporary || dbgAddrBreak.fTemporary) {
|
|
fFound = true;
|
|
if (fRemove) {
|
|
if (!dbgAddrBreak.fTemporary && !fQuiet) {
|
|
this.printBreakpoint(aBreak, i, "cleared");
|
|
}
|
|
aBreak.splice(i, 1);
|
|
if (aBreak != this.aBreakExec) {
|
|
this.bus.removeMemBreak(addr, aBreak == this.aBreakWrite);
|
|
}
|
|
/*
|
|
* We'll mirror the logic in addBreakpoint() and leave the history buffer alone if this
|
|
* was a temporary breakpoint.
|
|
*/
|
|
if (!dbgAddrBreak.fTemporary) {
|
|
this.historyInit();
|
|
}
|
|
break;
|
|
}
|
|
if (!fQuiet) this.printBreakpoint(aBreak, i, "exists");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return fFound;
|
|
};
|
|
|
|
/**
|
|
* listBreakpoints(aBreak)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aBreak
|
|
* @return {number} of breakpoints listed, 0 if none
|
|
*/
|
|
Debugger.prototype.listBreakpoints = function(aBreak)
|
|
{
|
|
for (var i = 1; i < aBreak.length; i++) {
|
|
this.printBreakpoint(aBreak, i);
|
|
}
|
|
return aBreak.length - 1;
|
|
};
|
|
|
|
/**
|
|
* printBreakpoint(aBreak, i, sAction)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array} aBreak
|
|
* @param {number} i
|
|
* @param {string} [sAction]
|
|
*/
|
|
Debugger.prototype.printBreakpoint = function(aBreak, i, sAction)
|
|
{
|
|
var dbgAddr = aBreak[i];
|
|
this.println(aBreak[0] + ' ' + this.toHexAddr(dbgAddr) + (sAction? (' ' + sAction) : (dbgAddr.sCmd? (' "' + dbgAddr.sCmd + '"') : '')));
|
|
};
|
|
|
|
/**
|
|
* setTempBreakpoint(dbgAddr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr of new temp breakpoint
|
|
*/
|
|
Debugger.prototype.setTempBreakpoint = function(dbgAddr)
|
|
{
|
|
this.addBreakpoint(this.aBreakExec, dbgAddr, true);
|
|
};
|
|
|
|
/**
|
|
* clearTempBreakpoint(addr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number|undefined} [addr] clear all temp breakpoints if no address specified
|
|
*/
|
|
Debugger.prototype.clearTempBreakpoint = function(addr)
|
|
{
|
|
if (addr !== undefined) {
|
|
this.checkBreakpoint(addr, 1, this.aBreakExec, true);
|
|
this.nStep = 0;
|
|
} else {
|
|
for (var i = 1; i < this.aBreakExec.length; i++) {
|
|
var dbgAddrBreak = this.aBreakExec[i];
|
|
if (dbgAddrBreak.fTemporary) {
|
|
if (!this.findBreakpoint(this.aBreakExec, dbgAddrBreak, true, true)) break;
|
|
i = 0;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* checkBreakpoint(addr, nb, aBreak, fTemporary)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} addr
|
|
* @param {number} nb (# of bytes)
|
|
* @param {Array} aBreak
|
|
* @param {boolean} [fTemporary]
|
|
* @return {boolean} true if breakpoint has been hit, false if not
|
|
*/
|
|
Debugger.prototype.checkBreakpoint = function(addr, nb, aBreak, fTemporary)
|
|
{
|
|
/*
|
|
* Time to check for execution breakpoints; note that this should be done BEFORE updating frequency
|
|
* or history data (see checkInstruction), since we might not actually execute the current instruction.
|
|
*/
|
|
var fBreak = false;
|
|
|
|
if (!this.nSuppressBreaks++) {
|
|
|
|
for (var i = 1; !fBreak && i < aBreak.length; i++) {
|
|
|
|
var dbgAddrBreak = aBreak[i];
|
|
|
|
if (fTemporary && !dbgAddrBreak.fTemporary) continue;
|
|
|
|
/*
|
|
* We used to calculate the linear address of the breakpoint at the time the
|
|
* breakpoint was added, so that a breakpoint set in one mode (eg, in real-mode)
|
|
* would still work as intended if the mode changed later (eg, to protected-mode).
|
|
*
|
|
* However, that created difficulties setting protected-mode breakpoints in segments
|
|
* that might not be defined yet, or that could move in physical memory.
|
|
*
|
|
* If you want to create a real-mode breakpoint that will break regardless of mode,
|
|
* use the physical address of the real-mode memory location instead.
|
|
*/
|
|
var addrBreak = this.getAddr(dbgAddrBreak);
|
|
for (var n = 0; n < nb; n++) {
|
|
if (addr + n == addrBreak) {
|
|
var a;
|
|
fBreak = true;
|
|
if (dbgAddrBreak.fTemporary) {
|
|
this.findBreakpoint(aBreak, dbgAddrBreak, true, true);
|
|
fTemporary = true;
|
|
}
|
|
if (a = dbgAddrBreak.aCmds) {
|
|
/*
|
|
* When one or more commands are attached to a breakpoint, we don't halt by default.
|
|
* Instead, we set fBreak to true only if, at the completion of all the commands, the
|
|
* CPU is halted; in other words, you should include "h" as one of the breakpoint commands
|
|
* if you want the breakpoint to stop execution.
|
|
*
|
|
* Another useful command is "if", which will return false if the expression is false,
|
|
* at which point we'll jump ahead to the next "else" command, and if there isn't an "else",
|
|
* we abort.
|
|
*/
|
|
fBreak = false;
|
|
for (var j = 0; j < a.length; j++) {
|
|
if (!this.doCommand(a[j], true)) {
|
|
if (a[j].indexOf("if")) {
|
|
fBreak = true; // the failed command wasn't "if", so abort
|
|
break;
|
|
}
|
|
var k = j + 1;
|
|
for (; k < a.length; k++) {
|
|
if (!a[k].indexOf("else")) break;
|
|
j++;
|
|
}
|
|
if (k == a.length) { // couldn't find an "else" after the "if", so abort
|
|
fBreak = true;
|
|
break;
|
|
}
|
|
/*
|
|
* If we're still here, we'll execute the "else" command (which is just a no-op),
|
|
* followed by any remaining commands.
|
|
*/
|
|
}
|
|
}
|
|
if (!this.cpu.isRunning()) fBreak = true;
|
|
}
|
|
if (fBreak) {
|
|
if (!fTemporary) this.printBreakpoint(aBreak, i, "hit");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.nSuppressBreaks--;
|
|
|
|
return fBreak;
|
|
};
|
|
|
|
/**
|
|
* getInstruction(dbgAddr, sComment, nSequence)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {string} [sComment] is an associated comment
|
|
* @param {number} [nSequence] is an associated sequence number, undefined if none
|
|
* @return {string} (and dbgAddr is updated to the next instruction)
|
|
*/
|
|
Debugger.prototype.getInstruction = function(dbgAddr, sComment, nSequence)
|
|
{
|
|
var dbgAddrIns = this.newAddr(dbgAddr.addr);
|
|
|
|
var bOpcode = this.getByte(dbgAddr, 1);
|
|
|
|
var asOpcodes = this.style != Debugger.STYLE_8086? Debugger.INS_NAMES : Debugger.INS_NAMES_8086;
|
|
var aOpDesc = this.aaOpDescs[bOpcode];
|
|
var iIns = aOpDesc[0];
|
|
|
|
var sOperands = "";
|
|
var sOpcode = asOpcodes[iIns];
|
|
var cOperands = aOpDesc.length - 1;
|
|
var typeSizeDefault = Debugger.TYPE_NONE, type;
|
|
|
|
for (var iOperand = 1; iOperand <= cOperands; iOperand++) {
|
|
|
|
var disp, off, cch;
|
|
var sOperand = "";
|
|
|
|
type = aOpDesc[iOperand];
|
|
if (type === undefined) continue;
|
|
if ((type & Debugger.TYPE_OPT) && this.style == Debugger.STYLE_8080) continue;
|
|
|
|
var typeMode = type & Debugger.TYPE_MODE;
|
|
if (!typeMode) continue;
|
|
|
|
var typeSize = type & Debugger.TYPE_SIZE;
|
|
if (!typeSize) {
|
|
type |= typeSizeDefault;
|
|
} else {
|
|
typeSizeDefault = typeSize;
|
|
}
|
|
|
|
var typeOther = type & Debugger.TYPE_OTHER;
|
|
if (!typeOther) {
|
|
type |= (iOperand == 1? Debugger.TYPE_OUT : Debugger.TYPE_IN);
|
|
}
|
|
|
|
if (typeMode & Debugger.TYPE_IMM) {
|
|
sOperand = this.getImmOperand(type, dbgAddr);
|
|
}
|
|
else if (typeMode & Debugger.TYPE_REG) {
|
|
sOperand = this.getRegOperand((type & Debugger.TYPE_IREG) >> 8, type, dbgAddr);
|
|
}
|
|
else if (typeMode & Debugger.TYPE_INT) {
|
|
sOperand = ((bOpcode >> 3) & 0x7).toString();
|
|
}
|
|
|
|
if (!sOperand || !sOperand.length) {
|
|
sOperands = "INVALID";
|
|
break;
|
|
}
|
|
if (sOperands.length > 0) sOperands += ',';
|
|
sOperands += (sOperand || "???");
|
|
}
|
|
|
|
var sBytes = "";
|
|
var sLine = this.toHexAddr(dbgAddrIns) + ' ';
|
|
if (dbgAddrIns.addr !== CPUDef.ADDR_INVALID && dbgAddr.addr !== CPUDef.ADDR_INVALID) {
|
|
do {
|
|
sBytes += str.toHex(this.getByte(dbgAddrIns, 1), 2);
|
|
if (dbgAddrIns.addr == null) break;
|
|
} while (dbgAddrIns.addr != dbgAddr.addr);
|
|
}
|
|
|
|
sLine += str.pad(sBytes, 10);
|
|
sLine += (type & Debugger.TYPE_UNDOC)? '*' : ' ';
|
|
sLine += str.pad(sOpcode, 7);
|
|
if (sOperands) sLine += ' ' + sOperands;
|
|
|
|
if (sComment) {
|
|
sLine = str.pad(sLine, 40) + ';' + sComment;
|
|
if (!this.cpu.flags.fChecksum) {
|
|
sLine += (nSequence != null? '=' + nSequence.toString() : "");
|
|
} else {
|
|
var nCycles = this.cpu.getCycles();
|
|
sLine += "cycles=" + nCycles.toString() + " cs=" + str.toHex(this.cpu.aCounts.nChecksum);
|
|
}
|
|
}
|
|
return sLine;
|
|
};
|
|
|
|
/**
|
|
* getImmOperand(type, dbgAddr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} type
|
|
* @param {DbgAddr} dbgAddr
|
|
* @return {string} operand
|
|
*/
|
|
Debugger.prototype.getImmOperand = function(type, dbgAddr)
|
|
{
|
|
var sOperand = ' ';
|
|
var typeSize = type & Debugger.TYPE_SIZE;
|
|
|
|
switch (typeSize) {
|
|
case Debugger.TYPE_BYTE:
|
|
sOperand = str.toHex(this.getByte(dbgAddr, 1), 2);
|
|
break;
|
|
case Debugger.TYPE_SBYTE:
|
|
sOperand = str.toHex((this.getByte(dbgAddr, 1) << 24) >> 24, 4);
|
|
break;
|
|
case Debugger.TYPE_WORD:
|
|
sOperand = str.toHex(this.getShort(dbgAddr, 2), 4);
|
|
break;
|
|
default:
|
|
return "imm(" + str.toHexWord(type) + ')';
|
|
}
|
|
if (this.style == Debugger.STYLE_8086 && (type & Debugger.TYPE_MEM)) {
|
|
sOperand = '[' + sOperand + ']';
|
|
} else if (!(type & Debugger.TYPE_REG)) {
|
|
sOperand = (this.style == Debugger.STYLE_8080? '$' : "0x") + sOperand;
|
|
}
|
|
return sOperand;
|
|
};
|
|
|
|
/**
|
|
* getRegOperand(iReg, type, dbgAddr)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} iReg
|
|
* @param {number} type
|
|
* @param {DbgAddr} dbgAddr
|
|
* @return {string} operand
|
|
*/
|
|
Debugger.prototype.getRegOperand = function(iReg, type, dbgAddr)
|
|
{
|
|
/*
|
|
* Although this breaks with 8080 assembler conventions, I'm going to experiment with some different
|
|
* mnemonics; specifically, "[HL]" instead of "M". This is also more in keeping with how getImmOperand()
|
|
* displays memory references (ie, by enclosing them in brackets).
|
|
*/
|
|
var sOperand = Debugger.REGS[iReg];
|
|
if (this.style == Debugger.STYLE_8086 && (type & Debugger.TYPE_MEM)) {
|
|
if (iReg == Debugger.REG_M) {
|
|
sOperand = "HL";
|
|
}
|
|
sOperand = '[' + sOperand + ']';
|
|
}
|
|
return sOperand;
|
|
};
|
|
|
|
/**
|
|
* parseInstruction(sOp, sOperand, addr)
|
|
*
|
|
* TODO: Unimplemented. See parseInstruction() in modules/c1pjs/lib/debugger.js for a working implementation.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sOp
|
|
* @param {string|undefined} sOperand
|
|
* @param {DbgAddr} dbgAddr of memory where this instruction is being assembled
|
|
* @return {Array.<number>} of opcode bytes; if the instruction can't be parsed, the array will be empty
|
|
*/
|
|
Debugger.prototype.parseInstruction = function(sOp, sOperand, dbgAddr)
|
|
{
|
|
var aOpBytes = [];
|
|
this.println("not supported yet");
|
|
return aOpBytes;
|
|
};
|
|
|
|
/**
|
|
* getFlagOutput(sFlag)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sFlag
|
|
* @return {string} value of flag
|
|
*/
|
|
Debugger.prototype.getFlagOutput = function(sFlag)
|
|
{
|
|
var b;
|
|
switch (sFlag) {
|
|
case "IF":
|
|
b = this.cpu.getIF();
|
|
break;
|
|
case "SF":
|
|
b = this.cpu.getSF();
|
|
break;
|
|
case "ZF":
|
|
b = this.cpu.getZF();
|
|
break;
|
|
case "AF":
|
|
b = this.cpu.getAF();
|
|
break;
|
|
case "PF":
|
|
b = this.cpu.getPF();
|
|
break;
|
|
case "CF":
|
|
b = this.cpu.getCF();
|
|
break;
|
|
default:
|
|
b = 0;
|
|
break;
|
|
}
|
|
return sFlag.charAt(0) + (b? '1' : '0') + ' ';
|
|
};
|
|
|
|
/**
|
|
* getRegOutput(iReg)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number} iReg
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.getRegOutput = function(iReg)
|
|
{
|
|
var sReg = Debugger.REGS[iReg];
|
|
return sReg + '=' + this.getRegString(iReg) + ' ';
|
|
};
|
|
|
|
/**
|
|
* getRegDump()
|
|
*
|
|
* Sample 8080 register dump:
|
|
*
|
|
* A=00 BC=0000 DE=0000 HL=0000 SP=0000 I0 S0 Z0 A0 P0 C0
|
|
* 0000 00 NOP
|
|
*
|
|
* @this {Debugger}
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.getRegDump = function()
|
|
{
|
|
var s;
|
|
s = this.getRegOutput(Debugger.REG_A) +
|
|
this.getRegOutput(Debugger.REG_BC) +
|
|
this.getRegOutput(Debugger.REG_DE) +
|
|
this.getRegOutput(Debugger.REG_HL) +
|
|
this.getRegOutput(Debugger.REG_SP) +
|
|
this.getFlagOutput("IF") + this.getFlagOutput("SF") + this.getFlagOutput("ZF") +
|
|
this.getFlagOutput("AF") + this.getFlagOutput("PF") + this.getFlagOutput("CF");
|
|
return s;
|
|
};
|
|
|
|
Debugger.aBinOpPrecedence = {
|
|
'||': 0, // logical OR
|
|
'&&': 1, // logical AND
|
|
'|': 2, // bitwise OR
|
|
'^': 3, // bitwise XOR
|
|
'&': 4, // bitwise AND
|
|
'!=': 5, // inequality
|
|
'==': 5, // equality
|
|
'>=': 6, // greater than or equal to
|
|
'>': 6, // greater than
|
|
'<=': 6, // less than or equal to
|
|
'<': 6, // less than
|
|
'>>>': 7, // unsigned bitwise right shift
|
|
'>>': 7, // bitwise right shift
|
|
'<<': 7, // bitwise left shift
|
|
'-': 8, // subtraction
|
|
'+': 8, // addition
|
|
'%': 9, // remainder
|
|
'/': 9, // division
|
|
'*': 9 // multiplication
|
|
};
|
|
|
|
/**
|
|
* evalExpression(aVals, aOps, cOps)
|
|
*
|
|
* In Node, if you set a variable to 0x80000001; ie:
|
|
*
|
|
* foo=0x80000001|0
|
|
*
|
|
* and then calculate foo*foo using "(foo*foo).toString(2)", the result is:
|
|
*
|
|
* '11111111111111111111111111111100000000000000000000000000000000'
|
|
*
|
|
* which is slightly incorrect because it has overflowed JavaScript's floating-point precision.
|
|
*
|
|
* 0x80000001 in decimal is -2147483647, so the product is 4611686014132420609, which is 0x3FFFFFFF00000001.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<number>} aVals
|
|
* @param {Array.<string>} aOps
|
|
* @param {number} [cOps] (default is all)
|
|
* @return {boolean} true if successful, false if error
|
|
*/
|
|
Debugger.prototype.evalExpression = function(aVals, aOps, cOps)
|
|
{
|
|
cOps = cOps || -1;
|
|
while (cOps-- && aOps.length) {
|
|
var chOp = aOps.pop();
|
|
if (aVals.length < 2) return false;
|
|
var valNew;
|
|
var val2 = aVals.pop();
|
|
var val1 = aVals.pop();
|
|
switch(chOp) {
|
|
case '*':
|
|
valNew = val1 * val2;
|
|
break;
|
|
case '/':
|
|
if (!val2) return false;
|
|
valNew = val1 / val2;
|
|
break;
|
|
case '%':
|
|
if (!val2) return false;
|
|
valNew = val1 % val2;
|
|
break;
|
|
case '+':
|
|
valNew = val1 + val2;
|
|
break;
|
|
case '-':
|
|
valNew = val1 - val2;
|
|
break;
|
|
case '<<':
|
|
valNew = val1 << val2;
|
|
break;
|
|
case '>>':
|
|
valNew = val1 >> val2;
|
|
break;
|
|
case '>>>':
|
|
valNew = val1 >>> val2;
|
|
break;
|
|
case '<':
|
|
valNew = (val1 < val2? 1 : 0);
|
|
break;
|
|
case '<=':
|
|
valNew = (val1 <= val2? 1 : 0);
|
|
break;
|
|
case '>':
|
|
valNew = (val1 > val2? 1 : 0);
|
|
break;
|
|
case '>=':
|
|
valNew = (val1 >= val2? 1 : 0);
|
|
break;
|
|
case '==':
|
|
valNew = (val1 == val2? 1 : 0);
|
|
break;
|
|
case '!=':
|
|
valNew = (val1 != val2? 1 : 0);
|
|
break;
|
|
case '&':
|
|
valNew = val1 & val2;
|
|
break;
|
|
case '^':
|
|
valNew = val1 ^ val2;
|
|
break;
|
|
case '|':
|
|
valNew = val1 | val2;
|
|
break;
|
|
case '&&':
|
|
valNew = (val1 && val2? 1 : 0);
|
|
break;
|
|
case '||':
|
|
valNew = (val1 || val2? 1 : 0);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
aVals.push(valNew|0);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* parseExpression(sExp, fPrint)
|
|
*
|
|
* A quick-and-dirty expression parser. It takes an expression like:
|
|
*
|
|
* EDX+EDX*4+12345678
|
|
*
|
|
* and builds a value stack in aVals and a "binop" (binary operator) stack in aOps:
|
|
*
|
|
* aVals aOps
|
|
* ----- ----
|
|
* EDX +
|
|
* EDX *
|
|
* 4 +
|
|
* ...
|
|
*
|
|
* We pop 1 "binop" from aOps and 2 values from aVals whenever a "binop" of lower priority than its
|
|
* predecessor is encountered, evaluate, and push the result back onto aVals.
|
|
*
|
|
* Unary operators like '~' and ternary operators like '?:' are not supported; neither are parentheses.
|
|
*
|
|
* However, parseReference() now makes it possible to write parenthetical-style sub-expressions by using
|
|
* {...} (braces), as well as address references by using [...] (brackets).
|
|
*
|
|
* Why am I using braces instead of parentheses for sub-expressions? Because parseReference() serves
|
|
* multiple purposes, the other being reference replacement in message strings passing through replaceRegs(),
|
|
* and I didn't want parentheses taking on a new meaning in message strings.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sExp
|
|
* @param {boolean} [fPrint] is true to print all resolved values, false for quiet parsing
|
|
* @return {number|undefined} numeric value, or undefined if sExp contains any undefined or invalid values
|
|
*/
|
|
Debugger.prototype.parseExpression = function(sExp, fPrint)
|
|
{
|
|
var value;
|
|
|
|
if (sExp) {
|
|
/*
|
|
* First process (and eliminate) any references, aka sub-expressions.
|
|
*/
|
|
sExp = this.parseReference(sExp);
|
|
|
|
var i = 0;
|
|
var fError = false;
|
|
var sExpOrig = sExp;
|
|
var aVals = [], aOps = [];
|
|
/*
|
|
* All browsers (including, I believe, IE9 and up) support the following idiosyncrasy of a regexp split():
|
|
* when the regexp uses a capturing pattern, the resulting array will include entries for all the pattern
|
|
* matches along with the non-matches. This effectively means that, in the set of expressions that we
|
|
* support, all even entries in asValues will contain "values" and all odd entries will contain "operators".
|
|
*
|
|
* And although I tried to list the supported operators in "precedential" order, bitwise operators must
|
|
* be out-of-order so that we don't mistakenly match either '>' or '<' when they're part of '>>' or '<<'.
|
|
*/
|
|
var regExp = /(\|\||&&|\||^|&|!=|==|>=|>>>|>>|>|<=|<<|<|-|\+|%|\/|\*)/;
|
|
var asValues = sExp.split(regExp);
|
|
while (i < asValues.length) {
|
|
var sValue = asValues[i++];
|
|
var cchValue = sValue.length;
|
|
var s = str.trim(sValue);
|
|
if (!s) {
|
|
fError = true;
|
|
break;
|
|
}
|
|
var v = this.parseValue(s, null, fPrint === false);
|
|
if (v === undefined) {
|
|
fError = true;
|
|
fPrint = false;
|
|
break;
|
|
}
|
|
aVals.push(v);
|
|
if (i == asValues.length) break;
|
|
var sOp = asValues[i++], cchOp = sOp.length;
|
|
|
|
if (aOps.length && Debugger.aBinOpPrecedence[sOp] < Debugger.aBinOpPrecedence[aOps[aOps.length-1]]) {
|
|
this.evalExpression(aVals, aOps, 1);
|
|
}
|
|
aOps.push(sOp);
|
|
sExp = sExp.substr(cchValue + cchOp);
|
|
}
|
|
if (!this.evalExpression(aVals, aOps) || aVals.length != 1) {
|
|
fError = true;
|
|
}
|
|
if (!fError) {
|
|
value = aVals.pop();
|
|
if (fPrint) this.printValue(null, value);
|
|
} else {
|
|
if (fPrint) this.println("error parsing '" + sExpOrig + "' at character " + (sExpOrig.length - sExp.length));
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* parseReference(s)
|
|
*
|
|
* Returns the given string with any "{expression}" sequences replaced with the value of the expression,
|
|
* and any "[address]" references replaced with the contents of the address. Expressions are parsed BEFORE
|
|
* addresses. Owing to this function's simplistic parsing, nested braces/brackets are not supported
|
|
* (define intermediate variables if needed).
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} s
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.parseReference = function(s)
|
|
{
|
|
var a;
|
|
while (a = s.match(/\{(.*?)}/)) {
|
|
if (a[1].indexOf('{') >= 0) break; // unsupported nested brace(s)
|
|
var value = this.parseExpression(a[1]);
|
|
s = s.replace('{' + a[1] + '}', value != null? str.toHex(value) : "undefined");
|
|
}
|
|
while (a = s.match(/\[(.*?)]/)) {
|
|
if (a[1].indexOf('[') >= 0) break; // unsupported nested bracket(s)
|
|
var dbgAddr = this.parseAddr(a[1]);
|
|
s = s.replace('[' + a[1] + ']', dbgAddr? str.toHex(this.getWord(dbgAddr), 4) : "undefined");
|
|
}
|
|
return this.parseSysVars(s);
|
|
};
|
|
|
|
/**
|
|
* parseSysVars(s)
|
|
*
|
|
* Returns the given string with any recognized "$var" replaced with its value; eg:
|
|
*
|
|
* $ops: the number of opcodes executed since the last time it was displayed (or reset)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} s
|
|
* @return {string}
|
|
*/
|
|
Debugger.prototype.parseSysVars = function(s)
|
|
{
|
|
var a;
|
|
while (a = s.match(/\$([a-z]+)/i)) {
|
|
var v = null;
|
|
switch(a[1].toLowerCase()) {
|
|
case "ops":
|
|
v = this.cOpcodes - this.cOpcodesStart;
|
|
break;
|
|
}
|
|
if (v == null) break;
|
|
s = s.replace(a[0], v.toString());
|
|
}
|
|
return s;
|
|
};
|
|
|
|
/**
|
|
* parseValue(sValue, sName, fQuiet)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sValue
|
|
* @param {string|null} [sName] is the name of the value, if any
|
|
* @param {boolean} [fQuiet]
|
|
* @return {number|undefined} numeric value, or undefined if sValue is either undefined or invalid
|
|
*/
|
|
Debugger.prototype.parseValue = function(sValue, sName, fQuiet)
|
|
{
|
|
var value;
|
|
if (sValue !== undefined) {
|
|
var iReg = this.getRegIndex(sValue);
|
|
if (iReg >= 0) {
|
|
value = this.getRegValue(iReg);
|
|
} else {
|
|
value = this.getVariable(sValue);
|
|
if (value === undefined) value = str.parseInt(sValue);
|
|
}
|
|
if (value === undefined && !fQuiet) this.println("invalid " + (sName? sName : "value") + ": " + sValue);
|
|
} else {
|
|
if (!fQuiet) this.println("missing " + (sName || "value"));
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* printValue(sVar, value)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|null} sVar
|
|
* @param {number|undefined} value
|
|
* @return {boolean} true if value defined, false if not
|
|
*/
|
|
Debugger.prototype.printValue = function(sVar, value)
|
|
{
|
|
var sValue;
|
|
var fDefined = false;
|
|
if (value !== undefined) {
|
|
fDefined = true;
|
|
sValue = str.toHexLong(value) + " " + value + ". (" + str.toBinBytes(value) + ")";
|
|
}
|
|
sVar = (sVar != null? (sVar + ": ") : "");
|
|
this.println(sVar + sValue);
|
|
return fDefined;
|
|
};
|
|
|
|
/**
|
|
* printVariable(sVar)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sVar]
|
|
* @return {boolean} true if all value(s) defined, false if not
|
|
*/
|
|
Debugger.prototype.printVariable = function(sVar)
|
|
{
|
|
if (sVar) {
|
|
return this.printValue(sVar, this.aVariables[sVar]);
|
|
}
|
|
var cVariables = 0;
|
|
for (sVar in this.aVariables) {
|
|
this.printValue(sVar, this.aVariables[sVar]);
|
|
cVariables++;
|
|
}
|
|
return cVariables > 0;
|
|
};
|
|
|
|
/**
|
|
* delVariable(sVar)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sVar
|
|
*/
|
|
Debugger.prototype.delVariable = function(sVar)
|
|
{
|
|
delete this.aVariables[sVar];
|
|
};
|
|
|
|
/**
|
|
* getVariable(sVar)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sVar
|
|
* @return {number|undefined}
|
|
*/
|
|
Debugger.prototype.getVariable = function(sVar)
|
|
{
|
|
return this.aVariables[sVar];
|
|
};
|
|
|
|
/**
|
|
* setVariable(sVar, value)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sVar
|
|
* @param {number} value
|
|
*/
|
|
Debugger.prototype.setVariable = function(sVar, value)
|
|
{
|
|
this.aVariables[sVar] = value;
|
|
};
|
|
|
|
/**
|
|
* comparePairs(p1, p2)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {number|string|Array|Object} p1
|
|
* @param {number|string|Array|Object} p2
|
|
* @return {number}
|
|
*/
|
|
Debugger.prototype.comparePairs = function(p1, p2)
|
|
{
|
|
return p1[0] > p2[0]? 1 : p1[0] < p2[0]? -1 : 0;
|
|
};
|
|
|
|
/**
|
|
* addSymbols(sModule, addr, len, aSymbols)
|
|
*
|
|
* As filedump.js (formerly convrom.php) explains, aSymbols is a JSON-encoded object whose properties consist
|
|
* of all the symbols (in upper-case), and the values of those properties are objects containing any or all of
|
|
* the following properties:
|
|
*
|
|
* 'v': the value of an absolute (unsized) value
|
|
* 'b': either 1, 2, 4 or undefined if an unsized value
|
|
* 's': either a hard-coded segment or undefined
|
|
* 'o': the offset of the symbol within the associated address space
|
|
* 'l': the original-case version of the symbol, present only if it wasn't originally upper-case
|
|
* 'a': annotation for the specified offset; eg, the original assembly language, with optional comment
|
|
*
|
|
* To that list of properties, we also add:
|
|
*
|
|
* 'p': the physical address (calculated whenever both 's' and 'o' properties are defined)
|
|
*
|
|
* Note that values for any 'v', 'b', 's' and 'o' properties are unquoted decimal values, and the values
|
|
* for any 'l' or 'a' properties are quoted strings. Also, if double-quotes were used in any of the original
|
|
* annotation ('a') values, they will have been converted to two single-quotes, so we're responsible for
|
|
* converting them back to individual double-quotes.
|
|
*
|
|
* For example:
|
|
* {
|
|
* 'HF_PORT': {
|
|
* 'v':800
|
|
* },
|
|
* 'HDISK_INT': {
|
|
* 'b':4, 's':0, 'o':52
|
|
* },
|
|
* 'ORG_VECTOR': {
|
|
* 'b':4, 's':0, 'o':76
|
|
* },
|
|
* 'CMD_BLOCK': {
|
|
* 'b':1, 's':64, 'o':66
|
|
* },
|
|
* 'DISK_SETUP': {
|
|
* 'o':3
|
|
* },
|
|
* '.40': {
|
|
* 'o':40, 'a':"MOV AX,WORD PTR ORG_VECTOR ;GET DISKETTE VECTOR"
|
|
* }
|
|
* }
|
|
*
|
|
* If a symbol only has an offset, then that offset value can be assigned to the symbol property directly:
|
|
*
|
|
* 'DISK_SETUP': 3
|
|
*
|
|
* The last property is an example of an "anonymous" entry, for offsets where there is no associated symbol.
|
|
* Such entries are identified by a period followed by a unique number (usually the offset of the entry), and
|
|
* they usually only contain offset ('o') and annotation ('a') properties. I could eliminate the leading
|
|
* period, but it offers a very convenient way of quickly discriminating among genuine vs. anonymous symbols.
|
|
*
|
|
* We add all these entries to our internal symbol table, which is an array of 4-element arrays, each of which
|
|
* look like:
|
|
*
|
|
* [addr, len, aSymbols, aOffsets]
|
|
*
|
|
* There are two basic symbol operations: findSymbol(), which takes an address and finds the symbol, if any,
|
|
* at that address, and findSymbolAddr(), which takes a string and attempts to match it to a non-anonymous
|
|
* symbol with a matching offset ('o') property.
|
|
*
|
|
* To implement findSymbol() efficiently, addSymbols() creates an array of [offset, sSymbol] pairs
|
|
* (aOffsets), one pair for each symbol that corresponds to an offset within the specified address space.
|
|
*
|
|
* We guarantee the elements of aOffsets are in offset order, because we build it using binaryInsert();
|
|
* it's quite likely that the MAP file already ordered all its symbols in offset order, but since they're
|
|
* hand-edited files, we can't assume that, and we need to ensure that findSymbol()'s binarySearch() operates
|
|
* properly.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|null} sModule
|
|
* @param {number|null} addr (physical address where the symbols are located, if the memory is physical; eg, ROM)
|
|
* @param {number} len (the size of the region, in bytes)
|
|
* @param {Object} aSymbols (collection of symbols in this group; the format of this collection is described below)
|
|
*/
|
|
Debugger.prototype.addSymbols = function(sModule, addr, len, aSymbols)
|
|
{
|
|
var dbgAddr = {};
|
|
var aOffsets = [];
|
|
for (var sSymbol in aSymbols) {
|
|
var symbol = aSymbols[sSymbol];
|
|
if (typeof symbol == "number") {
|
|
aSymbols[sSymbol] = symbol = {'o': symbol};
|
|
}
|
|
var offSymbol = symbol['o'];
|
|
var sAnnotation = symbol['a'];
|
|
if (offSymbol !== undefined) {
|
|
usr.binaryInsert(aOffsets, [offSymbol >>> 0, sSymbol], this.comparePairs);
|
|
}
|
|
if (sAnnotation) symbol['a'] = sAnnotation.replace(/''/g, "\"");
|
|
}
|
|
var symbolTable = {
|
|
sModule: sModule,
|
|
addr: addr,
|
|
len: len,
|
|
aSymbols: aSymbols,
|
|
aOffsets: aOffsets
|
|
};
|
|
this.aSymbolTable.push(symbolTable);
|
|
};
|
|
|
|
/**
|
|
* dumpSymbols()
|
|
*
|
|
* TODO: Add "numerical" and "alphabetical" dump options. This is simply dumping them in whatever
|
|
* order they appeared in the original MAP file.
|
|
*
|
|
* @this {Debugger}
|
|
*/
|
|
Debugger.prototype.dumpSymbols = function()
|
|
{
|
|
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
|
|
var symbolTable = this.aSymbolTable[iTable];
|
|
for (var sSymbol in symbolTable.aSymbols) {
|
|
if (sSymbol.charAt(0) == '.') continue;
|
|
var symbol = symbolTable.aSymbols[sSymbol];
|
|
var offSymbol = symbol['o'];
|
|
if (offSymbol === undefined) continue;
|
|
var sSymbolOrig = symbolTable.aSymbols[sSymbol]['l'];
|
|
if (sSymbolOrig) sSymbol = sSymbolOrig;
|
|
this.println(this.toHexOffset(offSymbol) + ' ' + sSymbol);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* findSymbol(dbgAddr, fNearest)
|
|
*
|
|
* Search aSymbolTable for dbgAddr, and return an Array for the corresponding symbol (empty if not found).
|
|
*
|
|
* If fNearest is true, and no exact match was found, then the Array returned will contain TWO sets of
|
|
* entries: [0]-[3] will refer to closest preceding symbol, and [4]-[7] will refer to the closest subsequent symbol.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @param {boolean} [fNearest]
|
|
* @return {Array} where [0] == symbol name, [1] == symbol value, [2] == any annotation, and [3] == any associated comment
|
|
*/
|
|
Debugger.prototype.findSymbol = function(dbgAddr, fNearest)
|
|
{
|
|
var aSymbol = [];
|
|
var addrSymbol = this.getAddr(dbgAddr) >>> 0;
|
|
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
|
|
var symbolTable = this.aSymbolTable[iTable];
|
|
var addr = symbolTable.addr >>> 0;
|
|
var len = symbolTable.len;
|
|
if (addrSymbol >= addr && addrSymbol < addr + len) {
|
|
var offSymbol = addrSymbol - addr;
|
|
var result = usr.binarySearch(symbolTable.aOffsets, [offSymbol], this.comparePairs);
|
|
if (result >= 0) {
|
|
this.returnSymbol(iTable, result, aSymbol);
|
|
}
|
|
else if (fNearest) {
|
|
result = ~result;
|
|
this.returnSymbol(iTable, result-1, aSymbol);
|
|
this.returnSymbol(iTable, result, aSymbol);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return aSymbol;
|
|
};
|
|
|
|
/**
|
|
* findSymbolAddr(sSymbol)
|
|
*
|
|
* Search aSymbolTable for sSymbol, and if found, return a dbgAddr (same as parseAddr())
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sSymbol
|
|
* @return {DbgAddr|undefined}
|
|
*/
|
|
Debugger.prototype.findSymbolAddr = function(sSymbol)
|
|
{
|
|
var dbgAddr;
|
|
if (sSymbol.match(/^[a-z_][a-z0-9_]*$/i)) {
|
|
var sUpperCase = sSymbol.toUpperCase();
|
|
for (var iTable = 0; iTable < this.aSymbolTable.length; iTable++) {
|
|
var symbolTable = this.aSymbolTable[iTable];
|
|
var symbol = symbolTable.aSymbols[sUpperCase];
|
|
if (symbol !== undefined) {
|
|
var offSymbol = symbol['o'];
|
|
if (offSymbol !== undefined) {
|
|
/*
|
|
* We assume that every ROM is ORG'ed at 0x0000, and therefore unless the symbol has an
|
|
* explicitly-defined segment, we return the segment associated with the entire group; for
|
|
* a ROM, that segment is normally "addrROM >>> 4". Down the road, we may want/need to
|
|
* support a special symbol entry (eg, ".ORG") that defines an alternate origin.
|
|
*/
|
|
dbgAddr = this.newAddr(offSymbol);
|
|
}
|
|
/*
|
|
* The symbol matched, but it wasn't for an address (no 'o' offset), and there's no point
|
|
* looking any farther, since each symbol appears only once, so we indicate it's an unknown symbol.
|
|
*/
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return dbgAddr;
|
|
};
|
|
|
|
/**
|
|
* returnSymbol(iTable, iOffset, aSymbol)
|
|
*
|
|
* Helper function for findSymbol().
|
|
*
|
|
* @param {number} iTable
|
|
* @param {number} iOffset
|
|
* @param {Array} aSymbol is updated with the specified symbol, if it exists
|
|
*/
|
|
Debugger.prototype.returnSymbol = function(iTable, iOffset, aSymbol)
|
|
{
|
|
var symbol = {};
|
|
var aOffsets = this.aSymbolTable[iTable].aOffsets;
|
|
var offset = 0, sSymbol = null;
|
|
if (iOffset >= 0 && iOffset < aOffsets.length) {
|
|
offset = aOffsets[iOffset][0];
|
|
sSymbol = aOffsets[iOffset][1];
|
|
}
|
|
if (sSymbol) {
|
|
symbol = this.aSymbolTable[iTable].aSymbols[sSymbol];
|
|
sSymbol = (sSymbol.charAt(0) == '.'? null : (symbol['l'] || sSymbol));
|
|
}
|
|
aSymbol.push(sSymbol);
|
|
aSymbol.push(offset);
|
|
aSymbol.push(symbol['a']);
|
|
aSymbol.push(symbol['c']);
|
|
};
|
|
|
|
/**
|
|
* doHelp()
|
|
*
|
|
* @this {Debugger}
|
|
*/
|
|
Debugger.prototype.doHelp = function()
|
|
{
|
|
var s = "commands:";
|
|
for (var sCommand in Debugger.COMMANDS) {
|
|
s += '\n' + str.pad(sCommand, 7) + Debugger.COMMANDS[sCommand];
|
|
}
|
|
if (!this.checksEnabled()) s += "\nnote: frequency/history disabled if no exec breakpoints";
|
|
this.println(s);
|
|
};
|
|
|
|
/**
|
|
* doAssemble(asArgs)
|
|
*
|
|
* This always receives the complete argument array, where the order of the arguments is:
|
|
*
|
|
* [0]: the assemble command (assumed to be "a")
|
|
* [1]: the target address (eg, "200")
|
|
* [2]: the operation code, aka instruction name (eg, "adc")
|
|
* [3]: the operation mode operand, if any (eg, "14", "[1234]", etc)
|
|
*
|
|
* The Debugger enters "assemble mode" whenever only the first (or first and second) arguments are present.
|
|
* As long as "assemble mode is active, the user can omit the first two arguments on all later assemble commands
|
|
* until "assemble mode" is cancelled with an empty command line; the command processor automatically prepends "a"
|
|
* and the next available target address to the argument array.
|
|
*
|
|
* Entering "assemble mode" is optional; one could enter a series of fully-qualified assemble commands; eg:
|
|
*
|
|
* a ff00 cld
|
|
* a ff01 ldx 28
|
|
* ...
|
|
*
|
|
* without ever entering "assemble mode", but of course, that requires more typing and doesn't take advantage
|
|
* of automatic target address advancement (see dbgAddrAssemble).
|
|
*
|
|
* NOTE: As the previous example implies, you can even assemble new instructions into ROM address space;
|
|
* as our setByte() function explains, the ROM write-notification handlers only refuse writes from the CPU.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs is the complete argument array, beginning with the "a" command in asArgs[0]
|
|
*/
|
|
Debugger.prototype.doAssemble = function(asArgs)
|
|
{
|
|
var dbgAddr = this.parseAddr(asArgs[1], true);
|
|
if (!dbgAddr) return;
|
|
|
|
this.dbgAddrAssemble = dbgAddr;
|
|
if (asArgs[2] === undefined) {
|
|
this.println("begin assemble at " + this.toHexAddr(dbgAddr));
|
|
this.fAssemble = true;
|
|
this.cpu.updateCPU();
|
|
return;
|
|
}
|
|
|
|
var aOpBytes = this.parseInstruction(asArgs[2], asArgs[3], dbgAddr);
|
|
if (aOpBytes.length) {
|
|
for (var i = 0; i < aOpBytes.length; i++) {
|
|
this.setByte(dbgAddr, aOpBytes[i], 1);
|
|
}
|
|
/*
|
|
* Since getInstruction() also updates the specified address, dbgAddrAssemble is automatically advanced.
|
|
*/
|
|
this.println(this.getInstruction(this.dbgAddrAssemble));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doBreak(sCmd, sAddr, sOptions)
|
|
*
|
|
* As the "help" output below indicates, the following breakpoint commands are supported:
|
|
*
|
|
* bp [a] set exec breakpoint on linear addr [a]
|
|
* br [a] set read breakpoint on linear addr [a]
|
|
* bw [a] set write breakpoint on linear addr [a]
|
|
* bc [a] clear breakpoint on linear addr [a] (use "*" for all breakpoints)
|
|
* bl list breakpoints
|
|
*
|
|
* to which we have recently added the following I/O breakpoint commands:
|
|
*
|
|
* bi [p] toggle input breakpoint on port [p] (use "*" for all input ports)
|
|
* bo [p] toggle output breakpoint on port [p] (use "*" for all output ports)
|
|
*
|
|
* These two new commands operate as toggles so that if "*" is used to trap all input (or output),
|
|
* you can also use these commands to NOT trap specific ports.
|
|
*
|
|
* bn [n] break after [n] instructions
|
|
*
|
|
* TODO: Update the "bl" command to include any/all I/O breakpoints, and the "bc" command to
|
|
* clear them. Because "bi" and "bo" commands are piggy-backing on Bus functions, those breakpoints
|
|
* are currently outside the realm of what the "bl" and "bc" commands are aware of.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
* @param {string|undefined} [sAddr]
|
|
* @param {string} [sOptions] (the rest of the breakpoint command-line)
|
|
*/
|
|
Debugger.prototype.doBreak = function(sCmd, sAddr, sOptions)
|
|
{
|
|
if (sAddr == '?') {
|
|
this.println("breakpoint commands:");
|
|
this.println("\tbi [p]\ttoggle break on input port [p]");
|
|
this.println("\tbo [p]\ttoggle break on output port [p]");
|
|
this.println("\tbp [a]\tset exec breakpoint at addr [a]");
|
|
this.println("\tbr [a]\tset read breakpoint at addr [a]");
|
|
this.println("\tbw [a]\tset write breakpoint at addr [a]");
|
|
this.println("\tbc [a]\tclear breakpoint at addr [a]");
|
|
this.println("\tbl\tlist all breakpoints");
|
|
this.println("\tbn [n]\tbreak after [n] instruction(s)");
|
|
return;
|
|
}
|
|
|
|
var sParm = sCmd.charAt(1);
|
|
if (sParm == 'l') {
|
|
var cBreaks = 0;
|
|
cBreaks += this.listBreakpoints(this.aBreakExec);
|
|
cBreaks += this.listBreakpoints(this.aBreakRead);
|
|
cBreaks += this.listBreakpoints(this.aBreakWrite);
|
|
if (!cBreaks) this.println("no breakpoints");
|
|
return;
|
|
}
|
|
|
|
if (sParm == 'n') {
|
|
this.nBreakIns = this.parseValue(sAddr);
|
|
this.println("break after " + this.nBreakIns + " instruction(s)");
|
|
return;
|
|
}
|
|
|
|
if (sAddr === undefined) {
|
|
this.println("missing breakpoint address");
|
|
return;
|
|
}
|
|
|
|
var dbgAddr = this.newAddr();
|
|
if (sAddr != '*') {
|
|
dbgAddr = this.parseAddr(sAddr, true, true);
|
|
if (!dbgAddr) return;
|
|
}
|
|
|
|
sAddr = str.toHexWord(dbgAddr.addr);
|
|
|
|
if (sParm == 'c') {
|
|
if (dbgAddr.addr == null) {
|
|
this.clearBreakpoints();
|
|
this.println("all breakpoints cleared");
|
|
return;
|
|
}
|
|
if (this.findBreakpoint(this.aBreakExec, dbgAddr, true))
|
|
return;
|
|
if (this.findBreakpoint(this.aBreakRead, dbgAddr, true))
|
|
return;
|
|
if (this.findBreakpoint(this.aBreakWrite, dbgAddr, true))
|
|
return;
|
|
this.println("breakpoint missing: " + this.toHexAddr(dbgAddr));
|
|
return;
|
|
}
|
|
|
|
if (sParm == 'i') {
|
|
this.println("breakpoint " + (this.bus.addPortInputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (input)");
|
|
return;
|
|
}
|
|
|
|
if (sParm == 'o') {
|
|
this.println("breakpoint " + (this.bus.addPortOutputBreak(dbgAddr.addr)? "enabled" : "cleared") + ": port " + sAddr + " (output)");
|
|
return;
|
|
}
|
|
|
|
if (dbgAddr.addr == null) return;
|
|
|
|
this.parseAddrOptions(dbgAddr, sOptions);
|
|
|
|
if (sParm == 'p') {
|
|
this.addBreakpoint(this.aBreakExec, dbgAddr);
|
|
return;
|
|
}
|
|
if (sParm == 'r') {
|
|
this.addBreakpoint(this.aBreakRead, dbgAddr);
|
|
return;
|
|
}
|
|
if (sParm == 'w') {
|
|
this.addBreakpoint(this.aBreakWrite, dbgAddr);
|
|
return;
|
|
}
|
|
this.println("unknown breakpoint command: " + sParm);
|
|
};
|
|
|
|
/**
|
|
* doClear(sCmd)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sCmd] (eg, "cls" or "clear")
|
|
*/
|
|
Debugger.prototype.doClear = function(sCmd)
|
|
{
|
|
/*
|
|
* TODO: There should be a clear() component method that the Control Panel overrides to perform this function.
|
|
*/
|
|
if (this.controlPrint) this.controlPrint.value = "";
|
|
};
|
|
|
|
/**
|
|
* doDump(asArgs)
|
|
*
|
|
* The length parameter is interpreted as a number of bytes, in hex, which we convert to the appropriate number
|
|
* of lines, because we always display whole lines. If the length is omitted/undefined, it defaults to 0x80 (128.)
|
|
* bytes, which normally translates to 8 lines.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs (formerly sCmd, [sAddr], [sLen] and [sBytes])
|
|
*/
|
|
Debugger.prototype.doDump = function(asArgs)
|
|
{
|
|
var m;
|
|
var sCmd = asArgs[0];
|
|
var sAddr = asArgs[1];
|
|
var sLen = asArgs[2];
|
|
var sBytes = asArgs[3];
|
|
|
|
if (sAddr == '?') {
|
|
var sDumpers = "";
|
|
for (m in Debugger.MESSAGES) {
|
|
if (this.afnDumpers[m]) {
|
|
if (sDumpers) sDumpers += ',';
|
|
sDumpers = sDumpers + m;
|
|
}
|
|
}
|
|
sDumpers += ",state,symbols";
|
|
this.println("dump memory commands:");
|
|
this.println("\tdb [a] [#] dump # bytes at address a");
|
|
this.println("\tdw [a] [#] dump # words at address a");
|
|
this.println("\tdd [a] [#] dump # dwords at address a");
|
|
this.println("\tdh [#] [#] dump # instructions from history");
|
|
if (sDumpers.length) this.println("dump extension commands:\n\t" + sDumpers);
|
|
return;
|
|
}
|
|
|
|
if (sAddr == "state") {
|
|
var sState = this.cmp.powerOff(true);
|
|
if (sLen == "console") {
|
|
/*
|
|
* Console buffers are notoriously small, and even the following code, which breaks the
|
|
* data into parts (eg, "d state console 1", "d state console 2", etc) just isn't that helpful.
|
|
*
|
|
* var nPart = +sBytes;
|
|
* if (nPart) sState = sState.substr(1000000 * (nPart-1), 1000000);
|
|
*
|
|
* So, the best way to capture a large machine state is to use the new "Save Machine" link
|
|
* that downloads a machine's entire state. Alternatively, run your own local server and use
|
|
* server-side storage. Take a look at the "Save" binding in computer.js, which binds an HTML
|
|
* control to the computer.powerOff() and computer.saveServerState() functions.
|
|
*/
|
|
console.log(sState);
|
|
} else {
|
|
this.doClear();
|
|
this.println(sState);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (sAddr == "symbols") {
|
|
this.dumpSymbols();
|
|
return;
|
|
}
|
|
|
|
if (sCmd == "d") {
|
|
for (m in Debugger.MESSAGES) {
|
|
if (asArgs[1] == m) {
|
|
var fnDumper = this.afnDumpers[m];
|
|
if (fnDumper) {
|
|
asArgs.shift();
|
|
asArgs.shift();
|
|
fnDumper(asArgs);
|
|
} else {
|
|
this.println("no dump registered for " + sAddr);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (!sAddr) sCmd = this.sCmdDumpPrev || "db";
|
|
} else {
|
|
this.sCmdDumpPrev = sCmd;
|
|
}
|
|
|
|
if (sCmd == "dh") {
|
|
this.dumpHistory(sAddr, sLen);
|
|
return;
|
|
}
|
|
|
|
var dbgAddr = this.parseAddr(sAddr);
|
|
if (!dbgAddr) return;
|
|
|
|
var len = 0; // 0 is not a default; it triggers the appropriate default below
|
|
if (sLen) {
|
|
if (sLen.charAt(0) == 'l') {
|
|
sLen = sLen.substr(1) || sBytes;
|
|
}
|
|
len = this.parseValue(sLen) >>> 0; // negative lengths not allowed
|
|
if (len > 0x10000) len = 0x10000; // prevent bad user (or variable) input from producing excessive output
|
|
}
|
|
|
|
var sDump = "";
|
|
var size = (sCmd == "dd"? 4 : (sCmd == "dw"? 2 : 1));
|
|
var cb = (size * len) || 128;
|
|
var cLines = ((cb + 15) >> 4) || 1;
|
|
|
|
while (cLines-- && cb > 0) {
|
|
var data = 0, iByte = 0, i;
|
|
var sData = "", sChars = "";
|
|
sAddr = this.toHexAddr(dbgAddr);
|
|
for (i = 16; i > 0 && cb > 0; i--) {
|
|
var b = this.getByte(dbgAddr, 1);
|
|
data |= (b << (iByte++ << 3));
|
|
if (iByte == size) {
|
|
sData += str.toHex(data, size * 2);
|
|
sData += (size == 1? (i == 9? '-' : ' ') : " ");
|
|
data = iByte = 0;
|
|
}
|
|
sChars += (b >= 32 && b < 128? String.fromCharCode(b) : '.');
|
|
cb--;
|
|
}
|
|
if (sDump) sDump += '\n';
|
|
sDump += sAddr + " " + sData + ((i == 0)? (' ' + sChars) : "");
|
|
}
|
|
|
|
if (sDump) this.println(sDump);
|
|
this.dbgAddrNextData = dbgAddr;
|
|
};
|
|
|
|
/**
|
|
* doEdit(asArgs)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs
|
|
*/
|
|
Debugger.prototype.doEdit = function(asArgs)
|
|
{
|
|
var size = 1;
|
|
var mask = 0xff;
|
|
var fnGet = this.getByte;
|
|
var fnSet = this.setByte;
|
|
if (asArgs[0] == "ew") {
|
|
size = 2;
|
|
mask = 0xffff;
|
|
fnGet = this.getShort;
|
|
fnSet = this.setShort;
|
|
}
|
|
var cch = size << 1;
|
|
|
|
var sAddr = asArgs[1];
|
|
if (sAddr == null) {
|
|
this.println("edit memory commands:");
|
|
this.println("\teb [a] [...] edit bytes at address a");
|
|
this.println("\tew [a] [...] edit words at address a");
|
|
return;
|
|
}
|
|
|
|
var dbgAddr = this.parseAddr(sAddr);
|
|
if (!dbgAddr) return;
|
|
|
|
for (var i = 2; i < asArgs.length; i++) {
|
|
var vNew = this.parseExpression(asArgs[i]);
|
|
if (vNew === undefined) {
|
|
this.println("unrecognized value: " + asArgs[i]);
|
|
break;
|
|
}
|
|
if (vNew & ~mask) {
|
|
this.println("warning: " + str.toHex(vNew) + " exceeds " + size + "-byte value");
|
|
}
|
|
var vOld = fnGet.call(this, dbgAddr);
|
|
this.println("changing " + this.toHexAddr(dbgAddr) + " from 0x" + str.toHex(vOld, cch) + " to 0x" + str.toHex(vNew, cch));
|
|
fnSet.call(this, dbgAddr, vNew, size);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doFreqs(sParm)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sParm
|
|
*/
|
|
Debugger.prototype.doFreqs = function(sParm)
|
|
{
|
|
if (sParm == '?') {
|
|
this.println("frequency commands:");
|
|
this.println("\tclear\tclear all frequency counts");
|
|
return;
|
|
}
|
|
var i;
|
|
var cData = 0;
|
|
if (this.aaOpcodeCounts) {
|
|
if (sParm == "clear") {
|
|
for (i = 0; i < this.aaOpcodeCounts.length; i++)
|
|
this.aaOpcodeCounts[i] = [i, 0];
|
|
this.println("frequency data cleared");
|
|
cData++;
|
|
}
|
|
else if (sParm !== undefined) {
|
|
this.println("unknown frequency command: " + sParm);
|
|
cData++;
|
|
}
|
|
else {
|
|
var aaSortedOpcodeCounts = this.aaOpcodeCounts.slice();
|
|
aaSortedOpcodeCounts.sort(function(p, q) {
|
|
return q[1] - p[1];
|
|
});
|
|
var asOpcodes = this.style != Debugger.STYLE_8086? Debugger.INS_NAMES : Debugger.INS_NAMES_8086;
|
|
for (i = 0; i < aaSortedOpcodeCounts.length; i++) {
|
|
var bOpcode = aaSortedOpcodeCounts[i][0];
|
|
var cFreq = aaSortedOpcodeCounts[i][1];
|
|
if (cFreq) {
|
|
this.println((asOpcodes[this.aaOpDescs[bOpcode][0]] + " ").substr(0, 5) + " (" + str.toHexByte(bOpcode) + "): " + cFreq + " times");
|
|
cData++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!cData) {
|
|
this.println("no frequency data available");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doHalt(fQuiet)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {boolean} [fQuiet]
|
|
*/
|
|
Debugger.prototype.doHalt = function(fQuiet)
|
|
{
|
|
var sMsg;
|
|
if (this.flags.fRunning) {
|
|
sMsg = "halting";
|
|
this.stopCPU();
|
|
} else {
|
|
if (this.isBusy(true)) return;
|
|
sMsg = "already halted";
|
|
}
|
|
if (!fQuiet) this.println(sMsg);
|
|
};
|
|
|
|
/**
|
|
* doIf(sCmd, fQuiet)
|
|
*
|
|
* NOTE: Don't forget that the default base for all numeric constants is 16 (hex), so when you evaluate
|
|
* an expression like "a==10", it will compare the value of the variable "a" to 0x10; use a trailing period
|
|
* (eg, "10.") if you really intend decimal.
|
|
*
|
|
* Also, if no variable named "a" exists, "a" will evaluate to 0x0A, so the expression "a==10" becomes
|
|
* "0x0A==0x10" (false), whereas the expression "a==10." becomes "0x0A==0x0A" (true).
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
* @param {boolean} [fQuiet]
|
|
* @return {boolean} true if expression is non-zero, false if zero (or undefined due to a parse error)
|
|
*/
|
|
Debugger.prototype.doIf = function(sCmd, fQuiet)
|
|
{
|
|
sCmd = str.trim(sCmd);
|
|
if (!this.parseExpression(sCmd)) {
|
|
if (!fQuiet) this.println("false: " + sCmd);
|
|
return false;
|
|
}
|
|
if (!fQuiet) this.println("true: " + sCmd);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* doInfo(asArgs)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs
|
|
* @return {boolean} true only if the instruction info command ("n") is supported
|
|
*/
|
|
Debugger.prototype.doInfo = function(asArgs)
|
|
{
|
|
if (DEBUG) {
|
|
this.println("msPerYield: " + this.cpu.aCounts.msPerYield);
|
|
this.println("nCyclesPerBurst: " + this.cpu.aCounts.nCyclesPerBurst);
|
|
this.println("nCyclesPerYield: " + this.cpu.aCounts.nCyclesPerYield);
|
|
this.println("nCyclesPerVideoUpdate: " + this.cpu.aCounts.nCyclesPerVideoUpdate);
|
|
this.println("nCyclesPerStatusUpdate: " + this.cpu.aCounts.nCyclesPerStatusUpdate);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* doInput(sPort)
|
|
*
|
|
* Simulate a 1-byte port input operation.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sPort
|
|
*/
|
|
Debugger.prototype.doInput = function(sPort)
|
|
{
|
|
if (!sPort || sPort == '?') {
|
|
this.println("input commands:");
|
|
this.println("\ti [p]\tread port [p]");
|
|
/*
|
|
* TODO: Regarding this warning, consider adding an "unchecked" version of
|
|
* bus.checkPortInputNotify(), since all Debugger memory accesses are unchecked, too.
|
|
*
|
|
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),
|
|
* but changing them all to be non-destructive would take time, and situations where you
|
|
* actually want to affect the hardware state are just as likely as not....
|
|
*/
|
|
this.println("warning: port accesses can affect hardware state");
|
|
return;
|
|
}
|
|
var port = this.parseValue(sPort);
|
|
if (port !== undefined) {
|
|
var bIn = this.bus.checkPortInputNotify(port, 1);
|
|
this.println(str.toHexWord(port) + ": " + str.toHexByte(bIn));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doVar(sCmd)
|
|
*
|
|
* The command must be of the form "{variable} = [{expression}]", where expression may contain constants,
|
|
* operators, registers, symbols, other variables, or nothing at all; in the latter case, the variable, if
|
|
* any, is deleted.
|
|
*
|
|
* Other supported shorthand: "var" with no parameters prints the values of all variables, and "var {variable}"
|
|
* prints the value of the specified variable.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
* @return {boolean} true if valid "var" assignment, false if not
|
|
*/
|
|
Debugger.prototype.doVar = function(sCmd)
|
|
{
|
|
var a = sCmd.match(/^\s*([A-Z_]?[A-Z0-9_]*)\s*(=?)\s*(.*)$/i);
|
|
if (a) {
|
|
if (!a[1]) {
|
|
if (!this.printVariable()) this.println("no variables");
|
|
return true; // it's not considered an error to print an empty list of variables
|
|
}
|
|
if (!a[2]) {
|
|
return this.printVariable(a[1]);
|
|
}
|
|
if (!a[3]) {
|
|
this.delVariable(a[1]);
|
|
return true; // it's not considered an error to delete a variable that didn't exist
|
|
}
|
|
var v = this.parseExpression(a[3]);
|
|
if (v !== undefined) {
|
|
this.setVariable(a[1], v);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
this.println("invalid assignment:" + sCmd);
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* doList(sAddr, fPrint)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sAddr
|
|
* @param {boolean} [fPrint]
|
|
* @return {string|null}
|
|
*/
|
|
Debugger.prototype.doList = function(sAddr, fPrint)
|
|
{
|
|
var sSymbol = null;
|
|
|
|
var dbgAddr = this.parseAddr(sAddr, true);
|
|
if (dbgAddr) {
|
|
var addr = this.getAddr(dbgAddr);
|
|
var aSymbol = this.findSymbol(dbgAddr, true);
|
|
if (aSymbol.length) {
|
|
var nDelta, sDelta, s;
|
|
if (aSymbol[0]) {
|
|
sDelta = "";
|
|
nDelta = dbgAddr.addr - aSymbol[1];
|
|
if (nDelta) sDelta = " + " + str.toHexWord(nDelta);
|
|
s = aSymbol[0] + " (" + this.toHexOffset(aSymbol[1]) + ')' + sDelta;
|
|
if (fPrint) this.println(s);
|
|
sSymbol = s;
|
|
}
|
|
if (aSymbol.length > 4 && aSymbol[4]) {
|
|
sDelta = "";
|
|
nDelta = aSymbol[5] - dbgAddr.addr;
|
|
if (nDelta) sDelta = " - " + str.toHexWord(nDelta);
|
|
s = aSymbol[4] + " (" + this.toHexOffset(aSymbol[5]) + ')' + sDelta;
|
|
if (fPrint) this.println(s);
|
|
if (!sSymbol) sSymbol = s;
|
|
}
|
|
} else {
|
|
if (fPrint) this.println("no symbols");
|
|
}
|
|
}
|
|
return sSymbol;
|
|
};
|
|
|
|
/**
|
|
* doMessages(asArgs)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs
|
|
*/
|
|
Debugger.prototype.doMessages = function(asArgs)
|
|
{
|
|
var m;
|
|
var fCriteria = null;
|
|
var sCategory = asArgs[1];
|
|
if (sCategory == '?') sCategory = undefined;
|
|
|
|
if (sCategory !== undefined) {
|
|
var bitsMessage = 0;
|
|
if (sCategory == "all") {
|
|
bitsMessage = (0xffffffff|0) & ~(Messages.HALT | Messages.KEYS | Messages.LOG);
|
|
sCategory = null;
|
|
} else if (sCategory == "on") {
|
|
fCriteria = true;
|
|
sCategory = null;
|
|
} else if (sCategory == "off") {
|
|
fCriteria = false;
|
|
sCategory = null;
|
|
} else {
|
|
/*
|
|
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
|
|
* but externally, we allow the user to specify "keys"; "kbd" is also allowed as shorthand for "keyboard".
|
|
*/
|
|
if (sCategory == "keys") sCategory = "key";
|
|
if (sCategory == "kbd") sCategory = "keyboard";
|
|
for (m in Debugger.MESSAGES) {
|
|
if (sCategory == m) {
|
|
bitsMessage = Debugger.MESSAGES[m];
|
|
fCriteria = !!(this.bitsMessage & bitsMessage);
|
|
break;
|
|
}
|
|
}
|
|
if (!bitsMessage) {
|
|
this.println("unknown message category: " + sCategory);
|
|
return;
|
|
}
|
|
}
|
|
if (bitsMessage) {
|
|
if (asArgs[2] == "on") {
|
|
this.bitsMessage |= bitsMessage;
|
|
fCriteria = true;
|
|
}
|
|
else if (asArgs[2] == "off") {
|
|
this.bitsMessage &= ~bitsMessage;
|
|
fCriteria = false;
|
|
if (bitsMessage == Messages.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 Debugger.MESSAGES) {
|
|
if (!sCategory || sCategory == m) {
|
|
var bitMessage = Debugger.MESSAGES[m];
|
|
var fEnabled = !!(this.bitsMessage & bitMessage);
|
|
if (fCriteria !== null && fCriteria != fEnabled) continue;
|
|
if (sCategories) sCategories += ',';
|
|
if (!(++n % 10)) sCategories += "\n\t"; // jshint ignore:line
|
|
/*
|
|
* Internally, we use "key" instead of "keys", since the latter is a method on JavasScript objects,
|
|
* but externally, we allow the user to specify "keys".
|
|
*/
|
|
if (m == "key") m = "keys";
|
|
sCategories += m;
|
|
}
|
|
}
|
|
|
|
if (sCategory === undefined) {
|
|
this.println("message commands:\n\tm [category] [on|off]\tturn categories on/off");
|
|
}
|
|
|
|
this.println((fCriteria !== null? (fCriteria? "messages on: " : "messages off: ") : "message categories:\n\t") + (sCategories || "none"));
|
|
|
|
this.historyInit(); // call this just in case Messages.INT was turned on
|
|
};
|
|
|
|
/**
|
|
* doOptions(asArgs)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs
|
|
*/
|
|
Debugger.prototype.doOptions = function(asArgs)
|
|
{
|
|
switch (asArgs[1]) {
|
|
case "8080":
|
|
this.style = Debugger.STYLE_8080;
|
|
break;
|
|
|
|
case "8086":
|
|
this.style = Debugger.STYLE_8086;
|
|
break;
|
|
|
|
case "cs":
|
|
var nCycles;
|
|
if (asArgs[3] !== undefined) nCycles = +asArgs[3]; // warning: decimal instead of hex conversion
|
|
switch (asArgs[2]) {
|
|
case "int":
|
|
this.cpu.aCounts.nCyclesChecksumInterval = nCycles;
|
|
break;
|
|
case "start":
|
|
this.cpu.aCounts.nCyclesChecksumStart = nCycles;
|
|
break;
|
|
case "stop":
|
|
this.cpu.aCounts.nCyclesChecksumStop = nCycles;
|
|
break;
|
|
default:
|
|
this.println("unknown cs option");
|
|
return;
|
|
}
|
|
if (nCycles !== undefined) {
|
|
this.cpu.resetChecksum();
|
|
}
|
|
this.println("checksums " + (this.cpu.flags.fChecksum? "enabled" : "disabled"));
|
|
return;
|
|
|
|
case "sp":
|
|
if (asArgs[2] !== undefined) {
|
|
if (!this.cpu.setSpeed(+asArgs[2])) {
|
|
this.println("warning: using 1x multiplier, previous target not reached");
|
|
}
|
|
}
|
|
this.println("target speed: " + this.cpu.getSpeedTarget() + " (" + this.cpu.getSpeed() + "x)");
|
|
return;
|
|
|
|
case "?":
|
|
this.println("debugger options:");
|
|
this.println("\t8080\t\tselect 8080-style mnemonics");
|
|
this.println("\t8086\t\tselect 8086-style mnemonics");
|
|
this.println("\tcs int #\tset checksum cycle interval to #");
|
|
this.println("\tcs start #\tset checksum cycle start count to #");
|
|
this.println("\tcs stop #\tset checksum cycle stop count to #");
|
|
this.println("\tsp #\t\tset speed multiplier to #");
|
|
break;
|
|
|
|
default:
|
|
if (asArgs[1]) {
|
|
this.println("unknown option: " + asArgs[1]);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
this.println(this.style + "-style mnemonics enabled");
|
|
};
|
|
|
|
/**
|
|
* doOutput(sPort, sByte)
|
|
*
|
|
* Simulate a 1-byte port output operation.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sPort
|
|
* @param {string|undefined} sByte (string representation of 1 byte)
|
|
*/
|
|
Debugger.prototype.doOutput = function(sPort, sByte)
|
|
{
|
|
if (!sPort || sPort == '?') {
|
|
this.println("output commands:");
|
|
this.println("\to [p] [b]\twrite byte [b] to port [p]");
|
|
/*
|
|
* TODO: Regarding this warning, consider adding an "unchecked" version of
|
|
* bus.checkPortOutputNotify(), since all Debugger memory accesses are unchecked, too.
|
|
*
|
|
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),
|
|
* but changing them all to be non-destructive would take time, and situations where you
|
|
* actually want to affect the hardware state are just as likely as not....
|
|
*/
|
|
this.println("warning: port accesses can affect hardware state");
|
|
return;
|
|
}
|
|
var port = this.parseValue(sPort, "port #");
|
|
var bOut = this.parseValue(sByte);
|
|
if (port !== undefined && bOut !== undefined) {
|
|
this.bus.checkPortOutputNotify(port, 1, bOut);
|
|
this.println(str.toHexWord(port) + ": " + str.toHexByte(bOut));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doRegisters(asArgs, fInstruction)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} [asArgs]
|
|
* @param {boolean} [fInstruction] (true to include the current instruction; default is true)
|
|
*/
|
|
Debugger.prototype.doRegisters = function(asArgs, fInstruction)
|
|
{
|
|
if (asArgs && asArgs[1] == '?') {
|
|
this.println("register commands:");
|
|
this.println("\tr\tdump registers");
|
|
this.println("\trx [#]\tset flag or register x to [#]");
|
|
return;
|
|
}
|
|
|
|
var cpu = this.cpu;
|
|
if (fInstruction == null) fInstruction = true;
|
|
|
|
if (asArgs != null && asArgs.length > 1) {
|
|
var sReg = asArgs[1];
|
|
var sValue = null;
|
|
var i = sReg.indexOf('=');
|
|
if (i > 0) {
|
|
sValue = sReg.substr(i + 1);
|
|
sReg = sReg.substr(0, i);
|
|
}
|
|
else if (asArgs.length > 2) {
|
|
sValue = asArgs[2];
|
|
}
|
|
else {
|
|
this.println("missing value for " + asArgs[1]);
|
|
return;
|
|
}
|
|
|
|
var fValid = false;
|
|
var w = this.parseExpression(sValue);
|
|
|
|
if (w !== undefined) {
|
|
fValid = true;
|
|
var sRegMatch = sReg.toUpperCase();
|
|
switch (sRegMatch) {
|
|
case "A":
|
|
cpu.regA = w & 0xff;
|
|
break;
|
|
case "B":
|
|
cpu.regB = w & 0xff;
|
|
break;
|
|
case "BC":
|
|
cpu.regB = ((w >> 8) & 0xff);
|
|
/* falls through */
|
|
case "C":
|
|
cpu.regC = w & 0xff;
|
|
break;
|
|
case "D":
|
|
cpu.regD = w & 0xff;
|
|
break;
|
|
case "DE":
|
|
cpu.regD = ((w >> 8) & 0xff);
|
|
/* falls through */
|
|
case "E":
|
|
cpu.regE = w & 0xff;
|
|
break;
|
|
case "H":
|
|
cpu.regH = w & 0xff;
|
|
break;
|
|
case "HL":
|
|
cpu.regH = ((w >> 8) & 0xff);
|
|
/* falls through */
|
|
case "L":
|
|
cpu.regL = w & 0xff;
|
|
break;
|
|
case "SP":
|
|
cpu.setSP(w);
|
|
break;
|
|
case "PC":
|
|
cpu.setPC(w);
|
|
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
|
|
break;
|
|
case "PS":
|
|
cpu.setPS(w);
|
|
break;
|
|
case "PSW":
|
|
cpu.setPSW(w);
|
|
break;
|
|
case "CF":
|
|
if (w) cpu.setCF(); else cpu.clearCF();
|
|
break;
|
|
case "PF":
|
|
if (w) cpu.setPF(); else cpu.clearPF();
|
|
break;
|
|
case "AF":
|
|
if (w) cpu.setAF(); else cpu.clearAF();
|
|
break;
|
|
case "ZF":
|
|
if (w) cpu.setZF(); else cpu.clearZF();
|
|
break;
|
|
case "SF":
|
|
if (w) cpu.setSF(); else cpu.clearSF();
|
|
break;
|
|
case "IF":
|
|
if (w) cpu.setIF(); else cpu.clearIF();
|
|
break;
|
|
default:
|
|
this.println("unknown register: " + sReg);
|
|
return;
|
|
}
|
|
}
|
|
if (!fValid) {
|
|
this.println("invalid value: " + sValue);
|
|
return;
|
|
}
|
|
cpu.updateCPU();
|
|
this.println("updated registers:");
|
|
}
|
|
|
|
this.println(this.getRegDump());
|
|
|
|
if (fInstruction) {
|
|
this.dbgAddrNextCode = this.newAddr(cpu.getPC());
|
|
this.doUnassemble(this.toHexAddr(this.dbgAddrNextCode));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doRun(sCmd, sAddr, sOptions, fQuiet)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
* @param {string|undefined} [sAddr]
|
|
* @param {string} [sOptions] (the rest of the breakpoint command-line)
|
|
* @param {boolean} [fQuiet]
|
|
*/
|
|
Debugger.prototype.doRun = function(sCmd, sAddr, sOptions, fQuiet)
|
|
{
|
|
if (sCmd == "gt") {
|
|
this.fIgnoreNextCheckFault = true;
|
|
}
|
|
if (sAddr !== undefined) {
|
|
var dbgAddr = this.parseAddr(sAddr, true);
|
|
if (!dbgAddr) return;
|
|
this.parseAddrOptions(dbgAddr, sOptions);
|
|
this.setTempBreakpoint(dbgAddr);
|
|
}
|
|
if (!this.runCPU(true)) {
|
|
if (!fQuiet) this.println("cpu busy or unavailable, run command ignored");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doPrint(sCmd)
|
|
*
|
|
* NOTE: If the string to print is a quoted string, then we run it through replaceRegs(), so that
|
|
* you can take advantage of all the special replacement options used for software interrupt logging.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
*/
|
|
Debugger.prototype.doPrint = function(sCmd)
|
|
{
|
|
sCmd = str.trim(sCmd);
|
|
var a = sCmd.match(/^(['"])(.*?)\1$/);
|
|
if (!a) {
|
|
this.parseExpression(sCmd, true);
|
|
} else {
|
|
this.println(this.replaceRegs(a[2]));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* doStep(sCmd)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sCmd] "p" or "pr"
|
|
*/
|
|
Debugger.prototype.doStep = function(sCmd)
|
|
{
|
|
var fCallStep = true;
|
|
var fRegs = (sCmd == "pr"? 1 : 0);
|
|
/*
|
|
* Set up the value for this.nStep (ie, 1 or 2) depending on whether the user wants
|
|
* a subsequent register dump ("pr") or not ("p").
|
|
*/
|
|
var nStep = 1 + fRegs;
|
|
if (!this.nStep) {
|
|
var dbgAddr = this.newAddr(this.cpu.getPC());
|
|
var bOpcode = this.getByte(dbgAddr);
|
|
|
|
switch (bOpcode) {
|
|
case CPUDef.OPCODE.CALL:
|
|
if (fCallStep) {
|
|
this.nStep = nStep;
|
|
this.incAddr(dbgAddr, 3);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (this.nStep) {
|
|
this.setTempBreakpoint(dbgAddr);
|
|
if (!this.runCPU()) {
|
|
if (this.cmp) this.cmp.updateFocus();
|
|
this.nStep = 0;
|
|
}
|
|
/*
|
|
* A successful run will ultimately call stop(), which will in turn call clearTempBreakpoint(),
|
|
* which will clear nStep, so there's your assurance that nStep will be reset. Now we may have
|
|
* stopped for reasons unrelated to the temporary breakpoint, but that's OK.
|
|
*/
|
|
} else {
|
|
this.doTrace(fRegs? "tr" : "t");
|
|
}
|
|
} else {
|
|
this.println("step in progress");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getCall(dbgAddr)
|
|
*
|
|
* Given a possible return address (typically from the stack), look for a matching CALL (or INT) that
|
|
* immediately precedes that address.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {DbgAddr} dbgAddr
|
|
* @return {string|null} CALL instruction at or near dbgAddr, or null if none
|
|
*/
|
|
Debugger.prototype.getCall = function(dbgAddr)
|
|
{
|
|
var sCall = null;
|
|
var addr = dbgAddr.addr;
|
|
var addrOrig = addr;
|
|
for (var n = 1; n <= 6 && !!addr; n++) {
|
|
if (n > 2) {
|
|
dbgAddr.addr = addr;
|
|
var s = this.getInstruction(dbgAddr);
|
|
if (s.indexOf("CALL") >= 0) {
|
|
/*
|
|
* Verify that the length of this CALL (or INT), when added to the address of the CALL (or INT),
|
|
* matches the original return address. We do this by getting the string index of the opcode bytes,
|
|
* subtracting that from the string index of the next space, and dividing that difference by two,
|
|
* to yield the length of the CALL (or INT) instruction, in bytes.
|
|
*/
|
|
var i = s.indexOf(' ');
|
|
var j = s.indexOf(' ', i+1);
|
|
if (addr + (j - i - 1)/2 == addrOrig) {
|
|
sCall = s;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
addr--;
|
|
}
|
|
dbgAddr.addr = addrOrig;
|
|
return sCall;
|
|
};
|
|
|
|
/**
|
|
* doStackTrace(sCmd, sAddr)
|
|
*
|
|
* Use "k" for a normal stack trace and "ks" for a stack trace with symbolic info.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sCmd]
|
|
* @param {string} [sAddr] (not used yet)
|
|
*/
|
|
Debugger.prototype.doStackTrace = function(sCmd, sAddr)
|
|
{
|
|
if (sAddr == '?') {
|
|
this.println("stack trace commands:");
|
|
this.println("\tk\tshow frame addresses");
|
|
this.println("\tks\tshow symbol information");
|
|
return;
|
|
}
|
|
|
|
var nFrames = 10, cFrames = 0;
|
|
var dbgAddrCall = this.newAddr();
|
|
var dbgAddrStack = this.newAddr(this.cpu.getSP());
|
|
this.println("stack trace for " + this.toHexAddr(dbgAddrStack));
|
|
|
|
while (cFrames < nFrames) {
|
|
var sCall = null, sCallPrev = null, cTests = 256;
|
|
while ((dbgAddrStack.addr >>> 0) < 0x10000) {
|
|
dbgAddrCall.addr = this.getWord(dbgAddrStack, true);
|
|
/*
|
|
* Because we're using the auto-increment feature of getWord(), and because that will automatically
|
|
* wrap the offset around the end of the segment, we must also check the addr property to detect the wrap.
|
|
*/
|
|
if (dbgAddrStack.addr == null || !cTests--) break;
|
|
sCall = this.getCall(dbgAddrCall);
|
|
if (sCall) break;
|
|
}
|
|
/*
|
|
* The sCallPrev check eliminates duplicate sequential calls, which are usually (but not always)
|
|
* indicative of a false positive, in which case the previous call is probably bogus as well, but
|
|
* at least we won't duplicate that mistake. Of course, there are always exceptions, recursion
|
|
* being one of them, but it's rare that we're debugging recursive code.
|
|
*/
|
|
if (!sCall || sCall == sCallPrev) break;
|
|
var sSymbol = null;
|
|
if (sCmd == "ks") {
|
|
var a = sCall.match(/[0-9A-F]+$/);
|
|
if (a) sSymbol = this.doList(a[0]);
|
|
}
|
|
sCall = str.pad(sCall, 50) + " ;" + (sSymbol || "stack=" + this.toHexAddr(dbgAddrStack)); // + " return=" + this.toHexAddr(dbgAddrCall));
|
|
this.println(sCall);
|
|
sCallPrev = sCall;
|
|
cFrames++;
|
|
}
|
|
if (!cFrames) this.println("no return addresses found");
|
|
};
|
|
|
|
/**
|
|
* doTrace(sCmd, sCount)
|
|
*
|
|
* The "t" and "tr" commands interpret the count as a number of instructions, and since
|
|
* we call the Debugger's stepCPU() for each iteration, a single instruction includes
|
|
* any/all prefixes; the CPU's stepCPU() treats prefixes as discrete operations. The only
|
|
* difference between "t" and "tr": the former displays only the next instruction, while
|
|
* the latter also displays the (updated) registers.
|
|
*
|
|
* The "tc" command interprets the count as a number of cycles rather than instructions,
|
|
* allowing you to quickly execute large chunks of instructions with a single command; it
|
|
* doesn't display anything until the the chunk has finished.
|
|
*
|
|
* However, generally a more useful command is "bn", which allows you to break after some
|
|
* number of instructions have been executed (as opposed to some number of cycles).
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sCmd] ("t", "tc", or "tr")
|
|
* @param {string} [sCount] # of instructions to step
|
|
*/
|
|
Debugger.prototype.doTrace = function(sCmd, sCount)
|
|
{
|
|
var dbg = this;
|
|
var fRegs = (sCmd != "t");
|
|
var nCount = this.parseValue(sCount, null, true) || 1;
|
|
var nCycles = (nCount == 1? 0 : 1);
|
|
if (sCmd == "tc") {
|
|
nCycles = nCount;
|
|
nCount = 1;
|
|
}
|
|
web.onCountRepeat(
|
|
nCount,
|
|
function onCountStep() {
|
|
return dbg.setBusy(true) && dbg.stepCPU(nCycles, fRegs, false);
|
|
},
|
|
function onCountStepComplete() {
|
|
/*
|
|
* We explicitly called stepCPU() with fUpdateCPU === false, because repeatedly
|
|
* calling updateCPU() can be very slow, especially when fDisplayLiveRegs is true,
|
|
* so once the repeat count has been exhausted, we must perform a final updateCPU().
|
|
*/
|
|
dbg.cpu.updateCPU();
|
|
dbg.setBusy(false);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* doUnassemble(sAddr, sAddrEnd, n)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} [sAddr]
|
|
* @param {string} [sAddrEnd]
|
|
* @param {number} [n]
|
|
*/
|
|
Debugger.prototype.doUnassemble = function(sAddr, sAddrEnd, n)
|
|
{
|
|
var dbgAddr = this.parseAddr(sAddr, true);
|
|
if (!dbgAddr) return;
|
|
|
|
if (n === undefined) n = 1;
|
|
|
|
var cb = 0x100;
|
|
if (sAddrEnd !== undefined) {
|
|
|
|
var dbgAddrEnd = this.parseAddr(sAddrEnd, true);
|
|
if (!dbgAddrEnd || dbgAddrEnd.addr < dbgAddr.addr) return;
|
|
|
|
cb = dbgAddrEnd.addr - dbgAddr.addr;
|
|
if (!DEBUG && cb > 0x100) {
|
|
/*
|
|
* Limiting the amount of disassembled code to 256 bytes in non-DEBUG builds is partly to
|
|
* prevent the user from wedging the browser by dumping too many lines, but also a recognition
|
|
* that, in non-DEBUG builds, this.println() keeps print output buffer truncated to 8Kb anyway.
|
|
*/
|
|
this.println("range too large");
|
|
return;
|
|
}
|
|
n = -1;
|
|
}
|
|
|
|
var cLines = 0;
|
|
var sInstruction;
|
|
|
|
while (cb > 0 && n--) {
|
|
|
|
var nSequence = (this.isBusy(false) || this.nStep)? this.nCycles : null;
|
|
var sComment = (nSequence != null? "cycles" : null);
|
|
var aSymbol = this.findSymbol(dbgAddr);
|
|
|
|
var addr = dbgAddr.addr; // we snap dbgAddr.addr *after* calling findSymbol(), which re-evaluates it
|
|
|
|
if (aSymbol[0] && n) {
|
|
if (!cLines && n || aSymbol[0].indexOf('+') < 0) {
|
|
var sLabel = aSymbol[0] + ':';
|
|
if (aSymbol[2]) sLabel += ' ' + aSymbol[2];
|
|
this.println(sLabel);
|
|
}
|
|
}
|
|
|
|
if (aSymbol[3]) {
|
|
sComment = aSymbol[3];
|
|
nSequence = null;
|
|
}
|
|
|
|
sInstruction = this.getInstruction(dbgAddr, sComment, nSequence);
|
|
|
|
this.println(sInstruction);
|
|
this.dbgAddrNextCode = dbgAddr;
|
|
cb -= dbgAddr.addr - addr;
|
|
cLines++;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* parseCommand(sCmd, fSave, chSep)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string|undefined} sCmd
|
|
* @param {boolean} [fSave] is true to save the command, false if not
|
|
* @param {string} [chSep] is the command separator character (default is ';')
|
|
* @return {Array.<string>}
|
|
*/
|
|
Debugger.prototype.parseCommand = function(sCmd, fSave, chSep)
|
|
{
|
|
if (fSave) {
|
|
if (!sCmd) {
|
|
if (this.fAssemble) {
|
|
sCmd = "end";
|
|
} else {
|
|
sCmd = this.aPrevCmds[this.iPrevCmd+1];
|
|
}
|
|
} else {
|
|
if (this.iPrevCmd < 0 && this.aPrevCmds.length) {
|
|
this.iPrevCmd = 0;
|
|
}
|
|
if (this.iPrevCmd < 0 || sCmd != this.aPrevCmds[this.iPrevCmd]) {
|
|
this.aPrevCmds.splice(0, 0, sCmd);
|
|
this.iPrevCmd = 0;
|
|
}
|
|
this.iPrevCmd--;
|
|
}
|
|
}
|
|
var a = [];
|
|
if (sCmd) {
|
|
/*
|
|
* With the introduction of breakpoint commands (ie, quoted command sequences
|
|
* associated with a breakpoint), we can no longer perform simplistic splitting.
|
|
*
|
|
* a = sCmd.split(chSep || ';');
|
|
* for (var i = 0; i < a.length; i++) a[i] = str.trim(a[i]);
|
|
*
|
|
* We may now split on semi-colons ONLY if they are outside a quoted sequence.
|
|
*
|
|
* Also, to allow quoted strings *inside* breakpoint commands, we first replace all
|
|
* DOUBLE double-quotes with single quotes.
|
|
*/
|
|
sCmd = sCmd.toLowerCase().replace(/""/g, "'");
|
|
|
|
var iPrev = 0;
|
|
var chQuote = null;
|
|
chSep = chSep || ';';
|
|
/*
|
|
* NOTE: Processing charAt() up to and INCLUDING length is not a typo; we're taking
|
|
* advantage of the fact that charAt() with an invalid index returns an empty string,
|
|
* allowing us to use the same substring() call to capture the final portion of sCmd.
|
|
*
|
|
* In a sense, it allows us to pretend that the string ends with a zero terminator.
|
|
*/
|
|
for (var i = 0; i <= sCmd.length; i++) {
|
|
var ch = sCmd.charAt(i);
|
|
if (ch == '"' || ch == "'") {
|
|
if (!chQuote) {
|
|
chQuote = ch;
|
|
} else if (ch == chQuote) {
|
|
chQuote = null;
|
|
}
|
|
}
|
|
else if (ch == chSep && !chQuote || !ch) {
|
|
/*
|
|
* Recall that substring() accepts starting (inclusive) and ending (exclusive)
|
|
* indexes, whereas substr() accepts a starting index and a length. We need the former.
|
|
*/
|
|
a.push(str.trim(sCmd.substring(iPrev, i)));
|
|
iPrev = i + 1;
|
|
}
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* shiftArgs(asArgs)
|
|
*
|
|
* Used with any command (eg, "r") that allows but doesn't require whitespace between command and first argument.
|
|
*
|
|
* @this {Debugger}
|
|
* @param {Array.<string>} asArgs
|
|
* @return {Array.<string>}
|
|
*/
|
|
Debugger.prototype.shiftArgs = function(asArgs)
|
|
{
|
|
if (asArgs && asArgs.length) {
|
|
var s0 = asArgs[0];
|
|
var ch0 = s0.charAt(0);
|
|
for (var i = 1; i < s0.length; i++) {
|
|
var ch = s0.charAt(i);
|
|
if (ch0 == '?' || ch0 == 'r' || ch < 'a' || ch > 'z') {
|
|
asArgs[0] = s0.substr(i);
|
|
asArgs.unshift(s0.substr(0, i));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return asArgs;
|
|
};
|
|
|
|
/**
|
|
* doCommand(sCmd, fQuiet)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmd
|
|
* @param {boolean} [fQuiet]
|
|
* @return {boolean} true if command processed, false if unrecognized
|
|
*/
|
|
Debugger.prototype.doCommand = function(sCmd, fQuiet)
|
|
{
|
|
var result = true;
|
|
|
|
try {
|
|
if (!sCmd.length || sCmd == "end") {
|
|
if (this.fAssemble) {
|
|
this.println("ended assemble at " + this.toHexAddr(this.dbgAddrAssemble));
|
|
this.dbgAddrNextCode = this.dbgAddrAssemble;
|
|
this.fAssemble = false;
|
|
}
|
|
sCmd = "";
|
|
}
|
|
else if (!fQuiet) {
|
|
var sPrompt = ">> ";
|
|
this.println(sPrompt + sCmd);
|
|
}
|
|
|
|
var ch = sCmd.charAt(0);
|
|
if (ch == '"' || ch == "'") return true;
|
|
|
|
/*
|
|
* Zap the previous message buffer to ensure the new command's output is not tossed out as a repeat.
|
|
*/
|
|
this.sMessagePrev = null;
|
|
|
|
/*
|
|
* I've relaxed the !isBusy() requirement, to maximize our ability to issue Debugger commands externally.
|
|
*/
|
|
if (this.isReady() /* && !this.isBusy(true) */ && sCmd.length > 0) {
|
|
|
|
if (this.fAssemble) {
|
|
sCmd = "a " + this.toHexAddr(this.dbgAddrAssemble) + ' ' + sCmd;
|
|
}
|
|
|
|
var asArgs = this.shiftArgs(sCmd.replace(/ +/g, ' ').split(' '));
|
|
|
|
switch (asArgs[0].charAt(0)) {
|
|
case 'a':
|
|
this.doAssemble(asArgs);
|
|
break;
|
|
case 'b':
|
|
this.doBreak(asArgs[0], asArgs[1], sCmd);
|
|
break;
|
|
case 'c':
|
|
this.doClear(asArgs[0]);
|
|
break;
|
|
case 'd':
|
|
if (!COMPILED && sCmd == "debug") {
|
|
window.DEBUG = true;
|
|
this.println("DEBUG checks on");
|
|
break;
|
|
}
|
|
this.doDump(asArgs);
|
|
break;
|
|
case 'e':
|
|
if (asArgs[0] == "else") break;
|
|
this.doEdit(asArgs);
|
|
break;
|
|
case 'f':
|
|
this.doFreqs(asArgs[1]);
|
|
break;
|
|
case 'g':
|
|
this.doRun(asArgs[0], asArgs[1], sCmd, fQuiet);
|
|
break;
|
|
case 'h':
|
|
this.doHalt(fQuiet);
|
|
break;
|
|
case 'i':
|
|
if (asArgs[0] == "if") {
|
|
if (!this.doIf(sCmd.substr(2), fQuiet)) {
|
|
result = false;
|
|
}
|
|
break;
|
|
}
|
|
this.doInput(asArgs[1]);
|
|
break;
|
|
case 'k':
|
|
this.doStackTrace(asArgs[0], asArgs[1]);
|
|
break;
|
|
case 'l':
|
|
if (asArgs[0] == "ln") {
|
|
this.doList(asArgs[1], true);
|
|
break;
|
|
}
|
|
break;
|
|
case 'm':
|
|
this.doMessages(asArgs);
|
|
break;
|
|
case 'o':
|
|
this.doOutput(asArgs[1], asArgs[2]);
|
|
break;
|
|
case 'p':
|
|
if (asArgs[0] == "print") {
|
|
this.doPrint(sCmd.substr(5));
|
|
break;
|
|
}
|
|
this.doStep(asArgs[0]);
|
|
break;
|
|
case 'r':
|
|
if (sCmd == "reset") {
|
|
if (this.cmp) this.cmp.reset();
|
|
break;
|
|
}
|
|
this.doRegisters(asArgs);
|
|
break;
|
|
case 's':
|
|
this.doOptions(asArgs);
|
|
break;
|
|
case 't':
|
|
this.doTrace(asArgs[0], asArgs[1]);
|
|
break;
|
|
case 'u':
|
|
this.doUnassemble(asArgs[1], asArgs[2], 8);
|
|
break;
|
|
case 'v':
|
|
if (asArgs[0] == "var") {
|
|
if (!this.doVar(sCmd.substr(3))) {
|
|
result = false;
|
|
}
|
|
break;
|
|
}
|
|
this.println((APPNAME || "PC8080") + " version " + (XMLVERSION || APPVERSION) + " (" + this.cpu.model + (COMPILED? ",RELEASE" : (DEBUG? ",DEBUG" : ",NODEBUG")) + (TYPEDARRAYS? ",TYPEDARRAYS" : (BYTEARRAYS? ",BYTEARRAYS" : ",LONGARRAYS")) + ')');
|
|
this.println(web.getUserAgent());
|
|
break;
|
|
case '?':
|
|
if (asArgs[1]) {
|
|
this.doPrint(sCmd.substr(1));
|
|
break;
|
|
}
|
|
this.doHelp();
|
|
break;
|
|
case 'n':
|
|
if (!COMPILED && sCmd == "nodebug") {
|
|
window.DEBUG = false;
|
|
this.println("DEBUG checks off");
|
|
break;
|
|
}
|
|
if (this.doInfo(asArgs)) break;
|
|
/* falls through */
|
|
default:
|
|
this.println("unknown command: " + sCmd);
|
|
result = false;
|
|
break;
|
|
}
|
|
}
|
|
} catch(e) {
|
|
this.println("debugger error: " + (e.stack || e.message));
|
|
result = false;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* doCommands(sCmds, fSave)
|
|
*
|
|
* @this {Debugger}
|
|
* @param {string} sCmds
|
|
* @param {boolean} [fSave]
|
|
* @return {boolean} true if all commands processed, false if not
|
|
*/
|
|
Debugger.prototype.doCommands = function(sCmds, fSave)
|
|
{
|
|
var a = this.parseCommand(sCmds, fSave);
|
|
for (var s in a) {
|
|
if (!this.doCommand(a[s])) return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Debugger.init()
|
|
*
|
|
* This function operates on every HTML element of class "debugger", extracting the
|
|
* JSON-encoded parameters for the Debugger constructor from the element's "data-value"
|
|
* attribute, invoking the constructor to create a Debugger component, and then binding
|
|
* any associated HTML controls to the new component.
|
|
*/
|
|
Debugger.init = function()
|
|
{
|
|
var aeDbg = Component.getElementsByClass(document, PC8080.APPCLASS, "debugger");
|
|
for (var iDbg = 0; iDbg < aeDbg.length; iDbg++) {
|
|
var eDbg = aeDbg[iDbg];
|
|
var parmsDbg = Component.getComponentParms(eDbg);
|
|
var dbg = new Debugger(parmsDbg);
|
|
Component.bindComponentControls(dbg, eDbg, PC8080.APPCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Debugger module on the page (as IF there's ever going to be more than one ;-))
|
|
*/
|
|
web.onInit(Debugger.init);
|
|
|
|
} // endif DEBUGGER
|
|
|
|
|
|
// ./modules/pc8080/lib/state.js
|
|
|
|
/**
|
|
* @fileoverview The State class used by PCjs machines.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-19
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* State(component, sVersion, sSuffix)
|
|
*
|
|
* State objects are used by components to save/restore their state.
|
|
*
|
|
* During a save operation, components add data to a State object via set(),
|
|
* and then return the resulting data using data().
|
|
*
|
|
* During a restore operation, the Computer component passes the results of each
|
|
* data() call back to the originating component.
|
|
*
|
|
* WARNING: Since State objects are low-level objects that have no UI requirements,
|
|
* they do not inherit from the Component class, so you should only use class methods
|
|
* of Component, such as Component.assert(), or Debugger methods if the Debugger
|
|
* is available.
|
|
*
|
|
* @constructor
|
|
* @param {Component} component
|
|
* @param {string} [sVersion] is used to append a major version number to the key
|
|
* @param {string} [sSuffix] is used to append any additional suffixes to the key
|
|
*/
|
|
function State(component, sVersion, sSuffix) {
|
|
this.id = component.id;
|
|
this.key = State.key(component, sVersion, sSuffix);
|
|
this.dbg = component.dbg;
|
|
this.unload(component.parms);
|
|
}
|
|
|
|
/**
|
|
* State.key(component, sVersion, sSuffix)
|
|
*
|
|
* This encapsulates the key generation code.
|
|
*
|
|
* @param {Component} component
|
|
* @param {string} [sVersion] is used to append a major version number to the key
|
|
* @param {string} [sSuffix] is used to append any additional suffixes to the key
|
|
* @return {string} key
|
|
*/
|
|
State.key = function(component, sVersion, sSuffix) {
|
|
var key = component.id;
|
|
if (sVersion) {
|
|
var i = sVersion.indexOf('.');
|
|
if (i > 0) key += ".v" + sVersion.substr(0, i);
|
|
}
|
|
if (sSuffix) {
|
|
key += "." + sSuffix;
|
|
}
|
|
return key;
|
|
};
|
|
|
|
/**
|
|
* State.compress(aSrc)
|
|
*
|
|
* @param {Array.<number>|null} aSrc
|
|
* @return {Array.<number>|null} is either the original array (aSrc), or a smaller array of "count, value" pairs (aComp)
|
|
*/
|
|
State.compress = function(aSrc) {
|
|
if (aSrc) {
|
|
var iSrc = 0;
|
|
var iComp = 0;
|
|
var aComp = [];
|
|
while (iSrc < aSrc.length) {
|
|
var n = aSrc[iSrc];
|
|
|
|
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>}
|
|
*/
|
|
State.decompress = function(aComp, nLength) {
|
|
var iDst = 0;
|
|
var aDst = new Array(nLength);
|
|
var iComp = 0;
|
|
while (iComp < aComp.length - 1) {
|
|
var c = aComp[iComp++];
|
|
var n = aComp[iComp++];
|
|
while (c--) {
|
|
aDst[iDst++] = n;
|
|
}
|
|
}
|
|
|
|
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)
|
|
*/
|
|
State.compressEvenOdd = function(aSrc) {
|
|
if (aSrc) {
|
|
var iComp = 0, aComp = [];
|
|
if (aSrc[0] !== undefined) {
|
|
for (var off = 0; off < 2; off++) {
|
|
var iSrc = off;
|
|
while (iSrc < aSrc.length) {
|
|
var n = aSrc[iSrc];
|
|
var iCompare = iSrc + 2;
|
|
while (iCompare < aSrc.length && aSrc[iCompare] === n) iCompare += 2;
|
|
aComp[iComp++] = (iCompare - iSrc) >> 1;
|
|
aComp[iComp++] = n;
|
|
iSrc = iCompare;
|
|
}
|
|
}
|
|
}
|
|
if (aComp.length < aSrc.length) return aComp;
|
|
}
|
|
return aSrc;
|
|
};
|
|
|
|
/**
|
|
* State.decompressEvenOdd(aComp, nLength)
|
|
*
|
|
* This is the counterpart to compressEvenOdd(). Note that because there's nothing in the compressed sequence
|
|
* that differentiates a compress() sequence from a compressEvenOdd() sequence, you simply have to be consistent:
|
|
* if you used even/odd compression, then you must use even/odd decompression.
|
|
*
|
|
* @param {Array.<number>} aComp
|
|
* @param {number} nLength is expected length of decompressed data
|
|
* @return {Array.<number>}
|
|
*/
|
|
State.decompressEvenOdd = function(aComp, nLength) {
|
|
var iDst = 0;
|
|
var aDst = new Array(nLength);
|
|
var iComp = 0;
|
|
while (iComp < aComp.length - 1) {
|
|
var c = aComp[iComp++];
|
|
var n = aComp[iComp++];
|
|
while (c--) {
|
|
aDst[iDst] = n;
|
|
iDst += 2;
|
|
}
|
|
/*
|
|
* The output of a "count,value" pair will never exceed the end of the output array, so as soon as we reach it
|
|
* the first time, we know it's time to switch to ODD elements, and as soon as we reach it again, we should be
|
|
* done.
|
|
*/
|
|
|
|
if (iDst == nLength) iDst = 1;
|
|
}
|
|
|
|
return aDst;
|
|
};
|
|
|
|
State.prototype = {
|
|
constructor: State,
|
|
/**
|
|
* set(id, data)
|
|
*
|
|
* @this {State}
|
|
* @param {number|string} id
|
|
* @param {Object|string} data
|
|
*/
|
|
set: function(id, data) {
|
|
try {
|
|
this[this.id][id] = data;
|
|
} catch(e) {
|
|
Component.log(e.message);
|
|
}
|
|
},
|
|
/**
|
|
* get(id)
|
|
*
|
|
* @this {State}
|
|
* @param {number|string} id
|
|
* @return {Object|string|null}
|
|
*/
|
|
get: function(id) {
|
|
return this[this.id][id] || null;
|
|
},
|
|
/**
|
|
* value()
|
|
*
|
|
* Use this instead of data() if you haven't called parse() yet.
|
|
*
|
|
* @this {State}
|
|
* @return {string}
|
|
*/
|
|
value: function() {
|
|
return this[this.id];
|
|
},
|
|
/**
|
|
* data()
|
|
*
|
|
* @this {State}
|
|
* @return {Object}
|
|
*/
|
|
data: function() {
|
|
return this[this.id];
|
|
},
|
|
/**
|
|
* load(s)
|
|
*
|
|
* WARNING: Make sure you follow this call with either a call to parse() or unload(),
|
|
* because any stringified data that we've loaded isn't usable until it's been parsed.
|
|
*
|
|
* @this {State}
|
|
* @param {Object|string|null} [s]
|
|
* @return {boolean} true if state exists in localStorage, false if not
|
|
*/
|
|
load: function(s) {
|
|
if (s) {
|
|
this[this.id] = s;
|
|
this.fLoaded = true;
|
|
return true;
|
|
}
|
|
if (this.fLoaded) {
|
|
/*
|
|
* This is assumed to be a redundant load().
|
|
*/
|
|
return true;
|
|
}
|
|
if (web.hasLocalStorage()) {
|
|
s = web.getLocalStorageItem(this.key);
|
|
if (s) {
|
|
this[this.id] = s;
|
|
this.fLoaded = true;
|
|
if (DEBUG) this.printString("localStorage(" + this.key + "): " + s.length + " bytes loaded");
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
/**
|
|
* parse()
|
|
*
|
|
* This completes the load() operation, by parsing what was loaded, on the assumption there
|
|
* might be some benefit to deferring parsing until we've given the user a chance to confirm.
|
|
* Otherwise, load() could have just as easily done this, too.
|
|
*
|
|
* @this {State}
|
|
* @return {boolean} true if successful, false if error
|
|
*/
|
|
parse: function() {
|
|
var fSuccess = true;
|
|
try {
|
|
this[this.id] = JSON.parse(this[this.id]);
|
|
} catch (e) {
|
|
Component.error(e.message || e);
|
|
fSuccess = false;
|
|
}
|
|
return fSuccess;
|
|
},
|
|
/**
|
|
* store()
|
|
*
|
|
* @this {State}
|
|
* @return {boolean} true if successful, false if error
|
|
*/
|
|
store: function() {
|
|
var fSuccess = true;
|
|
if (web.hasLocalStorage()) {
|
|
var s = JSON.stringify(this[this.id]);
|
|
if (web.setLocalStorageItem(this.key, s)) {
|
|
if (DEBUG) this.printString("localStorage(" + this.key + "): " + s.length + " bytes stored");
|
|
} else {
|
|
/*
|
|
* WARNING: Because browsers tend to disable all alerts() during an "unload" operation,
|
|
* it's unlikely anyone will ever see the "quota" errors that occur at this point. Need to
|
|
* think of some way to notify the user that there's a problem, and offer a way of cleaning
|
|
* up old states.
|
|
*/
|
|
Component.error("Unable to store " + s.length + " bytes in browser local storage");
|
|
fSuccess = false;
|
|
}
|
|
}
|
|
return fSuccess;
|
|
},
|
|
/**
|
|
* toString()
|
|
*
|
|
* We can't know whether this might be called before parse() or after parse(), so we check.
|
|
* If before, then this[this.id] will still be in string form; if after, it will be an Object.
|
|
*
|
|
* @this {State}
|
|
* @return {string} JSON-encoded state
|
|
*/
|
|
toString: function() {
|
|
var value = this[this.id];
|
|
return (typeof value == "string"? value : JSON.stringify(value));
|
|
},
|
|
/**
|
|
* unload(parms)
|
|
*
|
|
* This discards any data saved via set() or loaded via load(), creating an empty State object.
|
|
* Note that you have to follow this call with an explicit call to store() if you want to remove
|
|
* the state from localStorage as well.
|
|
*
|
|
* @this {State}
|
|
* @param {Object} [parms]
|
|
*/
|
|
unload: function(parms) {
|
|
this[this.id] = {};
|
|
if (parms) this.set("parms", parms);
|
|
this.fLoaded = false;
|
|
},
|
|
/**
|
|
* clear(fAll)
|
|
*
|
|
* This unloads the current state, and then clears ALL localStorage for the current machine,
|
|
* independent of version, to reduce the chance of orphaned states wasting part of our limited allocation.
|
|
*
|
|
* @this {State}
|
|
* @param {boolean} [fAll] true to unconditionally clear ALL localStorage for the current domain
|
|
*/
|
|
clear: function(fAll) {
|
|
this.unload();
|
|
var aKeys = web.getLocalStorageKeys();
|
|
for (var i = 0; i < aKeys.length; i++) {
|
|
var sKey = aKeys[i];
|
|
if (sKey && (fAll || sKey.substr(0, this.key.length) == this.key)) {
|
|
web.removeLocalStorageItem(sKey);
|
|
if (DEBUG) this.printString("localStorage(" + sKey + ") removed");
|
|
aKeys.splice(i, 1);
|
|
i = 0;
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* printString(s)
|
|
*
|
|
* @this {State}
|
|
* @param {string} s is any caller-defined string
|
|
*/
|
|
printString: function(s) {
|
|
if (DEBUG && DEBUGGER && this.dbg) {
|
|
if (this.dbg.messageEnabled(Messages.LOG)) {
|
|
this.dbg.message(s);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// ./modules/pc8080/lib/computer.js
|
|
|
|
/**
|
|
* @fileoverview Implements the PC8080 Computer component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Apr-18
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
|
|
/**
|
|
* Computer(parmsComputer, parmsMachine, fSuspended)
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsComputer
|
|
* @param {Object} [parmsMachine]
|
|
* @param {boolean} [fSuspended]
|
|
*
|
|
* The Computer component has no required (parmsComputer) properties, but it does
|
|
* support the following:
|
|
*
|
|
* autoPower: true to automatically power the computer (default), false to wait;
|
|
* false is honored only if a "power" button binding exists.
|
|
*
|
|
* busWidth: number of memory address lines (address bits) on the computer's "bus";
|
|
* 20 is the minimum (and the default), which implies 8086/8088 real-mode addressing,
|
|
* while 24 is required for 80286 protected-mode addressing. This value is passed
|
|
* directly through to the Bus component; see that component for more details.
|
|
*
|
|
* resume: one of the Computer.RESUME constants, which are as follows:
|
|
* '0' if resume disabled (default)
|
|
* '1' if enabled without prompting
|
|
* '2' if enabled with prompting
|
|
* '3' if enabled with prompting and auto-delete
|
|
* or a string containing the path of a predefined JSON-encoded state
|
|
*
|
|
* state: the path to JSON-encoded state file (see details regarding 'state' below)
|
|
*
|
|
* The parmsMachine object, if provided, may contain any of:
|
|
*
|
|
* autoMount: if set, this should override any 'autoMount' property in the FDC's
|
|
* parmsFDC object.
|
|
*
|
|
* autoPower: if set, this should override any 'autoPower' property in the Computer's
|
|
* parmsComputer object.
|
|
*
|
|
* messages: if set, this should override any 'messages' property in the Debugger's
|
|
* parmsDbg object.
|
|
*
|
|
* state: if set, this should override any 'state' property in the Computer's
|
|
* parmsComputer object.
|
|
*
|
|
* url: the location of the machine XML file
|
|
*
|
|
* If a predefined state is supplied AND it's successfully loaded, then resume behavior
|
|
* defaults to '1' (ie, resume enabled without prompting).
|
|
*
|
|
* This component insures that all components are ready before "powering" them.
|
|
*
|
|
* Different components become ready at different times, and initialization order (ie,
|
|
* the order the scripts are combined on the page) only partially determines readiness.
|
|
* This is because components like ROM and Video must finish loading their resource files
|
|
* before they are ready. Other components become ready after we call their initBus()
|
|
* function, because they have a Bus or CPU dependency, such as access to memory management
|
|
* functions. And other components, like CPU and Panel, are ready as soon as their
|
|
* constructor finishes.
|
|
*
|
|
* Once a component has indicated it's ready, we call its powerUp() notification
|
|
* function (if it has one--it's optional). We call the CPU's powerUp() function last,
|
|
* so that the CPU is assured that all other components are ready and "powered".
|
|
*/
|
|
function Computer(parmsComputer, parmsMachine, fSuspended) {
|
|
|
|
Component.call(this, "Computer", parmsComputer, Computer, Messages.COMPUTER);
|
|
|
|
this.flags.fPowered = false;
|
|
|
|
this.setMachineParms(parmsMachine);
|
|
|
|
this.fAutoPower = this.getMachineParm('autoPower', parmsComputer);
|
|
|
|
/*
|
|
* nPowerChange is 0 while the power state is stable, 1 while power is transitioning
|
|
* to "on", and -1 while power is transitioning to "off".
|
|
*/
|
|
this.nPowerChange = 0;
|
|
|
|
/*
|
|
* TODO: Deprecate 'buswidth' (it should have always used camelCase)
|
|
*/
|
|
this.nBusWidth = parmsComputer['busWidth'] || parmsComputer['buswidth'];
|
|
|
|
this.resume = Computer.RESUME_NONE;
|
|
this.sStateData = null;
|
|
this.fStateData = false; // remembers if sStateData was loaded
|
|
this.fServerState = false;
|
|
|
|
this.url = this.getMachineParm('url') || "";
|
|
|
|
/*
|
|
* Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be
|
|
* non-zero, convert to base 36, and chop off the leading digit and "decimal" point.
|
|
*/
|
|
this.sMachineID = (Math.random() + 0.1).toString(36).substr(2,12);
|
|
this.sUserID = this.queryUserID();
|
|
|
|
/*
|
|
* Find the appropriate CPU (and Debugger and Control Panel, if any)
|
|
*
|
|
* CLOSURE COMPILER TIP: To override the type of a right-hand expression (as we need to do here,
|
|
* where we know getComponentByType() will only return an CPUState object or null), wrap the expression
|
|
* in parentheses. I never knew this until I stumbled across it in "Closure: The Definitive Guide".
|
|
*/
|
|
this.cpu = /** @type {CPUState} */ (Component.getComponentByType("CPU", this.id));
|
|
if (!this.cpu) {
|
|
Component.error("Unable to find CPU component");
|
|
return;
|
|
}
|
|
this.dbg = /** @type {Debugger} */ (Component.getComponentByType("Debugger", this.id));
|
|
|
|
/*
|
|
* Enumerate all Video components for future updateVideo() calls.
|
|
*/
|
|
this.aVideo = [];
|
|
for (var video = null; (video = this.getMachineComponent("Video", video));) {
|
|
this.aVideo.push(video);
|
|
}
|
|
|
|
/*
|
|
* Initialize the Bus component
|
|
*/
|
|
this.bus = new Bus({'id': this.idMachine + '.bus', 'busWidth': this.nBusWidth}, this.cpu, this.dbg);
|
|
|
|
/*
|
|
* Iterate through all the components and connect them to the Control Panel, if any
|
|
*/
|
|
var iComponent, component;
|
|
var aComponents = Component.getComponents(this.id);
|
|
this.panel = /** @type {Panel} */ (Component.getComponentByType("Panel", this.id));
|
|
|
|
if (this.panel && this.panel.controlPrint) {
|
|
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
component = aComponents[iComponent];
|
|
/*
|
|
* I can think of many "cleaner" ways for the Control Panel component to pass its
|
|
* notice(), println(), etc, overrides on to all the other components, but it's just
|
|
* too darn convenient to slam those overrides into the components directly.
|
|
*/
|
|
component.notice = this.panel.notice;
|
|
component.println = this.panel.println;
|
|
component.controlPrint = this.panel.controlPrint;
|
|
}
|
|
}
|
|
|
|
this.println(PC8080.APPNAME + " v" + PC8080.APPVERSION + "\n" + COPYRIGHT + "\n" + LICENSE);
|
|
|
|
if (DEBUG && this.messageEnabled()) this.printMessage("TYPEDARRAYS: " + TYPEDARRAYS);
|
|
|
|
/*
|
|
* Iterate through all the components again and call their initBus() handler, if any
|
|
*/
|
|
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
component = aComponents[iComponent];
|
|
if (component.initBus) component.initBus(this, this.bus, this.cpu, this.dbg);
|
|
}
|
|
|
|
var sStatePath = null;
|
|
var sResume = parmsComputer['resume'];
|
|
if (sResume !== undefined) {
|
|
/*
|
|
* DEPRECATE: This goofiness is a holdover from when the 'resume' property was a string (either a
|
|
* single-digit string or a path); now it's always a number, so it never has a 'length' property and
|
|
* the call to parseInt() is unnecessary.
|
|
*/
|
|
if (sResume.length > 1) {
|
|
sStatePath = this.sResumePath = sResume;
|
|
} else {
|
|
this.resume = parseInt(sResume, 10);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The Computer 'state' property allows a state file to be specified independent of the 'resume' feature;
|
|
* previously, you could only use 'resume' to load a state file -- which we still support, but loading a state
|
|
* file that way prevents the machine's state from being saved, since we always resume from the 'resume' file.
|
|
*
|
|
* The other wrinkle is on the restore side: we need to IGNORE the 'state' property if a saved state now exists.
|
|
* So we have to peek at localStorage, and unfortunately, the only way to "peek" is to actually load the data,
|
|
* but we're not ready to use it yet, so powerUp() has been changed to use any existing stateComputer that we've
|
|
* already loaded.
|
|
*
|
|
* However, there's now a wrinkle to the wrinkle: if a 'state' parameter has been passed via the URL, then that
|
|
* OVERRIDES everything; it overrides any 'state' Computer parameter AND it disables resume of any saved state in
|
|
* localStorage (in other words, it prevents fAllowResume from being true, and forcing resume off).
|
|
*/
|
|
var fAllowResume;
|
|
var sState = this.getMachineParm('state') || (fAllowResume = true) && parmsComputer['state'];
|
|
|
|
if (sState) {
|
|
sStatePath = this.sStatePath = sState;
|
|
if (!fAllowResume) {
|
|
this.fServerState = true;
|
|
this.resume = Computer.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);
|
|
}
|
|
|
|
Component.subclass(Computer);
|
|
|
|
Computer.STATE_FAILSAFE = "failsafe";
|
|
Computer.STATE_VALIDATE = "validate";
|
|
Computer.STATE_TIMESTAMP = "timestamp";
|
|
Computer.STATE_VERSION = "version";
|
|
Computer.STATE_HOSTURL = "url";
|
|
Computer.STATE_BROWSER = "browser";
|
|
Computer.STATE_USERID = "user";
|
|
|
|
/*
|
|
* The following constants define all the resume options. Negative values (eg, RESUME_REPOWER) are for
|
|
* internal use only, and RESUME_DELETE is not documented (it provides a way of deleting ALL saved states
|
|
* whenever a resume is declined). As a result, the only "end-user" values are 0, 1 and 2.
|
|
*/
|
|
Computer.RESUME_REPOWER = -1; // resume without changing any state (for internal use only)
|
|
Computer.RESUME_NONE = 0; // default (no resume)
|
|
Computer.RESUME_AUTO = 1; // automatically save/restore state
|
|
Computer.RESUME_PROMPT = 2; // automatically save but conditionally restore (WARNING: if restore is declined, any state is discarded)
|
|
Computer.RESUME_DELETE = 3; // same as RESUME_PROMPT but discards ALL machines states whenever ANY machine restore is declined (undocumented)
|
|
|
|
/**
|
|
* getMachineID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
Computer.prototype.getMachineID = function()
|
|
{
|
|
return this.sMachineID;
|
|
};
|
|
|
|
/**
|
|
* setMachineParms(parmsMachine)
|
|
*
|
|
* If no explicit machine parms were provided, then we check for 'parms' in the bundled resources (if any).
|
|
*
|
|
* @param {Object} [parmsMachine]
|
|
*/
|
|
Computer.prototype.setMachineParms = function(parmsMachine)
|
|
{
|
|
if (!parmsMachine) {
|
|
var sParms;
|
|
if (typeof resources == 'object' && (sParms = resources['parms'])) {
|
|
try {
|
|
parmsMachine = /** @type {Object} */ (eval("(" + sParms + ")"));
|
|
} catch(e) {
|
|
Component.error(e.message + " (" + sParms + ")");
|
|
}
|
|
}
|
|
}
|
|
this.parmsMachine = parmsMachine;
|
|
};
|
|
|
|
/**
|
|
* getMachineParm(sParm, parmsComponent)
|
|
*
|
|
* If the machine parameter doesn't exist, we check for a matching component parameter (if parmsComponent is provided),
|
|
* and failing that, we check the bundled resources (if any).
|
|
*
|
|
* At the moment, the only bundled resource request we expect to encounter is 'state'; if it exists, then we return
|
|
* 'state' back to the caller (ie, the name of the resource), so that the caller will then attempt to load the 'state'
|
|
* resource to obtain the actual state.
|
|
*
|
|
* @param {string} sParm
|
|
* @param {Object} [parmsComponent]
|
|
* @return {string|undefined}
|
|
*/
|
|
Computer.prototype.getMachineParm = function(sParm, parmsComponent)
|
|
{
|
|
/*
|
|
* When checking parmsURL, the check is allowed be a bit looser, because URL parameters are
|
|
* user-supplied, whereas most other parameters are developer-supplied. Granted, a developer
|
|
* may also be sloppy and neglect to use correct case (eg, 'automount' instead of 'autoMount'),
|
|
* but there are limits to my paranoia.
|
|
*/
|
|
var sParmLC = sParm.toLowerCase();
|
|
var value = Component.parmsURL[sParm] || Component.parmsURL[sParmLC];
|
|
|
|
if (value === undefined && this.parmsMachine) {
|
|
value = this.parmsMachine[sParm];
|
|
}
|
|
if (value === undefined && parmsComponent) {
|
|
value = parmsComponent[sParm];
|
|
}
|
|
if (value === undefined && typeof resources == 'object' && resources[sParm]) {
|
|
value = sParm;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* saveMachineParms()
|
|
*
|
|
* @return {string|null}
|
|
*/
|
|
Computer.prototype.saveMachineParms = function()
|
|
{
|
|
return this.parmsMachine? JSON.stringify(this.parmsMachine) : null;
|
|
};
|
|
|
|
/**
|
|
* getUserID()
|
|
*
|
|
* @return {string}
|
|
*/
|
|
Computer.prototype.getUserID = function()
|
|
{
|
|
return this.sUserID || "";
|
|
};
|
|
|
|
/**
|
|
* doneLoad(sURL, sStateData, nErrorCode)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sURL
|
|
* @param {string} sStateData
|
|
* @param {number} nErrorCode
|
|
*/
|
|
Computer.prototype.doneLoad = function(sURL, sStateData, nErrorCode)
|
|
{
|
|
if (!nErrorCode) {
|
|
this.sStateData = sStateData;
|
|
this.fStateData = true;
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("loaded state file " + sURL.replace(this.sUserID || "xxx", "xxx"));
|
|
}
|
|
} else {
|
|
this.sResumePath = null;
|
|
this.fServerState = false;
|
|
this.notice('Unable to load machine state from server (error ' + nErrorCode + (sStateData? ': ' + str.trim(sStateData) : '') + ')');
|
|
}
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* wait(fn, parms)
|
|
*
|
|
* wait() waits until every component is ready (including ourselves, the last component we check), then calls the
|
|
* specified Computer method.
|
|
*
|
|
* TODO: The Closure Compiler makes it difficult for us to define a function type for "fn" that works in all cases;
|
|
* sometimes we want to pass a function that takes only a "number", and other times we want to pass a function that
|
|
* takes only an "Array" (the type will mirror that of the "parms" parameter). However, the Closure Compiler insists
|
|
* that both functions must be declared as accepting both types of parameters. So once again, we must use an untyped
|
|
* function declaration, instead of something stricter like:
|
|
*
|
|
* param {function(this:Computer, (number|Array|undefined)): undefined} fn
|
|
*
|
|
* @this {Computer}
|
|
* @param {function(...)} fn
|
|
* @param {number|Array} [parms] optional parameters
|
|
*/
|
|
Computer.prototype.wait = function(fn, parms)
|
|
{
|
|
var computer = this;
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent <= aComponents.length; iComponent++) {
|
|
var component = (iComponent < aComponents.length ? aComponents[iComponent] : this);
|
|
if (!component.isReady()) {
|
|
component.isReady(function onComponentReady() {
|
|
computer.wait(fn, parms);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
if (DEBUG && this.messageEnabled()) this.printMessage("Computer.wait(ready)");
|
|
fn.call(this, parms);
|
|
};
|
|
|
|
/**
|
|
* validateState(stateComputer)
|
|
*
|
|
* NOTE: We clear() stateValidate only when there's no stateComputer.
|
|
*
|
|
* @this {Computer}
|
|
* @param {State|null} [stateComputer]
|
|
* @return {boolean} true if state passes validation, false if not
|
|
*/
|
|
Computer.prototype.validateState = function(stateComputer)
|
|
{
|
|
var fValid = true;
|
|
var stateValidate = new State(this, PC8080.APPVERSION, Computer.STATE_VALIDATE);
|
|
if (stateValidate.load() && stateValidate.parse()) {
|
|
var sTimestampValidate = stateValidate.get(Computer.STATE_TIMESTAMP);
|
|
var sTimestampComputer = stateComputer ? stateComputer.get(Computer.STATE_TIMESTAMP) : "unknown";
|
|
if (sTimestampValidate != sTimestampComputer) {
|
|
this.notice("Machine state may be out-of-date\n(" + sTimestampValidate + " vs. " + sTimestampComputer + ")\nCheck your browser's local storage limits");
|
|
fValid = false;
|
|
if (!stateComputer) stateValidate.clear();
|
|
} else {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("Last state: " + sTimestampComputer + " (validate: " + sTimestampValidate + ")");
|
|
}
|
|
}
|
|
}
|
|
return fValid;
|
|
};
|
|
|
|
/**
|
|
* powerOn(resume)
|
|
*
|
|
* Power every component "up", applying any previously available state information.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} [resume] is a valid RESUME value; default is this.resume
|
|
*/
|
|
Computer.prototype.powerOn = function(resume)
|
|
{
|
|
if (resume === undefined) {
|
|
resume = this.resume || (this.sStateData? Computer.RESUME_AUTO : Computer.RESUME_NONE);
|
|
}
|
|
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("Computer.powerOn(" + (resume == Computer.RESUME_REPOWER ? "repower" : (resume ? "resume" : "")) + ")");
|
|
}
|
|
|
|
if (this.nPowerChange) {
|
|
return;
|
|
}
|
|
this.nPowerChange++;
|
|
|
|
var fRepower = false;
|
|
var fRestore = false;
|
|
this.fRestoreError = false;
|
|
var stateComputer = this.stateComputer || new State(this, PC8080.APPVERSION);
|
|
|
|
if (resume == Computer.RESUME_REPOWER) {
|
|
fRepower = true;
|
|
}
|
|
else if (resume > Computer.RESUME_NONE) {
|
|
if (stateComputer.load(this.sStateData)) {
|
|
/*
|
|
* Since we're resuming something (either a predefined state or a state from localStorage), let's
|
|
* create a "failsafe" checkpoint in localStorage, and destroy it at the end of a successful powerOn().
|
|
* Which means, of course, that if a previous "failsafe" checkpoint already exists, something bad
|
|
* may have happened the last time around.
|
|
*/
|
|
this.stateFailSafe = new State(this, PC8080.APPVERSION, Computer.STATE_FAILSAFE);
|
|
if (this.stateFailSafe.load()) {
|
|
this.powerReport(stateComputer);
|
|
/*
|
|
* We already know resume is something other than RESUME_NONE, so we'll go ahead and bump it
|
|
* all the way to RESUME_PROMPT, so that the user will be prompted, and if the user declines to
|
|
* restore, the state will be removed.
|
|
*/
|
|
resume = Computer.RESUME_PROMPT;
|
|
/*
|
|
* To ensure that the set() below succeeds, we need to call unload(), otherwise it may fail
|
|
* with a "read only" error (eg, "TypeError: Cannot assign to read only property 'timestamp'").
|
|
*/
|
|
this.stateFailSafe.unload();
|
|
}
|
|
|
|
this.stateFailSafe.set(Computer.STATE_TIMESTAMP, usr.getTimestamp());
|
|
this.stateFailSafe.store();
|
|
|
|
var fValidate = this.resume && !this.fServerState;
|
|
if (resume == Computer.RESUME_AUTO || web.confirmUser("Click OK to restore the previous " + PC8080.APPNAME + " machine state, or CANCEL to reset the machine.")) {
|
|
fRestore = stateComputer.parse();
|
|
if (fRestore) {
|
|
var sCode = stateComputer.get(UserAPI.RES.CODE);
|
|
var sData = stateComputer.get(UserAPI.RES.DATA);
|
|
if (sCode) {
|
|
if (sCode == UserAPI.CODE.OK) {
|
|
stateComputer.load(sData);
|
|
} else {
|
|
/*
|
|
* A missing (or not yet created) state file is no cause for alarm, but other errors might be
|
|
*/
|
|
if (sCode == UserAPI.CODE.FAIL && sData != UserAPI.FAIL.NOSTATE) {
|
|
this.notice("Error: " + sData);
|
|
if (sData == UserAPI.FAIL.VERIFY) this.resetUserID();
|
|
} else {
|
|
this.println(sCode + ": " + sData);
|
|
}
|
|
/*
|
|
* Try falling back to the state that we should have saved in localStorage, as a backup to the
|
|
* server-side state.
|
|
*/
|
|
stateComputer.unload(); // discard the invalid server-side state first
|
|
if (stateComputer.load()) {
|
|
fRestore = stateComputer.parse();
|
|
fValidate = true;
|
|
} else {
|
|
fRestore = false; // hmmm, there was nothing in localStorage either
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* If the load/parse was successful, and it was from localStorage (not sStateData),
|
|
* then we should to try verify that localStorage snapshot is current. One reason it may
|
|
* NOT be current is if localStorage was full and we got a quota error during the last
|
|
* powerOff().
|
|
*/
|
|
if (fValidate) this.validateState(fRestore? stateComputer : null);
|
|
} else {
|
|
/*
|
|
* RESUME_PROMPT indicates we should delete the state if they clicked Cancel to confirm() above.
|
|
*/
|
|
if (resume == Computer.RESUME_PROMPT) stateComputer.clear();
|
|
}
|
|
} else {
|
|
/*
|
|
* If there's no state, then there should also be no validation timestamp; if there is, then once again,
|
|
* we're probably dealing with a quota error.
|
|
*/
|
|
this.validateState();
|
|
}
|
|
delete this.sStateData;
|
|
delete this.stateComputer;
|
|
}
|
|
|
|
/*
|
|
* Start powering all components, including any data they may need to restore their state;
|
|
* we restore power to the CPU last.
|
|
*/
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (component !== this && component != this.cpu) {
|
|
fRestore = this.powerRestore(component, stateComputer, fRepower, fRestore);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Assuming this is not a repower, we must perform another wait, because some components may
|
|
* have marked themselves as "not ready" again (eg, the FDC component, if the restore forced it
|
|
* to mount one or more additional disk images).
|
|
*/
|
|
var aParms = [stateComputer, resume, fRestore];
|
|
|
|
if (resume != Computer.RESUME_REPOWER) {
|
|
this.wait(this.donePowerOn, aParms);
|
|
return;
|
|
}
|
|
this.donePowerOn(aParms);
|
|
};
|
|
|
|
/**
|
|
* powerRestore(component, stateComputer, fRepower, fRestore)
|
|
*
|
|
* @this {Computer}
|
|
* @param {Component} component
|
|
* @param {State} stateComputer
|
|
* @param {boolean} fRepower
|
|
* @param {boolean} fRestore
|
|
* @return {boolean} true if restore should continue, false if not
|
|
*/
|
|
Computer.prototype.powerRestore = function(component, stateComputer, fRepower, fRestore)
|
|
{
|
|
if (!component.flags.fPowered) {
|
|
|
|
component.flags.fPowered = true;
|
|
|
|
if (component.powerUp) {
|
|
|
|
var data = null;
|
|
if (fRestore) {
|
|
data = stateComputer.get(component.id);
|
|
if (!data) {
|
|
/*
|
|
* This is a hack that makes it possible for a machine whose ID has been
|
|
* supplemented with a suffix (a single letter or digit) to find object IDs
|
|
* in states created from a machine without the suffix.
|
|
*
|
|
* For example, if a state file was created from a machine with ID "ibm5160"
|
|
* but the current machine is "ibm5160a", this attempts a second lookup with
|
|
* "ibm5160", enabling us to find objects that match the original machine ID
|
|
* (eg, "ibm5160.romEGA").
|
|
*/
|
|
data = stateComputer.get(component.id.replace(/[a-z0-9]\./i, '.'));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* State.get() will return whatever was originally passed to State.set() (eg, an
|
|
* Object or a string), but components are supposed to store only Objects, so if a
|
|
* string comes back, something went wrong. By explicitly eliminating "string" data,
|
|
* the Closure Compiler stops complaining that we might be passing strings to our
|
|
* powerUp() functions (even though we know we're not).
|
|
*
|
|
* TODO: Determine if there's some way to coerce the Closure Compiler into treating
|
|
* data as Object or null, without having to include this runtime check. An assert
|
|
* would be a good idea, but this is overkill.
|
|
*/
|
|
if (typeof data === "string") data = null;
|
|
|
|
/*
|
|
* If computer is null, this is simply a repower notification, which most components
|
|
* don't do anything with. Exceptions include: CPU (since it may be halted) and Video
|
|
* (since its screen may be "turned off").
|
|
*/
|
|
if (!component.powerUp(data, fRepower) && data) {
|
|
|
|
Component.error("Unable to restore state for " + component.type);
|
|
/*
|
|
* If this is a resume error for a machine that also has a predefined state
|
|
* AND we're not restoring from that state, then throw away the current state,
|
|
* prevent any new state from being created, and then force a reload, which will
|
|
* hopefully restore us to the functioning predefined state.
|
|
*
|
|
* TODO: Considering doing this in ALL cases, not just in situations where a
|
|
* 'state' exists but we're not actually resuming from it.
|
|
*/
|
|
if (this.sStatePath && !this.fStateData) {
|
|
stateComputer.clear();
|
|
this.resume = Computer.RESUME_NONE;
|
|
web.reloadPage();
|
|
} else {
|
|
/*
|
|
* In all other cases, we set fRestoreError, which should trigger a call to
|
|
* powerReport() and then delete the offending state.
|
|
*/
|
|
this.fRestoreError = true;
|
|
}
|
|
/*
|
|
* Any failure triggers an automatic to call powerUp() again, without any state,
|
|
* in the hopes that the component can recover by performing a reset.
|
|
*/
|
|
component.powerUp(null);
|
|
/*
|
|
* We also disable the rest of the restore operation, because it's not clear
|
|
* the remaining state information can be trusted; the machine is already in an
|
|
* inconsistent state, so we're not likely to make things worse, and the only
|
|
* alternative (starting over and performing a state-less reset) isn't likely to make
|
|
* the user any happier. But, we'll see... we need some experience with the code.
|
|
*/
|
|
fRestore = false;
|
|
}
|
|
}
|
|
|
|
if (!fRepower && component.comment) {
|
|
var asComments = component.comment.split("|");
|
|
for (var i = 0; i < asComments.length; i++) {
|
|
component.status(asComments[i]);
|
|
}
|
|
}
|
|
}
|
|
return fRestore;
|
|
};
|
|
|
|
/**
|
|
* donePowerOn(aParms)
|
|
*
|
|
* This is nothing more than a continuation of powerOn(), giving us the option of calling wait() one more time.
|
|
*
|
|
* @this {Computer}
|
|
* @param {Array} aParms containing [stateComputer, resume, fRestore]
|
|
*/
|
|
Computer.prototype.donePowerOn = function(aParms)
|
|
{
|
|
var stateComputer = aParms[0];
|
|
var fRepower = (aParms[1] < 0);
|
|
var fRestore = aParms[2];
|
|
|
|
if (DEBUG && this.flags.fPowered && this.messageEnabled()) {
|
|
this.printMessage("Computer.donePowerOn(): redundant");
|
|
}
|
|
|
|
this.fInitialized = true;
|
|
this.flags.fPowered = true;
|
|
var controlPower = this.bindings["power"];
|
|
if (controlPower) controlPower.textContent = "Shutdown";
|
|
|
|
/*
|
|
* Once we get to this point, we're guaranteed that all components are ready, so it's safe to power the CPU;
|
|
* the CPU should begin executing immediately, unless a debugger is attached.
|
|
*/
|
|
if (this.cpu) {
|
|
/*
|
|
* TODO: Do we not care about the return value here? (ie, is checking fRestoreError sufficient)?
|
|
*/
|
|
this.powerRestore(this.cpu, stateComputer, fRepower, fRestore);
|
|
this.cpu.autoStart();
|
|
}
|
|
|
|
/*
|
|
* If the state was bad, offer to report it and then delete it. Deleting may be moot, since invariably a new
|
|
* state will be created on powerOff() before the next powerOn(), but it seems like good paranoia all the same.
|
|
*/
|
|
if (this.fRestoreError) {
|
|
this.powerReport(stateComputer);
|
|
stateComputer.clear();
|
|
}
|
|
|
|
if (!fRepower && this.stateFailSafe) {
|
|
this.stateFailSafe.clear();
|
|
delete this.stateFailSafe;
|
|
}
|
|
|
|
this.nPowerChange = 0;
|
|
};
|
|
|
|
/**
|
|
* checkPower()
|
|
*
|
|
* @this {Computer}
|
|
* @return {boolean} true if the computer is fully powered, false otherwise
|
|
*/
|
|
Computer.prototype.checkPower = function()
|
|
{
|
|
if (this.flags.fPowered) return true;
|
|
|
|
var component = null, iComponent;
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
component = aComponents[iComponent];
|
|
if (component !== this && !component.flags.fReady) break;
|
|
}
|
|
if (iComponent == aComponents.length) {
|
|
for (iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
component = aComponents[iComponent];
|
|
if (component !== this && !component.flags.fPowered) break;
|
|
}
|
|
}
|
|
if (iComponent == aComponents.length) component = this;
|
|
var s = "The " + component.type + " component (" + component.id + ") is not " + (!component.flags.fReady? "ready yet" + (component.fnReady? " (waiting for notification)" : "") : "powered yet") + ".";
|
|
web.alertUser(s);
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* powerReport(stateComputer)
|
|
*
|
|
* @this {Computer}
|
|
* @param {State} stateComputer
|
|
*/
|
|
Computer.prototype.powerReport = function(stateComputer)
|
|
{
|
|
if (web.confirmUser("There may be a problem with your " + 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 {Computer}
|
|
* @param {boolean} [fSave] is true to request a saved state
|
|
* @param {boolean} [fShutdown] is true if the machine is being shut down
|
|
* @return {string|null} string representing the saved state (or null if error)
|
|
*/
|
|
Computer.prototype.powerOff = function(fSave, fShutdown)
|
|
{
|
|
var data;
|
|
var sState = "none";
|
|
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("Computer.powerOff(" + (fSave ? "save" : "nosave") + (fShutdown ? ",shutdown" : "") + ")");
|
|
}
|
|
|
|
if (this.nPowerChange) {
|
|
return null;
|
|
}
|
|
this.nPowerChange--;
|
|
|
|
var stateComputer = new State(this, PC8080.APPVERSION);
|
|
var stateValidate = new State(this, PC8080.APPVERSION, Computer.STATE_VALIDATE);
|
|
|
|
var sTimestamp = usr.getTimestamp();
|
|
stateValidate.set(Computer.STATE_TIMESTAMP, sTimestamp);
|
|
stateComputer.set(Computer.STATE_TIMESTAMP, sTimestamp);
|
|
stateComputer.set(Computer.STATE_VERSION, APPVERSION);
|
|
stateComputer.set(Computer.STATE_HOSTURL, web.getHostURL());
|
|
stateComputer.set(Computer.STATE_BROWSER, web.getUserAgent());
|
|
|
|
/*
|
|
* Always power the CPU "down" first, just to help insure it doesn't ask other components to do anything
|
|
* after they're no longer ready.
|
|
*/
|
|
if (this.cpu && this.cpu.powerDown) {
|
|
if (fShutdown) this.cpu.stopCPU();
|
|
data = this.cpu.powerDown(fSave, fShutdown);
|
|
if (typeof data === "object") stateComputer.set(this.cpu.id, data);
|
|
if (fShutdown) {
|
|
this.cpu.flags.fPowered = false;
|
|
if (data === false) sState = null;
|
|
}
|
|
}
|
|
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (component.flags.fPowered) {
|
|
if (component.powerDown) {
|
|
data = component.powerDown(fSave, fShutdown);
|
|
if (typeof data === "object") stateComputer.set(component.id, data);
|
|
}
|
|
if (fShutdown) {
|
|
component.flags.fPowered = false;
|
|
if (data === false) sState = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sState) {
|
|
if (fShutdown) {
|
|
var fClear = false;
|
|
var fClearAll = false;
|
|
if (fSave) {
|
|
if (this.sUserID) {
|
|
this.saveServerState(this.sUserID, stateComputer.toString());
|
|
}
|
|
if (!stateValidate.store() || !stateComputer.store()) {
|
|
sState = null;
|
|
/*
|
|
* New behavior as of v1.13.2: if it appears that localStorage is full, we blow it ALL away.
|
|
* Dedicated server-side storage is the only way we'll ever be able to reliably preserve a
|
|
* particular machine's state. Historically, attempting to limp along with whatever localStorage
|
|
* is left just generates the same useless and annoying warnings over and over.
|
|
*/
|
|
fClear = fClearAll = true;
|
|
}
|
|
}
|
|
else {
|
|
/*
|
|
* I used to ALWAYS clear (ie, delete) any associated computer state, but now I do this only if the
|
|
* current machine is "resumable", because there are situations where I have two configurations
|
|
* for the same machine -- one resumable and one not -- and I don't want the latter throwing away the
|
|
* state of the former.
|
|
*
|
|
* So this code is here now strictly for callers to delete the state of a "resumable" machine, not as
|
|
* some paranoid clean-up operation.
|
|
*
|
|
* An undocumented feature of this operation is that if your configuration uses the special 'resume="3"'
|
|
* value, and you click the "Reset" button, and then you click OK to reset the everything, this will
|
|
* actually reset EVERYTHING (ie, all localStorage for ALL configs will be reclaimed).
|
|
*/
|
|
if (this.resume) {
|
|
fClear = true;
|
|
fClearAll = (this.resume == Computer.RESUME_DELETE);
|
|
}
|
|
}
|
|
if (fClear) {
|
|
stateComputer.clear(fClearAll);
|
|
}
|
|
} else {
|
|
sState = stateComputer.toString();
|
|
}
|
|
}
|
|
|
|
if (fShutdown) {
|
|
this.flags.fPowered = false;
|
|
var controlPower = this.bindings["power"];
|
|
if (controlPower) controlPower.textContent = "Power";
|
|
}
|
|
|
|
this.nPowerChange = 0;
|
|
|
|
return sState;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* Notify all (other) components with a reset() method that the Computer is being reset.
|
|
*
|
|
* NOTE: We'd like to reset the Bus first (due to the importance of the A20 line), but since we
|
|
* allocated the Bus object ourselves, after all the other components were allocated, it ends
|
|
* up near the end of Component's list of components. Hence the special case for this.bus below.
|
|
*
|
|
* @this {Computer}
|
|
*/
|
|
Computer.prototype.reset = function()
|
|
{
|
|
if (this.bus && this.bus.reset) {
|
|
/*
|
|
* TODO: Why does WebStorm think that this.bus.type is undefined? The base class (Component)
|
|
* constructor defines it.
|
|
*/
|
|
this.printMessage("Resetting " + this.bus.type);
|
|
this.bus.reset();
|
|
}
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (component !== this && component !== this.bus && component.reset) {
|
|
this.printMessage("Resetting " + component.type);
|
|
component.reset();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* start(ms, nCycles)
|
|
*
|
|
* Notify all (other) components with a start() method that the CPU has started.
|
|
*
|
|
* Note that we're called by runCPU(), which is why we exclude the CPU component,
|
|
* as well as ourselves.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Computer.prototype.start = function(ms, nCycles)
|
|
{
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (component.type == "CPU" || component === this) continue;
|
|
if (component.start) {
|
|
component.start(ms, nCycles);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* stop(ms, nCycles)
|
|
*
|
|
* Notify all (other) components with a stop() method that the CPU has stopped.
|
|
*
|
|
* Note that we're called by runCPU(), which is why we exclude the CPU component,
|
|
* as well as ourselves.
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} ms
|
|
* @param {number} nCycles
|
|
*/
|
|
Computer.prototype.stop = function(ms, nCycles)
|
|
{
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (component.type == "CPU" || component === this) continue;
|
|
if (component.stop) {
|
|
component.stop(ms, nCycles);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLType, sBinding, control, sValue)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "reset")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @param {string} [sValue] optional data value
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Computer.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|
{
|
|
var computer = this;
|
|
|
|
switch (sBinding) {
|
|
case "power":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickPower() {
|
|
computer.onPower();
|
|
};
|
|
return true;
|
|
|
|
case "reset":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickReset() {
|
|
computer.onReset();
|
|
};
|
|
return true;
|
|
|
|
/*
|
|
* Technically, this binding should now be called "saveState", to clearly distinguish it from
|
|
* the "Save Machine" control that's normally bound to the savePC() function in save.js. Saving
|
|
* an entire machine includes everything needed to start/restore the machine; eg, the machine
|
|
* XML configuration file(s) *and* the JSON-encoded machine state.
|
|
*/
|
|
case "save":
|
|
/*
|
|
* Since this feature depends on the server supporting the PCjs User API (see userapi.js),
|
|
* and since pcjs.org is no longer running a Node web server, we disable the feature for that
|
|
* particular host.
|
|
*/
|
|
if (str.endsWith(web.getHost(), "pcjs.org")) {
|
|
if (DEBUG) this.log("Remote user API not available");
|
|
/*
|
|
* We could also simply hide the control; eg:
|
|
*
|
|
* control.style.display = "none";
|
|
*
|
|
* but removing the control altogether seems better.
|
|
*/
|
|
control.parentNode.removeChild(/** @type {Node} */ (control));
|
|
return false;
|
|
}
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickSave() {
|
|
var sUserID = computer.queryUserID(true);
|
|
if (sUserID) {
|
|
/*
|
|
* I modified the test to include a check for sStatePath so that I could save new states
|
|
* for machines with existing states; otherwise, I'd have no (easy) way of capturing and
|
|
* updating their state. Making the machine (even temporarily) resumable would have been
|
|
* one work-around, but it's not appropriate for some machines, as their state is simply
|
|
* too large (for localStorage anyway, which is the default storage solution).
|
|
*/
|
|
var fSave = !!(computer.resume && !computer.sResumePath || computer.sStatePath);
|
|
var sState = computer.powerOff(fSave);
|
|
if (fSave) {
|
|
computer.saveServerState(sUserID, sState);
|
|
} else {
|
|
computer.notice("Resume disabled, machine state not saved");
|
|
}
|
|
}
|
|
/*
|
|
* This seemed like a handy alternative, but it turned out to be a no-go, at least for large states:
|
|
*
|
|
* var sState = computer.powerOff(true);
|
|
* if (sState) {
|
|
* sState = "data:text/json;charset=utf-8," + encodeURIComponent(sState);
|
|
* window.open(sState);
|
|
* }
|
|
*
|
|
* Perhaps if I embedded the data in a link on the current page instead; eg:
|
|
*
|
|
* $('<a href="' + sState + '" download="state.json">Download</a>').appendTo('#container');
|
|
*/
|
|
};
|
|
return true;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* resetUserID()
|
|
*/
|
|
Computer.prototype.resetUserID = function()
|
|
{
|
|
web.setLocalStorageItem(Computer.STATE_USERID, "");
|
|
this.sUserID = null;
|
|
};
|
|
|
|
/**
|
|
* queryUserID(fPrompt)
|
|
*
|
|
* @param {boolean} [fPrompt]
|
|
* @returns {string|null|undefined}
|
|
*/
|
|
Computer.prototype.queryUserID = function(fPrompt)
|
|
{
|
|
var sUserID = this.sUserID;
|
|
if (!sUserID) {
|
|
sUserID = web.getLocalStorageItem(Computer.STATE_USERID);
|
|
if (sUserID !== undefined) {
|
|
if (!sUserID && fPrompt) {
|
|
/*
|
|
* NOTE: Warning the user here that "Save" operations are not currently supported by pcjs.org is
|
|
* merely a precaution, because ordinarily, setBinding() should have already determined if we are
|
|
* running from pcjs.org and disabled any "Save" button.
|
|
*/
|
|
sUserID = web.promptUser("Saving machine states on the pcjs.org server is currently unsupported.\n\nIf you're running your own server, enter your user ID below.");
|
|
if (sUserID) {
|
|
sUserID = this.verifyUserID(sUserID);
|
|
if (!sUserID) this.notice("The user ID is invalid.");
|
|
}
|
|
}
|
|
} else if (fPrompt) {
|
|
this.notice("Browser local storage is not available");
|
|
}
|
|
}
|
|
return sUserID;
|
|
};
|
|
|
|
/**
|
|
* verifyUserID(sUserID)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sUserID
|
|
* @return {string} validated user ID, or null if error
|
|
*/
|
|
Computer.prototype.verifyUserID = function(sUserID)
|
|
{
|
|
this.sUserID = null;
|
|
var fMessages = DEBUG && this.messageEnabled();
|
|
if (fMessages) this.printMessage("verifyUserID(" + sUserID + ")");
|
|
var sRequest = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.VERIFY + '&' + UserAPI.QUERY.USER + '=' + sUserID;
|
|
var response = web.getResource(sRequest);
|
|
var nErrorCode = response[0];
|
|
var sResponse = response[1];
|
|
if (!nErrorCode && sResponse) {
|
|
try {
|
|
response = eval("(" + sResponse + ")");
|
|
if (response.code && response.code == UserAPI.CODE.OK) {
|
|
web.setLocalStorageItem(Computer.STATE_USERID, response.data);
|
|
if (fMessages) this.printMessage(Computer.STATE_USERID + " updated: " + response.data);
|
|
this.sUserID = response.data;
|
|
} else {
|
|
if (fMessages) this.printMessage(response.code + ": " + response.data);
|
|
}
|
|
} catch (e) {
|
|
Component.error(e.message + " (" + sResponse + ")");
|
|
}
|
|
} else {
|
|
if (fMessages) this.printMessage("invalid response (error " + nErrorCode + ")");
|
|
}
|
|
return this.sUserID;
|
|
};
|
|
|
|
/**
|
|
* getServerStatePath()
|
|
*
|
|
* @this {Computer}
|
|
* @return {string|null} sStatePath (null if no localStorage or no USERID stored in localStorage)
|
|
*/
|
|
Computer.prototype.getServerStatePath = function()
|
|
{
|
|
var sStatePath = null;
|
|
if (this.sUserID) {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage(Computer.STATE_USERID + " for load: " + this.sUserID);
|
|
}
|
|
sStatePath = web.getHost() + UserAPI.ENDPOINT + '?' + UserAPI.QUERY.REQ + '=' + UserAPI.REQ.LOAD + '&' + UserAPI.QUERY.USER + '=' + this.sUserID + '&' + UserAPI.QUERY.STATE + '=' + State.key(this, PC8080.APPVERSION);
|
|
} else {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage(Computer.STATE_USERID + " unavailable");
|
|
}
|
|
}
|
|
return sStatePath;
|
|
};
|
|
|
|
/**
|
|
* saveServerState(sUserID, sState)
|
|
*
|
|
* @param {string} sUserID
|
|
* @param {string|null} sState
|
|
*/
|
|
Computer.prototype.saveServerState = function(sUserID, sState)
|
|
{
|
|
/*
|
|
* We must pass fSync == true, because (as I understand it) browsers will blow off any async
|
|
* requests when a page is being closed. Since our request is synchronous, storeServerState()
|
|
* should also return a result, but there's not much we can do with it, since browsers ALSO
|
|
* tend to blow off alerts() and the like when closing down.
|
|
*/
|
|
if (sState) {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("size of server state: " + sState.length + " bytes");
|
|
}
|
|
var response = this.storeServerState(sUserID, sState, true);
|
|
if (response && response[UserAPI.RES.CODE] == UserAPI.CODE.OK) {
|
|
this.notice("Machine state saved to server");
|
|
} else if (sState) {
|
|
var sError = (response && response[UserAPI.RES.DATA]) || UserAPI.FAIL.BADSTORE;
|
|
if (response[UserAPI.RES.CODE] == UserAPI.CODE.FAIL) {
|
|
sError = "Error: " + sError;
|
|
} else {
|
|
sError = "Error " + response[UserAPI.RES.CODE] + ": " + sError;
|
|
}
|
|
this.notice(sError);
|
|
this.resetUserID();
|
|
}
|
|
} else {
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage("no state to store");
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* storeServerState(sUserID, sState, fSync)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sUserID
|
|
* @param {string} sState
|
|
* @param {boolean} [fSync] is true if we're powering down and should perform a synchronous request (default is async)
|
|
* @return {*} server response if fSync is true and a response was received; otherwise null
|
|
*/
|
|
Computer.prototype.storeServerState = function(sUserID, sState, fSync)
|
|
{
|
|
if (DEBUG && this.messageEnabled()) {
|
|
this.printMessage(Computer.STATE_USERID + " for store: " + sUserID);
|
|
}
|
|
/*
|
|
* TODO: Determine whether or not any browsers cancel our request if we're called during a browser "shutdown" event,
|
|
* and whether or not it matters if we do an async request (currently, we're not, to try to ensure the request goes through).
|
|
*/
|
|
var dataPost = {};
|
|
dataPost[UserAPI.QUERY.REQ] = UserAPI.REQ.STORE;
|
|
dataPost[UserAPI.QUERY.USER] = sUserID;
|
|
dataPost[UserAPI.QUERY.STATE] = State.key(this, 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 {Computer}
|
|
*/
|
|
Computer.prototype.onPower = function()
|
|
{
|
|
if (!this.nPowerChange) {
|
|
if (!this.flags.fPowered) {
|
|
this.wait(this.powerOn);
|
|
} else {
|
|
this.powerOff(false, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* onReset()
|
|
*
|
|
* This handles UI requests to reset the computer's state (eg, see the "reset" button binding).
|
|
*
|
|
* @this {Computer}
|
|
*/
|
|
Computer.prototype.onReset = function()
|
|
{
|
|
/*
|
|
* I'm going to start with the presumption that it makes little sense for an "unpowered" computer to be "reset";
|
|
* ditto if the power state is currently being changed.
|
|
*/
|
|
if (!this.flags.fPowered || this.nPowerChange) return;
|
|
|
|
/*
|
|
* If this is a "resumable" machine (and it's not using a predefined state), then we overload the reset
|
|
* operation to offer an explicit "save or discard" option first. This is currently the only UI we offer to
|
|
* discard a machine's state, including any disk changes. The traditional "reset" operation is still available
|
|
* for non-resumable machines.
|
|
*
|
|
* TODO: Break this behavior out into a separate "discard" operation, in case the designer of the machine really
|
|
* wants to clutter the UI with confusing options. ;-)
|
|
*/
|
|
if (this.resume && !this.sResumePath) {
|
|
/*
|
|
* I used to bypass the prompt if this.resume == Computer.RESUME_AUTO, setting fSave to true automatically,
|
|
* but that gives the user no means of resetting a resumable machine that contains errors in its resume state.
|
|
*/
|
|
var fSave = (/* this.resume == Computer.RESUME_AUTO || */ web.confirmUser("Click OK to save changes to this " + 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(Computer.RESUME_NONE);
|
|
this.fReload = false;
|
|
} else {
|
|
this.reset();
|
|
if (this.cpu) this.cpu.autoStart();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getMachineComponent(sType, componentPrev)
|
|
*
|
|
* @this {Computer}
|
|
* @param {string} sType
|
|
* @param {Component|null} [componentPrev] of previously returned component, if any
|
|
* @return {Component|null}
|
|
*/
|
|
Computer.prototype.getMachineComponent = function(sType, componentPrev)
|
|
{
|
|
var aComponents = Component.getComponents(this.id);
|
|
for (var iComponent = 0; iComponent < aComponents.length; iComponent++) {
|
|
var component = aComponents[iComponent];
|
|
if (componentPrev) {
|
|
if (componentPrev == component) componentPrev = null;
|
|
continue;
|
|
}
|
|
if (component.type == sType) return component;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* updateFocus(fScroll)
|
|
*
|
|
* NOTE: When soft keyboard buttons call us to return focus to the machine (and away from the button),
|
|
* the scroll feature has annoying effect on iOS, so we no longer do it by default (fScroll must be true).
|
|
*
|
|
* @this {Computer}
|
|
* @param {boolean} [fScroll]
|
|
*/
|
|
Computer.prototype.updateFocus = function(fScroll)
|
|
{
|
|
if (this.aVideo.length) {
|
|
/*
|
|
* This seems to be recommended work-around to prevent the browser from scrolling the focused element
|
|
* into view. The CPU is not a visual component, so when the CPU wants to set focus, the primary intent
|
|
* is to ensure that keyboard input is fielded properly.
|
|
*/
|
|
var x = 0, y = 0;
|
|
if (fScroll && window) {
|
|
x = window.scrollX;
|
|
y = window.scrollY;
|
|
}
|
|
/*
|
|
* TODO: We need a mechanism to determine the "active" display, instead of hard-coding this to aVideo[0].
|
|
*/
|
|
this.aVideo[0].setFocus();
|
|
if (fScroll && window) {
|
|
window.scrollTo(x, y);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* updateStatus(fForce)
|
|
*
|
|
* If any DOM controls were bound to the CPU, then we need to call its updateStatus() handler; if there are no
|
|
* such bindings, then cpu.updateStatus() does nothing.
|
|
*
|
|
* Similarly, if there's a Panel, then we need to call its updateStatus() handler, in case it created its own canvas
|
|
* and implemented its own register display (eg, dumpRegisters()); if not, then panel.updateStatus() also does nothing.
|
|
*
|
|
* In practice, there will *either* be a Panel with a custom canvas *or* a set of DOM controls bound to the CPU *or*
|
|
* neither. In theory, there could be BOTH, but that would be unusual.
|
|
*
|
|
* TODO: Consider alternate approaches to these largely register-oriented display updates. Ordinarily, we like to
|
|
* separate logic from presentation, and currently the CPUState contains both, since it's the component that intimately
|
|
* knows the names, number, sizes, etc, of all the active registers. The Panel component is the logical candidate,
|
|
* but Panel is an optional component; generally, only machines that include Debugger also include Panel.
|
|
*
|
|
* @this {Computer}
|
|
* @param {boolean} [fForce] (true will display registers even if the CPU is running and "live" registers are not enabled)
|
|
*/
|
|
Computer.prototype.updateStatus = function(fForce)
|
|
{
|
|
/*
|
|
* fForce is generally set to true whenever the CPU is transitioning to/from a running state, in which case
|
|
* cpu.updateStatus() will definitely want to hide/show register contents; however, at other times, when the
|
|
* CPU is running, constantly updating the DOM controls too frequently can adversely impact overall performance.
|
|
*
|
|
* So fForce serves as a hint to help cpu.updateStatus() make a more informed decision. panel.updateStatus()
|
|
* currently doesn't care, on the theory that canvas updates should be significantly faster than DOM updates,
|
|
* but we still pass fForce on.
|
|
*/
|
|
if (this.cpu) this.cpu.updateStatus(fForce);
|
|
if (this.panel) this.panel.updateStatus(fForce);
|
|
};
|
|
|
|
/**
|
|
* updateVideo(n)
|
|
*
|
|
* Any high-frequency updates should be performed here (avoid updating DOM elements).
|
|
*
|
|
* @this {Computer}
|
|
* @param {number} n (where 0 <= n < VIDEO_UPDATES_PER_SECOND for a normal update, or -1 for a forced update)
|
|
*/
|
|
Computer.prototype.updateVideo = function(n)
|
|
{
|
|
for (var i = 0; i < this.aVideo.length; i++) {
|
|
this.aVideo[i].updateScreen(n);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.init()
|
|
*
|
|
* For every machine represented by an HTML element of class "pcjs-machine", this function
|
|
* locates the HTML element of class "computer", extracting the JSON-encoded parameters for the
|
|
* Computer constructor from the element's "data-value" attribute, invoking the constructor to
|
|
* create a Computer component, and then binding any associated HTML controls to the new component.
|
|
*/
|
|
Computer.init = function()
|
|
{
|
|
/*
|
|
* In non-COMPILED builds, embedMachine() may have set XMLVERSION.
|
|
*/
|
|
if (!COMPILED && PC8080.XMLVERSION) PC8080.APPVERSION = PC8080.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 Computer(parmsComputer, parmsMachine, true);
|
|
|
|
if (DEBUG && computer.messageEnabled()) {
|
|
computer.printMessage("onInit(" + computer.flags.fPowered + ")");
|
|
}
|
|
|
|
/*
|
|
* Bind any "power", "reset" and "save" buttons. An "erase" button was also considered,
|
|
* but "reset" now provides a way to force the machine to start from scratch again, so "erase"
|
|
* may be redundant now.
|
|
*/
|
|
Component.bindComponentControls(computer, eComputer, PC8080.APPCLASS);
|
|
|
|
/*
|
|
* Power on the computer, giving every component the opportunity to reset or restore itself.
|
|
*/
|
|
if (computer.fAutoPower) computer.wait(computer.powerOn);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.show()
|
|
*
|
|
* When exit() is using an "onbeforeunload" handler, this "onpageshow" handler allows us to repower everything,
|
|
* without either resetting or restoring. We call powerOn() with a special resume value (RESUME_REPOWER) if the
|
|
* computer is already marked as "ready", meaning the browser didn't change anything. This "repower" process
|
|
* should be very quick, essentially just marking all components as powered again (so that, for example, the Video
|
|
* component will start drawing again) and firing the CPU up again.
|
|
*/
|
|
Computer.show = function()
|
|
{
|
|
var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer");
|
|
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
|
|
var eComputer = aeComputers[iComputer];
|
|
var parmsComputer = Component.getComponentParms(eComputer);
|
|
var computer = /** @type {Computer} */ (Component.getComponentByType("Computer", parmsComputer['id']));
|
|
if (computer) {
|
|
|
|
if (DEBUG && computer.messageEnabled()) {
|
|
computer.printMessage("onShow(" + computer.fInitialized + "," + computer.flags.fPowered + ")");
|
|
}
|
|
|
|
if (computer.fInitialized && !computer.flags.fPowered) {
|
|
/**
|
|
* Repower the computer, notifying every component to continue running as-is.
|
|
*/
|
|
computer.powerOn(Computer.RESUME_REPOWER);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Computer.exit()
|
|
*
|
|
* The Computer is currently the only component that uses an "exit" handler, which web.onExit() defines as
|
|
* either an "unload" or "onbeforeunload" handler. This gives us the opportunity to save the machine state,
|
|
* using our powerOff() function, before the page goes away.
|
|
*
|
|
* It's worth noting that "onbeforeunload" offers one nice feature when used instead of "onload": the entire
|
|
* page (and therefore this entire application) is retained in its current state by the browser (well, some
|
|
* browsers), so that if you go to a new URL, either by entering a new URL in the same window/tab, or by pressing
|
|
* the FORWARD button, and then you press the BACK button, the page is immediately restored to its previous state.
|
|
*
|
|
* In fact, that's how some browsers operate whether you have an "onbeforeunload" handler or not; in other words,
|
|
* an "onbeforeunload" handler doesn't change the page retention behavior of the browser. By contrast, the mere
|
|
* presence of an "onunload" handler generally causes a browser to throw the page away once the handler returns.
|
|
*
|
|
* However, in order to safely use "onbeforeunload", we must add yet another handler ("onpageshow") to repower
|
|
* everything, without either resetting or restoring. Hence, the Computer.show() function, which calls powerOn()
|
|
* with a special resume value (RESUME_REPOWER) if the computer is already marked as "ready", meaning the browser
|
|
* didn't change anything. This "repower" process should be very quick, essentially just marking all components as
|
|
* powered again (so that, for example, the Video component will start drawing again) and firing the CPU up again.
|
|
*
|
|
* Reportedly, some browsers (eg, Opera) don't support "onbeforeunload", in which case Component will have to use
|
|
* "unload" instead. But even when the page must be rebuilt from scratch, the combination of browser cache and
|
|
* localStorage means the simulation should be restored and become operational almost immediately.
|
|
*/
|
|
Computer.exit = function()
|
|
{
|
|
var aeComputers = Component.getElementsByClass(document, PC8080.APPCLASS, "computer");
|
|
for (var iComputer = 0; iComputer < aeComputers.length; iComputer++) {
|
|
var eComputer = aeComputers[iComputer];
|
|
var parmsComputer = Component.getComponentParms(eComputer);
|
|
var computer = /** @type {Computer} */ (Component.getComponentByType("Computer", parmsComputer['id']));
|
|
if (computer) {
|
|
|
|
if (DEBUG && computer.messageEnabled()) {
|
|
computer.printMessage("onExit(" + computer.flags.fPowered + ")");
|
|
}
|
|
|
|
if (computer.flags.fPowered) {
|
|
/**
|
|
* Power off the computer, giving every component an opportunity to save its state,
|
|
* but only if 'resume' has been set AND there is no valid resume path (because if a valid resume
|
|
* path exists, we'll always load our state from there, and not from whatever we save here).
|
|
*/
|
|
computer.powerOff(!!(computer.resume && !computer.sResumePath), true);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Computer on the page.
|
|
*/
|
|
web.onInit(Computer.init);
|
|
web.onShow(Computer.show);
|
|
web.onExit(Computer.exit);
|
|
|
|
|
|
// ./modules/shared/lib/embed.js
|
|
|
|
/**
|
|
* @fileoverview C1Pjs and PCjs embedding functionality.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2012-Aug-28
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/* global document: true, window: true, XSLTProcessor: false, APPNAME: false, APPVERSION: false, XMLVERSION: true, DEBUG: true */
|
|
|
|
|
|
/*
|
|
* 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, 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} 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, 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, sAppClass, sParms, fResolve, display, done);
|
|
};
|
|
display("Loading " + sXMLFile + "...");
|
|
web.getResource(sXMLFile, null, fAsync, doneLoadXML);
|
|
}
|
|
|
|
/**
|
|
* parseXML(sXML, sXMLFile, idMachine, 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} 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, 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
|
|
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="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).
|
|
*/
|
|
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(sName, 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} sName is the app name (eg, "PCjs")
|
|
* @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(sName, 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);
|
|
}
|
|
|
|
var sAppClass = sName.toLowerCase(); // eg, "pcx86" or "c1pjs"
|
|
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.
|
|
*/
|
|
if (DEBUG || sVersion == "1.x.x") {
|
|
sXSLFile = "/modules/" + sAppClass + "/templates/components.xsl";
|
|
} else {
|
|
sXSLFile = "/versions/" + sAppClass + "/" + 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, sAppClass, null, false, displayMessage, transformXML);
|
|
};
|
|
|
|
if (sXMLFile.charAt(0) != '<') {
|
|
loadXML(sXMLFile, idMachine, sAppClass, sParms, true, displayMessage, processXML);
|
|
} else {
|
|
parseXML(sXMLFile, null, idMachine, 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", 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", 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", APPVERSION, idMachine, sXMLFile, sXSLFile, sParms);
|
|
}
|
|
|
|
/**
|
|
* Prevent the Closure Compiler from renaming functions we want to export,
|
|
* by adding them as (named) properties of a global object.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
window['enableEvents'] = web.enablePageEvents;
|
|
window['sendEvent'] = web.sendPageEvent;
|
|
|
|
// ./modules/shared/lib/save.js
|
|
|
|
/**
|
|
* @fileoverview PCjs save functionality.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* Created 2016-Feb-17
|
|
*
|
|
* Copyright © 2012-2016 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, a computer emulation software project at <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see COPYRIGHT in /modules/shared/lib/defines.js).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
|
|
* for purposes of the GNU General Public License, and the author does not claim any copyright
|
|
* as to their contents.
|
|
*/
|
|
|
|
/* global window: true, APPVERSION: false, XMLVERSION: true, DEBUG: true */
|
|
|
|
|
|
/**
|
|
* savePC(idMachine, sPCJSFile, callback)
|
|
*
|
|
* @param {string} idMachine
|
|
* @param {string} sPCJSFile
|
|
* @param {function(Object)} [callback]
|
|
* @return {boolean} true if successful, false if error
|
|
*/
|
|
function savePC(idMachine, sPCJSFile, callback)
|
|
{
|
|
var cmp = /** @type {Computer} */ (Component.getComponentByType("Computer", idMachine));
|
|
var dbg = /** @type {Debugger} */ (Component.getComponentByType("Debugger", idMachine));
|
|
if (cmp) {
|
|
var sState = cmp.powerOff(true);
|
|
var sParms = cmp.saveMachineParms();
|
|
if (!sPCJSFile) {
|
|
if (DEBUG) {
|
|
sPCJSFile = "/tmp/pcjs/" + (XMLVERSION || APPVERSION) + "/pc.js"
|
|
} else {
|
|
sPCJSFile = "/versions/pcjs/" + (XMLVERSION || APPVERSION) + "/pc" + (dbg? "-dbg" : "") + ".js";
|
|
}
|
|
}
|
|
if (callback && callback({ state: sState, parms: sParms })) return true;
|
|
web.getResource(sPCJSFile, null, true, function(sURL, sResponse, nErrorCode) {
|
|
downloadCSS(sURL, sResponse, nErrorCode, [idMachine, str.getBaseName(sPCJSFile, true), sParms, sState]);
|
|
});
|
|
return true;
|
|
}
|
|
web.alertUser("Unable to identify machine '" + idMachine + "'");
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
|
|
*
|
|
* @param {string} sURL
|
|
* @param {string} sPCJS
|
|
* @param {number} nErrorCode
|
|
* @param {Array} aMachineInfo ([0] = idMachine, [1] = sScript, [2] = sParms, [3] = sState)
|
|
*/
|
|
function downloadCSS(sURL, sPCJS, nErrorCode, aMachineInfo)
|
|
{
|
|
if (!nErrorCode && sPCJS) {
|
|
aMachineInfo.push(sPCJS);
|
|
var res = Component.getMachineResources(aMachineInfo[0]);
|
|
var sCSSFile = null;
|
|
for (var sName in res) {
|
|
if (str.endsWith(sName, "components.xsl")) {
|
|
sCSSFile = sName.replace(".xsl", ".css");
|
|
break;
|
|
}
|
|
}
|
|
if (!sCSSFile) {
|
|
/*
|
|
* This is probably a bad idea (ie, allowing downloadPC() to proceed with our stylesheet)...
|
|
*/
|
|
downloadPC(sURL, null, 0, aMachineInfo);
|
|
} else {
|
|
web.getResource(sCSSFile, null, true, function(sURL, sResponse, nErrorCode) {
|
|
downloadPC(sURL, sResponse, nErrorCode, aMachineInfo);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
web.alertUser("Error (" + nErrorCode + ") requesting " + sURL);
|
|
}
|
|
|
|
/**
|
|
* downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
|
|
*
|
|
* @param {string} sURL
|
|
* @param {string|null} sCSS
|
|
* @param {number} nErrorCode
|
|
* @param {Array} aMachineInfo ([0] = idMachine, [1] = sScript, [2] = sParms, [3] = sState, [4] = sPCJS)
|
|
*/
|
|
function downloadPC(sURL, sCSS, nErrorCode, aMachineInfo)
|
|
{
|
|
var matchScript, sXMLFile, sXSLFile;
|
|
var idMachine = aMachineInfo[0], sScript = aMachineInfo[1], sPCJS = aMachineInfo[4];
|
|
|
|
/*
|
|
* sPCJS is supposed to contain the entire PCjs script, which has been wrapped with:
|
|
*
|
|
* (function(){...
|
|
*
|
|
* at the top and:
|
|
*
|
|
* ...})();
|
|
*
|
|
* at the bottom, thanks to the following Closure Compiler option:
|
|
*
|
|
* --output_wrapper "(function(){%output%})();"
|
|
*
|
|
* Immediately inside that wrapping, we want to embed all the specified machine's resources, using:
|
|
*
|
|
* var resources = {"xml": "...", "xsl": "...", ...};
|
|
*
|
|
* Note that the "resources" variable has been added to our externs.js, to prevent it from being renamed
|
|
* by the Closure Compiler.
|
|
*/
|
|
matchScript = sPCJS.match(/^(\s*\(function\(\)\{)([\s\S]*)(}\)\(\);\s*)$/);
|
|
if (!matchScript) {
|
|
/*
|
|
* If the match failed, we assume that a DEBUG (uncompiled) script is being used,
|
|
* so we'll provide a fake match that should work with whatever script was provided.
|
|
*/
|
|
if (DEBUG) {
|
|
matchScript = [sPCJS, "", sPCJS, ""];
|
|
} else {
|
|
sPCJS = "";
|
|
}
|
|
}
|
|
|
|
var resOld = Component.getMachineResources(idMachine), resNew = {};
|
|
for (var sName in resOld) {
|
|
var data = resOld[sName];
|
|
var sExt = str.getExtension(sName);
|
|
if (sExt == "xml") {
|
|
/*
|
|
* Look through this resource for <disk> entries whose paths do not appear as one of the
|
|
* other machine resources, and remove those entries.
|
|
*/
|
|
var matchDisk, reDisk = /[ \t]*<disk [^>]*path=(['"])(.*?)\1.*?<\/disk>\n?/g;
|
|
while (matchDisk = reDisk.exec(resOld[sName])) {
|
|
var path = matchDisk[2];
|
|
if (path) {
|
|
if (resOld[path]) {
|
|
Component.log("recording disk: '" + path + "'");
|
|
} else {
|
|
data = data.replace(matchDisk[0], "");
|
|
}
|
|
}
|
|
}
|
|
sXMLFile = sName = str.getBaseName(sName);
|
|
}
|
|
else if (sExt == "xsl") {
|
|
sXSLFile = sName = str.getBaseName(sName);
|
|
}
|
|
Component.log("saving resource: '" + sName + "' (" + data.length + " bytes)");
|
|
resNew[sName] = data;
|
|
}
|
|
|
|
if (sCSS) {
|
|
resNew[sName = 'css'] = sCSS;
|
|
Component.log("saving resource: '" + sName + "' (" + sCSS.length + " bytes)");
|
|
}
|
|
|
|
if (aMachineInfo[2]) {
|
|
var sParms = resNew[sName = 'parms'] = aMachineInfo[2];
|
|
Component.log("saving resource: '" + sName + "' (" + sParms.length + " bytes)");
|
|
}
|
|
|
|
if (aMachineInfo[3]) {
|
|
var sState = resNew[sName = 'state'] = aMachineInfo[3];
|
|
Component.log("saving resource: '" + sName + "' (" + sState.length + " bytes)");
|
|
}
|
|
|
|
if (sXMLFile && sXSLFile) {
|
|
var sResources = JSON.stringify(resNew);
|
|
|
|
sScript += ".js";
|
|
sPCJS = matchScript[1] + "var resources=" + sResources + ";" + matchScript[2] + matchScript[3];
|
|
Component.log("saving machine: '" + idMachine + "' (" + sPCJS.length + " bytes)");
|
|
|
|
sPCJS = sPCJS.replace(/\u00A9/g, "©");
|
|
|
|
var sAlert = web.downloadFile(sPCJS, "javascript", false, sScript);
|
|
|
|
sAlert += ', copy it to your web server as "' + sScript + '", and then add the following to your web page:\n\n';
|
|
sAlert += '<div id="' + idMachine + '"></div>\n';
|
|
sAlert += '...\n';
|
|
sAlert += '<script type="text/javascript" src="' + sScript + '"></script>\n';
|
|
sAlert += '<script type="text/javascript">embedPC("' + idMachine + '","' + sXMLFile + '","' + sXSLFile + '");</script>\n\n';
|
|
sAlert += 'The machine should appear where the <div> is located.';
|
|
web.alertUser(sAlert);
|
|
return;
|
|
}
|
|
web.alertUser("Missing XML/XSL resources");
|
|
}
|
|
|
|
/**
|
|
* Prevent the Closure Compiler from renaming functions we want to export, by adding them
|
|
* as (named) properties of a global object.
|
|
*/
|
|
window['savePC'] = savePC;
|