Merged next-release
This commit is contained in:
commit
ab0eade969
274 changed files with 38279 additions and 804 deletions
|
|
@ -72,4 +72,4 @@ var C1PJS = {
|
|||
PRIVATE: PRIVATE, // shared
|
||||
SITEHOST: SITEHOST, // shared
|
||||
XMLVERSION: XMLVERSION // shared
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -794,8 +794,10 @@ Bus.prototype.addPortInputNotify = function(start, end, fn)
|
|||
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));
|
||||
if (table) {
|
||||
for (var port in table) {
|
||||
this.addPortInputNotify(+port + offset, +port + offset, table[port].bind(component));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -951,8 +953,10 @@ Bus.prototype.addPortOutputNotify = function(start, end, fn)
|
|||
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));
|
||||
if (table) {
|
||||
for (var port in table) {
|
||||
this.addPortOutputNotify(+port + offset, +port + offset, table[port].bind(component));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ function ChipSet(parmsChipSet)
|
|||
Component.notice("Unrecognized ChipSet model: " + model);
|
||||
}
|
||||
|
||||
this.model = ChipSet.MODELS[model] || ChipSet.SI_1978.MODEL;
|
||||
this.config = ChipSet.MODELS[model] || ChipSet.SI1978;
|
||||
this.model = this.config.MODEL;
|
||||
|
||||
this.bSwitches = this.parseDIPSwitches(parmsChipSet['swDIP']);
|
||||
|
||||
|
|
@ -104,9 +105,9 @@ function ChipSet(parmsChipSet)
|
|||
|
||||
Component.subclass(ChipSet);
|
||||
|
||||
ChipSet.SI_1978 = {
|
||||
ChipSet.SI1978 = {
|
||||
MODEL: 1978.1,
|
||||
STATUS0: { // NOTE: STATUS0 not used by the SI_1978 ROMs; refer to STATUS1 instead
|
||||
STATUS0: { // NOTE: STATUS0 not used by the SI1978 ROMs; refer to STATUS1 instead
|
||||
PORT: 0,
|
||||
DIP4: 0x01, // self-test request at power up?
|
||||
FIRE: 0x10, // 1 = fire
|
||||
|
|
@ -165,11 +166,80 @@ ChipSet.SI_1978 = {
|
|||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* One of the many chips in the VT100 is an 8224, which operates at 24.8832MHz. That frequency is divided by 9
|
||||
* to yield a 361.69ns clock period for the 8080 CPU, which means the CPU is running at 2.76Mhz (cycles per second).
|
||||
* Hence the CPU component in the VT100's machine.xml is defined as:
|
||||
*
|
||||
* <cpu id="cpu8080" model="8080" cycles="2764798"/>
|
||||
*
|
||||
* Beyond that, we don't really care about that particular 8224. I only mention it because knowing the CPU frequency
|
||||
* is helpful for simulating some of the other circuits below that we DO care about.
|
||||
*/
|
||||
ChipSet.VT100 = {
|
||||
MODEL: 100.0,
|
||||
FLAGS_BUFFER: {
|
||||
PORT: 0x42, // read-only
|
||||
XMIT: 0x01, // active if SET
|
||||
NO_AVO: 0x02, // AVO present if CLEAR
|
||||
NO_GFX: 0x04, // VT125 graphics board present if CLEAR
|
||||
OPTION: 0x08, // OPTION present if SET
|
||||
NO_EVEN: 0x10, // EVEN FIELD active if CLEAR
|
||||
NVR_DATA: 0x20, // NVR DATA if SET
|
||||
NVR_CLK: 0x40, // NVR CLOCK if SET
|
||||
KBD_XMIT: 0x80 // KBD XMIT BUFFER empty if SET
|
||||
},
|
||||
BRIGHTNESS_LATCH: {
|
||||
PORT: 0x42, // write-only
|
||||
INIT: 0x00 // for lack of a better guess
|
||||
},
|
||||
NVR_LATCH: {
|
||||
PORT: 0x62, // write-only
|
||||
INIT: 0x00 // for lack of a better guess
|
||||
},
|
||||
DC012: { // generates scan counts for the Video Processor
|
||||
PORT: 0xA2, // write-only
|
||||
INIT: 0x00 // for lack of a better guess
|
||||
},
|
||||
/*
|
||||
* As p. 4-55 (105) of the July 1982 Technical Manual explains:
|
||||
*
|
||||
* The DCO11 is a custom designed bipolar circuit that provides most of the timing signals required by the
|
||||
* video processor. Internal counters divide the output of a 24.0734 MHz oscillator (located elsewhere on the
|
||||
* terminal controller module) into the lower frequencies that define dot, character, scan, and frame timing.
|
||||
* The counters are programmable through various input pins to control the number of characters per line,
|
||||
* the frequency at which the screen is refreshed, and whether the display is interlaced or noninterlaced.
|
||||
* These parameters can be controlled through SET-UP mode or by the host.
|
||||
*
|
||||
* On p. 4-56, the DC011 Block Diagram shows 8 outputs labeled LBA0 through LBA7. From p. 4-61:
|
||||
*
|
||||
* Several of the LBAs are used as general purpose clocks in the VT100. LBA 3 and LBA 4 are used to generate
|
||||
* timing for the keyboard. These signals satisfy the keyboard's requirement of two square-waves, one twice the
|
||||
* frequency of the other, even though every 16th transition is delayed (the second stage of the horizontal
|
||||
* counter divides by 17, not 16). LBA 7 is used by the nonvolatile RAM.
|
||||
*
|
||||
* And on p. 4-62, timings are provided for the LBA0 through LBA7 when the VT100 is in 80-column mode; in particular:
|
||||
*
|
||||
* LBA6: 16.82353us (when LBA6 is low, for a period is 33.64706us)
|
||||
* LBA7: 31.77778us (when LBA7 is high, for a period is 63.55556us)
|
||||
*
|
||||
* If we assume that the CPU cycle count increments once every 361.69ns, it will increment roughly 88 times every
|
||||
* time LBA7 toggles. So we can divide the CPU cycle count by 88 and set LBA to the low bit of that truncated
|
||||
* result. An even faster (but less accurate) solution would be to mask bit 6 of the CPU cycle count, which will
|
||||
* doesn't change until the count has been incremented 64 times. See getVT100LBA() for the chosen implementation.
|
||||
*/
|
||||
DC011: { // generates Line Buffer Addresses (LBAs) for the Video Processor
|
||||
PORT: 0xC2, // write-only
|
||||
INIT: 0x00 // for lack of a better guess
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Supported model strings
|
||||
*/
|
||||
ChipSet.MODELS = {
|
||||
"SI1978": ChipSet.SI_1978.MODEL
|
||||
"SI1978": ChipSet.SI1978,
|
||||
"VT100": ChipSet.VT100
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -227,10 +297,8 @@ ChipSet.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|||
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);
|
||||
}
|
||||
bus.addPortInputTable(this, this.config.portsInput);
|
||||
bus.addPortOutputTable(this, this.config.portsOutput);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -266,6 +334,25 @@ ChipSet.prototype.powerDown = function(fSave, fShutdown)
|
|||
return fSave? this.save() : true;
|
||||
};
|
||||
|
||||
ChipSet.SI1978.init = [
|
||||
[
|
||||
ChipSet.SI1978.STATUS0.ALWAYS_SET,
|
||||
ChipSet.SI1978.STATUS1.ALWAYS_SET,
|
||||
ChipSet.SI1978.STATUS2.ALWAYS_SET,
|
||||
0, 0, 0, 0
|
||||
]
|
||||
];
|
||||
|
||||
ChipSet.VT100.init = [
|
||||
[
|
||||
ChipSet.VT100.BRIGHTNESS_LATCH.INIT,
|
||||
ChipSet.VT100.NVR_LATCH.INIT,
|
||||
ChipSet.VT100.FLAGS_BUFFER.NO_AVO | ChipSet.VT100.FLAGS_BUFFER.NO_GFX,
|
||||
ChipSet.VT100.DC012.INIT,
|
||||
ChipSet.VT100.DC011.INIT
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* reset()
|
||||
*
|
||||
|
|
@ -273,12 +360,9 @@ ChipSet.prototype.powerDown = function(fSave, fShutdown)
|
|||
*/
|
||||
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;
|
||||
if (!this.restore(this.config.init)) {
|
||||
this.notice("reset error");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -292,8 +376,13 @@ ChipSet.prototype.reset = function()
|
|||
ChipSet.prototype.save = function()
|
||||
{
|
||||
var state = new State(this);
|
||||
if (this.model == ChipSet.SI_1978.MODEL) {
|
||||
switch(this.model) {
|
||||
case ChipSet.SI1978.MODEL:
|
||||
state.set(0, [this.bStatus0, this.bStatus1, this.bStatus2, this.wShiftData, this.bShiftCount, this.bSound1, this.bSound2]);
|
||||
break;
|
||||
case ChipSet.VT100.MODEL:
|
||||
state.set(0, [this.bBrightnessLatch, this.bNVRLatch, this.bFlagsBuffer, this.bDC012, this.bDC011]);
|
||||
break;
|
||||
}
|
||||
return state.data();
|
||||
};
|
||||
|
|
@ -309,18 +398,28 @@ ChipSet.prototype.save = function()
|
|||
*/
|
||||
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];
|
||||
var a;
|
||||
if (data && (a = data[0]) && a.length) {
|
||||
switch(this.model) {
|
||||
case ChipSet.SI1978.MODEL:
|
||||
this.bStatus0 = a[0];
|
||||
this.bStatus1 = a[1];
|
||||
this.bStatus2 = a[2];
|
||||
this.wShiftData = a[3];
|
||||
this.bShiftCount = a[4];
|
||||
this.bSound1 = a[5];
|
||||
this.bSound2 = a[6];
|
||||
return true;
|
||||
case ChipSet.VT100.MODEL:
|
||||
this.bBrightnessLatch = a[0];
|
||||
this.bNVRLatch = a[1];
|
||||
this.bFlagsBuffer = a[2];
|
||||
this.bDC012 = a[3];
|
||||
this.bDC011 = a[4];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -519,20 +618,109 @@ ChipSet.prototype.outSIWatchdog = function(port, b, addrFrom)
|
|||
this.printMessageIO(port, b, addrFrom, "WATCHDOG", null, true);
|
||||
};
|
||||
|
||||
/*
|
||||
* Port input notification tables
|
||||
/**
|
||||
* getVT100LBA(nBit)
|
||||
*
|
||||
* Returns the state of the requested (simulated) LBA bit.
|
||||
*
|
||||
* NOTE: This is currently only used to obtain LBA7, which we approximate with the slightly faster approach
|
||||
* of masking bit 6 of the CPU cycle count (see the DC011 discussion above). This will result in a shorter LBA7
|
||||
* period than if we divided the cycle count by 88, but a shorter LBA7 period is probably helpful in terms of
|
||||
* overall performance.
|
||||
*
|
||||
* @param {number} nBit
|
||||
* @return {number}
|
||||
*/
|
||||
ChipSet.aPortInput = {
|
||||
ChipSet.prototype.getVT100LBA = function(nBit)
|
||||
{
|
||||
return (this.cpu.getCycles() & (1 << (nBit - 1))) << 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* inVT100FlagsBuffer(port, addrFrom)
|
||||
*
|
||||
* @this {ChipSet}
|
||||
* @param {number} port (0x42)
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to read the specified port)
|
||||
* @return {number} simulated port value
|
||||
*/
|
||||
ChipSet.prototype.inVT100FlagsBuffer = function(port, addrFrom)
|
||||
{
|
||||
/*
|
||||
* The NVR_CLK bit is driven by LBA7 (ie, bit 7 from Line Buffer Address generation); see the DC011 discussion above.
|
||||
*/
|
||||
var b = this.bFlagsBuffer = (this.bFlagsBuffer & ~ChipSet.VT100.FLAGS_BUFFER.NVR_CLK) | (this.getVT100LBA(7)? ChipSet.VT100.FLAGS_BUFFER.NVR_CLK : 0);
|
||||
this.printMessageIO(port, null, addrFrom, "FLAGS.BUFFER", b, true);
|
||||
return b;
|
||||
};
|
||||
|
||||
/**
|
||||
* outVT100BrightnessLatch(port, b, addrFrom)
|
||||
*
|
||||
* @this {ChipSet}
|
||||
* @param {number} port (0x42)
|
||||
* @param {number} b
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
||||
*/
|
||||
ChipSet.prototype.outVT100BrightnessLatch = function(port, b, addrFrom)
|
||||
{
|
||||
this.printMessageIO(port, b, addrFrom, "BRIGHTNESS.LATCH", null, true);
|
||||
this.bBrightnessLatch = b;
|
||||
};
|
||||
|
||||
/**
|
||||
* outVT100NVRLatch(port, b, addrFrom)
|
||||
*
|
||||
* @this {ChipSet}
|
||||
* @param {number} port (0x62)
|
||||
* @param {number} b
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
||||
*/
|
||||
ChipSet.prototype.outVT100NVRLatch = function(port, b, addrFrom)
|
||||
{
|
||||
this.printMessageIO(port, b, addrFrom, "NVR.LATCH", null, true);
|
||||
this.bNVRLatch = b;
|
||||
};
|
||||
|
||||
/**
|
||||
* outVT100DC012(port, b, addrFrom)
|
||||
*
|
||||
* @this {ChipSet}
|
||||
* @param {number} port (0xA2)
|
||||
* @param {number} b
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
||||
*/
|
||||
ChipSet.prototype.outVT100DC012 = function(port, b, addrFrom)
|
||||
{
|
||||
this.printMessageIO(port, b, addrFrom, "DC012", null, true);
|
||||
this.bDC012 = b;
|
||||
};
|
||||
|
||||
/**
|
||||
* outVT100DC011(port, b, addrFrom)
|
||||
*
|
||||
* @this {ChipSet}
|
||||
* @param {number} port (0xC2)
|
||||
* @param {number} b
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
||||
*/
|
||||
ChipSet.prototype.outVT100DC011 = function(port, b, addrFrom)
|
||||
{
|
||||
this.printMessageIO(port, b, addrFrom, "DC011", null, true);
|
||||
this.bDC011 = b;
|
||||
};
|
||||
|
||||
/*
|
||||
* Port notification tables
|
||||
*/
|
||||
ChipSet.SI1978.portsInput = {
|
||||
0x00: ChipSet.prototype.inSIStatus0,
|
||||
0x01: ChipSet.prototype.inSIStatus1,
|
||||
0x02: ChipSet.prototype.inSIStatus2,
|
||||
0x03: ChipSet.prototype.inSIShiftResult
|
||||
};
|
||||
|
||||
/*
|
||||
* Port output notification tables
|
||||
*/
|
||||
ChipSet.aPortOutput = {
|
||||
ChipSet.SI1978.portsOutput = {
|
||||
0x02: ChipSet.prototype.outSIShiftCount,
|
||||
0x03: ChipSet.prototype.outSISound1,
|
||||
0x04: ChipSet.prototype.outSIShiftData,
|
||||
|
|
@ -540,6 +728,17 @@ ChipSet.aPortOutput = {
|
|||
0x06: ChipSet.prototype.outSIWatchdog
|
||||
};
|
||||
|
||||
ChipSet.VT100.portsInput = {
|
||||
0x42: ChipSet.prototype.inVT100FlagsBuffer
|
||||
};
|
||||
|
||||
ChipSet.VT100.portsOutput = {
|
||||
0x42: ChipSet.prototype.outVT100BrightnessLatch,
|
||||
0x62: ChipSet.prototype.outVT100NVRLatch,
|
||||
0xA2: ChipSet.prototype.outVT100DC012,
|
||||
0xC2: ChipSet.prototype.outVT100DC011
|
||||
};
|
||||
|
||||
/**
|
||||
* ChipSet.init()
|
||||
*
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ if (NODE) {
|
|||
var UserAPI = require("../../shared/lib/userapi");
|
||||
var ReportAPI = require("../../shared/lib/reportapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
/*
|
||||
* TODO: I'm confused why WebStorm complains if the following require() is missing in THIS file but not other files.
|
||||
*/
|
||||
var PC8080 = require("./defines");
|
||||
var Messages = require("./messages");
|
||||
var Bus = require("./bus");
|
||||
var State = require("./state");
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ var PC8080 = {
|
|||
TYPEDARRAYS: TYPEDARRAYS,
|
||||
SITEHOST: SITEHOST, // shared
|
||||
XMLVERSION: XMLVERSION // shared
|
||||
}
|
||||
};
|
||||
|
||||
if (NODE) {
|
||||
global.APPCLASS = APPCLASS;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ function Keyboard(parmsKbd)
|
|||
|
||||
Component.subclass(Keyboard);
|
||||
|
||||
Keyboard.VT100 = {
|
||||
STATUS: {
|
||||
PORT: 0x82, // write-only
|
||||
LED4: 0x01,
|
||||
LED3: 0x02,
|
||||
LED2: 0x04,
|
||||
LED1: 0x08,
|
||||
LOCKED: 0x10,
|
||||
ONLINE: 0x20,
|
||||
START: 0x40,
|
||||
CLICK: 0x80
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Alphanumeric and other common (printable) ASCII codes.
|
||||
*
|
||||
|
|
@ -234,6 +248,8 @@ Keyboard.SOFTCODES = {
|
|||
'fire': Keyboard.KEYCODE.SPACE
|
||||
};
|
||||
|
||||
Keyboard.MINPRESSTIME = 100; // 100ms
|
||||
|
||||
/**
|
||||
* Alternate keyCode mappings (to support the popular WASD directional mappings)
|
||||
*
|
||||
|
|
@ -245,35 +261,14 @@ 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 = {};
|
||||
Keyboard.LEDSTATES = {
|
||||
'l4': Keyboard.VT100.STATUS.LED4,
|
||||
'l3': Keyboard.VT100.STATUS.LED3,
|
||||
'l2': Keyboard.VT100.STATUS.LED2,
|
||||
'l1': Keyboard.VT100.STATUS.LED1,
|
||||
'locked': Keyboard.VT100.STATUS.LOCKED,
|
||||
'online': Keyboard.VT100.STATUS.ONLINE,
|
||||
'local': ~Keyboard.VT100.STATUS.ONLINE
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -310,6 +305,12 @@ Keyboard.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|||
var id = sHTMLType + '-' + sBinding;
|
||||
|
||||
if (this.bindings[id] === undefined) {
|
||||
|
||||
if (sHTMLType == "led" && Keyboard.LEDSTATES[sBinding]) {
|
||||
this.bindings[id] = control;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (sBinding) {
|
||||
case "kbd":
|
||||
/*
|
||||
|
|
@ -369,6 +370,176 @@ 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");
|
||||
this.model = this.chipset.model;
|
||||
switch(this.model) {
|
||||
case ChipSet.VT100.MODEL:
|
||||
this.config = Keyboard.VT100;
|
||||
break;
|
||||
}
|
||||
if (this.config) {
|
||||
bus.addPortInputTable(this, this.config.portsInput);
|
||||
bus.addPortOutputTable(this, this.config.portsOutput);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
Keyboard.prototype.powerUp = function(data, fRepower)
|
||||
{
|
||||
if (!fRepower) {
|
||||
if (!data) {
|
||||
this.reset();
|
||||
} else {
|
||||
if (!this.restore(data)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* powerDown(fSave, fShutdown)
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @param {boolean} [fSave]
|
||||
* @param {boolean} [fShutdown]
|
||||
* @return {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
|
||||
*/
|
||||
Keyboard.prototype.powerDown = function(fSave, fShutdown)
|
||||
{
|
||||
return fSave? this.save() : true;
|
||||
};
|
||||
|
||||
Keyboard.VT100.init = [
|
||||
[
|
||||
0
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* reset()
|
||||
*
|
||||
* @this {Keyboard}
|
||||
*/
|
||||
Keyboard.prototype.reset = function()
|
||||
{
|
||||
/*
|
||||
* As keyDown events are encountered, a corresponding "softcode" property in keysPressed
|
||||
* is set to the timestamp of the keyDown event. When the corresponding keyUp event occurs,
|
||||
* we look at the elapsed time: if it is less than MINPRESSTIME, then we move the key
|
||||
* to keysToRelease and set a timeout handler to release the key later; otherwise, we process
|
||||
* the keyUp event immediately.
|
||||
*/
|
||||
this.keysPressed = {};
|
||||
this.keysToRelease = {};
|
||||
|
||||
if (this.config && !this.restore(this.config.init)) {
|
||||
this.notice("reset error");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* save()
|
||||
*
|
||||
* This implements save support for the Keyboard component.
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @return {Object}
|
||||
*/
|
||||
Keyboard.prototype.save = function()
|
||||
{
|
||||
var state = new State(this);
|
||||
switch(this.model) {
|
||||
case ChipSet.SI1978.MODEL:
|
||||
break;
|
||||
case ChipSet.VT100.MODEL:
|
||||
state.set(0, [this.bLEDs]);
|
||||
break;
|
||||
}
|
||||
return state.data();
|
||||
};
|
||||
|
||||
/**
|
||||
* restore(data)
|
||||
*
|
||||
* This implements restore support for the Keyboard component.
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @param {Object} data
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
Keyboard.prototype.restore = function(data)
|
||||
{
|
||||
var a;
|
||||
if (data && (a = data[0]) && a.length) {
|
||||
switch(this.model) {
|
||||
case ChipSet.SI1978.MODEL:
|
||||
return true;
|
||||
case ChipSet.VT100.MODEL:
|
||||
this.bLEDs = a[0];
|
||||
this.updateLEDs();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* setLED(control, f)
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @param {Object} control is an HTML control DOM object
|
||||
* @param {boolean} f is true if the LED represented by control should be "on", false if "off"
|
||||
*/
|
||||
Keyboard.prototype.setLED = function(control, f)
|
||||
{
|
||||
/*
|
||||
* TODO: Add support for user-definable LED colors
|
||||
*/
|
||||
control.style.backgroundColor = (f? "#ff0000" : "#000000");
|
||||
};
|
||||
|
||||
/**
|
||||
* updateLEDs()
|
||||
*
|
||||
* @this {Keyboard}
|
||||
*/
|
||||
Keyboard.prototype.updateLEDs = function()
|
||||
{
|
||||
for (var sBinding in Keyboard.LEDSTATES) {
|
||||
var id = "led-" + sBinding;
|
||||
var control = this.bindings[id];
|
||||
if (control) {
|
||||
var bitLED = Keyboard.LEDSTATES[sBinding];
|
||||
var fOn = !!(this.bLEDs & bitLED);
|
||||
if (bitLED & (bitLED-1)) {
|
||||
fOn = !(this.bLEDs & ~bitLED);
|
||||
}
|
||||
this.setLED(control, fOn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -376,7 +547,7 @@ Keyboard.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|||
*
|
||||
* @this {Keyboard}
|
||||
* @param {Object} event
|
||||
* @param {boolean} fDown is true for a keyDown event, false for a keyUp event
|
||||
* @param {boolean} fDown is true for a keyDown event, false for up
|
||||
* @return {boolean} true to pass the event along, false to consume it
|
||||
*/
|
||||
Keyboard.prototype.onKeyDown = function(event, fDown)
|
||||
|
|
@ -387,9 +558,6 @@ Keyboard.prototype.onKeyDown = function(event, fDown)
|
|||
|
||||
if (sSoftCode) {
|
||||
fPass = this.onSoftKeyDown(sSoftCode, fDown);
|
||||
}
|
||||
|
||||
if (!fPass) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
|
@ -405,41 +573,112 @@ Keyboard.prototype.onKeyDown = function(event, fDown)
|
|||
*
|
||||
* @this {Keyboard}
|
||||
* @param {string} sSoftCode
|
||||
* @param {boolean} fDown is true for a down event, false for an up event
|
||||
* @param {boolean} fDown is true for a down event, false for up
|
||||
* @return {boolean} true to pass the event along, false to consume it
|
||||
*/
|
||||
Keyboard.prototype.onSoftKeyDown = function(sSoftCode, fDown)
|
||||
{
|
||||
this.keysPressed[sSoftCode] = fDown;
|
||||
if (fDown) {
|
||||
// this.println(sSoftCode + " down");
|
||||
this.keysPressed[sSoftCode] = Date.now();
|
||||
delete this.keysToRelease[sSoftCode];
|
||||
} else {
|
||||
// this.println(sSoftCode + " up");
|
||||
var msDown = this.keysPressed[sSoftCode];
|
||||
if (msDown) {
|
||||
var msElapsed = Date.now() - msDown;
|
||||
if (msElapsed < Keyboard.MINPRESSTIME) {
|
||||
// this.println(sSoftCode + " released after only " + msElapsed + "ms");
|
||||
this.keysToRelease[sSoftCode] = msDown;
|
||||
this.checkSoftKeysToRelease();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
delete this.keysPressed[sSoftCode];
|
||||
}
|
||||
|
||||
if (this.chipset) {
|
||||
switch(sSoftCode) {
|
||||
case '1p':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.P1, fDown);
|
||||
break;
|
||||
|
||||
case '2p':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P2, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.P2, fDown);
|
||||
break;
|
||||
|
||||
case 'coin':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.CREDIT, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.CREDIT, fDown);
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_LEFT, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.P1_LEFT, fDown);
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_RIGHT, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.P1_RIGHT, fDown);
|
||||
break;
|
||||
|
||||
case 'fire':
|
||||
this.chipset.updateStatus1(ChipSet.SI_1978.STATUS1.P1_FIRE, fDown);
|
||||
this.chipset.updateStatus1(ChipSet.SI1978.STATUS1.P1_FIRE, fDown);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* checkSoftKeysToRelease()
|
||||
*
|
||||
* @this {Keyboard}
|
||||
*/
|
||||
Keyboard.prototype.checkSoftKeysToRelease = function()
|
||||
{
|
||||
var msDelayMin = -1;
|
||||
var asSoftCodes = Object.keys(this.keysToRelease);
|
||||
for (var i = 0; i < asSoftCodes.length; i++) {
|
||||
var sSoftCode = asSoftCodes[i];
|
||||
var msDown = this.keysToRelease[sSoftCode];
|
||||
var msElapsed = Date.now() - msDown;
|
||||
var msDelay = Keyboard.MINPRESSTIME - msElapsed;
|
||||
if (msDelay > 0) {
|
||||
if (msDelayMin < 0 || msDelayMin > msDelay) {
|
||||
msDelayMin = msDelay;
|
||||
}
|
||||
} else {
|
||||
delete this.keysToRelease[sSoftCode];
|
||||
this.onSoftKeyDown(sSoftCode, false);
|
||||
}
|
||||
}
|
||||
if (msDelayMin >= 0) {
|
||||
var kbd = this;
|
||||
setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* outVT100UARTStatus(port, b, addrFrom)
|
||||
*
|
||||
* @this {Keyboard}
|
||||
* @param {number} port (0x82)
|
||||
* @param {number} b
|
||||
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
|
||||
*/
|
||||
Keyboard.prototype.outVT100UARTStatus = function(port, b, addrFrom)
|
||||
{
|
||||
this.printMessageIO(port, b, addrFrom, "KBDUART.STATUS", null, true);
|
||||
this.bLEDs = b;
|
||||
this.updateLEDs();
|
||||
};
|
||||
|
||||
/*
|
||||
* Port notification tables
|
||||
*/
|
||||
Keyboard.VT100.portsInput = {
|
||||
};
|
||||
|
||||
Keyboard.VT100.portsOutput = {
|
||||
0x82: Keyboard.prototype.outVT100UARTStatus
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ if (NODE) {
|
|||
var web = require("../../shared/lib/weblib");
|
||||
var DumpAPI = require("../../shared/lib/dumpapi");
|
||||
var Component = require("../../shared/lib/component");
|
||||
var ChipSet = require("./chipset");
|
||||
var Memory = require("./memory");
|
||||
var Messages = require("./messages");
|
||||
var State = require("./state");
|
||||
|
|
@ -52,6 +53,8 @@ if (NODE) {
|
|||
* 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)
|
||||
|
|
@ -60,6 +63,13 @@ if (NODE) {
|
|||
* interruptRate: normally the same as (or some multiple of) refreshRate (eg, 120)
|
||||
* refreshRate: how many times updateScreen() should be performed per second (eg, 60)
|
||||
*
|
||||
* In addition, if a text-only display is being emulated, define the following properties:
|
||||
*
|
||||
* fontROM: URL of font ROM
|
||||
* fontColor: default is white
|
||||
* cellWidth: number (eg, 10 for VT100)
|
||||
* cellHeight: number (eg, 10 for VT100)
|
||||
*
|
||||
* We record all the above values now, but we defer creation of the frame buffer until our initBus()
|
||||
* handler is called. At that point, we will also compute the extent of the frame buffer, determine the
|
||||
* appropriate "cell" size (ie, the number of pixels that updateScreen() will fetch and process at once),
|
||||
|
|
@ -74,7 +84,7 @@ if (NODE) {
|
|||
* 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.
|
||||
* of the off-screen buffer and then relies on setPixel() to "rotate" the data into the proper location.
|
||||
*
|
||||
* @constructor
|
||||
* @extends Component
|
||||
|
|
@ -96,10 +106,21 @@ function Video(parmsVideo, canvas, context, textarea, container)
|
|||
this.cyScreen = parmsVideo['screenHeight'];
|
||||
|
||||
this.addrBuffer = parmsVideo['bufferAddr'];
|
||||
this.cxBuffer = parmsVideo['bufferCols'];
|
||||
this.cyBuffer = parmsVideo['bufferRows'];
|
||||
this.fUseRAM = parmsVideo['bufferRAM'];
|
||||
|
||||
var sFormat = parmsVideo['bufferFormat'];
|
||||
this.nFormat = sFormat && Video.FORMATS[sFormat.toLowerCase()] || Video.FORMAT.UNKNOWN;
|
||||
|
||||
this.nColsBuffer = parmsVideo['bufferCols'];
|
||||
this.nRowsBuffer = parmsVideo['bufferRows'];
|
||||
|
||||
this.cxCellDefault = this.cxCell = parmsVideo['cellWidth'] || 1;
|
||||
this.cyCellDefault = this.cyCell = parmsVideo['cellHeight'] || 1;
|
||||
this.abFontData = null;
|
||||
|
||||
this.nBitsPerPixel = parmsVideo['bufferBits'] || 1;
|
||||
this.iBitFirstPixel = parmsVideo['bufferLeft'] || 0;
|
||||
|
||||
this.rotateBuffer = parmsVideo['bufferRotate'];
|
||||
if (this.rotateBuffer) {
|
||||
this.rotateBuffer = this.rotateBuffer % 360;
|
||||
|
|
@ -110,14 +131,30 @@ function Video(parmsVideo, canvas, context, textarea, container)
|
|||
}
|
||||
}
|
||||
|
||||
this.interruptRate = parmsVideo['interruptRate'];
|
||||
this.refreshRate = parmsVideo['refreshRate'] || 60;
|
||||
this.rateInterrupt = parmsVideo['interruptRate'];
|
||||
this.rateRefresh = parmsVideo['refreshRate'] || 60;
|
||||
|
||||
this.canvasScreen = canvas;
|
||||
this.contextScreen = context;
|
||||
this.textareaScreen = textarea;
|
||||
this.inputScreen = textarea || canvas || null;
|
||||
|
||||
/*
|
||||
* These variables are here in case we want/need to add support for borders later...
|
||||
*/
|
||||
this.xScreenOffset = this.yScreenOffset = 0;
|
||||
this.cxScreenOffset = this.cxScreen;
|
||||
this.cyScreenOffset = this.cyScreen;
|
||||
|
||||
this.cxScreenCell = (this.cxScreen / this.nColsBuffer)|0;
|
||||
this.cyScreenCell = (this.cyScreen / this.nRowsBuffer)|0;
|
||||
|
||||
/*
|
||||
* Now that we've finished using nRowsBuffer to help define the screen size, we add one more
|
||||
* row for text modes, to simplify smooth-scrolling down the road.
|
||||
*/
|
||||
if (this.cyCell > 1) this.nRowsBuffer++;
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
|
@ -147,6 +184,10 @@ function Video(parmsVideo, canvas, context, textarea, container)
|
|||
if (this.rotateScreen) {
|
||||
this.rotateScreen = this.rotateScreen % 360;
|
||||
if (this.rotateScreen > 0) this.rotateScreen -= 360;
|
||||
/*
|
||||
* TODO: Consider also disallowing any rotateScreen value if bufferRotate was already set; setting
|
||||
* both is most likely a mistake, but who knows, maybe someone wants to use both for 180-degree rotation?
|
||||
*/
|
||||
if (this.rotateScreen != -90) {
|
||||
this.notice("unsupported screen rotation: " + this.rotateScreen);
|
||||
this.rotateScreen = 0;
|
||||
|
|
@ -157,23 +198,6 @@ function Video(parmsVideo, canvas, context, textarea, container)
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -207,6 +231,19 @@ function Video(parmsVideo, canvas, context, textarea, container)
|
|||
}
|
||||
}
|
||||
|
||||
this.sFontROM = parmsVideo['fontROM'];
|
||||
if (this.sFontROM) {
|
||||
var sFileExt = str.getExtension(this.sFontROM);
|
||||
if (sFileExt != "json") {
|
||||
this.sFontROM = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFontROM + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES;
|
||||
}
|
||||
web.getResource(this.sFontROM, null, true, function(sURL, sResponse, nErrorCode) {
|
||||
video.doneLoad(sURL, sResponse, nErrorCode);
|
||||
});
|
||||
}
|
||||
|
||||
this.ledBindings = {};
|
||||
|
||||
if (DEBUG) this.nCyclesPrev = 0;
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +255,131 @@ Video.COLORS = {
|
|||
OVERLAY_TOTAL: 2
|
||||
};
|
||||
|
||||
Video.FORMAT = {
|
||||
UNKNOWN: 0,
|
||||
SI1978: 1,
|
||||
VT100: 2
|
||||
};
|
||||
|
||||
Video.FORMATS = {
|
||||
"vt100": Video.FORMAT.VT100
|
||||
};
|
||||
|
||||
|
||||
Video.VT100 = {
|
||||
/*
|
||||
* The following font IDs are nothing more than all the possible LINEATTR values masked with FONTMASK;
|
||||
* also, note that double-high implies double-wide; the VT100 doesn't support a double-high single-wide font.
|
||||
*/
|
||||
FONT: {
|
||||
NORML: 0x60, // normal font (eg, 10x10)
|
||||
DWIDE: 0x40, // double-wide, single-high font (eg, 20x10)
|
||||
DHIGH: 0x20, // technically, this means display only the TOP half of the double-high font (eg, 20x20)
|
||||
DHIGH_BOT: 0x00 // technically, this means display only the BOTTOM half of the double-high font (eg, 20x20)
|
||||
},
|
||||
LINETERM: 0x7F,
|
||||
LINEATTR: {
|
||||
ADDRMASK: 0x0F,
|
||||
ADDRBIAS: 0x10, // 0x10 == ADDRBIAS_LO, 0x00 = ADDRBIAS_HI
|
||||
FONTMASK: 0x60,
|
||||
SCROLL: 0x80
|
||||
},
|
||||
ADDRBIAS_LO: 0x2000,
|
||||
ADDRBIAS_HI: 0x4000
|
||||
};
|
||||
|
||||
/**
|
||||
* initBuffers()
|
||||
*/
|
||||
Video.prototype.initBuffers = function()
|
||||
{
|
||||
/*
|
||||
* Allocate off-screen buffers now
|
||||
*/
|
||||
this.cxBuffer = this.nColsBuffer * this.cxCell;
|
||||
this.cyBuffer = this.nRowsBuffer * this.cyCell;
|
||||
|
||||
var cxBuffer = this.cxBuffer;
|
||||
var cyBuffer = this.cyBuffer;
|
||||
if (this.rotateBuffer) {
|
||||
cxBuffer = this.cyBuffer;
|
||||
cyBuffer = this.cxBuffer;
|
||||
}
|
||||
|
||||
this.sizeBuffer = ((this.cxBuffer * this.nBitsPerPixel) >> 3) * this.cyBuffer;
|
||||
if (!this.fUseRAM) {
|
||||
if (!this.bus.addMemory(this.addrBuffer, this.sizeBuffer, Memory.TYPE.VIDEO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* imageBuffer is only used for graphics modes. For text modes, we create a canvas
|
||||
* for each font and draw characters by drawing from the font canvas to the target canvas.
|
||||
*/
|
||||
if (this.cxCell > 1) {
|
||||
this.initCellCache(this.nColsBuffer * this.nRowsBuffer);
|
||||
} else {
|
||||
this.imageBuffer = this.contextScreen.createImageData(cxBuffer, cyBuffer);
|
||||
this.nPixelsPerCell = (16 / this.nBitsPerPixel)|0;
|
||||
this.initCellCache(this.sizeBuffer >> 1);
|
||||
}
|
||||
|
||||
this.canvasBuffer = document.createElement("canvas");
|
||||
this.canvasBuffer.width = cxBuffer;
|
||||
this.canvasBuffer.height = cyBuffer;
|
||||
this.contextBuffer = this.canvasBuffer.getContext("2d");
|
||||
|
||||
this.aFonts = {};
|
||||
this.initColors();
|
||||
|
||||
if (this.nFormat == Video.FORMAT.VT100) {
|
||||
/*
|
||||
* Beyond fonts, VT100 support requires that we maintain a number of additional properties:
|
||||
*
|
||||
* rateMonitor: must be either 50 or 60 (defaults to 60); we don't emulate the monitor refresh rate,
|
||||
* but we do need to keep track of which rate has been selected, because that affects the number of
|
||||
* "fill lines" present at the top of the VT100's frame buffer: 2 lines for 60Hz, 5 lines for 50Hz.
|
||||
*
|
||||
* The VT100 July 1982 Technical Manual, p. 4-89, shows the following sample frame buffer layout:
|
||||
*
|
||||
* 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
|
||||
* --------------------------------------------------------------
|
||||
* 0x2000: 7F 70 03 7F F2 D0 7F 70 06 7F 70 0C 7F 70 0F 7F
|
||||
* 0x2010: 70 03 .. .. .. .. .. .. .. .. .. .. .. .. .. ..
|
||||
* ...
|
||||
* 0x22D0: 'D' 'A' 'T' 'A' ' ' 'F' 'O' 'R' ' ' 'F' 'I' 'R' 'S' 'T' ' ' 'L'
|
||||
* 0x22E0: 'I' 'N' 'E' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
|
||||
* ...
|
||||
* 0x2320: 7F F3 23 'D' 'A' 'T' 'A' ' ' 'F' 'O' 'R' ' ' 'S' 'E' 'C' 'O'
|
||||
* 0x2330: 'N' 'D' ' ' 'L' 'I' 'N' 'E' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
|
||||
* ...
|
||||
* 0x2BE0: ' ' ' ' 'E' 'N' 'D' ' ' 'O' 'F' ' ' 'L' 'A' 'S' 'T' ' ' 'L' 'I'
|
||||
* 0x2BF0: 'N' 'E' 7F 70 06 .. .. .. .. .. .. .. .. .. .. ..
|
||||
* 0x2C00: [AVO SCREEN RAM, IF ANY, BEGINS HERE]
|
||||
*
|
||||
* ERRATA: The manual claims that if you change the byte at 0x2002 from 03 to 09, the number of "fill
|
||||
* lines" will change from 2 to 5 (for 50Hz operation), but it shows 06 instead of 0C at location 0x200B;
|
||||
* if you follow the links, it's pretty clear that byte has to be 0C to yield 5 "fill lines". Since the
|
||||
* address following the terminator at 0x2006 points to itself, it never makes sense for that terminator
|
||||
* to be used EXCEPT at the end of the frame buffer.
|
||||
*
|
||||
* As an alternative to tracking the monitor refresh rate, we could hard-code some knowledge about how
|
||||
* the VT100's 8080 code uses memory, and simply ignore lines below address 0x22D0. But the VT100 Video
|
||||
* Processor makes no such assumption, and it would also break our test code in createFonts(), which
|
||||
* builds a contiguous screen of test data starting at the default frame buffer address (0x2000).
|
||||
*/
|
||||
this.rateMonitor = 60;
|
||||
|
||||
/*
|
||||
* The default character-selectable attribute (reverse video vs. underline) is controlled by fUnderline.
|
||||
*/
|
||||
this.fUnderline = false;
|
||||
|
||||
this.abLineBuffer = new Array(this.nColsBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* initBus(cmp, bus, cpu, dbg)
|
||||
*
|
||||
|
|
@ -233,30 +395,273 @@ Video.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|||
this.bus = bus;
|
||||
this.cpu = cpu;
|
||||
this.dbg = dbg;
|
||||
this.chipset = cmp.getMachineComponent("ChipSet");
|
||||
|
||||
/*
|
||||
* Compute the size of the frame buffer and allocate.
|
||||
*/
|
||||
this.sizeBuffer = ((this.cxBuffer * this.nBitsPerPixel) >> 3) * this.cyBuffer;
|
||||
if (this.bus.addMemory(this.addrBuffer, this.sizeBuffer, Memory.TYPE.VIDEO)) {
|
||||
/*
|
||||
* Compute the number of cells and initialize the cell cache.
|
||||
*/
|
||||
this.nCellCache = this.sizeBuffer >> 1;
|
||||
this.nPixelsPerCell = (16 / this.nBitsPerPixel)|0;
|
||||
this.initCache();
|
||||
if (!this.nFormat && this.chipset && this.chipset.model == ChipSet.SI1978.MODEL) {
|
||||
this.nFormat = Video.FORMAT.SI1978;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we have an associated keyboard, then ensure that the keyboard will be notified whenever the canvas
|
||||
* gets focus and receives input.
|
||||
* Allocate the frame buffer (as needed) along with all other buffers.
|
||||
*/
|
||||
this.initBuffers();
|
||||
|
||||
/*
|
||||
* If we have an associated keyboard, then ensure that the keyboard will be notified
|
||||
* whenever the canvas gets focus and receives input.
|
||||
*/
|
||||
this.kbd = cmp.getMachineComponent("Keyboard");
|
||||
if (this.kbd && this.canvasScreen) {
|
||||
this.kbd.setBinding(this.textareaScreen? "textarea" : "canvas", "kbd", this.inputScreen);
|
||||
if (this.kbd) {
|
||||
for (var s in this.ledBindings) {
|
||||
this.kbd.setBinding("led", s, this.ledBindings[s]);
|
||||
}
|
||||
if (this.canvasScreen) {
|
||||
this.kbd.setBinding(this.textareaScreen? "textarea" : "canvas", "kbd", this.inputScreen);
|
||||
}
|
||||
}
|
||||
|
||||
this.setReady();
|
||||
if (!this.sFontROM) this.setReady();
|
||||
};
|
||||
|
||||
/**
|
||||
* doneLoad(sURL, sFontData, nErrorCode)
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {string} sURL
|
||||
* @param {string} sFontData
|
||||
* @param {number} nErrorCode (response from server if anything other than 200)
|
||||
*/
|
||||
Video.prototype.doneLoad = function(sURL, sFontData, nErrorCode)
|
||||
{
|
||||
if (nErrorCode) {
|
||||
this.notice("Unable to load font ROM (error " + nErrorCode + ": " + sURL + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
Component.addMachineResource(this.idMachine, sURL, sFontData);
|
||||
|
||||
try {
|
||||
/*
|
||||
* The most likely source of any exception will be here: parsing the JSON-encoded data.
|
||||
*/
|
||||
var ab = eval("(" + sFontData + ")");
|
||||
|
||||
var abFontData = ab['bytes'] || ab;
|
||||
|
||||
if (!abFontData || !abFontData.length) {
|
||||
Component.error("Empty font ROM: " + sURL);
|
||||
return;
|
||||
}
|
||||
else if (abFontData.length == 1) {
|
||||
Component.error(abFontData[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Minimal font data validation, just to make sure we're not getting garbage from the server.
|
||||
*/
|
||||
if (abFontData.length == 2048) {
|
||||
this.createFonts(abFontData);
|
||||
}
|
||||
else {
|
||||
this.notice("Unrecognized font data length (" + abFontData.length + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this.notice("Font ROM data error: " + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we're still here, then we're ready!
|
||||
*
|
||||
* UPDATE: Per issue #21, I'm issuing setReady() *only* if a valid contextScreen exists *or* a Debugger is attached.
|
||||
*
|
||||
* TODO: Consider a more general-purpose solution for deciding whether or not the user wants to run in a "headless" mode.
|
||||
*/
|
||||
if (this.contextScreen || this.dbg) this.setReady();
|
||||
};
|
||||
|
||||
/**
|
||||
* createFonts(abFontData)
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {Array.<number>} abFontData
|
||||
*/
|
||||
Video.prototype.createFonts = function(abFontData)
|
||||
{
|
||||
/*
|
||||
* We retain abFontData in case we have to rebuild the fonts (eg, when we switch from 80 to 132 columns)
|
||||
*/
|
||||
this.abFontData = abFontData;
|
||||
this.aFonts[Video.VT100.FONT.NORML] = [
|
||||
this.createFontVariation(this.cxCell, this.cyCell),
|
||||
this.createFontVariation(this.cxCell, this.cyCell, this.fUnderline)
|
||||
];
|
||||
this.aFonts[Video.VT100.FONT.DWIDE] = [
|
||||
this.createFontVariation(this.cxCell*2, this.cyCell),
|
||||
this.createFontVariation(this.cxCell*2, this.cyCell, this.fUnderline)
|
||||
];
|
||||
this.aFonts[Video.VT100.FONT.DHIGH] = this.aFonts[Video.VT100.FONT.DHIGH_BOT] = [
|
||||
this.createFontVariation(this.cxCell*2, this.cyCell*2),
|
||||
this.createFontVariation(this.cxCell*2, this.cyCell*2, this.fUnderline)
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* createFontVariation(cxCell, cyCell, fUnderline)
|
||||
*
|
||||
* This creates a 16x16 character grid for the requested font variation. Variations include:
|
||||
*
|
||||
* 1) no variation (cell size is this.cxCell x this.cyCell)
|
||||
* 2) double-wide characters (cell size is this.cxCell*2 x this.cyCell)
|
||||
* 3) double-high double-wide characters (cell size is this.cxCell*2 x this.cyCell*2)
|
||||
* 4) any of the above with either reverse video or underline enabled (default is neither)
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {number} cxCell is the target width of each character in the grid
|
||||
* @param {number} cyCell is the target height of each character in the grid
|
||||
* @param {boolean} [fUnderline] (null for unmodified font, false for reverse video, true for underline)
|
||||
* @return {Object}
|
||||
*/
|
||||
Video.prototype.createFontVariation = function(cxCell, cyCell, fUnderline)
|
||||
{
|
||||
/*
|
||||
* On a VT100, cxCell,cyCell is initially 10,10, but may change to 9,10 for 132-column mode.
|
||||
*/
|
||||
this.assert(cxCell == this.cxCell || cxCell == this.cxCell*2);
|
||||
this.assert(cyCell == this.cyCell || cyCell == this.cyCell*2);
|
||||
|
||||
/*
|
||||
* Create a font canvas that is both 16 times the target character width and the target character height,
|
||||
* ensuring that it will accommodate 16x16 characters (for a maximum of 256). Note that the VT100 font ROM
|
||||
* defines only 128 characters, so that canvas will contain only 16x8 entries.
|
||||
*/
|
||||
var nFontBytesPerChar = this.cxCellDefault <= 8? 8 : 16;
|
||||
var nFontByteOffset = nFontBytesPerChar > 8? 15 : 0;
|
||||
var nChars = this.abFontData.length / nFontBytesPerChar;
|
||||
|
||||
/*
|
||||
* The absence of a boolean for fUnderline means that both fReverse and fUnderline are "falsey". The presence
|
||||
* of a boolean means that fReverse will be true OR fUnderline will be true, but NOT both.
|
||||
*/
|
||||
var fReverse = (fUnderline === false);
|
||||
|
||||
var font = {cxCell: cxCell, cyCell: cyCell};
|
||||
font.canvas = document.createElement("canvas");
|
||||
font.canvas.width = cxCell * 16;
|
||||
font.canvas.height = cyCell * (nChars / 16);
|
||||
font.context = font.canvas.getContext("2d");
|
||||
|
||||
var imageChar = font.context.createImageData(cxCell, cyCell);
|
||||
|
||||
for (var iChar = 0; iChar < nChars; iChar++) {
|
||||
for (var y = 0, yDst = y; y < this.cyCell; y++) {
|
||||
var offFontData = iChar * nFontBytesPerChar + ((nFontByteOffset + y) & (nFontBytesPerChar - 1));
|
||||
var bits = (fUnderline && y == 8? 0xff : this.abFontData[offFontData]);
|
||||
for (var nRows = 0; nRows < (cyCell / this.cyCell); nRows++) {
|
||||
for (var x = 0, xDst = x; x < this.cxCell; x++) {
|
||||
/*
|
||||
* While x goes from 0 to cxCell-1, obviously we will run out of bits after x is 7;
|
||||
* since the final bit must be replicated all the way to the right edge of the cell
|
||||
* (so that line-drawing characters seamlessly connect), we ensure that the effective
|
||||
* shift count remains stuck at 7 once it reaches 7.
|
||||
*/
|
||||
var bit = bits & (0x80 >> (x > 7? 7 : x));
|
||||
for (var nCols = 0; nCols < (cxCell / this.cxCell); nCols++) {
|
||||
if (fReverse) bit = !bit;
|
||||
this.setPixel(imageChar, xDst, yDst, bit? 1 : 0);
|
||||
xDst++;
|
||||
}
|
||||
}
|
||||
yDst++;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* (iChar >> 4) performs the integer equivalent of Math.floor(iChar / 16), and (iChar & 0xf) is the equivalent of (iChar % 16).
|
||||
*/
|
||||
font.context.putImageData(imageChar, (iChar & 0xf) * cxCell, (iChar >> 4) * cyCell);
|
||||
}
|
||||
return font;
|
||||
};
|
||||
|
||||
/**
|
||||
* powerUp(data, fRepower)
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {Object|null} data
|
||||
* @param {boolean} [fRepower]
|
||||
* @return {boolean} true if successful, false if failure
|
||||
*/
|
||||
Video.prototype.powerUp = function(data, fRepower)
|
||||
{
|
||||
/*
|
||||
* Because the VT100 frame buffer can be located anywhere in RAM (above 0x2000), we must defer this
|
||||
* test code until the powerUp() notification handler is called, when all RAM has (hopefully) been allocated.
|
||||
*/
|
||||
if (DEBUG && this.nFormat == Video.FORMAT.VT100) {
|
||||
/*
|
||||
* Build a test screen in the VT100 frame buffer; we'll mimic the "SET-UP A" screen, since it uses
|
||||
* all the font variations. The process involves iterating over 0-based row numbers -2 (or -5 if 50Hz
|
||||
* operation is selected) through 24, checking aLineData for a matching row number, and converting the
|
||||
* corresponding string(s) to appropriate byte values. Negative row numbers correspond to "fill lines"
|
||||
* and do not require a row entry. If multiple strings are present for a given row, we invert the
|
||||
* default character attribute for subsequent strings. An empty array ends the screen build process.
|
||||
*/
|
||||
var aLineData = {
|
||||
0: [Video.VT100.FONT.DHIGH, 'SET-UP A'],
|
||||
2: [Video.VT100.FONT.DWIDE, 'TO EXIT PRESS "SET-UP"'],
|
||||
22: [Video.VT100.FONT.NORML, ' T T T T T T T T T'],
|
||||
23: [Video.VT100.FONT.NORML, '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890', '1234567890'],
|
||||
24: []
|
||||
};
|
||||
var addr = this.addrBuffer;
|
||||
var addrNext = -1, font = -1;
|
||||
var b, nFill = (this.rateMonitor == 60? 2 : 5);
|
||||
for (var iRow = -nFill; iRow < this.nRowsBuffer; iRow++) {
|
||||
var lineData = aLineData[iRow];
|
||||
if (addrNext >= 0) {
|
||||
var fBreak = false;
|
||||
addrNext = addr + 2;
|
||||
if (!lineData) {
|
||||
if (font == Video.VT100.FONT.DHIGH) {
|
||||
lineData = aLineData[iRow-1];
|
||||
font = Video.VT100.FONT.DHIGH_BOT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (lineData.length) {
|
||||
font = lineData[0];
|
||||
} else {
|
||||
addrNext = addr - 1;
|
||||
fBreak = true;
|
||||
}
|
||||
}
|
||||
b = (font & Video.VT100.LINEATTR.FONTMASK) | ((addrNext >> 8) & Video.VT100.LINEATTR.ADDRMASK) | Video.VT100.LINEATTR.ADDRBIAS;
|
||||
this.bus.setByteDirect(addr++, b);
|
||||
this.bus.setByteDirect(addr++, addrNext & 0xff);
|
||||
if (fBreak) break;
|
||||
}
|
||||
if (lineData) {
|
||||
var attr = 0;
|
||||
for (var j = 1; j < lineData.length; j++) {
|
||||
var s = lineData[j];
|
||||
for (var k = 0; k < s.length; k++) {
|
||||
this.bus.setByteDirect(addr++, s.charCodeAt(k) | attr);
|
||||
}
|
||||
attr ^= 0x80;
|
||||
}
|
||||
}
|
||||
this.bus.setByteDirect(addr++, Video.VT100.LINETERM);
|
||||
addrNext = addr;
|
||||
}
|
||||
/*
|
||||
* NOTE: By calling updateVT100() directly, we are bypassing the normal checks (eg, isVideoEnabled())
|
||||
*/
|
||||
this.updateVT100();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -273,6 +678,14 @@ Video.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|||
{
|
||||
var video = this;
|
||||
|
||||
/*
|
||||
* TODO: A more general-purpose binding mechanism would be nice someday....
|
||||
*/
|
||||
if (sHTMLType == "led" || sHTMLType == "rled") {
|
||||
this.ledBindings[sBinding] = control;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (sBinding) {
|
||||
case "fullScreen":
|
||||
this.bindings[sBinding] = control;
|
||||
|
|
@ -398,18 +811,20 @@ Video.prototype.setFocus = function()
|
|||
*/
|
||||
Video.prototype.getRefreshRate = function()
|
||||
{
|
||||
return Math.max(this.refreshRate, this.interruptRate);
|
||||
return Math.max(this.rateRefresh, this.rateInterrupt);
|
||||
};
|
||||
|
||||
/**
|
||||
* initCache()
|
||||
* initCellCache(nCells)
|
||||
*
|
||||
* Initializes the contents of our internal cell cache.
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {number} nCells
|
||||
*/
|
||||
Video.prototype.initCache = function()
|
||||
Video.prototype.initCellCache = function(nCells)
|
||||
{
|
||||
this.nCellCache = nCells;
|
||||
this.fCellCacheValid = false;
|
||||
if (this.aCellCache === undefined || this.aCellCache.length != this.nCellCache) {
|
||||
this.aCellCache = new Array(this.nCellCache);
|
||||
|
|
@ -427,34 +842,37 @@ Video.prototype.initColors = function()
|
|||
{
|
||||
var rgbBlack = [0x00, 0x00, 0x00, 0xff];
|
||||
var rgbWhite = [0xff, 0xff, 0xff, 0xff];
|
||||
var rgbGreen = [0x00, 0xff, 0x00, 0xff];
|
||||
var rgbYellow = [0xff, 0xff, 0x00, 0xff];
|
||||
this.nColors = (1 << this.nBitsPerPixel);
|
||||
this.aRGB = new Array(this.nColors + Video.COLORS.OVERLAY_TOTAL);
|
||||
this.aRGB[0] = rgbBlack;
|
||||
this.aRGB[1] = rgbWhite;
|
||||
this.aRGB[this.nColors + Video.COLORS.OVERLAY_TOP] = rgbYellow;
|
||||
this.aRGB[this.nColors + Video.COLORS.OVERLAY_BOTTOM] = rgbGreen;
|
||||
if (this.nFormat == Video.FORMAT.SI1978) {
|
||||
var rgbGreen = [0x00, 0xff, 0x00, 0xff];
|
||||
//noinspection UnnecessaryLocalVariableJS
|
||||
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)
|
||||
* setPixel(image, x, y, bPixel)
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {Object} imageBuffer
|
||||
* @param {Object} image
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} bPixel (ie, an index into aRGB)
|
||||
*/
|
||||
Video.prototype.setPixel = function(imageBuffer, x, y, bPixel)
|
||||
Video.prototype.setPixel = function(image, x, y, bPixel)
|
||||
{
|
||||
var index;
|
||||
if (!this.rotateBuffer) {
|
||||
index = (x + y * imageBuffer.width);
|
||||
index = (x + y * image.width);
|
||||
} else {
|
||||
index = (imageBuffer.height - x - 1) * imageBuffer.width + y;
|
||||
index = (image.height - x - 1) * image.width + y;
|
||||
}
|
||||
if (bPixel) {
|
||||
if (bPixel && this.nFormat == Video.FORMAT.SI1978) {
|
||||
if (x >= 208 && x < 236) {
|
||||
bPixel = this.nColors + Video.COLORS.OVERLAY_TOP;
|
||||
}
|
||||
|
|
@ -464,10 +882,157 @@ Video.prototype.setPixel = function(imageBuffer, x, y, bPixel)
|
|||
}
|
||||
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];
|
||||
image.data[index] = rgb[0];
|
||||
image.data[index+1] = rgb[1];
|
||||
image.data[index+2] = rgb[2];
|
||||
image.data[index+3] = rgb[3];
|
||||
};
|
||||
|
||||
/**
|
||||
* updateChar(idFont, col, row, data, context)
|
||||
*
|
||||
* Updates a particular character cell (row,col) in the associated window.
|
||||
*
|
||||
* @this {Video}
|
||||
* @param {number} idFont
|
||||
* @param {number} col
|
||||
* @param {number} row
|
||||
* @param {number} data
|
||||
* @param {Object} [context]
|
||||
*/
|
||||
Video.prototype.updateChar = function(idFont, col, row, data, context)
|
||||
{
|
||||
var bChar = data & 0x7f;
|
||||
var font = this.aFonts[idFont][(data & 0x80)? 1 : 0];
|
||||
if (!font) return;
|
||||
|
||||
var xSrc = (bChar & 0xf) * font.cxCell;
|
||||
var ySrc = (bChar >> 4) * font.cyCell;
|
||||
|
||||
var xDst, yDst, cxDst, cyDst;
|
||||
|
||||
var cxSrc = font.cxCell;
|
||||
var cySrc = font.cyCell;
|
||||
|
||||
if (context) {
|
||||
xDst = col * this.cxCell;
|
||||
yDst = row * this.cyCell;
|
||||
cxDst = this.cxCell;
|
||||
cyDst = this.cyCell;
|
||||
} else {
|
||||
xDst = col * this.cxScreenCell;
|
||||
yDst = row * this.cyScreenCell;
|
||||
cxDst = this.cxScreenCell;
|
||||
cyDst = this.cyScreenCell;
|
||||
}
|
||||
|
||||
/*
|
||||
* If font.cxCell > this.cxCell, then we assume the caller wants to draw a double-wide character,
|
||||
* so we will double xDst and cxDst.
|
||||
*/
|
||||
if (font.cxCell > this.cxCell) {
|
||||
xDst *= 2;
|
||||
cxDst *= 2;
|
||||
this.assert(font.cxCell == this.cxCell * 2);
|
||||
}
|
||||
|
||||
/*
|
||||
* If font.cyCell > this.cyCell, then we rely on idFont to indicate whether the top half or bottom half
|
||||
* of the character should be drawn.
|
||||
*/
|
||||
if (font.cyCell > this.cyCell) {
|
||||
if (idFont == Video.VT100.FONT.DHIGH_BOT) ySrc += this.cyCell;
|
||||
cySrc = this.cyCell;
|
||||
this.assert(font.cyCell == this.cyCell * 2);
|
||||
}
|
||||
|
||||
if (context) {
|
||||
context.drawImage(font.canvas, xSrc, ySrc, cxSrc, cySrc, xDst, yDst, cxDst, cyDst);
|
||||
} else {
|
||||
xDst += this.xScreenOffset;
|
||||
yDst += this.yScreenOffset;
|
||||
this.contextScreen.drawImage(font.canvas, xSrc, ySrc, cxSrc, cySrc, xDst, yDst, cxDst, cyDst);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* updateVT100()
|
||||
*
|
||||
* @this {Video}
|
||||
*/
|
||||
Video.prototype.updateVT100 = function()
|
||||
{
|
||||
var addrNext = this.addrBuffer, fontNext = -1;
|
||||
|
||||
var nRows = 0;
|
||||
var nFill = (this.rateMonitor == 60? 2 : 5);
|
||||
this.assert(this.abLineBuffer.length == this.nColsBuffer);
|
||||
|
||||
var iCell = 0, cUpdated = 0;
|
||||
while (nRows < this.nRowsBuffer) {
|
||||
/*
|
||||
* Populate the line buffer
|
||||
*/
|
||||
var nCols = 0;
|
||||
var addr = addrNext;
|
||||
var font = fontNext;
|
||||
while (true) {
|
||||
var data = this.bus.getByteDirect(addr++);
|
||||
if ((data & Video.VT100.LINETERM) == Video.VT100.LINETERM) {
|
||||
var b = this.bus.getByteDirect(addr++);
|
||||
fontNext = b & Video.VT100.LINEATTR.FONTMASK;
|
||||
addrNext = ((b & Video.VT100.LINEATTR.ADDRMASK) << 8) | this.bus.getByteDirect(addr);
|
||||
addrNext += (b & Video.VT100.LINEATTR.ADDRBIAS)? Video.VT100.ADDRBIAS_LO : Video.VT100.ADDRBIAS_HI;
|
||||
break;
|
||||
}
|
||||
if (nCols < this.abLineBuffer.length) {
|
||||
this.abLineBuffer[nCols++] = data;
|
||||
} else {
|
||||
break; // ideally, we would wait for a LINETERM byte, but it's not safe to loop without limit
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip the first few "fill lines"
|
||||
*/
|
||||
if (nFill) {
|
||||
nFill--;
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pad the line buffer as needed
|
||||
*/
|
||||
while (nCols < this.abLineBuffer.length) {
|
||||
this.abLineBuffer[nCols++] = 0; // character code 0 is a empty font character
|
||||
}
|
||||
|
||||
/*
|
||||
* Display the line buffer; ordinarily, font would always be valid after processing the "fill lines",
|
||||
* but if the buffer was filled with garbage, the usual LINETERM might be missing, so font might not be set.
|
||||
*/
|
||||
if (font >= 0) {
|
||||
for (var iCol = 0; iCol < nCols; iCol++) {
|
||||
data = this.abLineBuffer[iCol];
|
||||
if (!this.fCellCacheValid || data !== this.aCellCache[iCell]) {
|
||||
this.updateChar(font, iCol, nRows, data, this.contextBuffer);
|
||||
cUpdated++;
|
||||
}
|
||||
iCell++;
|
||||
}
|
||||
}
|
||||
nRows++;
|
||||
}
|
||||
|
||||
this.fCellCacheValid = true;
|
||||
|
||||
if (cUpdated && this.contextBuffer) {
|
||||
/*
|
||||
* NOTE: We must subtract cyCell from cyBuffer to avoid displaying the extra row that we normally buffer
|
||||
* in support of smooth-scrolling.
|
||||
*/
|
||||
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.cxBuffer, this.cyBuffer - this.cyCell, this.xScreenOffset, this.yScreenOffset, this.cxScreenOffset, this.cyScreenOffset);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -478,7 +1043,7 @@ Video.prototype.setPixel = function(imageBuffer, x, y, bPixel)
|
|||
* 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
|
||||
* and then update the cell cache to match. Since initCellCache() sets every cell in the cell cache to an
|
||||
* invalid value, we're assured that the next call to updateScreen() will redraw the entire (visible) video buffer.
|
||||
*
|
||||
* @this {Video}
|
||||
|
|
@ -490,19 +1055,22 @@ Video.prototype.updateScreen = function(n)
|
|||
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;
|
||||
|
||||
if (this.rateInterrupt) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -521,8 +1089,39 @@ Video.prototype.updateScreen = function(n)
|
|||
this.nCyclesPrev = nCycles;
|
||||
this.printMessage("updateScreen(" + n + "): clean=" + fClean + ", update=" + fUpdate + ", cycles=" + nCycles + ", delta=" + nCyclesDelta);
|
||||
}
|
||||
if (!fUpdate) return;
|
||||
|
||||
if (!fUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cxCell > 1) {
|
||||
this.updateScreenText();
|
||||
} else {
|
||||
this.updateScreenGraphics();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* updateScreenText()
|
||||
*
|
||||
* @this {Video}
|
||||
*/
|
||||
Video.prototype.updateScreenText = function()
|
||||
{
|
||||
switch(this.nFormat) {
|
||||
case Video.FORMAT.VT100:
|
||||
this.updateVT100();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* updateScreenGraphics()
|
||||
*
|
||||
* @this {Video}
|
||||
*/
|
||||
Video.prototype.updateScreenGraphics = function()
|
||||
{
|
||||
var addr = this.addrBuffer;
|
||||
var addrLimit = addr + this.sizeBuffer;
|
||||
|
||||
|
|
@ -593,6 +1192,11 @@ Video.prototype.updateScreen = function(n)
|
|||
cyDirty = cxDirtyOrig;
|
||||
}
|
||||
this.contextBuffer.putImageData(this.imageBuffer, 0, 0, xDirty, yDirty, cxDirty, cyDirty);
|
||||
/*
|
||||
* As originally noted in /modules/pcx86/lib/video.js, I would prefer to draw only the dirty portion of
|
||||
* canvasBuffer, but there usually isn't a 1-1 pixel mapping between canvasBuffer and contextScreen, so
|
||||
* if we draw interior rectangles, we can end up with subpixel artifacts along the edges of those rectangles.
|
||||
*/
|
||||
this.contextScreen.drawImage(this.canvasBuffer, 0, 0, this.canvasBuffer.width, this.canvasBuffer.height, 0, 0, this.cxScreen, this.cyScreen);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ var PCX86 = {
|
|||
SITEHOST: SITEHOST, // shared
|
||||
SYMBOLS: SYMBOLS,
|
||||
XMLVERSION: XMLVERSION // shared
|
||||
}
|
||||
};
|
||||
|
||||
if (NODE) {
|
||||
global.APPCLASS = APPCLASS;
|
||||
|
|
|
|||
|
|
@ -1867,6 +1867,9 @@ Keyboard.prototype.injectKeysFromBuffer = function(msDelay)
|
|||
*/
|
||||
Keyboard.prototype.setLED = function(control, f)
|
||||
{
|
||||
/*
|
||||
* TODO: Add support for user-definable LED colors
|
||||
*/
|
||||
control.style.backgroundColor = (f? "#00ff00" : "#000000");
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2955,8 +2955,8 @@ Video.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|||
}
|
||||
|
||||
/*
|
||||
* If we have an associated keyboard, then ensure that the keyboard will be notified whenever the canvas
|
||||
* gets focus and receives input.
|
||||
* 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) {
|
||||
|
|
@ -3006,7 +3006,9 @@ Video.prototype.setBinding = function(sHTMLType, sBinding, control, sValue)
|
|||
|
||||
/*
|
||||
* We now save every binding that comes in, so that if there are bindings for "caps-lock' and the like,
|
||||
* we can forward them to the Keyboard.
|
||||
* we can forward them to the Keyboard. TODO: Perhaps we should limit this to sHTMLType == "led", and collect
|
||||
* them in a separate object (eg, ledBindings), so that initBus() can safely enumerate JUST the LEDs. This
|
||||
* is what we do in PC8080. Be aware that's there's also sHTMLType == "rled" now, too.
|
||||
*/
|
||||
this.bindings[sBinding] = control;
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ Component.notice = function(s, fPrintOnly, id)
|
|||
if (DEBUG) {
|
||||
Component.println(s, "notice", id);
|
||||
}
|
||||
if (!fPrintOnly) web.alertUser(s);
|
||||
if (!fPrintOnly) web.alertUser((id? (id + ": ") : "") + s);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -724,7 +724,7 @@ Component.prototype = {
|
|||
* @param {string} [id]
|
||||
*/
|
||||
this.notice = function noticePanel(s, fPrintOnly, id) {
|
||||
this.println(s, "notice", id);
|
||||
this.println(s, this.idComponent);
|
||||
};
|
||||
}
|
||||
return true;
|
||||
|
|
@ -838,7 +838,7 @@ Component.prototype = {
|
|||
* @param {string} [id] is the caller's ID, if any
|
||||
*/
|
||||
notice: function(s, fPrintOnly, id) {
|
||||
Component.notice(s, fPrintOnly, id || this.id);
|
||||
Component.notice(s, fPrintOnly, id || this.type);
|
||||
},
|
||||
/**
|
||||
* setError(s)
|
||||
|
|
|
|||
|
|
@ -111,6 +111,17 @@
|
|||
line-height: 19px; /* the equivalent of "vertical-align: middle" for single-line elements */
|
||||
background-color: #000000;
|
||||
}
|
||||
.pcjs-rled {
|
||||
float: left;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 4px;
|
||||
border: 1px solid black;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 19px; /* the equivalent of "vertical-align: middle" for single-line elements */
|
||||
background-color: #000000;
|
||||
}
|
||||
.pcjs-screen {
|
||||
clear: both;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
</xsl:when>
|
||||
<xsl:when test="@type = 'led'">
|
||||
<xsl:when test="@type = 'led' or @type = 'rled'">
|
||||
<div class="{$APPCLASS}-binding {$CSSCLASS}-{@type}" data-value="{{{$type},{$binding}}}"><xsl:value-of select="."/></div>
|
||||
</xsl:when>
|
||||
<xsl:when test="@type = 'separator'">
|
||||
|
|
@ -1003,6 +1003,18 @@
|
|||
<xsl:otherwise>0</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="bufferRAM">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@bufferRAM"><xsl:value-of select="@bufferRAM"/></xsl:when>
|
||||
<xsl:otherwise>false</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="bufferFormat">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@bufferFormat"><xsl:value-of select="@bufferFormat"/></xsl:when>
|
||||
<xsl:otherwise>1bpp</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="bufferCols">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@bufferCols"><xsl:value-of select="@bufferCols"/></xsl:when>
|
||||
|
|
@ -1056,6 +1068,18 @@
|
|||
<xsl:otherwise>false</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="cellWidth">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@cellWidth"><xsl:value-of select="@cellWidth"/></xsl:when>
|
||||
<xsl:otherwise>1</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="cellHeight">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@cellHeight"><xsl:value-of select="@cellHeight"/></xsl:when>
|
||||
<xsl:otherwise>1</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="charCols">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@cols"><xsl:value-of select="@cols"/></xsl:when>
|
||||
|
|
@ -1078,6 +1102,12 @@
|
|||
<xsl:otherwise/>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="fontColor">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@fontColor"><xsl:value-of select="@fontColor"/></xsl:when>
|
||||
<xsl:otherwise/>
|
||||
</xsl:choose>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="touchScreen">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@touchscreen"><xsl:value-of select="@touchscreen"/></xsl:when>
|
||||
|
|
@ -1120,7 +1150,7 @@
|
|||
<xsl:call-template name="component">
|
||||
<xsl:with-param name="machine" select="$machine"/>
|
||||
<xsl:with-param name="class">video</xsl:with-param>
|
||||
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,screenColor:'<xsl:value-of select="$screenColor"/>',screenRotate:<xsl:value-of select="$screenRotate"/>,bufferAddr:<xsl:value-of select="$bufferAddr"/>,bufferCols:<xsl:value-of select="$bufferCols"/>,bufferRows:<xsl:value-of select="$bufferRows"/>,bufferBits:<xsl:value-of select="$bufferBits"/>,bufferLeft:<xsl:value-of select="$bufferLeft"/>,bufferRotate:<xsl:value-of select="$bufferRotate"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/>,aspectRatio:<xsl:value-of select="$aspectRatio"/>,smoothing:<xsl:value-of select="$smoothing"/>,interruptRate:<xsl:value-of select="$interruptRate"/>,refreshRate:<xsl:value-of select="$refreshRate"/></xsl:with-param>
|
||||
<xsl:with-param name="parms">,model:'<xsl:value-of select="$model"/>',mode:<xsl:value-of select="$mode"/>,screenWidth:<xsl:value-of select="$screenWidth"/>,screenHeight:<xsl:value-of select="$screenHeight"/>,screenColor:'<xsl:value-of select="$screenColor"/>',screenRotate:<xsl:value-of select="$screenRotate"/>,bufferAddr:<xsl:value-of select="$bufferAddr"/>,bufferRAM:<xsl:value-of select="$bufferRAM"/>,bufferFormat:'<xsl:value-of select="$bufferFormat"/>',bufferCols:<xsl:value-of select="$bufferCols"/>,bufferRows:<xsl:value-of select="$bufferRows"/>,bufferBits:<xsl:value-of select="$bufferBits"/>,bufferLeft:<xsl:value-of select="$bufferLeft"/>,bufferRotate:<xsl:value-of select="$bufferRotate"/>,memory:<xsl:value-of select="$memory"/>,switches:'<xsl:value-of select="$switches"/>',scale:<xsl:value-of select="$scale"/>,cellWidth:<xsl:value-of select="$cellWidth"/>,cellHeight:<xsl:value-of select="$cellHeight"/>,charCols:<xsl:value-of select="$charCols"/>,charRows:<xsl:value-of select="$charRows"/>,fontROM:'<xsl:value-of select="$fontROM"/>',fontColor:'<xsl:value-of select="$fontColor"/>',touchScreen:'<xsl:value-of select="$touchScreen"/>',autoLock:<xsl:value-of select="$autoLock"/>,aspectRatio:<xsl:value-of select="$aspectRatio"/>,smoothing:<xsl:value-of select="$smoothing"/>,interruptRate:<xsl:value-of select="$interruptRate"/>,refreshRate:<xsl:value-of select="$refreshRate"/></xsl:with-param>
|
||||
</xsl:call-template>
|
||||
</xsl:template>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue