Space Invaders works on the desktop now

This commit is contained in:
Jeff Parsons 2016-05-14 23:12:39 -07:00
commit f09189c121
9 changed files with 269 additions and 116 deletions

View file

@ -107,24 +107,24 @@ Component.subclass(ChipSet);
ChipSet.SI_1978 = {
MODEL: 1978.1,
STATUS0: {
STATUS0: { // NOTE: STATUS0 not used by the SI_1978 ROMs; refer to STATUS1 instead
PORT: 0,
DIP4: 0x01, // self-test request at power up?
ALWAYS_SET: 0x0E, // always set
FIRE: 0x10, // 1 = fire
LEFT: 0x20, // 1 = move left
RIGHT: 0x40, // 1 = move right
PORT7: 0x80 // some connection to (undocumented) port 7
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
ALWAYS_SET: 0x08, // always set
P1_FIRE: 0x10, // 1 = P1 fire (cocktail machines only?)
P1_LEFT: 0x20, // 1 = P1 left (cocktail machines only?)
P1_RIGHT: 0x40 // 1 = P1 right (cocktail machines only?)
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,
@ -134,7 +134,8 @@ ChipSet.SI_1978 = {
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
DIP7: 0x80, // 0 = display coin info on demo ("attract") screen
ALWAYS_SET: 0x00
},
SHIFT_RESULT: { // bits 0-7 of barrel shifter result
PORT: 3
@ -273,9 +274,9 @@ ChipSet.prototype.powerDown = function(fSave, fShutdown)
*/
ChipSet.prototype.reset = function()
{
this.bStatus0 = 0;
this.bStatus1 = 0;
this.bStatus2 = 0;
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;
@ -351,6 +352,45 @@ ChipSet.prototype.stop = function()
*/
};
/**
* 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)
*

View file

@ -37,8 +37,7 @@ if (NODE) {
var web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
var Messages = require("./messages");
var State = require("./state");
var CPU = require("./cpu");
var ChipSet = require("./chipset");
}
/**
@ -52,6 +51,8 @@ function Keyboard(parmsKbd)
{
Component.call(this, "Keyboard", parmsKbd, Keyboard, Messages.KEYBOARD);
this.reset();
this.setReady();
}
@ -122,6 +123,16 @@ Keyboard.KEYCODE = {
/* 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
@ -210,6 +221,62 @@ Keyboard.STUPID_KEYCODES[Keyboard.KEYCODE.RBRACK] = Keyboard.ASCII[']']; // 2
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.FIVE,
'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)
*
@ -257,68 +324,24 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
control.onkeydown = function onKeyDown(event) {
return kbd.onKeyDown(event, true);
};
control.onkeypress = function onKeyPressKbd(event) {
return kbd.onKeyPress(event);
};
control.onkeyup = function onKeyUp(event) {
return kbd.onKeyDown(event, false);
};
return true;
case "caps-lock":
this.bindings[id] = control;
control.onclick = function onClickCapsLock(event) {
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleCapsLock();
};
return true;
case "num-lock":
this.bindings[id] = control;
control.onclick = function onClickNumLock(event) {
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleNumLock();
};
return true;
case "scroll-lock":
this.bindings[id] = control;
control.onclick = function onClickScrollLock(event) {
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.toggleScrollLock();
};
return true;
default:
/*
* Maintain support for older button codes; eg, map button code "ctrl-c" to CLICKCODE "CTRL_C"
*/
var sCode = sBinding.toUpperCase().replace(/-/g, '_');
if (Keyboard.CLICKCODES[sCode] !== undefined && sHTMLType == "button") {
if (Keyboard.SOFTCODES[sBinding] !== undefined) {
this.bindings[id] = control;
control.onclick = function(kbd, sKey, simCode) {
return function onClickKeyboard(event) {
if (!COMPILED && kbd.messageEnabled()) kbd.printMessage(sKey + " clicked", Messages.KEYS);
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);
};
}(this, sCode, Keyboard.CLICKCODES[sCode]);
return true;
}
else if (Keyboard.SOFTCODES[sBinding] !== undefined) {
this.cSoftCodes++;
this.bindings[id] = control;
var fnDown = function(kbd, sKey, simCode) {
var fnDown = function(kbd, sSoftCode) {
return function onMouseOrTouchDownKeyboard(event) {
kbd.addActiveKey(simCode);
kbd.onSoftKeyDown(sSoftCode, true);
};
}(this, sBinding, Keyboard.SOFTCODES[sBinding]);
var fnUp = function (kbd, sKey, simCode) {
}(this, sBinding);
var fnUp = function (kbd, sSoftCode) {
return function onMouseOrTouchUpKeyboard(event) {
kbd.removeActiveKey(simCode);
kbd.onSoftKeyDown(sSoftCode, false);
};
}(this, sBinding, Keyboard.SOFTCODES[sBinding]);
}(this, sBinding);
if ('ontouchstart' in window) {
control.ontouchstart = fnDown;
control.ontouchend = fnUp;
@ -328,19 +351,92 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
}
return true;
}
else if (sValue) {
/*
* Instead of just having a dedicated "test" control, we now treat any unrecognized control with
* a data value as a test control. The only caveat is that such controls must have binding IDs that
* do not conflict with predefined controls (which, of course, is the only way you can get here).
*/
this.bindings[id] = control;
control.onclick = function onClickTest(event) {
if (kbd.cmp) kbd.cmp.updateFocus();
return kbd.injectKeys(sValue);
};
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;
}
}

View file

@ -245,6 +245,16 @@ Video.prototype.initBus = function(cmp, bus, cpu, dbg)
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();
};

View file

@ -566,6 +566,11 @@ Keyboard.KEYSTATES[Keyboard.SIMCODE.SCROLL_LOCK] = Keyboard.STATE.SCROLL_LOCK;
/**
* Maps CLICKCODE (string) to SIMCODE (number).
*
* NOTE: Unlike SOFTCODES, CLICKCODES are upper-case and use underscores instead of dashes, so that this
* and other components can reference them using "dot" property syntax; using upper-case merely adheres to
* our convention for constants. setBinding() will automatically convert any incoming CLICKCODE bindings
* that use lower-case and dashes to upper-case and underscores before performing property lookup.
*
* @enum {number}
*/
Keyboard.CLICKCODES = {

View file

@ -165,7 +165,7 @@ function Video(parmsVideo, canvas, context, textarea, container)
*/
var fSmoothing = parmsVideo['smoothing'];
var sSmoothing = Component.parmsURL['smoothing'];
if (sSmoothing) fSmoothing = (sSmoothing == "true")? true : false;
if (sSmoothing) fSmoothing = (sSmoothing == "true");
if (fSmoothing != null) {
for (i = 0; i < asWebPrefixes.length; i++) {
sEvent = asWebPrefixes[i];
@ -4560,8 +4560,8 @@ Video.prototype.removeCursor = function()
/*
* If we're using an off-screen buffer in text mode, then we need to keep it in sync with "reality".
*/
if (this.contextScreenBuffer) {
this.updateChar(col, row, data, this.contextScreenBuffer);
if (this.contextBuffer) {
this.updateChar(col, row, data, this.contextBuffer);
}
/*
* While updating the on-screen canvas directly could open us up to potential subpixel artifacts again,
@ -4856,11 +4856,11 @@ Video.prototype.setDimensions = function()
}
/*
* In text modes, we have the option of setting all the *ScreenBuffer variables to null instead of
* In text modes, we have the option of setting all the *Buffer variables to null instead of
* allocating them, because updateChar(), as currently written, is capable of writing characters to
* either an off-screen or on-screen context.
*
* this.imageScreenBuffer = this.canvasScreenBuffer = this.contextScreenBuffer = null;
* this.imageBuffer = this.canvasBuffer = this.contextBuffer = null;
*/
this.cxBuffer = this.cyBuffer = 0;
if (font) {
@ -4888,11 +4888,11 @@ Video.prototype.setDimensions = function()
/*
* Allocate the off-screen buffers
*/
this.imageScreenBuffer = this.contextScreen.createImageData(this.cxBuffer, this.cyBuffer);
this.canvasScreenBuffer = document.createElement("canvas");
this.canvasScreenBuffer.width = this.cxBuffer;
this.canvasScreenBuffer.height = this.cyBuffer;
this.contextScreenBuffer = this.canvasScreenBuffer.getContext("2d");
this.imageBuffer = this.contextScreen.createImageData(this.cxBuffer, this.cyBuffer);
this.canvasBuffer = document.createElement("canvas");
this.canvasBuffer.width = this.cxBuffer;
this.canvasBuffer.height = this.cyBuffer;
this.contextBuffer = this.canvasBuffer.getContext("2d");
/*
* Since cxCell and cyCell were originally defined in terms of cxScreen/nCols and cyScreen/nRows, you might think
@ -5676,7 +5676,7 @@ Video.prototype.updateScreenText = function(addrScreen, addrScreenLimit, iCell,
if (!this.fCellCacheValid || data !== this.aCellCache[iCell]) {
var col = iCell % this.nCols;
var row = (iCell / this.nCols)|0;
this.updateChar(col, row, data, this.contextScreenBuffer);
this.updateChar(col, row, data, this.contextBuffer);
this.aCellCache[iCell] = data;
cUpdated++;
}
@ -5686,8 +5686,8 @@ Video.prototype.updateScreenText = function(addrScreen, addrScreenLimit, iCell,
this.fCellCacheValid = true;
if (cUpdated && this.contextScreenBuffer) {
this.contextScreen.drawImage(this.canvasScreenBuffer, 0, 0, this.cxBuffer, this.cyBuffer, this.xScreenOffset, this.yScreenOffset, this.cxScreenOffset, this.cyScreenOffset);
if (cUpdated && this.contextBuffer) {
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.cxBuffer, this.cyBuffer, this.xScreenOffset, this.yScreenOffset, this.cxScreenOffset, this.cyScreenOffset);
}
};
@ -5725,7 +5725,7 @@ Video.prototype.updateScreenGraphicsCGA = function(addrScreen, addrScreenLimit)
if (x < xDirty) xDirty = x;
for (var iPixel = 0; iPixel < nPixelsPerCell; iPixel++) {
var bPixel = (wPixels & (wMask >>= nPixelShift)) >> (nShift -= nPixelShift);
this.setPixel(this.imageScreenBuffer, x++, y, aPixelColors[bPixel]);
this.setPixel(this.imageBuffer, x++, y, aPixelColors[bPixel]);
}
if (x > xMaxDirty) xMaxDirty = x;
if (y < yDirty) yDirty = y;
@ -5748,30 +5748,30 @@ Video.prototype.updateScreenGraphicsCGA = function(addrScreen, addrScreenLimit)
this.fCellCacheValid = true;
/*
* Instead of blasting the ENTIRE imageScreenBuffer into contextScreenBuffer, and then blasting the ENTIRE
* canvasScreenBuffer onto contextScreen, even for the smallest change, let's try to be a bit smarter about
* 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.nCols) {
var cxDirty = xMaxDirty - xDirty;
var cyDirty = yMaxDirty - yDirty;
// this.contextScreenBuffer.putImageData(this.imageScreenBuffer, 0, 0);
this.contextScreenBuffer.putImageData(this.imageScreenBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
// this.contextBuffer.putImageData(this.imageBuffer, 0, 0);
this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
/*
* While ideally I would draw only the dirty portion of canvasScreenBuffer, there usually isn't a 1-1 pixel mapping
* between canvasScreenBuffer and contextScreen. In fact, the WHOLE POINT of the canvasScreenBuffer is to leverage
* While ideally I would draw only the dirty portion of canvasBuffer, there usually isn't a 1-1 pixel mapping
* between canvasBuffer and contextScreen. In fact, the WHOLE POINT of the canvasBuffer is to leverage
* drawImage()'s scaling ability; for example, a CGA graphics mode might be 640x200, whereas the canvas representing
* the screen might be 960x400. In those situations, if we draw interior rectangles, we often end up with subpixel
* artifacts along the edges of those rectangles. So it appears I must continue to redraw the entire canvasScreenBuffer
* artifacts along the edges of those rectangles. So it appears I must continue to redraw the entire canvasBuffer
* on every change.
*
var xScreen = (((xDirty * this.cxScreen) / this.nCols) | 0);
var yScreen = (((yDirty * this.cyScreen) / this.nRows) | 0);
var cxScreen = (((cxDirty * this.cxScreen) / this.nCols) | 0);
var cyScreen = (((cyDirty * this.cyScreen) / this.nRows) | 0);
this.contextScreen.drawImage(this.canvasScreenBuffer, xDirty, yDirty, cxDirty, cyDirty, xScreen, yScreen, cxScreen, cyScreen);
this.contextScreen.drawImage(this.canvasBuffer, xDirty, yDirty, cxDirty, cyDirty, xScreen, yScreen, cxScreen, cyScreen);
*/
this.contextScreen.drawImage(this.canvasScreenBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
}
};
@ -5863,7 +5863,7 @@ Video.prototype.updateScreenGraphicsEGA = function(addrBuffer, addrScreen, addrS
* index, which is what this code requires.
*/
var bPixel = Video.aEGADWToByte[dwPixel] || 0;
this.setPixel(this.imageScreenBuffer, x++, y, aPixelColors[bPixel]);
this.setPixel(this.imageBuffer, x++, y, aPixelColors[bPixel]);
data <<= 1;
}
if (x > xMaxDirty) xMaxDirty = x;
@ -5888,8 +5888,8 @@ Video.prototype.updateScreenGraphicsEGA = function(addrBuffer, addrScreen, addrS
if (xDirty < this.nCols) {
var cxDirty = xMaxDirty - xDirty;
var cyDirty = yMaxDirty - yDirty;
this.contextScreenBuffer.putImageData(this.imageScreenBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
this.contextScreen.drawImage(this.canvasScreenBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
}
};
@ -5957,7 +5957,7 @@ Video.prototype.updateScreenGraphicsVGA = function(addrBuffer, addrScreen, addrS
if (nPixels) {
if (x < xDirty) xDirty = x;
for (iPixel = 0; iPixel < nPixels; iPixel++) {
this.setPixel(this.imageScreenBuffer, x++, y, aPixelColors[data & 0xff]);
this.setPixel(this.imageBuffer, x++, y, aPixelColors[data & 0xff]);
data >>= 8;
}
if (x > xMaxDirty) xMaxDirty = x;
@ -5984,8 +5984,8 @@ Video.prototype.updateScreenGraphicsVGA = function(addrBuffer, addrScreen, addrS
if (xDirty < this.nCols) {
var cxDirty = xMaxDirty - xDirty;
var cyDirty = yMaxDirty - yDirty;
this.contextScreenBuffer.putImageData(this.imageScreenBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
this.contextScreen.drawImage(this.canvasScreenBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.nCols, this.nRows, 0, 0, this.cxScreen, this.cyScreen);
}
};