Merge branch 'next-release'

This commit is contained in:
Jeff Parsons 2016-04-01 15:46:22 -07:00
commit d0480ecb90
297 changed files with 9645 additions and 2541 deletions

View file

@ -311,9 +311,6 @@ BufferPF.prototype.slice = function(start, end)
/**
* DiskDump()
*
* TODO: Honor the caller's mbHD size. At the moment, any hard disk request translates to 10Mb,
* since we rely on a "canned" BPB in aDefaultBPBs.
*
* TODO: If sServerRoot is set, make sure sDiskPath refers to something in either /apps/ or /disks/,
* to prevent random enumeration of other server resources.
*
@ -322,12 +319,12 @@ BufferPF.prototype.slice = function(start, end)
* @param {Array|null} [asExclude] contains filename exclusions, if any
* @param {string} [sFormat] is the output format, one of "json"|"data"|"hex"|"bytes"|"img"
* @param {boolean|string} [fComments] enables comments and other readability enhancements in the JSON output
* @param {string} [mbHD] specifies a hard disk size, in megabytes, when building a new image
* @param {string} [sSize] specifies a target disk size, in kilobytes, when building a new image
* @param {string|null} [sServerRoot]
* @param {string} [sManifestFile]
* @param {Object} [argv] optional (experimental) arguments, if any
*/
function DiskDump(sDiskPath, asExclude, sFormat, fComments, mbHD, sServerRoot, sManifestFile, argv)
function DiskDump(sDiskPath, asExclude, sFormat, fComments, sSize, sServerRoot, sManifestFile, argv)
{
/*
* I used to set this.sServerRoot to "sServerRoot || process.cwd()", but in reality, the
@ -341,7 +338,7 @@ function DiskDump(sDiskPath, asExclude, sFormat, fComments, mbHD, sServerRoot, s
this.sDiskPath = path.join(this.sServerRoot, sDiskPath);
}
this.asExclude = asExclude || DiskDump.asExclusions;
this.mbHD = mbHD? parseInt(mbHD, 10) : 0;
this.kbTarget = sSize|0; // convert the numeric string to a 32-bit number (or 0 if invalid)
this.sFormat = (sFormat || DumpAPI.FORMAT.JSON);
this.fJSONNative = (this.sFormat == DumpAPI.FORMAT.JSON && !fComments);
this.nJSONIndent = 0;
@ -458,6 +455,22 @@ DiskDump.aDefaultBPBs = [
0x02, 0x00, // 0x1A: number of heads (2)
0x00, 0x00, 0x00, 0x00 // 0x1C: number of hidden sectors (always 0 for non-partitioned media)
],
[ // define BPB for 720Kb diskette
0xEB, 0xFE, 0x90, // 0x00: JMP instruction, following by 8-byte OEM signature
0x50, 0x43, 0x4A, 0x53, 0x2E, 0x4F, 0x52, 0x47, // MY_OEM_STRING
// 0x49, 0x42, 0x4D, 0x20, 0x20, 0x35, 0x2E, 0x30, // "IBM 5.0" (this is a real OEM signature)
0x00, 0x02, // 0x0B: bytes per sector (0x200 or 512)
0x02, // 0x0D: sectors per cluster (2)
0x01, 0x00, // 0x0E: reserved sectors; ie, # sectors preceding the first FAT--usually just the boot sector (1)
0x02, // 0x10: FAT copies (2)
0x70, 0x00, // 0x11: root directory entries (0x70 or 112) 0x70 * 0x20 = 0xE00 (1 sector is 0x200 bytes, total of 7 sectors)
0xA0, 0x05, // 0x13: number of sectors (0x5A0 or 1440)
0xF9, // 0x15: media type
0x03, 0x00, // 0x16: sectors per FAT (3)
0x09, 0x00, // 0x18: sectors per track (9)
0x02, 0x00, // 0x1A: number of heads (2)
0x00, 0x00, 0x00, 0x00 // 0x1C: number of hidden sectors (always 0 for non-partitioned media)
],
[ // define BPB for 1.2Mb diskette
0xEB, 0xFE, 0x90, // 0x00: JMP instruction, following by 8-byte OEM signature
0x50, 0x43, 0x4A, 0x53, 0x2E, 0x4F, 0x52, 0x47, // MY_OEM_STRING
@ -543,7 +556,8 @@ DiskDump.asTextFileExts = [".MD", ".ME", ".ASM", ".BAS", ".TXT", ".XML"];
*
* Additional command-line arguments include:
*
* --mbhd={number}: requests a hard disk image with the given number of megabytes (eg, 10 for a 10mb image)
* --mbhd={number}: requests a hard disk image with the given number of megabytes (DEPRECATED)
* --size={number}: requests a target disk size with the given number of kilobytes (eg, 360, 720, 1200, 1440, 10000)
* --exclude={filename}: specifies a filename that should be excluded from the image; repeat as often as needed
* --overwrite: allows the --output option to overwrite an existing file; default is to NOT overwrite
* --manifest[={filename}]: update the specified manifest.xml file with details about the disk image
@ -625,7 +639,13 @@ DiskDump.CLI = function()
var sManifestTitle = argv['title'];
if (sDiskPath) {
var disk = new DiskDump(sDiskPath, asExclude, argv['format'], argv['comments'], argv['mbhd'], sServerRoot, sManifestFile, argv);
var sSize = argv['mbhd'];
if (!sSize) {
sSize = argv['size'];
} else {
sSize = (sSize * 1000).toString();
}
var disk = new DiskDump(sDiskPath, asExclude, argv['format'], argv['comments'], sSize, sServerRoot, sManifestFile, argv);
if (sDir) {
disk.buildImage(true, function(err) {
DiskDump.outputDisk(err, disk, sDiskPath, sOutputFile, fOverwrite, sManifestTitle);
@ -2246,7 +2266,8 @@ DiskDump.prototype.buildImageFromFiles = function(aFiles, done)
/*
* Put reasonable upper limits on both individual file sizes and the total size of all files.
*/
var cbMax = (this.mbHD? this.mbHD * 1024 * 1024 : 1440 * 1024);
var cbMax = (this.kbTarget || 1440) * 1024;
var nTargetSectors = (this.kbTarget? this.kbTarget * 2 : 0);
var cbTotal = this.calcFileSizes(aFiles);
if (fDebug) DiskDump.logConsole("total calculated size for " + aFiles.length + " files/folders: " + cbTotal + " bytes (0x" + str.toHex(cbTotal) + ")");
@ -2269,7 +2290,7 @@ DiskDump.prototype.buildImageFromFiles = function(aFiles, done)
* If this BPB is for a hard disk but a hard disk size was not specified, skip it.
*/
abBoot = DiskDump.aDefaultBPBs[iBPB];
if ((abBoot[0x15] == 0xF8) != (this.mbHD > 0)) continue;
if ((abBoot[0x15] == 0xF8) != (this.kbTarget >= 10000)) continue;
cbSector = abBoot[0x0B] | (abBoot[0x0C] << 8);
cSectorsPerCluster = abBoot[0x0D];
cbCluster = cbSector * cSectorsPerCluster;
@ -2282,7 +2303,7 @@ DiskDump.prototype.buildImageFromFiles = function(aFiles, done)
cHeads = abBoot[0x1A] | (abBoot[0x1B] << 8);
cDataSectors = cTotalSectors - cRootSectors - cFATs * cFATSectors + 1;
cbAvail = cDataSectors * cbSector;
if (cbTotal <= cbAvail) break; // found a BPB that works!
if (nTargetSectors && cTotalSectors == nTargetSectors || !nTargetSectors && cbTotal <= cbAvail) break;
}
if (iBPB == DiskDump.aDefaultBPBs.length) {
@ -2318,7 +2339,7 @@ DiskDump.prototype.buildImageFromFiles = function(aFiles, done)
/*
* Output a Master Boot Record (MBR), if a hard disk image was requested
*/
if (this.mbHD > 0) {
if (this.kbTarget >= 10000) {
abSector = this.buildMBR(cHeads, cSectorsPerTrack, cbSector, cTotalSectors);
offDisk += this.copyData(offDisk, abSector);
}

View file

@ -659,7 +659,7 @@ HTMLOut.logError = function(err, fForce)
{
var sError = "";
if (err) {
sError = "htmlout error: " + err.message;
sError = "HTMLOut error: " + err.message;
if (fConsole || fForce) HTMLOut.logConsole(sError);
}
return sError;
@ -1459,6 +1459,15 @@ HTMLOut.prototype.getMachineXML = function(sToken, sIndent, aParms, sXMLFile, sS
}
}
/*
* If we were called from getManifestXML(), then let's fallback to getMarkdownFile() instead.
*/
if (fFromManifest) {
s = sIndent + "<p>" + HTMLOut.logError(err) + " (invalid manifest entry)</p>";
obj.getMarkdownFile(obj.sFile, sToken, sIndent, aParms, s);
return;
}
/*
* If we're still here, one of the following happened:
*
@ -1468,23 +1477,13 @@ HTMLOut.prototype.getMachineXML = function(sToken, sIndent, aParms, sXMLFile, sS
*
* But, instead of displaying a cryptic error message inside our beautiful HTML template, eg:
*
* htmlout error: ENOENT, open '/Users/Jeff/Sites/pcjs/devices/pc/machine/5160/cga/256kb/win101/debugger/machine.xml'
* HTMLOut error: ENOENT, open '/Users/Jeff/Sites/pcjs/devices/pc/machine/5160/cga/256kb/win101/debugger/machine.xml'
*
* we have one more fallback: a random string! Less useful, but more entertaining. Well, maybe not even that.
*
* s = HTMLOut.logError(err);
*/
/*
* If we were called from getManifestXML(), then let's fallback to getMarkdownFile() instead.
*/
s = obj.getRandomString(sIndent);
if (fFromManifest) {
obj.getMarkdownFile(obj.sFile, sToken, sIndent, aParms, s);
return;
}
obj.aTokens[sToken] = s;
obj.aTokens[sToken] = obj.getRandomString(sIndent);
obj.replaceTokens();
});
};
@ -1703,7 +1702,7 @@ HTMLOut.prototype.getMarkdownFile = function(sFile, sToken, sIndent, aParms, sPr
/*
* Instead of displaying a cryptic error message inside our beautiful HTML template, eg:
*
* htmlout error: ENOENT, open '/Users/Jeff/Sites/pcjs/devices/pc/machine/5160/cga/256kb/win101/debugger/README.md'
* HTMLOut error: ENOENT, open '/Users/Jeff/Sites/pcjs/devices/pc/machine/5160/cga/256kb/win101/debugger/README.md'
*
* which is all this will give us:
*

View file

@ -720,13 +720,18 @@ HTTPAPI.processDumpAPI = function(req, res)
sFormat = req.query[DumpAPI.QUERY.FORMAT] || DumpAPI.FORMAT.JSON;
fComments = (req.query[DumpAPI.QUERY.COMMENTS]? true : false);
var mbHD = req.query[DumpAPI.QUERY.MBHD];
var sSize = req.query[DumpAPI.QUERY.MBHD];
if (sSize) {
sSize = (sSize * 1000).toString();
} else {
sSize = req.query[DumpAPI.QUERY.SIZE];
}
/*
* TODO: Consider adding support for DiskDump's "exclusion" option to the API interface
* (the command-line interface supports it).
*/
var disk = new DiskDump(sDisk, null, sFormat, fComments, mbHD, sServerRoot);
var disk = new DiskDump(sDisk, null, sFormat, fComments, sSize, sServerRoot);
if (aCommand[0] == DumpAPI.QUERY.DISK || aCommand[0] == DumpAPI.QUERY.IMG) {
disk.loadFile(function(err) {
HTTPAPI.dumpDisk(err, disk, res);

File diff suppressed because it is too large Load diff

View file

@ -184,6 +184,14 @@ function Computer(parmsComputer, parmsMachine, fSuspended) {
}
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
*/
@ -1437,6 +1445,65 @@ Computer.prototype.getMachineComponent = function(sType, componentPrev)
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()
*
* @this {Computer}
*/
Computer.prototype.updateStatus = function()
{
if (this.panel) this.panel.updateStatus();
};
/**
* updateVideo(fForce)
*
* Any high-frequency updates should be performed here. Avoid DOM updates, since updateVideo() can be called up to
* 60 times per second (see VIDEO_UPDATES_PER_SECOND).
*
* @this {Computer}
* @param {boolean} [fForce] (true to force a video update)
*/
Computer.prototype.updateVideo = function(fForce)
{
for (var i = 0; i < this.aVideo.length; i++) {
this.aVideo[i].updateScreen(fForce);
}
if (this.panel) this.panel.updateAnimation();
};
/**
* Computer.init()
*

View file

@ -122,11 +122,6 @@ function CPU(parmsCPU, nCyclesDefault)
this.aCounts.nCyclesChecksumInterval = parmsCPU["csInterval"];
this.aCounts.nCyclesChecksumStop = parmsCPU["csStop"];
/*
* Initially, no video devices are attached that require CPU-driven updates. initBus() will update this.
*/
this.aVideo = [];
this.onRunTimeout = this.runCPU.bind(this); // function onRunTimeout() { cpu.runCPU(); };
this.setReady();
@ -180,14 +175,6 @@ CPU.prototype.initBus = function(cmp, bus, cpu, dbg)
this.fpu = cmp.getMachineComponent("FPU");
/*
* Attach the Video component to the CPU, so that the CPU can periodically update
* the video display via updateVideo(), as cycles permit.
*/
for (var video = null; (video = cmp.getMachineComponent("Video", video));) {
this.aVideo.push(video);
}
/*
* Attach the ChipSet component to the CPU so that it can obtain the IDT vector number
* of pending hardware interrupts in response to the ChipSet's updateINTR() notifications.
@ -323,7 +310,7 @@ CPU.prototype.autoStart = function()
*/
if (this.flags.fAutoStart || (!DEBUGGER || !this.dbg) && this.bindings["run"] === undefined) {
/*
* Now we ALSO set fSetFocus when calling runCPU(), on the assumption that in the "auto-starting" context,
* 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);
@ -483,66 +470,6 @@ CPU.prototype.displayValue = function(sLabel, nValue, cch)
}
};
/**
* updateStatus(fForce)
*
* This provides periodic Control Panel updates (eg, a few times per second; see STATUS_UPDATES_PER_SECOND).
* The X86CPU subclasses updateStatus() to take care of any DOM updates (eg, register values) while the CPU is running.
*
* @this {CPU}
* @param {boolean} [fForce]
*/
CPU.prototype.updateStatus = function(fForce)
{
if (this.cmp && this.cmp.panel) this.cmp.panel.updateStatus();
};
/**
* updateVideo(fForce)
*
* Any high-frequency updates should be performed here. Avoid DOM updates, since updateVideo() can be called up to
* 60 times per second (see VIDEO_UPDATES_PER_SECOND).
*
* @this {CPU}
* @param {boolean} [fForce] (true to force a video update)
*/
CPU.prototype.updateVideo = function(fForce)
{
for (var i = 0; i < this.aVideo.length; i++) {
this.aVideo[i].updateScreen(fForce);
}
if (this.cmp && this.cmp.panel) this.cmp.panel.updateAnimation();
};
/**
* setFocus(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 {CPU}
* @param {boolean} [fScroll]
*/
CPU.prototype.setFocus = 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;
}
this.aVideo[0].setFocus();
if (fScroll && window) {
window.scrollTo(x, y);
}
}
};
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
@ -823,20 +750,20 @@ CPU.prototype.getSpeedTarget = function()
};
/**
* setSpeed(nMultiplier, fSetFocus)
* 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} [fSetFocus] is true to give the CPU focus
* @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, fSetFocus)
CPU.prototype.setSpeed = function(nMultiplier, fUpdateFocus)
{
var fSuccess = false;
if (nMultiplier !== undefined) {
@ -857,7 +784,7 @@ CPU.prototype.setSpeed = function(nMultiplier, fSetFocus)
if (controlSpeed) controlSpeed.textContent = sSpeed;
this.println("target speed: " + sSpeed);
}
if (fSetFocus) this.setFocus();
if (fUpdateFocus && this.cmp) this.cmp.updateFocus();
}
this.addCycles(this.nRunCycles);
this.nRunCycles = 0;
@ -1014,12 +941,12 @@ CPU.prototype.calcRemainingTime = function()
};
/**
* runCPU(fSetFocus)
* runCPU(fUpdateFocus)
*
* @this {CPU}
* @param {boolean} [fSetFocus] is true to give the CPU focus
* @param {boolean} [fUpdateFocus] is true to update Computer focus
*/
CPU.prototype.runCPU = function(fSetFocus)
CPU.prototype.runCPU = function(fUpdateFocus)
{
if (!this.setBusy(true)) {
this.updateCPU();
@ -1027,7 +954,7 @@ CPU.prototype.runCPU = function(fSetFocus)
return;
}
this.startCPU(fSetFocus);
this.startCPU(fUpdateFocus);
/*
* calcStartTime() initializes the cycle counter and timestamp for this runCPU() invocation, and optionally
@ -1078,13 +1005,13 @@ CPU.prototype.runCPU = function(fSetFocus)
this.aCounts.nCyclesNextVideoUpdate -= nCycles;
if (this.aCounts.nCyclesNextVideoUpdate <= 0) {
this.aCounts.nCyclesNextVideoUpdate += this.aCounts.nCyclesPerVideoUpdate;
this.updateVideo();
if (this.cmp) this.cmp.updateVideo();
}
this.aCounts.nCyclesNextStatusUpdate -= nCycles;
if (this.aCounts.nCyclesNextStatusUpdate <= 0) {
this.aCounts.nCyclesNextStatusUpdate += this.aCounts.nCyclesPerStatusUpdate;
this.updateStatus();
if (this.cmp) this.cmp.updateStatus();
}
this.aCounts.nCyclesNextYield -= nCycles;
@ -1106,13 +1033,13 @@ CPU.prototype.runCPU = function(fSetFocus)
};
/**
* startCPU(fSetFocus)
* startCPU(fUpdateFocus)
*
* WARNING: Other components must use runCPU() to get the CPU running; this is a runCPU() helper function only.
*
* @param {boolean} [fSetFocus]
* @param {boolean} [fUpdateFocus]
*/
CPU.prototype.startCPU = function(fSetFocus)
CPU.prototype.startCPU = function(fUpdateFocus)
{
if (!this.flags.fRunning) {
/*
@ -1128,8 +1055,10 @@ CPU.prototype.startCPU = function(fSetFocus)
if (this.chipset) this.chipset.setSpeaker();
var controlRun = this.bindings["run"];
if (controlRun) controlRun.textContent = "Halt";
this.updateStatus(true);
if (fSetFocus) this.setFocus(true);
if (this.cmp) {
this.cmp.updateStatus();
if (fUpdateFocus) this.cmp.updateFocus(true);
}
}
};
@ -1187,8 +1116,10 @@ CPU.prototype.stopCPU = function(fComplete)
*/
CPU.prototype.updateCPU = function(fForce)
{
this.updateVideo(fForce);
this.updateStatus();
if (this.cmp) {
this.cmp.updateVideo(fForce);
this.cmp.updateStatus();
}
};
/**

View file

@ -2182,11 +2182,11 @@ if (DEBUGGER) {
};
/**
* setFocus()
* updateFocus()
*
* @this {Debugger}
*/
Debugger.prototype.setFocus = function()
Debugger.prototype.updateFocus = function()
{
if (this.controlDebug) this.controlDebug.focus();
};
@ -3945,16 +3945,16 @@ if (DEBUGGER) {
};
/**
* runCPU(fSetFocus)
* runCPU(fUpdateFocus)
*
* @this {Debugger}
* @param {boolean} [fSetFocus] is true to give the CPU focus
* @param {boolean} [fUpdateFocus] is true to update focus
* @return {boolean} true if run request successful, false if not
*/
Debugger.prototype.runCPU = function(fSetFocus)
Debugger.prototype.runCPU = function(fUpdateFocus)
{
if (!this.isCPUAvail()) return false;
this.cpu.runCPU(fSetFocus);
this.cpu.runCPU(fUpdateFocus);
return true;
};
@ -4258,7 +4258,7 @@ if (DEBUGGER) {
this.println(sStopped);
}
this.updateStatus(true);
this.setFocus();
this.updateFocus();
this.clearTempBreakpoint(this.cpu.regLIP);
}
};
@ -7618,7 +7618,7 @@ if (DEBUGGER) {
if (this.nStep) {
this.setTempBreakpoint(dbgAddr);
if (!this.runCPU()) {
this.cpu.setFocus();
if (this.cmp) this.cmp.updateFocus();
this.nStep = 0;
}
/*

View file

@ -771,7 +771,7 @@ FDC.prototype.initController = function(data)
if (this.aDrives === undefined) {
this.nDrives = 4; // default to the maximum number of drives
if (this.chipset) this.nDrives = this.chipset.getSWFloppyDrives();
if (this.chipset) this.nDrives = this.chipset.getDIPFloppyDrives();
/*
* I would prefer to allocate only nDrives, but as discussed in the handling of the FDC.REG_DATA.CMD.SENSE_INT
* command, we're faced with situations where the controller must respond to any drive in the range 0-3, regardless
@ -789,7 +789,7 @@ FDC.prototype.initController = function(data)
* the drive's physical limits accordingly (ie, max tracks, max heads, and max sectors/track).
*/
drive = this.aDrives[iDrive] = {};
var nKb = (this.chipset? this.chipset.getSWFloppyDriveSize(iDrive) : 0);
var nKb = (this.chipset? this.chipset.getDIPFloppyDriveSize(iDrive) : 0);
switch(nKb) {
case 160:
case 180:
@ -1279,7 +1279,7 @@ FDC.prototype.loadSelectedDrive = function(sDisketteName, sDiskettePath, file)
if (DEBUG) this.println("loading disk " + sDiskettePath + "...");
while (this.loadDiskette(iDrive, sDisketteName, sDiskettePath, false, file)) {
while (this.loadDiskette(iDrive, sDisketteName, sDiskettePath, false, file) < 0) {
if (!window.confirm("Click OK to reload the original disk.\n(WARNING: All disk changes will be discarded)")) {
return;
}
@ -1327,7 +1327,7 @@ FDC.prototype.mountDiskette = function(iDrive, sDisketteName, sDiskettePath)
* @param {string} sDiskettePath
* @param {boolean} [fAutoMount]
* @param {File} [file] is set if there's an associated File object
* @return {boolean} true if diskette (already) loaded, false if queued up (or busy)
* @return {number} 1 if diskette loaded, 0 if queued up (or busy), -1 if already loaded
*/
FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAutoMount, file)
{
@ -1336,7 +1336,7 @@ FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAut
this.unloadDrive(iDrive, fAutoMount, true);
if (drive.fBusy) {
this.notice("Drive " + iDrive + " busy");
return true;
return 0;
}
drive.fBusy = true;
if (fAutoMount) {
@ -1347,10 +1347,11 @@ FDC.prototype.loadDiskette = function(iDrive, sDisketteName, sDiskettePath, fAut
drive.fLocal = !!file;
var disk = new Disk(this, drive, DiskAPI.MODE.PRELOAD);
if (!disk.load(sDisketteName, sDiskettePath, file, this.doneLoadDiskette)) {
return false;
return 0;
}
return 1;
}
return true;
return -1;
};
/**
@ -1436,6 +1437,12 @@ FDC.prototype.doneLoadDiskette = function onFDCLoadNotify(drive, disk, sDiskette
drive.nDiskCylinders = aDiskInfo[0];
drive.nDiskHeads = aDiskInfo[1];
drive.nDiskSectors = aDiskInfo[2];
/*
* Since you usually want the Computer to have focus again after loading a new diskette, let's try automatically
* updating the focus after a successful load.
*/
if (this.cmp) this.cmp.updateFocus();
}
else {
drive.fLocal = false;
@ -1733,6 +1740,85 @@ FDC.prototype.outFDCOutput = function(port, bOut, addrFrom)
this.regOutput = bOut;
};
/**
* inFDCDiagnostic(port, addrFrom)
*
* It turns out that any 5170 configuration without an HDC component that attempts to use either the REV2 or REV3
* PC AT ROM BIOS will fail with error "601-Diskette Error", unless we also provide this "D/S/P DIAGNOSTIC REGISTER".
* The original 5170 REV1 BIOS didn't have this requirement.
*
* I'm unable to find any documentation on this so-called "D/S/P DIAGNOSTIC REGISTER" (port 0x3F1) or the "D/S/P CARD"
* to which the ROM BIOS refers. But it seems clear that if we don't provide the expected response from the DIAGNOSTIC
* REGISTER, and there's no HDC to respond to the MULTIPLE DATA RATE CAPABLE test that follows, then an error is inevitable.
* Clearly, there is a very intimate relationship between the FDC and HDC portions of this card.
*
* Here's the relevant code from the REV3 PC AT ROM BIOS (TEST2.ASM):
*
* ;----- CHECK FOR MULTIPLE DATA RATE CAPABILITY
*
* J_OK:
* MOV DX,03F1H ; D/S/P DIAGNOSTIC REGISTER
* IN AL,DX ; READ D/S/P TYPE CODE
* AND AL,11111000B ; KEEP ONLY UNIQUE CODE FOR D/S/P
* CMP AL,01010000B ; D/S/P CARD - MULTIPLE DATA RATE?
* JZ J_OK3 ; IF SO JUMP
*
* MOV DX,05F7H ; FIXED DISK DIAGNOSTIC REGISTER
* IN AL,DX ; READ FIXED DISK TYPE CODE
* AND AL,11110000B ; KEEP ONLY UNIQUE CODE FOR F/D
* CMP AL,10100000B ; FIXED DISK ADAPTER ?
* JZ J_FAIL ; MUST BE COMBO ELSE ERROR
*
* MOV BL,0FH ; OUTER LOOP COUNT WAIT FOR BUSY OFF
* SUB CX,CX
* MOV DX,01F7H ; HARD FILE STATUS PORT
* J_OK1:
* IN AL,DX ; GET THE STATUS
* TEST AL,080H ; IS THE CONTROLLER BUSY?
* JZ J_OK2 ; CONTINUE IF NOT
* LOOP J_OK1 ; TRY AGAIN
* DEC BL ; DECREMENT OUTER LOOP
* JNZ J_OK1 ; TRY AGAIN IF NOT ZERO
* AND AL,0CH ; BITS 2 & 3 = 0 IF MULTI DATA CAPABLE
* JZ J_OK3 ; GO IF YES
* JMP SHORT J_FAIL ; NO MULTIPLE DATA RATE CAPABILITY
* J_OK2:
* MOV DX,1F4H ; VERIFY MULTIPLE DATA RATE CAPABLE
* MOV AL,055H ; WRITE TO THE CYLINDER BYTE
* OUT DX,AL
* JMP $+2 ; I/O DELAY
* IN AL,DX ; CHECK DATA WRITTEN = DATA READ
* CMP AL,055H
* JNZ J_FAIL ; GO IF NOT
* MOV AL,0AAH ; WRITE ANOTHER PATTERN
* OUT DX,AL
* JMP $+2 ; I/O DELAY
* IN AL,DX
* CMP AL,0AAH ; IS DATA PATTERN THE SAME?
* JZ J_OK3 ; GO IF SO
*
* J_FAIL:
* OR @MFG_ERR_FLAG+1,DSK_FAIL; <><><><><><><><><><><><><>
* ; <><> DISKETTE FAILED <><>
* MOV SI,OFFSET E601 ; GET ADDRESS OF MESSAGE
* CALL E_MSG ; GO PRINT ERROR MESSAGE
* JMP SHORT F15C ; SKIP SETUP IF ERROR
*
* J_OK3:
* OR @LASTRATE,DUAL ; TURN ON DSP/COMBO FLAG
*
* @this {FDC}
* @param {number} port (0x3F1, input only)
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
* @return {number} simulated port value
*/
FDC.prototype.inFDCDiagnostic = function(port, addrFrom)
{
var b = 0x50; // we simply return the expected pattern (01010000B); see code excerpt above
this.printMessageIO(port, null, addrFrom, "DIAG", b);
return b;
};
/**
* inFDCStatus(port, addrFrom)
*
@ -2539,6 +2625,7 @@ FDC.prototype.writeFormat = function(drive, b)
* way out and always emulating it. So, consider an FDC parameter to disable that feature for stricter compatibility.
*/
FDC.aPortInput = {
0x3F1: FDC.prototype.inFDCDiagnostic,
0x3F4: FDC.prototype.inFDCStatus,
0x3F5: FDC.prototype.inFDCData,
0x3F7: FDC.prototype.inFDCInput

View file

@ -2958,6 +2958,11 @@ HDC.aXTCPortInput = {
0x322: HDC.prototype.inXTCConfig
};
/*
* For future reference, the REV2 and REV3 PC AT ROM BIOS also refer to a "FIXED DISK DIAGNOSTIC REGISTER" at
* port 0x5F7, but I have no documentation on it, and failure to respond is non-fatal. See the discussion of the
* FDC diagnostic register in inFDCDiagnostic() for more details.
*/
HDC.aATCPortInput = {
0x1F0: HDC.prototype.inATCData,
0x1F1: HDC.prototype.inATCError,

View file

@ -1187,7 +1187,7 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
case "caps-lock":
this.bindings[id] = control;
control.onclick = function onClickCapsLock(event) {
if (kbd.cpu) kbd.cpu.setFocus();
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleCapsLock();
};
return true;
@ -1195,7 +1195,7 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
case "num-lock":
this.bindings[id] = control;
control.onclick = function onClickNumLock(event) {
if (kbd.cpu) kbd.cpu.setFocus();
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleNumLock();
};
return true;
@ -1203,7 +1203,7 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
case "scroll-lock":
this.bindings[id] = control;
control.onclick = function onClickScrollLock(event) {
if (kbd.cpu) kbd.cpu.setFocus();
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleScrollLock();
};
return true;
@ -1218,7 +1218,7 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
control.onclick = function(kbd, sKey, simCode) {
return function onClickKeyboard(event) {
if (!COMPILED && kbd.messageEnabled()) kbd.printMessage(sKey + " clicked", Messages.KEYS);
if (kbd.cpu) kbd.cpu.setFocus();
if (kbd.cmp) kbd.cmp.updateFocus();
kbd.updateShiftState(simCode, true); // future-proofing if/when any LOCK keys are added to CLICKCODES
kbd.addActiveKey(simCode, true);
};
@ -1255,7 +1255,7 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
*/
this.bindings[id] = control;
control.onclick = function onClickTest(event) {
if (kbd.cpu) kbd.cpu.setFocus();
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.injectKeys(sValue);
};
return true;
@ -1323,6 +1323,7 @@ Keyboard.prototype.findBinding = function(simCode, sType, fDown)
*/
Keyboard.prototype.initBus = function(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;

View file

@ -158,7 +158,7 @@ RAM.prototype.powerDown = function(fSave, fShutdown)
RAM.prototype.reset = function()
{
if (!this.addrRAM && !this.fInstalled && this.chipset) {
var baseRAM = this.chipset.getSWMemorySize() * 1024;
var baseRAM = this.chipset.getDIPMemorySize() * 1024;
if (this.sizeRAM && baseRAM != this.sizeRAM) {
this.bus.removeMemory(this.addrRAM, this.sizeRAM);
this.fAllocated = false;

View file

@ -58,10 +58,10 @@ if (NODE) {
* scale: true for font scaling, false (default) to center the display on the screen
* charCols: number of character columns
* charRows: number of character rows
* fontROM: path to .rom file (or a JSON representation) that defines the character set
* fontROM: path to .rom file (or a JSON representation) containing the character set
* screenColor: background color of the screen canvas (default is black)
* touchScreen: string specifying desired touch-screen support (default is ''); see initBus()
* autoLock: true to (attempt to) automatically lock the mouse to the canvas (default is false)
* touchScreen: string specifying desired touch-screen support (default is none)
* autoLock: true to (attempt to) auto-lock the mouse to the canvas (default is false)
*
* An EGA may specify the following additional properties:
*
@ -73,22 +73,23 @@ if (NODE) {
* the port level, and whenever reset() is called. setMode() also invokes updateScreen(true),
* which forces reallocation of our internal buffer (aCellCache) that mirrors the video buffer.
*
* The CPU periodically calls updateScreen(), at an assumed rate of 60 times/second,
* to update any blinking elements (the cursor and any characters with the blink attribute),
* to compare/update the contents of our internal buffer with the video buffer, and to render
* any differences between the two buffers into the associated screen canvas, via either
* updateChar() or setPixel().
* The CPU periodically calls updateVideo(), which in turn calls updateScreen() for each Video
* instance. These updates should occur at a rate of 60 times/second, to update any blinking
* elements (the cursor and any cells with the blink attribute), to compare/update the contents
* of our internal buffer with the video buffer, and to render any differences between the two
* buffers into the associated screen canvas, via either updateChar() or setPixel().
*
* Thanks to the CPU's new block-based memory manager that allows us to sparse-allocate memory
* Thanks to the Bus' new block-based memory manager that allows us to sparse-allocate memory
* (in 4Kb increments on 20-bit buses, 16Kb increments on 24-bit buses), updateScreen()
* can also ask the CPU for the "dirty" state of all the blocks underlying the video buffer,
* bypassing the update completely if the buffer is still clean.
*
* Unfortunately, that optimization is defeated if our count of active blink elements is non-zero,
* Sadly, that optimization is defeated if the count of active blink elements is non-zero,
* because we must rescan the entire buffer to locate and redraw them all; I'm assuming for now
* that, more often than not, blink attributes will not be present, and therefore they're not worth
* a separate caching mechanism. If the only blinking element is the cursor, that's no problem,
* as we redraw only the one cell containing the cursor (assuming the buffer is otherwise clean).
* that, more often than not, very few (if any) blink attributes will be present, and therefore
* they're not worth a separate caching mechanism. If the only blinking element is the cursor,
* that's no problem, as we redraw only the one cell containing the cursor (assuming the buffer
* is otherwise clean).
*
* @constructor
* @extends Component
@ -1000,7 +1001,7 @@ Video.aEGADWToByte[0x80808000|0] = 0xe;
Video.aEGADWToByte[0x80808080|0] = 0xf;
/**
* Card(video, iCard, data, cbMemory)
* Card(video, nCard, data, cbMemory)
*
* Creates an object representing an initial video card state;
* can also restore a video card from state data created by saveCard().
@ -1011,25 +1012,25 @@ Video.aEGADWToByte[0x80808080|0] = 0xf;
*
* @constructor
* @param {Video} [video]
* @param {number} [iCard] (see Video.CARD.*)
* @param {number} [nCard] (see Video.CARD.*)
* @param {Array|null} [data]
* @param {number} [cbMemory] is specified if the card must allocate its own memory buffer
*/
function Card(video, iCard, data, cbMemory)
function Card(video, nCard, data, cbMemory)
{
/*
* If a card was originally not present (eg, EGA), then the state will be empty,
* so we need to detect that case and continue indicating that the card is not present.
*/
if (iCard !== undefined && (!data || data.length)) {
if (nCard !== undefined && (!data || data.length)) {
this.video = video;
var specs = Video.cardSpecs[iCard];
var specs = Video.cardSpecs[nCard];
var nMonitorType = video.nMonitorType || specs[5];
if (!data || data.length < 6) {
data = [false, 0, null, null, 0, new Array(iCard < Video.CARD.EGA? Card.CRTC.TOTAL_REGS : Card.CRTC.EGA.TOTAL_REGS)];
data = [false, 0, null, null, 0, new Array(nCard < Video.CARD.EGA? Card.CRTC.TOTAL_REGS : Card.CRTC.EGA.TOTAL_REGS)];
}
/*
@ -1041,7 +1042,7 @@ function Card(video, iCard, data, cbMemory)
this.port = specs[1];
}
this.nCard = iCard;
this.nCard = nCard;
this.addrBuffer = specs[2]; // default (physical) video buffer address
this.sizeBuffer = specs[3]; // default video buffer length (this is the total size, not the current visible size; this.cbScreen is calculated on the fly to reflect the latter)
@ -1073,7 +1074,7 @@ function Card(video, iCard, data, cbMemory)
this.nCRTCRegs = Card.CRTC.TOTAL_REGS;
this.asCRTCRegs = DEBUGGER? Card.CRTC.REGS : [];
if (iCard >= Video.CARD.EGA) {
if (nCard >= Video.CARD.EGA) {
this.nCRTCRegs = Card.CRTC.EGA.TOTAL_REGS;
this.asCRTCRegs = DEBUGGER? Card.CRTC.EGA_REGS : [];
this.initEGA(data[6], nMonitorType);
@ -2500,20 +2501,15 @@ Card.prototype.dumpRegs = function(sName, iReg, aRegs, asRegs)
{
if (DEBUGGER) {
if (!aRegs) {
this.dbg.println(sName + ": " + str.toHexByte(iReg));
this.dbg.println(sName + ": " + str.toHex(iReg, 2));
return;
}
var i, cchMax = 19, s = "";
/*
var s = "", i, cchMax = 0;
for (i = 0; i < asRegs.length; i++) {
if (cchMax < asRegs[i].length) cchMax = asRegs[i].length;
}
cchMax++;
*/
var i, cchMax = 18, s = "";
for (i = 0; i < asRegs.length; i++) {
var reg = (aRegs === this.regCRTData)? this.getCRTCReg(i) : aRegs[i];
if (s) s += '\n';
s += sName + "[" + str.toHexByte(i) + "]: " + str.pad(asRegs[i], cchMax) + str.toHexByte(aRegs[i]) + (i === iReg? "*" : "");
s += sName + "[" + str.toHex(i, 2) + "]: " + str.pad(asRegs[i], cchMax) + (i === iReg? '*' : ' ') + str.toHex(reg, reg > 0xff? 4 : 2);
if (reg != null) s += " (" + reg + ".)"
}
this.dbg.println(s);
}
@ -2536,11 +2532,11 @@ Card.prototype.dumpVideoCard = function()
this.dumpRegs(" GRC", this.regGRCIndx, this.regGRCData, this.asGRCRegs);
this.dumpRegs(" SEQ", this.regSEQIndx, this.regSEQData, this.asSEQRegs);
this.dumpRegs(" ATC", this.regATCIndx, this.regATCData, this.asATCRegs);
this.dumpRegs(" ATCINDX", this.regATCIndx);
this.dbg.println(" ATCDATA: " + this.fATCData);
this.dumpRegs(" FEAT", this.regFeat);
this.dumpRegs(" MISC", this.regMisc);
this.dumpRegs(" STATUS0", this.regStatus0);
this.dumpRegs(" ATCINDX", this.regATCIndx);
this.dbg.println(" ATCDATA: " + this.fATCData);
this.dumpRegs(" FEAT", this.regFeat);
this.dumpRegs(" MISC", this.regMisc);
this.dumpRegs(" STATUS0", this.regStatus0);
/*
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
*/
@ -2550,19 +2546,19 @@ Card.prototype.dumpVideoCard = function()
* TODO: This simply dumps the last value read from the STATUS1 register, not necessarily
* its current state; consider dumping getRetraceBits() instead of (or in addition to) this.
*/
this.dumpRegs(" STATUS1", this.regStatus);
this.dumpRegs(" STATUS1", this.regStatus);
if (this.nCard == Video.CARD.MDA || this.nCard == Video.CARD.CGA) {
this.dumpRegs(" MODEREG", this.regMode);
this.dumpRegs(" MODEREG", this.regMode);
}
if (this.nCard == Video.CARD.CGA) {
this.dumpRegs(" COLOR", this.regColor);
this.dumpRegs(" COLOR", this.regColor);
}
if (this.nCard >= Video.CARD.EGA) {
this.dbg.println(" LATCHES: 0x" + str.toHex(this.latches));
this.dbg.println(" ACCESS: " + str.toHexWord(this.nAccess));
this.dbg.println(" LATCHES: " + str.toHex(this.latches));
this.dbg.println(" ACCESS: " + str.toHex(this.nAccess, 4));
this.dbg.println("Use 'dump video [addr]' to dump video memory");
/*
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
@ -2782,6 +2778,52 @@ Card.prototype.setMemoryAccess = function(nAccess)
}
};
/**
* getCRTCReg()
*
* @this {Card}
* @param {number} iReg
* @return {number}
*/
Card.prototype.getCRTCReg = function(iReg)
{
var reg = this.regCRTData[iReg];
if (reg != null && this.nCard >= Video.CARD.EGA) {
var bOvrflowBit8 = 0, bOvrflowBit9 = 0, bMaxScanBit9 = 0;
switch(iReg) {
case Card.CRTC.EGA.VTOTAL: // 0x06
bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT8; // 0x01
if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT9;
break;
case Card.CRTC.EGA.CURSOR_START.INDX: // 0x0A
if (this.nCard == Video.CARD.EGA) bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.CURSOR_START_BIT8;
break;
case Card.CRTC.EGA.VRETRACE_START: // 0x10
bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VRETRACE_START_BIT8; // 0x04
if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VRETRACE_START_BIT9;
break;
case Card.CRTC.EGA.VDISP_END: // 0x12
bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VDISP_END_BIT8; // 0x02
if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VDISP_END_BIT9;
break;
case Card.CRTC.EGA.VBLANK_START: // 0x15
bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VBLANK_START_BIT8; // 0x08
if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAX_SCAN.VBLANK_START_BIT9;
break;
case Card.CRTC.EGA.LINE_COMPARE: // 0x18
bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.LINE_COMPARE_BIT8; // 0x10
if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAX_SCAN.LINE_COMPARE_BIT9;
break;
}
if (bOvrflowBit8) {
reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOvrflowBit8)? 0x100 : 0);
reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOvrflowBit9)? 0x200 : 0);
reg |= ((this.regCRTData[Card.CRTC.EGA.MAX_SCAN.INDX] & bMaxScanBit9)? 0x200 : 0);
}
}
return reg;
};
/*
* Card Specifications
*
@ -2904,7 +2946,9 @@ Video.prototype.initBus = function(cmp, bus, cpu, dbg)
this.bEGASwitches = 0x09; // our default "switches" setting (see aEGAMonitorSwitches)
this.chipset = cmp.getMachineComponent("ChipSet");
if (this.chipset && this.sSwitches) {
if (this.nCard == Video.CARD.EGA) this.bEGASwitches = this.chipset.parseSwitches(this.sSwitches, this.bEGASwitches);
if (this.nCard == Video.CARD.EGA) {
this.bEGASwitches = this.chipset.parseDIPSwitches(this.sSwitches, this.bEGASwitches);
}
}
/*
@ -3535,7 +3579,7 @@ Video.prototype.reset = function()
* on the EGA's own switch settings instead.
*/
if (this.chipset) {
nMonitorType = this.chipset.getSWVideoMonitor();
nMonitorType = this.chipset.getDIPVideoMonitor();
}
/*
@ -4693,14 +4737,14 @@ Video.prototype.setDimensions = function()
* to use the 9x14 "EGA" color font instead.
*
* TODO: Can an EGA with a monochrome monitor be programmed for 43-line mode as well? If so,
* then we'll need to load another MDA font variation, because we only load an 9x14 font for MDA.
* then we'll need to load another MDA font variation, because we only load the 9x14 font for MDA.
*/
if (this.cardActive === this.cardEGA && this.nFont == Video.FONT.CGA) {
if (this.cardEGA.regCRTData[Card.CRTC.EGA.MAX_SCAN.INDX] == 7) {
if ((this.cardEGA.regCRTData[Card.CRTC.EGA.MAX_SCAN.INDX] & Card.CRTC.EGA.MAX_SCAN.SCAN_LINE) == 7) {
/*
* Vertical resolution of 350 divided by 8 (ie, scan lines 0-7) yields 43 whole rows.
*/
this.nRows = 43;
this.nRows = this.cardEGA.getCRTCReg(Card.CRTC.EGA.VDISP_END) < 350? 43 : 50;
}
/*
* Since we can also be called before any hardware registers have been initialized,
@ -4950,17 +4994,12 @@ Video.prototype.checkMode = function(fForce)
}
}
var fSEQDotClock = (card.regSEQData[Card.SEQ.CLOCKING.INDX] & Card.SEQ.CLOCKING.DOTCLOCK);
var nCRTCVertTotal = card.regCRTData[Card.CRTC.EGA.VTOTAL];
nCRTCVertTotal |= ((card.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT8)? 0x100 : 0);
if (card.nCard == Video.CARD.VGA) {
nCRTCVertTotal |= ((card.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT9)? 0x200 : 0);
}
var nCRTCVertTotal = card.getCRTCReg(Card.CRTC.EGA.VTOTAL);
var nCRTCMaxScan = card.regCRTData[Card.CRTC.EGA.MAX_SCAN.INDX];
var nCRTCModeCtrl = card.regCRTData[Card.CRTC.EGA.MODE_CTRL.INDX];
var fSEQDotClock = (card.regSEQData[Card.SEQ.CLOCKING.INDX] & Card.SEQ.CLOCKING.DOTCLOCK);
if (nMode != Video.MODE.UNKNOWN) {
if (!(regGRCMisc & Card.GRC.MISC.GRAPHICS)) {
/*
@ -4991,6 +5030,11 @@ Video.prototype.checkMode = function(fForce)
*/
if (card.regGRCData[Card.GRC.MODE.INDX] & Card.GRC.MODE.COLOR256) {
if (nCRTCMaxScan & Card.CRTC.EGA.MAX_SCAN.SCAN_LINE) {
/*
* NOTE: Technically, VDISP_END is one of those CRTC registers that should be read using
* card.getCRTCReg(), because there are overflow bits (8 and 9). However, all known modes
* always SET bit 8 and CLEAR bit 9, so examining only bits 0-7 is sorta OK.
*/
if (card.regCRTData[Card.CRTC.EGA.VDISP_END] <= 0x8F) {
nMode = Video.MODE.VGA_320X200;
}
@ -5052,7 +5096,7 @@ Video.prototype.checkMode = function(fForce)
* setMode(nMode, fForce)
*
* Set fForce to true to update the mode regardless of previous mode, or false to perform
* a normal update that bypasses updateScreen() but still calls initCellCache().
* a normal update that bypasses updateScreen() but still calls initCache().
*
* @this {Video}
* @param {number|null} nMode
@ -5123,7 +5167,7 @@ Video.prototype.setMode = function(nMode, fForce)
}
}
this.setDimensions();
this.invalidateScreen(true);
this.invalidateCache(true);
this.updateScreen();
}
return true;
@ -5150,17 +5194,17 @@ Video.prototype.setPixel = function(imageData, x, y, rgb)
};
/**
* initCellCache()
* initCache()
*
* Initializes the contents of our internal cell cache.
*
* TODO: Consider changing this to a cache of RGB values, so that when the buffer is merely being color-cycled,
* we don't have to update the entire screen. This will also allow invalidateScreen() to honor the fModified flag,
* bypassing initCellCache() when it is false.
* we don't have to update the entire screen. This will also allow invalidateCache() to honor the fModified flag,
* bypassing initCache() when it is false.
*
* @this {Video}
*/
Video.prototype.initCellCache = function()
Video.prototype.initCache = function()
{
this.cBlinkVisible = -1; // invalidate the visible blinking character count, to force updateScreen() to recount
this.fCellCacheValid = false;
@ -5171,7 +5215,7 @@ Video.prototype.initCellCache = function()
};
/**
* invalidateScreen(fModified)
* invalidateCache(fModified)
*
* Ensure that the next updateScreen() will update every cell; intended for situations where the entire screen needs
* to be redrawn, even though the underlying data in the video buffer has not changed (and therefore cleanMemory() will
@ -5182,10 +5226,10 @@ Video.prototype.initCellCache = function()
* @this {Video}
* @param {boolean} [fModified] (true if the buffer may have been modified, false if only color(s) may have changed)
*/
Video.prototype.invalidateScreen = function(fModified)
Video.prototype.invalidateCache = function(fModified)
{
if (!fModified) this.fRGBValid = false;
this.initCellCache();
this.initCache();
};
/**
@ -5338,7 +5382,7 @@ Video.prototype.updateChar = function(col, row, data, context)
* are the periodic updates coming from the CPU.
*
* For every cell in the video buffer, compare it to the cell stored in the cell cache, render if it differs,
* and then update the cell cache to match. Since initCellCache() sets every cell in the cell cache to an
* 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}
@ -5370,7 +5414,7 @@ Video.prototype.updateScreen = function(fForce)
if (!fEnabled && !fForce) return;
if (fForce) {
this.initCellCache();
this.initCache();
}
else {
/*
@ -5433,7 +5477,14 @@ Video.prototype.updateScreen = function(fForce)
* multi-display configuration.
*/
if ((this.getRetraceBits(card) & Card.CGA.STATUS.VRETRACE) || card.nVertPeriodsStartAddr && card.nVertPeriodsStartAddr < card.nVertPeriods) {
card.offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
/*
* PARANOIA: Don't call invalidateCache() unless the address we're about to "latch" actually changed.
*/
var offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
if (card.offStartAddr !== offStartAddr) {
card.offStartAddr = offStartAddr;
this.invalidateCache();
}
card.nVertPeriodsStartAddr = 0;
}
@ -5495,8 +5546,18 @@ Video.prototype.updateScreen = function(fForce)
if (!fForce && this.fCellCacheValid && this.bus.cleanMemory(addrScreen, cbScreen)) {
if (!fBlinkUpdate) return;
if (!this.cBlinkVisible) {
if (this.iCellCursor < 0) return;
iCell = this.iCellCursor;
/*
* Note that since iCellCursor is a cell-based (not byte-based) index, we must subtract
* offStartAddr, which is also cell-based; subtracting offScreen would not be appropriate,
* as it has already been converted to a byte-based offset (remember that in text modes,
* cell are words, not bytes).
*/
iCell = this.iCellCursor - card.offStartAddr;
/*
* Note that iCellCursor may have already been negative (-1 hides the cursor), and
* since offStartAddr should never be negative, we only need one iCell underflow check.
*/
if (iCell < 0) return;
nCells = iCell + 1;
}
// else if (this.cBlinks & 0x1) return;
@ -5564,6 +5625,12 @@ Video.prototype.updateScreenText = function(addrScreen, addrScreenLimit, iCell,
fBlinkEnable = (this.cardActive.regATCData[Card.ATC.MODE.INDX] & Card.ATC.MODE.BLINK_ENABLE);
}
/*
* Since iCell is always relative to addrScreen, we must make iCellCursor similarly relative,
* otherwise the cursor test below fails when the active video page is something other than page 0.
*/
var iCellCursor = this.iCellCursor - this.cardActive.offStartAddr;
if (fBlinkEnable) {
dataBlink = (Video.ATTRS.BGND_BLINK << 8);
dataMask &= ~dataBlink;
@ -5578,7 +5645,7 @@ Video.prototype.updateScreenText = function(addrScreen, addrScreenLimit, iCell,
this.cBlinkVisible++;
data &= dataMask;
}
if (iCell == this.iCellCursor) {
if (iCell == iCellCursor) {
data |= ((this.cBlinks & 0x1)? (Video.ATTRS.DRAW_CURSOR << 8) : 0);
}
this.assert(iCell < this.aCellCache.length);
@ -5756,28 +5823,20 @@ Video.prototype.updateScreenGraphicsEGA = function(addrBuffer, addrScreen, addrS
if (x < xDirty) xDirty = x;
for (iPixel = 0; iPixel < nPixels; iPixel++) {
/*
* We must follow the golden JavaScript rule of appending "|0" to all hex constants with bit 31 set.
* The innocuous use of the bit-wise OR operator has the side-effect of producing a negative value,
* matching how entries in Video.aEGADWToByte are initialized (eg, "Video.aEGADWToByte[0x80000000|0]").
*/
var dwPixel = data & (0x80808080|0);
/*
* This was the old approach to dealing with negative hex values, by converting them to positive
* values that didn't alter the low 32 bits. But it's not ideal, because it requires using values here
* and in the array that are outside the signed 32-bit range, potentially triggering floating-point.
*
* if (dwPixel < 0) dwPixel += 0x100000000;
*
* An even simpler solution would be to use the unsigned right-shift operator:
*
* dwPixel >>> 0
*
* but again, all that does is produce a value outside the signed 32-bit range, which is sub-optimal.
* 0x80808080 may LOOK like a 32-bit value, but it is not, because JavaScript treats it as a POSITIVE
* number, and therefore outside the normal 32-bit integer range; however, the AND operator guarantees
* that the result will be a 32-bit value, so it doesn't matter.
*/
var dwPixel = data & 0x80808080;
this.assert(Video.aEGADWToByte[dwPixel] !== undefined);
/*
* Since assertions don't fix problems (only catch them, and only in DEBUG builds), I'm also ensuring
* that bPixel will always default to 0 if an undefined value ever slips through again.
* that bPixel will default to 0 if an undefined value ever slips through again.
*
* How did an undefined value slip through? We had (incorrectly) initialized entries in aEGADWToByte;
* for example, we used to set aEGADWToByte[0x80808080] instead of aEGADWToByte[0x80808080|0]. The
* former is a POSITIVE index that is outside the 32-bit integer range, whereas the latter is a NEGATIVE
* index, which is what this code requires.
*/
var bPixel = Video.aEGADWToByte[dwPixel] || 0;
this.setPixel(this.imageScreenBuffer, x++, y, aPixelColors[bPixel]);
@ -5921,8 +5980,8 @@ Video.prototype.getRetraceBits = function(card)
/*
* NOTE: The CGA bits CGA.STATUS.RETRACE (0x01) and CGA.STATUS.VRETRACE (0x08) match the EGA definitions,
* and they also correspond to the MDA bits MDA.STATUS.HDRIVE (0x01) and MDA.STATUS.BWVIDEO (0x08); I'm not sure why
* the MDA uses different designations, but the bits appear to serve the same purpose.
* and they also correspond to the MDA bits MDA.STATUS.HDRIVE (0x01) and MDA.STATUS.BWVIDEO (0x08); I'm not sure
* why the MDA uses different designations, but the bits appear to serve the same purpose.
*
* TODO: Decide whether this more faithful emulation of the retrace bits should be extended to the MDA/CGA, too;
* doing so might slow down the BIOS scroll code a bit, though.
@ -6168,8 +6227,14 @@ Video.prototype.outATC = function(port, bOut, addrFrom)
/*
* HACK: offStartAddr is supposed to be "latched" ONLY at the start of every VRETRACE interval, but
* other "triggers" are helpful; see updateScreen() for details.
*
* PARANOIA: Don't call invalidateCache() unless the start address we just "latched" actually changed.
*/
card.offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
var offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
if (card.offStartAddr != offStartAddr) {
card.offStartAddr = offStartAddr;
this.invalidateCache();
}
card.nVertPeriodsStartAddr = 0;
} else {
card.fATCData = false;
@ -6180,7 +6245,7 @@ Video.prototype.outATC = function(port, bOut, addrFrom)
this.printMessageIO(port, bOut, addrFrom, "ATC." + card.asATCRegs[iReg]);
}
card.regATCData[iReg] = bOut;
this.invalidateScreen(false);
this.invalidateCache(false);
}
}
}
@ -6516,7 +6581,7 @@ Video.prototype.outDACData = function(port, bOut, addrFrom)
var dwNew = (dw & ~(0x3f << this.cardEGA.regDACShift)) | ((bOut & 0x3f) << this.cardEGA.regDACShift);
if (dw !== dwNew) {
this.cardEGA.regDACData[this.cardEGA.regDACAddr] = dwNew;
this.invalidateScreen(false);
this.invalidateCache(false);
}
this.cardEGA.regDACShift += 6;
if (this.cardEGA.regDACShift > 12) {
@ -6801,7 +6866,7 @@ Video.prototype.outCGAColor = function(port, bOut, addrFrom)
}
if (this.cardColor.regColor !== bOut) {
this.cardColor.regColor = bOut;
this.invalidateScreen(false);
this.invalidateCache(false);
}
};
@ -6915,7 +6980,14 @@ Video.prototype.outCRTCData = function(card, port, bOut, addrFrom)
* the vertical period count and latch it later, in updateScreen(), once the count has advanced.
*/
if (this.getRetraceBits(card) & Card.CGA.STATUS.RETRACE) {
card.offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
/*
* PARANOIA: Don't call invalidateCache() unless the address we're about to "latch" actually changed.
*/
var offStartAddr = ((card.regCRTData[Card.CRTC.START_ADDR_HI] << 8) + card.regCRTData[Card.CRTC.START_ADDR_LO])|0;
if (card.offStartAddr !== offStartAddr) {
card.offStartAddr = offStartAddr;
this.invalidateCache();
}
} else if (!card.nVertPeriodsStartAddr) {
card.nVertPeriodsStartAddr = card.nVertPeriods;
}

View file

@ -1116,4 +1116,8 @@
</xsl:call-template>
</xsl:template>
<xsl:template match="comment">
<xsl:comment><xsl:apply-templates/></xsl:comment>
</xsl:template>
</xsl:stylesheet>

View file

@ -63,7 +63,8 @@ var DumpAPI = {
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 disk size in Mb (formerly "mbsize") (DiskDump only)
MBHD: "mbhd", // value is hard disk 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

View file

@ -630,21 +630,21 @@ web.downloadFile = function(sData, sType, fBase64, sFileName)
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 (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 requires the link to be in the body
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 + '"';
sAlert = 'Check your Downloads folder for ' + sFileName + '.';
} else {
window.open(sURI);
sAlert = 'Check your browser for a new window/tab containing the requested data';
sAlert = 'Check your browser for a new window/tab containing the requested data (' + sFileName + ').';
}
return sAlert;
};