What this means is that XDF disk images formatted as 80-track 23-sector-per-track images will work with XDF code that honors that format; however, I've not yet been able to test with "real" XDF disk images (ie, disk images created with DiskDump using the experimental --xdf flag). "Real" XDF disk images almost certainly need more work (eg, setting all the sector IDs properly), but the groundwork has been laid.
5130 lines
216 KiB
JavaScript
5130 lines
216 KiB
JavaScript
/**
|
|
* @fileoverview Implements the PCjs Video component.
|
|
* @author <a href="mailto:Jeff@pcjs.org">Jeff Parsons</a>
|
|
* @version 1.0
|
|
* @suppress {missingProperties}
|
|
* Created 2012-Jun-15
|
|
*
|
|
* Copyright © 2012-2014 Jeff Parsons <Jeff@pcjs.org>
|
|
*
|
|
* This file is part of PCjs, which is part of the JavaScript Machines Project (aka JSMachines)
|
|
* at <http://jsmachines.net/> and <http://pcjs.org/>.
|
|
*
|
|
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
|
|
* GNU General Public License as published by the Free Software Foundation, either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
|
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCjs. If not,
|
|
* see <http://www.gnu.org/licenses/gpl.html>.
|
|
*
|
|
* You are required to include the above copyright notice in every source code file of every
|
|
* copy or modified version of this work, and to display that copyright notice on every screen
|
|
* that loads or runs any version of this software (see Computer.sCopyright).
|
|
*
|
|
* Some PCjs files also attempt to load external resource files, such as character-image files,
|
|
* ROM files, and disk image files. Those external resource files are not considered part of the
|
|
* PCjs program for purposes of the GNU General Public License, and the author does not claim
|
|
* any copyright as to their contents.
|
|
*/
|
|
|
|
/*
|
|
* MDA/CGA Support
|
|
*
|
|
* This component implements a combination of the MDA and CGA cards, with the ability to
|
|
* dynamically switch between the two, by simply toggling the SW1 motherboard switch settings
|
|
* and resetting the virtual machine. As a result, this component always installs I/O port
|
|
* handlers for both MDA and CGA, regardless which card is initially active.
|
|
*
|
|
* Since there's a lot of similarity between the two cards (eg, their text-mode video buffer
|
|
* format, and their use of the 6845 CRT controller), since the MDA ROM contains the fonts
|
|
* used by both devices, and since the same ROM BIOS supports both (in fact, the BIOS rather
|
|
* indiscriminately initializes both, regardless which is actually installed), I decided to
|
|
* use the same component to emulate both devices. Note that it was possible for an IBM PC
|
|
* to have both an MDA and CGA running in the same machine, so if that's something we want to
|
|
* support later, it can be done by instantiating this component twice, with some additional
|
|
* properties to force each instance to register only those I/O ports belonging to its specific
|
|
* configuration.
|
|
*
|
|
* While supporting both devices is mostly a convenience, it also creates a few inconveniences.
|
|
* For one, the MDA prefers a native window size of 720x350, as it supports only one video mode,
|
|
* 80x25, with a 9x14 cell size. The CGA, on the other hand, has an 8x8 cell size, so when using
|
|
* an MDA-size window, an 80x25 CGA screen will end up with 40-pixel borders on the left and right,
|
|
* and 75-pixel borders on the top and bottom. The result is a rather tiny CGA font surrounded
|
|
* by lots of wasted space, so it's best to turn on font scaling (see the "scale" property) and
|
|
* go with a larger window size of, say, 960x400 (50% larger in width, 100% larger in height).
|
|
*
|
|
* I've also added support for font-doubling in createFont(). We use the 8x8 font for 80-column
|
|
* modes and the "doubled" 16x16 font for 40-column modes OR whenever the screen is large enough
|
|
* to use the 16x16 font, since font rendering without scaling provides the sharpest results.
|
|
* In fact, there's special logic in setDimensions() to ignore fScaleFont in certain cases (eg,
|
|
* 40-column modes, to improve sharpness and avoid stretching the font beyond readability).
|
|
*
|
|
* Graphics modes, on the other hand, are always scaled to the window size. Pixels are captured
|
|
* in an off-screen buffer, which is then drawn to match the size of the virtual display window.
|
|
*
|
|
* TODO: Whenever there are borders, they should be filled with the CGA's overscan colors. However,
|
|
* in the case of graphics modes (and text modes whenever font scaling is enabled), we don't reserve
|
|
* any space for borders, so if borders are important, explicit support will be required.
|
|
*/
|
|
|
|
/*
|
|
* EGA Support
|
|
*
|
|
* EGA support piggy-backs on the existing MDA/CGA support. All the existing MDA/CGA port handlers
|
|
* now refer to either cardMono or cardColor (instead of directly to cardMDA or cardCGA), enabling
|
|
* the handlers to be redirected to cardMDA, cardCGA or cardEGA as appropriate.
|
|
*
|
|
* Note that an MDA card supported only a Monochrome Display and a CGA card supported only a Color
|
|
* Display (well, OK, *or* a TV monitor, which we don't currently support), but the EGA is much
|
|
* more flexible: the Enhanced Color Display was the preferred display, but the EGA also supported
|
|
* older displays; a Color Display on EGA wasn't ideal (same low resolutions but with more colors),
|
|
* but the EGA also brought high-resolution graphics to Monochrome displays, which was nice. Anyway,
|
|
* while all those EGA/monitor combinations will be nice to support, our virtual display support
|
|
* will focus initially on the Enhanced Color Display.
|
|
*
|
|
* TODO: Add support for jumpers P1 and P3 (see EGA TechRef p.85). P1 selects either 5-color-output
|
|
* for a CGA monitor or 6-color-output for an EGA monitor; we would presumably use this only to
|
|
* control certain assumptions about the virtual display's capabilities (ie, Color Display vs. Enhanced
|
|
* Color Display). P3 can switch all the I/O ports from 0x3nn to 0x2nn; the default is 0x3nn, and
|
|
* that's the only port range the EGA ROM supports as well.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
if (typeof module !== 'undefined') {
|
|
var str = require("../../shared/lib/strlib");
|
|
var web = require("../../shared/lib/weblib");
|
|
var DumpAPI = require("../../shared/lib/dumpapi");
|
|
var Component = require("../../shared/lib/component");
|
|
var ChipSet = require("./chipset");
|
|
var Keyboard = require("./keyboard");
|
|
var State = require("./state");
|
|
}
|
|
|
|
/**
|
|
* @class Font
|
|
* @property {number} cxCell
|
|
* @property {number} cyCell
|
|
* @property {Array} aCSSColors
|
|
* @property {Array} aRGBColors
|
|
* @property {Array} aColorMap
|
|
* @property {Array} aCanvas
|
|
*/
|
|
|
|
/**
|
|
* Video(parmsVideo, canvas, context, textarea)
|
|
*
|
|
* The Video component can be configured with the following (parmsVideo) properties:
|
|
*
|
|
* model: model (eg, "mda" for Monochrome Display Adapter)
|
|
* mode: mode number (hardware-specific, 7 is the default)
|
|
* memory: amount of installed memory (ignored for MDA/CGA)
|
|
* screenWidth: width of the screen window, in pixels
|
|
* screenHeight: height of the screen window, in pixels
|
|
* 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
|
|
* screenColor: background color of the screen window (default is black)
|
|
*
|
|
* An EGA may specify the following additional properties:
|
|
*
|
|
* switches: string representing EGA switches (see "SW1-SW4" documentation below)
|
|
* memory: the size of the EGA's on-board memory (overrides EGA's Video.cardSpecs)
|
|
*
|
|
* This calls the CPU to allocate a video buffer at the appropriate memory location whenever
|
|
* a reset() or setMode() occurs; setMode() is called whenever a mode change is detected at
|
|
* 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 in the associated window, via either updateChar()
|
|
* or setPixel().
|
|
*
|
|
* Thanks to the CPU's new block-based memory manager that allows us to sparse-allocate memory
|
|
* in 4K increments, 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,
|
|
* 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).
|
|
*
|
|
* @constructor
|
|
* @extends Component
|
|
* @param {Object} parmsVideo
|
|
* @param {Object} [canvas]
|
|
* @param {Object} [context]
|
|
* @param {Object} [textarea]
|
|
*/
|
|
function Video(parmsVideo, canvas, context, textarea)
|
|
{
|
|
Component.call(this, "Video", parmsVideo, Video);
|
|
|
|
/*
|
|
* This records the model specified (eg, "mda", "cga", "ega" or "" if none specified);
|
|
* when a model is specified, it overrides whatever model we infer from the ChipSet's switches
|
|
* (since those motherboard switches tell us only the type of monitor, not the type of card).
|
|
*/
|
|
this.model = parmsVideo['model'];
|
|
this.cbMemory = 0;
|
|
|
|
if (this.model == "ega") {
|
|
this.sEGASW = parmsVideo['switches'];
|
|
this.cbMemory = parmsVideo['memory'] || 0; // zero means fallback to the cardSpec's default size
|
|
}
|
|
|
|
/*
|
|
* powerUp() uses the default mode ONLY if ChipSet doesn't give us a default.
|
|
*/
|
|
this.nModeDefault = parmsVideo['mode'];
|
|
if (this.nModeDefault === undefined || Video.aModeParms[this.nModeDefault] === undefined) {
|
|
this.nModeDefault = Video.MODES.MDA_80X25;
|
|
}
|
|
|
|
/*
|
|
* setDimensions() uses these values ONLY if it doesn't recognize the video mode.
|
|
*/
|
|
this.nDefaultCols = parmsVideo['charCols'];
|
|
this.nDefaultRows = parmsVideo['charRows'];
|
|
if (this.nDefaultCols === undefined || this.nDefaultRows === undefined) {
|
|
this.nDefaultCols = Video.aModeParms[this.nModeDefault][0];
|
|
this.nDefaultRows = Video.aModeParms[this.nModeDefault][1];
|
|
}
|
|
|
|
/*
|
|
* setDimensions() uses these values unconditionally, as the machine has no idea what the
|
|
* physical screen size should be.
|
|
*/
|
|
this.cxScreen = parmsVideo['screenWidth'];
|
|
this.cyScreen = parmsVideo['screenHeight'];
|
|
|
|
/*
|
|
* We might consider another component parameter to specify the font-doubling setting.
|
|
* For now, it's based on whether the default SCREEN cell size is sufficiently larger than
|
|
* the default FONT cell size.
|
|
*/
|
|
this.fScaleFont = parmsVideo['scale'];
|
|
this.fDoubleFont = Math.round(this.cxScreen / this.nDefaultCols) >= 12;
|
|
|
|
this.fTouchScreen = parmsVideo['touchScreen'];
|
|
|
|
this.canvasScreen = canvas;
|
|
this.contextScreen = context;
|
|
this.textareaScreen = textarea;
|
|
|
|
/*
|
|
* Originally, setMode() would map/unmap the video buffer ONLY when the active card changed,
|
|
* because as long as an MDA or CGA remained active, its video buffer never changed. However,
|
|
* since the EGA can change its video buffer on the fly, setMode() must also compare the card's
|
|
* hard-coded and/or programmed) buffer address/size to the "active" address/size; the latter
|
|
* is recorded here.
|
|
*/
|
|
this.addrBuffer = this.sizeBuffer = 0;
|
|
|
|
/*
|
|
* aFonts is an array of font objects (ie, arrays) indexed by FONT ID. Font characters are
|
|
* arranged in 16x16 grids, with one grid per canvas object in the aCanvas array of each font object.
|
|
*
|
|
* Each element is a Font object that describes the font size and provides bitmaps for all the font
|
|
* color permutations. aFonts.length will be non-zero if ANY fonts are loaded, but do NOT assume
|
|
* that EVERY font has been loaded; check for the existence of a font by checking for its unique ID
|
|
* within this sparse array.
|
|
*/
|
|
this.aFonts = [];
|
|
|
|
/*
|
|
* Instead of (re)allocating a new color array every time getCardColors() is called, we preallocate
|
|
* an array now and simply update the entries as needed.
|
|
*/
|
|
this.aRGB = new Array(16);
|
|
|
|
/*
|
|
* Since I've not found clear documentation on a reliable way to check whether a particular DOM element
|
|
* (other than the BODY element) has focus at any given time, I've added onfocus() and onblur() handlers
|
|
* to the canvas to maintain my own focus state.
|
|
*/
|
|
this.fHasFocus = false;
|
|
|
|
var video = this;
|
|
if (this.canvasScreen) {
|
|
this.canvasScreen.onfocus = function onFocusCanvas() {
|
|
return video.onFocusChange(true);
|
|
};
|
|
this.canvasScreen.onblur = function onBlurCanvas() {
|
|
return video.onFocusChange(false);
|
|
};
|
|
}
|
|
|
|
/*
|
|
* As per http://stackoverflow.com/questions/6740253/disable-scrolling-when-changing-focus-form-elements-ipad-web-app,
|
|
* I decided to try this work-around to prevent the webpage from scrolling around whenever the canvas is given
|
|
* focus. That sort of scrolling-into-view sounds great in principle, but in practice, if you were reading some other
|
|
* portion of the page, it can be irritating to be scrolled away from that portion when refreshing/returning to the page.
|
|
*
|
|
* However, this work-around doesn't seem to work with the latest version of Safari (or else I misunderstood something).
|
|
*
|
|
canvas.onfocus = function() {
|
|
window.scrollTo(0, 0);
|
|
window.document.body.scrollTop = 0;
|
|
}
|
|
*/
|
|
|
|
/*
|
|
* As far as overall image quality of scaled fonts, these options don't seem necessary for Safari (and
|
|
* don't have any discernible effect anyway). Turning 'webkitImageSmoothingEnabled' off DOES have an effect
|
|
* on Chrome, but it's not really a positive effect overall, so I'm leaving these off for now.
|
|
*
|
|
this.contextScreen['mozImageSmoothingEnabled'] = false;
|
|
this.contextScreen['webkitImageSmoothingEnabled'] = false;
|
|
*/
|
|
|
|
var sFileURL = parmsVideo['fontROM'];
|
|
if (sFileURL) {
|
|
var sFileExt = str.getExtension(sFileURL);
|
|
if (sFileExt != "json") {
|
|
sFileURL = web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + sFileURL + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES;
|
|
}
|
|
web.loadResource(sFileURL, true, null, this, this.onLoadSetFonts);
|
|
}
|
|
}
|
|
|
|
Component.subclass(Component, Video);
|
|
|
|
Video.TRAPALL = true; // monitor all I/O by default (not just deltas)
|
|
|
|
/*
|
|
* Supported Cards
|
|
*
|
|
* Note that we choose IDs that match the default font ID for each card as well, just for consistency.
|
|
*/
|
|
Video.CARDS = {};
|
|
Video.CARDS.MDA = 1;
|
|
Video.CARDS.CGA = 3;
|
|
Video.CARDS.EGA = 5;
|
|
|
|
/*
|
|
* Supported Monitors
|
|
*
|
|
* The MDA monitor displays 350 lines of vertical resolution, 720 lines of horizontal resolution, and refreshes
|
|
* at ~50Hz. The CGA monitor displays 200 lines vertically, 640 horizontally, and refreshes at ~60Hz.
|
|
*
|
|
* Based on actual MDA timings (see http://diylab.atwebpages.com/pressureDev.htm), the total horizontal
|
|
* period (drawing a line and retracing) is ~54.25uSec (1000000uSec / 18432) and the horizontal retrace interval
|
|
* is about 15% of that, or ~8.14uSec. Vertical sync occurs once every 370 horizontal periods. Of those 370,
|
|
* only 354 represent actively drawn lines (and of those, only 350 are visible); the remaining 16 horizontal
|
|
* periods, or 4% of the 370 total, represent the vertical retrace interval.
|
|
*
|
|
* I don't have similar numbers for the CGA or EGA, so for now, I assume similar percentages; ie, 15% of
|
|
* the horizontal period will represent horizontal retrace, and 4% of the vertical pixel maximum (262) will
|
|
* represent vertical retrace. However, 24% of the CGA's 262 vertical maximum represents non-visible lines,
|
|
* whereas only 5% of the MDA's 370 maximum represents non-visible lines; is there really that much "overscan"
|
|
* on the CGA?
|
|
*
|
|
* For each monitor type, there's a Video.monitorSpecs object that describes the horizontal and vertical
|
|
* timings, along with my assumptions about the percentage of time that drawing is "active" within those periods,
|
|
* and then based on the selected monitor type, I compute the number of CPU cycles that each period lasts,
|
|
* as well as the number of CPU cycles that drawing lasts within each period, so that the horizontal and vertical
|
|
* retrace status flags can be quickly calculated.
|
|
*
|
|
* For reference, here are some important numbers to know (from https://github.com/reenigne/reenigne/blob/master/8088/cga/register_values.txt):
|
|
*
|
|
* CGA MDA
|
|
* Pixel clock 14.318 MHz 16.257 MHz (aka "maximum video bandwidth", as IBM Tech Refs sometimes call it)
|
|
* Horizontal 15.700 KHz 18.432 KHz (aka "horizontal drive", as IBM Tech Refs sometimes call it)
|
|
* Vertical 59.923 Hz 49.816 Hz
|
|
* Usage 53.69% 77.22%
|
|
* H pix 912 = 114*8 882 = 98*9
|
|
* V pix 262 370
|
|
* Dots 238944 326340
|
|
*/
|
|
|
|
/**
|
|
* @class MonitorSpecs
|
|
* @property {number} nHorzPeriodsPerSec
|
|
* @property {number} nHorzPeriodsPerFrame
|
|
* @property {number} percentHorzActive
|
|
* @property {number} percentVertActive
|
|
*
|
|
* From these monitor specs, we calculate the following values for a given Card:
|
|
*
|
|
* nCyclesPerSecond = cpu.getCyclesPerSecond(); // eg, 4772727
|
|
* nCyclesHorzPeriod = (nCyclesPerSecond / monitorSpecs.nHorzPeriodsPerSec) | 0;
|
|
* nCyclesHorzActive = (nCyclesHorzPeriod * monitorSpecs.percentHorzActive / 100) | 0;
|
|
* nCyclesVertPeriod = nCyclesHorzPeriod * monitorSpecs.nHorzPeriodsPerFrame;
|
|
* nCyclesVertActive = (nCyclesVertPeriod * monitorSpecs.percentVertActive / 100) | 0;
|
|
*/
|
|
|
|
/**
|
|
* @type {Object}
|
|
*/
|
|
Video.monitorSpecs = {};
|
|
|
|
/**
|
|
* NOTE: Based on trial-and-error, 208 is the magic number of horizontal syncs per vertical sync that
|
|
* yielded the necessary number of "horizontal enables" (200 or 0xC8) in the EGA ROM BIOS at C000:03D0.
|
|
*
|
|
* @type {{MonitorSpecs}}
|
|
*/
|
|
Video.monitorSpecs[ChipSet.MONITOR.COLOR] = {
|
|
nHorzPeriodsPerSec: 15700,
|
|
nHorzPeriodsPerFrame: 208,
|
|
percentHorzActive: 85,
|
|
percentVertActive: 96
|
|
};
|
|
|
|
/**
|
|
* NOTE: Based on trial-and-error, 364 is the magic number of horizontal syncs per vertical sync that
|
|
* yielded the necessary number of "horizontal enables" (350 or 0x15E) in the EGA ROM BIOS at C000:03D0.
|
|
*
|
|
* @type {{MonitorSpecs}}
|
|
*/
|
|
Video.monitorSpecs[ChipSet.MONITOR.MONO] = {
|
|
nHorzPeriodsPerSec: 18432,
|
|
nHorzPeriodsPerFrame: 364,
|
|
percentHorzActive: 85,
|
|
percentVertActive: 96
|
|
};
|
|
|
|
/**
|
|
* @type {{MonitorSpecs}}
|
|
*/
|
|
Video.monitorSpecs[ChipSet.MONITOR.EGACOLOR] = {
|
|
nHorzPeriodsPerSec: 21850,
|
|
nHorzPeriodsPerFrame: 364,
|
|
percentHorzActive: 85,
|
|
percentVertActive: 96
|
|
};
|
|
|
|
/*
|
|
* EGA Miscellaneous ports and SW1-Sw4
|
|
*
|
|
* The Card.MISC.CLK_SELECT bits determine which of the EGA board's 4 configuration switches are
|
|
* returned via Card.STATUS0.SWSENSE (when SWSENSE is zero, the switch is closed):
|
|
*
|
|
* 0xC: return SW1
|
|
* 0x8: return SW2
|
|
* 0x4: return SW3
|
|
* 0x0: return SW4
|
|
*
|
|
* These 4 bits are also copied to the byte at 40:88h by the EGA BIOS, where bit 0 is SW1, bit 1 is SW2,
|
|
* bit 2 is SW3 and bit 3 is SW4. Our switch settings come from bEGASW, which in turn comes from sEGASW,
|
|
* which in turn comes from the "switches" property passed to the Video component, if any.
|
|
*
|
|
* As usual, the switch settings are reversed in both direction and sense from the switch settings; the
|
|
* good news, however, is that we can use the parseSwitches() method in the ChipSet component to parse them.
|
|
*
|
|
* The set of valid EGA switch values, after conversion, is stored in the table below. For each value,
|
|
* there is an array that defines the corresponding monitor type(s) for the EGA adapter and any secondary
|
|
* adapter. The third value is a boolean indicating whether the EGA is the primary adapter.
|
|
*/
|
|
Video.aEGAMonitorSwitches = {
|
|
0x06: [ChipSet.MONITOR.TV, ChipSet.MONITOR.MONO, true], // "1001"
|
|
0x07: [ChipSet.MONITOR.COLOR, ChipSet.MONITOR.MONO, true], // "0001"
|
|
0x08: [ChipSet.MONITOR.EGAEMULATION, ChipSet.MONITOR.MONO, true], // "1110"
|
|
0x09: [ChipSet.MONITOR.EGACOLOR, ChipSet.MONITOR.MONO, true], // "0110" [our default; see bEGASW below]
|
|
0x0a: [ChipSet.MONITOR.MONO, ChipSet.MONITOR.TV, true], // "1010"
|
|
0x0b: [ChipSet.MONITOR.MONO, ChipSet.MONITOR.COLOR, true], // "0010"
|
|
0x00: [ChipSet.MONITOR.TV, ChipSet.MONITOR.MONO, false], // "1111"
|
|
0x01: [ChipSet.MONITOR.COLOR, ChipSet.MONITOR.MONO, false], // "0111"
|
|
0x02: [ChipSet.MONITOR.EGAEMULATION, ChipSet.MONITOR.MONO, false], // "1011"
|
|
0x03: [ChipSet.MONITOR.EGACOLOR, ChipSet.MONITOR.MONO, false], // "0011"
|
|
0x04: [ChipSet.MONITOR.MONO, ChipSet.MONITOR.TV, false], // "1101"
|
|
0x05: [ChipSet.MONITOR.MONO, ChipSet.MONITOR.COLOR, false] // "0101"
|
|
};
|
|
|
|
/*
|
|
* Supported Modes
|
|
*
|
|
* Although this component is designed to be a video hardware emulation, not a "BIOS simulation", we DO
|
|
* look for changes to the hardware state that correspond to standard BIOS mode settings, so our internal
|
|
* mode setting will normally match the current BIOS mode. Since 99.9% of applications use only standard
|
|
* BIOS modes, knowing that mode is often helpful. However, this doesn't mean we're dependent on the BIOS;
|
|
* we simply use common, familiar values wherever it makes sense to do so. We do have some "BIOS awareness",
|
|
* (eg, when we look for a ROM-based font, or when we're trying to ensure all the BIOS diagnostics pass),
|
|
* but for the most part, we are treating the BIOS as just another (ROM-based) application.
|
|
*
|
|
* As we expand support to include more programmable cards like the EGA, it becomes quite easy for the card
|
|
* to enter a "mode" that has no BIOS counterpart (eg, non-standard combinations of frame buffer address,
|
|
* memory access modes, fonts, display regions, etc). Our hardware emulation routines will cope with those
|
|
* situations as best they can (and when they don't, it should be considered a bug if some application is
|
|
* broken as a result), but realistically, this is never going to be a completely accurate hardware emulation
|
|
* of any video card.
|
|
*/
|
|
Video.MODES = {};
|
|
Video.MODES.CGA_40X25_BW = 0;
|
|
Video.MODES.CGA_40X25 = 1;
|
|
Video.MODES.CGA_80X25_BW = 2;
|
|
Video.MODES.CGA_80X25 = 3;
|
|
Video.MODES.CGA_320X200 = 4;
|
|
Video.MODES.CGA_320X200_BW = 5;
|
|
Video.MODES.CGA_640X200 = 6;
|
|
Video.MODES.MDA_80X25 = 7;
|
|
Video.MODES.EGA_320X200 = 0x0D; // mapped at A000:0000
|
|
Video.MODES.EGA_640X200 = 0x0E; // mapped at A000:0000
|
|
Video.MODES.EGA_640X350_MONO = 0x0F; // mapped at A000:0000, monochrome
|
|
Video.MODES.EGA_640X350 = 0x10; // mapped at A000:0000, color
|
|
Video.MODES.UNKNOWN = 0xFF;
|
|
|
|
/*
|
|
* Supported Fonts
|
|
*
|
|
* Once we've finished loading the standard 8K font file, aFonts[] should contain one or more of the
|
|
* fonts listed below. For the standard MDA/CGA font ROM, the first (MDA) font resides in the first 4K,
|
|
* and the second and third (CGA) fonts reside in the two 2K halves of the second 4K.
|
|
*
|
|
* It may seem odd that the cell size for FONT_CGAD is *larger* than the cell size for FONT_CGA,
|
|
* since 40-column mode is actually lower resolution, but since we don't shrink the window when we shrink
|
|
* the mode, the characters must be drawn larger, and they look better if we don't have to scale them.
|
|
*
|
|
* From the IBM EGA Manual (p.5):
|
|
*
|
|
* "In alphanumeric modes, characters are formed from one of two ROM (Read Only Memory) character
|
|
* generators on the adapter. One character generator defines 7x9 characters in a 9x14 character box.
|
|
* For Enhanced Color Display support, the 9x14 character set is modified to provide an 8x14 character set.
|
|
* The second character generator defines 7x7 characters in an 8x8 character box. These generators contain
|
|
* dot patterns for 256 different characters. The character sets are identical to those provided by the
|
|
* IBM Monochrome Display Adapter and the IBM Color/Graphics Monitor Adapter."
|
|
*/
|
|
Video.FONTS = {};
|
|
Video.FONTS.MDA = 1; // 9x14 monochrome font
|
|
Video.FONTS.MDAD = 2; // 18x28 monochrome font (this is the 9x14 font doubled)
|
|
Video.FONTS.CGA = 3; // 8x8 color font
|
|
Video.FONTS.CGAD = 6; // 16x16 color font (this is the 8x8 CGA font doubled)
|
|
Video.FONTS.EGA = 5; // 9x14 color font
|
|
Video.FONTS.EGAD = 10; // 18x28 color font (this is the 9x14 EGA font doubled)
|
|
|
|
/*
|
|
* For each video mode, we need to know the following pieces of information:
|
|
*
|
|
* 0: # of columns (nCols)
|
|
* 1: # of rows (nRows)
|
|
* 2: # cells per word (nCellsPerWord: # of characters or pixels per word)
|
|
* 3: # bytes of visible screen padding, if any (used for CGA graphics modes only)
|
|
* 4: font ID (nFont: undefined if graphics mode)
|
|
*
|
|
* By calculating ([0] * [1]) / [2], we obtain the number of 16-bit words that mode actively displays;
|
|
* for example, the amount of visible memory used by mode 0x04 is (320 * 200) / 4, or 16000.
|
|
*
|
|
* The MODES.CGA_40X25 modes specify FONT_CGA instead of FONT_CGAD because we don't automatically
|
|
* load the FONT_CGAD unless the screen is large enough to accommodate it (see the fDoubleFont calculation).
|
|
*
|
|
* To compensate, we have code in setDimensions() that automatically switches to FONT_CGAD if it's loaded AND
|
|
* the cell size warrants the larger font. We could hard-code FONT_CGAD here, but then we'd always load it,
|
|
* and it might not always be the best fit.
|
|
*/
|
|
Video.aModeParms = []; // Mode
|
|
Video.aModeParms[Video.MODES.CGA_40X25] = [ 40, 25, 1, 0, Video.FONTS.CGA]; // 0x00
|
|
Video.aModeParms[Video.MODES.CGA_80X25] = [ 80, 25, 1, 0, Video.FONTS.CGA]; // 0x02
|
|
Video.aModeParms[Video.MODES.CGA_320X200] = [320, 200, 8, 192]; // 0x04
|
|
Video.aModeParms[Video.MODES.CGA_640X200] = [640, 200, 16, 192]; // 0x06
|
|
Video.aModeParms[Video.MODES.MDA_80X25] = [ 80, 25, 1, 0, Video.FONTS.MDA]; // 0x07
|
|
Video.aModeParms[Video.MODES.EGA_320X200] = [320, 200, 16]; // 0x0D
|
|
Video.aModeParms[Video.MODES.EGA_640X200] = [640, 200, 16]; // 0x0E
|
|
Video.aModeParms[Video.MODES.EGA_640X350_MONO] = [640, 350, 16]; // 0x0F
|
|
Video.aModeParms[Video.MODES.EGA_640X350] = [640, 350, 16]; // 0x10
|
|
|
|
Video.aModeParms[Video.MODES.CGA_40X25_BW] = Video.aModeParms[Video.MODES.CGA_40X25]; // 0x01
|
|
Video.aModeParms[Video.MODES.CGA_80X25_BW] = Video.aModeParms[Video.MODES.CGA_80X25]; // 0x03
|
|
Video.aModeParms[Video.MODES.CGA_320X200_BW] = Video.aModeParms[Video.MODES.CGA_320X200]; // 0x05
|
|
|
|
/*
|
|
* MDA attribute byte definitions
|
|
*
|
|
* For MDA, only the following group of ATTR definitions are supported; any FGND/BGND value combinations
|
|
* outside this group will be treated as "normal" (ATTR_FGND_WHITE | ATTR_BGND_BLACK).
|
|
*
|
|
* NOTE: Assuming MDA.MODE.BLINK_ENABLE is set (which the ROM BIOS sets by default), ATTR_BGND_BLINK will
|
|
* cause the *foreground* element of the cell to blink, even though it is part of the *background* attribute bits.
|
|
*
|
|
* Regarding blink rate, characters are supposed to blink every 16 vertical frames, which amounts to .26667 blinks
|
|
* per second, assuming a 60Hz vertical refresh rate. So roughly every 267ms, we need to take care of any blinking
|
|
* characters. updateScreen() maintains a global count (cBlinkVisible) of blinking characters, to simplify the
|
|
* decision of when to redraw the screen.
|
|
*/
|
|
Video.ATTRS = {};
|
|
Video.ATTRS.FGND_BLACK = 0x00;
|
|
Video.ATTRS.FGND_ULINE = 0x01;
|
|
Video.ATTRS.FGND_WHITE = 0x07;
|
|
Video.ATTRS.FGND_BRIGHT = 0x08;
|
|
Video.ATTRS.BGND_BLACK = 0x00;
|
|
Video.ATTRS.BGND_WHITE = 0x70;
|
|
Video.ATTRS.BGND_BLINK = 0x80;
|
|
Video.ATTRS.BGND_BRIGHT = 0x80;
|
|
Video.ATTRS.DRAW_FGND = 0x100; // this is an internal attribute bit, indicating the foreground should be drawn
|
|
Video.ATTRS.DRAW_CURSOR = 0x200; // this is an internal attribute bit, indicating when the cursor should be drawn
|
|
|
|
/*
|
|
* Here's a "cheat sheet" for attribute byte combinations that the IBM MDA could have supported. The original (Aug 1981)
|
|
* IBM Tech Ref is very terse and implies that only those marked with * are actually supported.
|
|
*
|
|
* *0x00: non-display ATTR_FGND_BLACK | ATTR_BGND_BLACK
|
|
* *0x01: underline ATTR_FGND_ULINE | ATTR_BGND_BLACK
|
|
* *0x07: normal (white on black) ATTR_FGND_WHITE | ATTR_BGND_BLACK
|
|
* **0x09: bright underline ATTR_FGND_ULINE | ATTR_FGND_BRIGHT | ATTR_BGND_BLACK
|
|
* **0x0F: bold (bright white on black) ATTR_FGND_WHITE | ATTR_FGND_BRIGHT | ATTR_BGND_BLACK
|
|
* *0x70: reverse (black on white) ATTR_FGND_BLACK | | ATTR_BGND_WHITE
|
|
* 0x81: blinking underline ATTR_FGND_ULINE | | ATTR_BGND_BLINK (or dim background if blink disabled)
|
|
* **0x87: blinking normal ATTR_FGND_WHITE | | ATTR_BGND_BLINK (or dim background if blink disabled)
|
|
* 0x89: blinking bright underline ATTR_FGND_ULINE | ATTR_FGND_BRIGHT | ATTR_BGND_BLINK (or dim background if blink disabled)
|
|
* **0x8F: blinking bold ATTR_FGND_WHITE | ATTR_FGND_BRIGHT | ATTR_BGND_BLINK (or dim background if blink disabled)
|
|
* **0xF0: blinking reverse ATTR_FGND_WHITE | ATTR_FGND_BRIGHT | ATTR_BGND_BLINK (or bright background if blink disabled)
|
|
*
|
|
* Unsupported attributes reportedly display as "normal" (ATTR_FGND_WHITE | ATTR_BGND_BLACK). However, precisely which
|
|
* attributes are unsupported on the MDA varies depending on the source. Some sources (eg, the IBM Tech Ref) imply that
|
|
* only those marked by * are supported, while others (eg, some--but not all--Peter Norton guides) include those marked
|
|
* by **, and still others include ALL the combinations listed above.
|
|
*
|
|
* Furthermore, according to http://www.seasip.info/VintagePC/mda.html:
|
|
*
|
|
* Attributes 0x00, 0x08, 0x80 and 0x88 display as black space;
|
|
* Attribute 0x78 displays as dark green on green; depending on the monitor, there may be a green "halo" where the dark and bright bits meet;
|
|
* Attribute 0xF0 displays as a blinking version of 0x70 if blink enabled, and black on bright green otherwise;
|
|
* Attribute 0xF8 displays as a blinking version of 0x78 if blink enabled, and as dark green on bright green otherwise.
|
|
*
|
|
* However, I'm rather skeptical about supporting 0x78 and 0xF8, until I see some evidence that "bright black" actually
|
|
* produced dark green on IBM equipment; it also doesn't sound like a combination many people would have used. I'll probably
|
|
* treat all of 0x08, 0x80 and 0x88 the same as 0x00, only because it seems logical (they're all "black on black" combinations
|
|
* with only BRIGHT and/or BLINK bits set). Beyond that, I'll likely treat any other combination not listed in the above cheat
|
|
* sheet as "normal".
|
|
*
|
|
* All the discrepancies/disagreements I've found are probably due in part to the proliferation of IBM and non-IBM MDA
|
|
* cards, combined with IBM and non-IBM monochrome monitors, and people assuming that their non-IBM card and/or monitor
|
|
* behaved exactly like the original IBM equipment, which probably wasn't true in all cases.
|
|
*
|
|
* I would like to limit my MDA display support to EXACTLY everything that the IBM MDA supported and nothing more, but
|
|
* since there will be combinations that will logically "fall out" unless I specifically exclude them, it's very likely
|
|
* this implementation will end up being a superset.
|
|
*/
|
|
|
|
/*
|
|
* CGA attribute byte definitions; these simply extend the set of MDA attributes, with the exception of ATTR_FNGD_ULINE,
|
|
* which the CGA can treat only as ATTR_FGND_BLUE.
|
|
*/
|
|
Video.ATTRS.FGND_BLUE = 0x01;
|
|
Video.ATTRS.FGND_GREEN = 0x02;
|
|
Video.ATTRS.FGND_CYAN = 0x03;
|
|
Video.ATTRS.FGND_RED = 0x04;
|
|
Video.ATTRS.FGND_MAGENTA = 0x05;
|
|
Video.ATTRS.FGND_BROWN = 0x06;
|
|
|
|
Video.ATTRS.BGND_BLUE = 0x10;
|
|
Video.ATTRS.BGND_GREEN = 0x20;
|
|
Video.ATTRS.BGND_CYAN = 0x30;
|
|
Video.ATTRS.BGND_RED = 0x40;
|
|
Video.ATTRS.BGND_MAGENTA = 0x50;
|
|
Video.ATTRS.BGND_BROWN = 0x60;
|
|
|
|
/* For the MDA, the length of aMDAColors is 5, based on the following supported FGND attribute values:
|
|
*
|
|
* 0x0: black font (attribute value 0x8 is mapped to 0x0)
|
|
* 0x1: green font with underline
|
|
* 0x7: green font without underline (attribute values 0x2-0x6 are mapped to 0x7)
|
|
* 0x9: bright green font with underline
|
|
* 0xf: bright green font without underline (attribute values 0xa-0xe are mapped to 0xf)
|
|
*
|
|
* I'm still not sure about 0x8 (dark green?); for now, I'm mapping it to 0x0, but it may become a 6th supported color.
|
|
*/
|
|
Video.aMDAColors = new Array(5);
|
|
Video.aMDAColors[0] = [0x00, 0x00, 0x00, 0xff];
|
|
Video.aMDAColors[1] = [0x7f, 0xc0, 0x7f, 0xff];
|
|
Video.aMDAColors[2] = [0x7f, 0xc0, 0x7f, 0xff];
|
|
Video.aMDAColors[3] = [0x7f, 0xff, 0x7f, 0xff];
|
|
Video.aMDAColors[4] = [0x7f, 0xff, 0x7f, 0xff];
|
|
Video.aMDAColorMap = [0x0, 0x1, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x3, 0x4, 0x4, 0x4, 0x4, 0x4, 0x4];
|
|
|
|
Video.aCGAColors = new Array(16);
|
|
Video.aCGAColors[0] = [0x00, 0x00, 0x00, 0xff]; // ATTR_FGND_BLACK
|
|
Video.aCGAColors[1] = [0x00, 0x00, 0xaa, 0xff]; // ATTR_FGND_BLUE
|
|
Video.aCGAColors[2] = [0x00, 0xaa, 0x00, 0xff]; // ATTR_FGND_GREEN
|
|
Video.aCGAColors[3] = [0x00, 0xaa, 0xaa, 0xff]; // ATTR_FGND_CYAN
|
|
Video.aCGAColors[4] = [0xaa, 0x00, 0x00, 0xff]; // ATTR_FGND_RED
|
|
Video.aCGAColors[5] = [0xaa, 0x00, 0xaa, 0xff]; // ATTR_FGND_MAGENTA
|
|
Video.aCGAColors[6] = [0xaa, 0x55, 0x00, 0xff]; // ATTR_FGND_BROWN
|
|
Video.aCGAColors[7] = [0xaa, 0xaa, 0xaa, 0xff]; // ATTR_FGND_WHITE (aka light gray)
|
|
Video.aCGAColors[8] = [0x55, 0x55, 0x55, 0xff]; // ATTR_FGND_BLACK | ATTR_FGND_BRIGHT (aka gray)
|
|
Video.aCGAColors[9] = [0x55, 0x55, 0xff, 0xff]; // ATTR_FGND_BLUE | ATTR_FGND_BRIGHT
|
|
Video.aCGAColors[10] = [0x55, 0xff, 0x55, 0xff]; // ATTR_FGND_GREEN | ATTR_FGND_BRIGHT
|
|
Video.aCGAColors[11] = [0x55, 0xff, 0xff, 0xff]; // ATTR_FGND_CYAN | ATTR_FGND_BRIGHT
|
|
Video.aCGAColors[12] = [0xff, 0x55, 0x55, 0xff]; // ATTR_FGND_RED | ATTR_FGND_BRIGHT
|
|
Video.aCGAColors[13] = [0xff, 0x55, 0xff, 0xff]; // ATTR_FGND_MAGENTA | ATTR_FGND_BRIGHT
|
|
Video.aCGAColors[14] = [0xff, 0xff, 0x55, 0xff]; // ATTR_FGND_BROWN | ATTR_FGND_BRIGHT (aka yellow)
|
|
Video.aCGAColors[15] = [0xff, 0xff, 0xff, 0xff]; // ATTR_FGND_WHITE | ATTR_FGND_BRIGHT (aka white)
|
|
|
|
Video.aCGAColorSet1 = [Video.ATTRS.FGND_GREEN, Video.ATTRS.FGND_RED, Video.ATTRS.FGND_BROWN];
|
|
Video.aCGAColorSet2 = [Video.ATTRS.FGND_CYAN, Video.ATTRS.FGND_MAGENTA, Video.ATTRS.FGND_WHITE];
|
|
|
|
/*
|
|
* Here is the EGA BIOS default ATC palette register set for color text modes, from which getCardColors()
|
|
* builds a default RGB array, similar to aCGAColors above.
|
|
*/
|
|
Video.aEGAPalDef = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x14, 0x07, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F];
|
|
|
|
Video.aEGAByteToDW = [
|
|
0x00000000, 0x000000ff, 0x0000ff00, 0x0000ffff,
|
|
0x00ff0000, 0x00ff00ff, 0x00ffff00, 0x00ffffff,
|
|
0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff,
|
|
0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff
|
|
];
|
|
|
|
Video.aEGADWToByte = [];
|
|
Video.aEGADWToByte[0x00000000] = 0x0;
|
|
Video.aEGADWToByte[0x00000080] = 0x1;
|
|
Video.aEGADWToByte[0x00008000] = 0x2;
|
|
Video.aEGADWToByte[0x00008080] = 0x3;
|
|
Video.aEGADWToByte[0x00800000] = 0x4;
|
|
Video.aEGADWToByte[0x00800080] = 0x5;
|
|
Video.aEGADWToByte[0x00808000] = 0x6;
|
|
Video.aEGADWToByte[0x00808080] = 0x7;
|
|
Video.aEGADWToByte[0x80000000] = 0x8;
|
|
Video.aEGADWToByte[0x80000080] = 0x9;
|
|
Video.aEGADWToByte[0x80008000] = 0xa;
|
|
Video.aEGADWToByte[0x80008080] = 0xb;
|
|
Video.aEGADWToByte[0x80800000] = 0xc;
|
|
Video.aEGADWToByte[0x80800080] = 0xd;
|
|
Video.aEGADWToByte[0x80808000] = 0xe;
|
|
Video.aEGADWToByte[0x80808080] = 0xf;
|
|
|
|
/**
|
|
* Card(video, iCard, data, cbMemory)
|
|
*
|
|
* Creates an object representing an initial video card state;
|
|
* can also restore a video card from state data created by saveCard().
|
|
*
|
|
* See new Card().
|
|
*
|
|
* @constructor
|
|
* @param {Video} [video]
|
|
* @param {number} [iCard] (see Video.CARDS.*)
|
|
* @param {Array|null} [data]
|
|
* @param {number} [cbMemory] is specified if the card must allocate its own memory buffer
|
|
*/
|
|
function Card(video, iCard, 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)) {
|
|
|
|
var specs = Video.cardSpecs[iCard];
|
|
var nMonitorType = video.nMonitorType || specs[5];
|
|
|
|
if (!data || data.length < 6) {
|
|
data = [false, 0, null, null, 0, new Array(Card.CRTC.TOTAL_REGS)];
|
|
}
|
|
|
|
/*
|
|
* If a Debugger is present, we want to stash a bit more info in each Card.
|
|
*/
|
|
if (DEBUGGER) {
|
|
this.dbg = video.dbg;
|
|
this.type = specs[0];
|
|
this.port = specs[1];
|
|
}
|
|
|
|
this.iCard = iCard;
|
|
this.addrBuffer = specs[2]; // default (physical) frame buffer address
|
|
this.sizeBuffer = specs[3]; // default frame buffer length (this is the total size, not the current visible size; this.cbScreen is calculated on the fly to reflect the latter)
|
|
|
|
/*
|
|
* If no memory size is specified, then setMode() will use addMemory() to automatically add enough
|
|
* memory blocks to cover the frame buffer specified above; otherwise, it instructs addMemory() to call
|
|
* getMemoryBuffer(), which will return a portion of the buffer (adwMemory) allocated below. This allows
|
|
* a card like the EGA to move/resize its frame buffer as needed, as well as giving it total control over
|
|
* the underlying memory.
|
|
*/
|
|
this.cbMemory = cbMemory || specs[4];
|
|
|
|
/*
|
|
* All of our cardSpec frame buffer sizes are based on the default text mode (eg, 4Kb for an MDA, 16Kb for
|
|
* a CGA), but for a card with 64Kb or more of memory (ie, any EGA card), the default text mode frame buffer
|
|
* size should be dynamically recalculated as the smaller of: cbMemory divided by 4, or 32Kb.
|
|
*/
|
|
if (this.cbMemory >= 0x10000 && this.addrBuffer >= 0xB0000) {
|
|
this.sizeBuffer = Math.min(this.cbMemory >> 2, 0x8000);
|
|
}
|
|
|
|
this.fActive = data[0];
|
|
this.modeReg = data[1]; // see MDA.MODE* or CGA.MODE_* (use (MDA.MODE.HIRES | MDA.MODE.VIDEO_ENABLE | MDA.MODE.BLINK_ENABLE) if you want to test blinking immediately after the initial power-on reset)
|
|
this.colorReg = data[2]; // see CGA.COLOR.* (undefined on MDA)
|
|
this.statusReg = data[3]; // see MDA.STATUS.* or CGA.STATUS.*
|
|
this.iCRTCReg = data[4] & 0xff;
|
|
this.iCRTCPrev = (data[4] >> 8) & 0xff;
|
|
this.aCRTCRegs = data[5];
|
|
this.nCRTCRegs = Card.CRTC.TOTAL_REGS;
|
|
this.asCRTCRegs = DEBUGGER? Card.CRTC.REGS : [];
|
|
|
|
if (iCard == Video.CARDS.EGA) {
|
|
this.nCRTCRegs = Card.CRTC.EGA.TOTAL_REGS;
|
|
this.asCRTCRegs = DEBUGGER? Card.CRTC.EGA_REGS : [];
|
|
this.initEGA(data[6], nMonitorType);
|
|
}
|
|
|
|
var monitorSpecs = Video.monitorSpecs[nMonitorType] || Video.monitorSpecs[ChipSet.MONITOR.MONO];
|
|
|
|
var nCyclesPerSecond = video.cpu.getCyclesPerSecond(); // eg, 4772727
|
|
this.nCyclesHorzPeriod = (nCyclesPerSecond / monitorSpecs.nHorzPeriodsPerSec) | 0;
|
|
this.nCyclesHorzActive = (this.nCyclesHorzPeriod * monitorSpecs.percentHorzActive / 100) | 0;
|
|
this.nCyclesVertPeriod = this.nCyclesHorzPeriod * monitorSpecs.nHorzPeriodsPerFrame;
|
|
this.nCyclesVertActive = (this.nCyclesVertPeriod * monitorSpecs.percentVertActive / 100) | 0;
|
|
this.nInitCycles = (data[7] == null? 0 : data[7]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* MDA I/O ports
|
|
*/
|
|
Card.MDA = {};
|
|
Card.MDA.CRTC = {};
|
|
Card.MDA.CRTC.INDX = {};
|
|
Card.MDA.CRTC.INDX.PORT = 0x3B4; // NOTE: the low byte of this port address (0xB4) is mirrored at 40:0063 (0x0463)
|
|
Card.MDA.CRTC.INDX.MASK = 0x1F;
|
|
Card.MDA.CRTC.DATA = {};
|
|
Card.MDA.CRTC.DATA.PORT = 0x3B5;
|
|
|
|
Card.MDA.MODE = {};
|
|
Card.MDA.MODE.PORT = 0x3B8; // Mode Select Register, aka CRT Control Port 1 (write-only); the BIOS mirrors this register at 40:0065 (0x0465)
|
|
Card.MDA.MODE.HIRES = 0x01;
|
|
Card.MDA.MODE.VIDEO_ENABLE = 0x08;
|
|
Card.MDA.MODE.BLINK_ENABLE = 0x20;
|
|
|
|
Card.MDA.STATUS = {};
|
|
Card.MDA.STATUS.PORT = 0x3BA;
|
|
Card.MDA.STATUS.HDRIVE = 0x01;
|
|
Card.MDA.STATUS.BWVIDEO = 0x08;
|
|
|
|
Card.MDA.PRT_DATA = {};
|
|
Card.MDA.PRT_DATA.PORT = 0x3BC;
|
|
Card.MDA.PRT_STATUS = {};
|
|
Card.MDA.PRT_STATUS.PORT = 0x3BD;
|
|
Card.MDA.PRT_CTRL = {};
|
|
Card.MDA.PRT_CTRL.PORT = 0x3BE;
|
|
|
|
/*
|
|
* CGA I/O ports
|
|
*/
|
|
Card.CGA = {};
|
|
Card.CGA.CRTC = {};
|
|
Card.CGA.CRTC.INDX = {};
|
|
Card.CGA.CRTC.INDX.PORT = 0x3D4; // NOTE: the low byte of this port address (0xB4) is mirrored at 40:0063 (0x0463)
|
|
Card.CGA.CRTC.INDX.MASK = 0x1F;
|
|
Card.CGA.CRTC.DATA = {};
|
|
Card.CGA.CRTC.DATA.PORT = 0x3D5;
|
|
|
|
Card.CGA.MODE = {};
|
|
Card.CGA.MODE.PORT = 0x3D8; // Mode Select Register (write-only); the BIOS mirrors this register at 40:0065 (0x0465)
|
|
Card.CGA.MODE._80X25 = 0x01;
|
|
Card.CGA.MODE.GRAPHIC_SEL = 0x02;
|
|
Card.CGA.MODE.BW_SEL = 0x04;
|
|
Card.CGA.MODE.VIDEO_ENABLE = 0x08; // same as MDA.MODE.VIDEO_ENABLE
|
|
Card.CGA.MODE.HIRES_BW = 0x10;
|
|
Card.CGA.MODE.BLINK_ENABLE = 0x20; // same as MDA.MODE.BLINK_ENABLE
|
|
|
|
Card.CGA.COLOR = {};
|
|
Card.CGA.COLOR.PORT = 0x3D9; // write-only
|
|
Card.CGA.COLOR.BORDER = 0x07;
|
|
Card.CGA.COLOR.BRIGHT = 0x08;
|
|
Card.CGA.COLOR.BGND_ALT = 0x10; // alternate, intensified background colors in text mode
|
|
Card.CGA.COLOR.COLORSET2 = 0x20; // selects aCGAColorSet2 colors for 320x200 graphics mode; aCGAColorSet1 otherwise
|
|
|
|
Card.CGA.STATUS = {};
|
|
Card.CGA.STATUS.PORT = 0x3DA; // read-only; same for EGA (although the EGA calls this STATUS1, to distinguish it from STATUS0)
|
|
Card.CGA.STATUS.DISP_ENABLE = 0x01;
|
|
Card.CGA.STATUS.PEN_TRIGGER = 0x02;
|
|
Card.CGA.STATUS.PEN_ON = 0x04;
|
|
Card.CGA.STATUS.VERT_RETRACE= 0x08; // when set, this indicates the CGA is performing a vertical retrace
|
|
|
|
Card.CGA.CLEAR_PEN = {};
|
|
Card.CGA.CLEAR_PEN.PORT = 0x3DB;
|
|
|
|
Card.CGA.PRESET_PEN = {};
|
|
Card.CGA.PRESET_PEN.PORT = 0x3DC;
|
|
|
|
/*
|
|
* Common CRT hardware registers, accessed via Card.XXA.CRTC.INDX.PORT and Card.XXA.CRTC.DATA.PORT
|
|
*
|
|
* NOTE: In this implementation, because we have to make at least two of the registers readable (CURSOR_ADDR_HI and CURSOR_ADDR_LO),
|
|
* we end up making ALL the registers readable, otherwise we would have to explicitly block any register marked write-only. I don't
|
|
* think making the CRT registers fully readable presents any serious compatibility issues, and it actually offers some benefits
|
|
* (eg, improved debugging).
|
|
*
|
|
* However, some things are broken: the (readable) light pen registers on the EGA are overloaded as (writable) vertical retrace
|
|
* registers, so the vertical retrace registers cannot actually be read that way. I'm sure the VGA solved that problem, but I haven't
|
|
* looked into it yet.
|
|
*/
|
|
Card.CRTC = {};
|
|
Card.CRTC.EGA = {};
|
|
Card.CRTC.HORZ_TOTAL = 0x00;
|
|
Card.CRTC.HORZ_DISP = 0x01; Card.CRTC.EGA.HORZ_DISP_END = 0x01;
|
|
Card.CRTC.HORZ_SYNC_POS = 0x02; Card.CRTC.EGA.HORZ_BLANK_START = 0x02;
|
|
Card.CRTC.HORZ_SYNC_WIDTH = 0x03; Card.CRTC.EGA.HORZ_BLANK_END = 0x03;
|
|
Card.CRTC.VERT_TOTAL = 0x04; Card.CRTC.EGA.HORZ_RETRACE_START = 0x04;
|
|
Card.CRTC.VERT_TOTAL_ADJ = 0x05; Card.CRTC.EGA.HORZ_RETRACE_END = 0x05;
|
|
Card.CRTC.VERT_DISP_TOTAL = 0x06; Card.CRTC.EGA.VERT_TOTAL = 0x06;
|
|
Card.CRTC.VERT_SYNC_POS = 0x07; Card.CRTC.EGA.OVERFLOW = {INDX: 0x07, VERT_TOTAL: 0x01};
|
|
Card.CRTC.INTERLACE_POS = 0x08; Card.CRTC.EGA.PRESET_ROW_SCAN = 0x08;
|
|
Card.CRTC.MAX_SCAN_LINE = 0x09;
|
|
Card.CRTC.CURSOR_START = {};
|
|
Card.CRTC.CURSOR_START.INDX = 0x0A;
|
|
Card.CRTC.CURSOR_START.MASK = 0x1F;
|
|
/*
|
|
* I don't entirely understand these cursor blink control bits. Here's what the MC6845 datasheet says:
|
|
*
|
|
* Bit 5 is the blink timing control. When bit 5 is low, the blink frequency is 1/16 of the vertical field rate,
|
|
* and when bit 5 is high, the blink frequency is 1/32 of the vertical field rate. Bit 6 is used to enable a blink.
|
|
*/
|
|
Card.CRTC.CURSOR_START.BLINKON = 0x00; // (supposedly, 0x04 has the same effect as 0x00)
|
|
Card.CRTC.CURSOR_START.BLINKOFF = 0x20; // if blinking is disabled, the cursor is effectively hidden
|
|
Card.CRTC.CURSOR_START.BLINKFAST= 0x60; // default is 1/16 of the frame rate; this switches to 1/32 of the frame rate
|
|
|
|
Card.CRTC.CURSOR_END = {};
|
|
Card.CRTC.CURSOR_END.INDX = 0x0B;
|
|
Card.CRTC.CURSOR_END.MASK = 0x1F;
|
|
Card.CRTC.START_ADDR_HI = 0x0C;
|
|
Card.CRTC.START_ADDR_LO = 0x0D;
|
|
Card.CRTC.CURSOR_ADDR_HI = 0x0E;
|
|
Card.CRTC.CURSOR_ADDR_LO = 0x0F;
|
|
Card.CRTC.LIGHT_PEN_HI = 0x10; Card.CRTC.EGA.VERT_RETRACE_START = 0x10;
|
|
Card.CRTC.LIGHT_PEN_LO = 0x11; Card.CRTC.EGA.VERT_RETRACE_END = 0x11;
|
|
Card.CRTC.TOTAL_REGS = 0x12; // total CRT registers on MDA/CGA
|
|
Card.CRTC.EGA.VERT_DISP_END = 0x12;
|
|
Card.CRTC.EGA.OFFSET = 0x13;
|
|
Card.CRTC.EGA.UNDERLINE = 0x14;
|
|
Card.CRTC.EGA.VERT_BLANK_START = 0x15;
|
|
Card.CRTC.EGA.VERT_BLANK_END = 0x16;
|
|
Card.CRTC.EGA.MODE_CTRL = {};
|
|
Card.CRTC.EGA.MODE_CTRL.INDX = 0x17;
|
|
Card.CRTC.EGA.MODE_CTRL.CMS = 0x01; // Compatibility Mode Support (CGA A13 control)
|
|
Card.CRTC.EGA.MODE_CTRL.SRSC = 0x02; // Select Row Scan Counter
|
|
Card.CRTC.EGA.MODE_CTRL.HRS = 0x04; // Horizontal Retrace Select
|
|
Card.CRTC.EGA.MODE_CTRL.CBT = 0x08; // Count By Two
|
|
Card.CRTC.EGA.MODE_CTRL.OC = 0x10; // Output Control
|
|
Card.CRTC.EGA.MODE_CTRL.AW = 0x20; // Address Wrap (in Word mode, 1 maps A15 to A0 and 0 maps A13; use the latter when only 64Kb is installed)
|
|
Card.CRTC.EGA.MODE_CTRL.BM = 0x40; // Byte Mode (1 selects Byte Mode; 0 selects Word Mode)
|
|
Card.CRTC.EGA.MODE_CTRL.HR = 0x80; // Hardware Reset
|
|
Card.CRTC.EGA.LINE_COMPARE = 0x18;
|
|
Card.CRTC.EGA.TOTAL_REGS = 0x19; // total CRT registers on EGA
|
|
|
|
Card.CRTC.ADDR_HI_MASK = 0x3F;
|
|
|
|
if (DEBUGGER) {
|
|
Card.CRTC.REGS = ["HORZ_TOTAL","HORZ_DISP","HORZ_SYNC_POS","HORZ_SYNC_WIDTH","VERT_TOTAL","VERT_TOTAL_ADJ",
|
|
"VERT_DISP","VERT_SYNC_POS","INTERLACE_POS","MAX_SCAN_LINE","CURSOR_START","CURSOR_END",
|
|
"START_ADDR_HI","START_ADDR_LO","CURSOR_ADDR_HI","CURSOR_ADDR_LO","LIGHT_PEN_HI","LIGHT_PEN_LO"];
|
|
|
|
Card.CRTC.EGA_REGS = ["HORZ_TOTAL","HORZ_DISP_END","HORZ_BLANK_START","HORZ_BLANK_END","HORZ_RETRACE_START","HORZ_RETRACE_END",
|
|
"VERT_TOTAL","CRTC_OVERFLOW","PRESET_ROW_SCAN","MAX_SCAN_LINE","CURSOR_START","CURSOR_END",
|
|
"START_ADDR_HI","START_ADDR_LO","CURSOR_ADDR_HI","CURSOR_ADDR_LO","LIGHT_PEN_HI","LIGHT_PEN_LO",
|
|
"VERT_DISP_END","OFFSET","UNDERLINE","VERT_BLANK_START","VERT_BLANK_END","MODE_CTRL","LONE_COMPARE"];
|
|
}
|
|
|
|
/*
|
|
* EGA Status port
|
|
*/
|
|
Card.STATUS1 = {};
|
|
Card.STATUS1.PORT = 0x3DA;
|
|
/*
|
|
* STATUS1 diagnostic bits 5 and 4 are set according to the Card.ATC.PLANES.MUX bits:
|
|
*
|
|
* MUX Bit 5 Bit 4
|
|
* --- ---- ----
|
|
* 00: Red Blue
|
|
* 01: SecBlue Green
|
|
* 10: SecRed SecGreen
|
|
* 11: unused unused
|
|
*/
|
|
Card.STATUS1.DIAGNOSTIC = 0x30; // these bits are controlled by the Card.ATC.PLANES.MUX bits
|
|
|
|
/*
|
|
* EGA Attribute Controller (ATC) ports
|
|
*
|
|
* The current ATC INDX value is stored in cardEGA.iATCReg (including the Card.ATC.INDX_ENABLE bit), and the
|
|
* ATC DATA values are stored in cardEGA.aATCRegs. Also, the state of the ATC INDX/DATA flip-flop is stored in fATCData.
|
|
*
|
|
* Note that the ATC palette registers (0x0-0xf) all use the following 6 bit assignments, with bits 6 and 7 unused:
|
|
*
|
|
* 0: Blue
|
|
* 1: Green
|
|
* 2: Red
|
|
* 3: SecBlue (or mono video)
|
|
* 4: SecGreen (or intensity)
|
|
* 5: SecRed
|
|
*/
|
|
Card.ATC = {};
|
|
Card.ATC.PORT = 0x3C0; // write-only
|
|
Card.ATC.INDX_MASK = 0x1F;
|
|
Card.ATC.INDX_PAL_ENABLE = 0x20; // must be clear when loading palette registers
|
|
Card.ATC.PALETTE = {};
|
|
Card.ATC.PALETTE.INDX = 0x00; // 16 registers: 0x00 - 0x0F
|
|
Card.ATC.PALETTE.BLUE = 0x01;
|
|
Card.ATC.PALETTE.GREEN = 0x02;
|
|
Card.ATC.PALETTE.RED = 0x04;
|
|
Card.ATC.PALETTE.SECBLUE = 0x08;
|
|
Card.ATC.PALETTE.BRIGHT = 0x10; // NOTE: The IBM EGA manual (p.56) also calls this the "intensity" bit
|
|
Card.ATC.PALETTE.SECGREEN = 0x10;
|
|
Card.ATC.PALETTE.SECRED = 0x20;
|
|
Card.ATC.PALETTE_REGS = 0x10; // 16 total palette registers
|
|
Card.ATC.MODE = {};
|
|
Card.ATC.MODE.INDX = 0x10; // MODE CONTROL
|
|
Card.ATC.OVRSCAN = {};
|
|
Card.ATC.OVRSCAN.INDX = 0x11; // OVERSCAN COLOR
|
|
Card.ATC.PLANES = {};
|
|
Card.ATC.PLANES.INDX = 0x12; // COLOR PLANES
|
|
Card.ATC.PLANES.MASK = 0x0F;
|
|
Card.ATC.PLANES.MUX = 0x30;
|
|
Card.ATC.HORZPAN = {};
|
|
Card.ATC.HORZPAN.INDX = 0x13; // HORZ PANNING
|
|
Card.ATC.TOTAL_REGS = 0x14;
|
|
|
|
if (DEBUGGER) {
|
|
Card.ATC.REGS = ["PAL00","PAL01","PAL02","PAL03","PAL04","PAL05","PAL06","PAL07",
|
|
"PAL08","PAL09","PAL0A","PAL0B","PAL0C","PAL0D","PAL0E","PAL0F",
|
|
"MODE","OVRSCAN","PLANES","HORZPAN"];
|
|
}
|
|
|
|
Card.MISC = {};
|
|
Card.MISC.PORT = 0x3C2; // write-only (apparently on a VGA, you can read the MISC register at port 0x3CC)
|
|
Card.MISC.IO_SELECT = 0x01; // 0 sets CRT ports to 0x3Bn, 1 sets CRT ports to 0x3Dn
|
|
Card.MISC.ENABLE_RAM = 0x02; // 0 disables video RAM, 1 enables
|
|
Card.MISC.CLK_SELECT = 0x0C; // 0x0: 14Mhz I/O clock, 0x4: 16Mhz on-board clock, 0x8: external clock, 0xC: unused
|
|
Card.MISC.DISABLE_DRV = 0x10; // 0 activates internal video drivers, 1 activates feature connector direct drive outputs
|
|
Card.MISC.PAGE_ODD_EVEN = 0x20; // 0 selects the low 64K page of video RAM for text modes, 1 selects the high page
|
|
Card.MISC.HORZ_POLARITY = 0x40; // 0 selects positive horizontal retrace
|
|
Card.MISC.VERT_POLARITY = 0x80; // 0 selects positive vertical retrace
|
|
|
|
/*
|
|
* The EGA BIOS writes 0x1 to Card.FEAT_CTRL.BITS and reads Card.STATUS0.FEAT, then writes 0x2 to
|
|
* Card.FEAT_CTRL.BITS and reads Card.STATUS0.FEAT. The bits from the first and second reads are shifted
|
|
* into the high nibble of the byte at 40:88h.
|
|
*/
|
|
Card.FEAT_CTRL = {};
|
|
Card.FEAT_CTRL.PORT = 0x3DA; // or 0x3BA; write-only (other than the two bits below, the rest are reserved and/or unused)
|
|
Card.FEAT_CTRL.BITS = 0x03; // feature control bits
|
|
|
|
Card.STATUS0 = {};
|
|
Card.STATUS0.PORT = 0x3C2; // read-only (aka STATUS0, to distinguish it from PORT_CGA_STATUS)
|
|
Card.STATUS0.SWSENSE = 0x10;
|
|
Card.STATUS0.SWSENSE_SHIFT = 4;
|
|
Card.STATUS0.FEAT = 0x60;
|
|
Card.STATUS0.INTERRUPT = 0x80; // 1: video is being displayed; 0: vertical retrace is occurring
|
|
|
|
/*
|
|
* EGA Sequencer (SEQ) ports
|
|
*/
|
|
Card.SEQ = {};
|
|
Card.SEQ.INDX = {};
|
|
Card.SEQ.INDX.PORT = 0x3C4;
|
|
Card.SEQ.INDX.MASK = 0x1F;
|
|
Card.SEQ.DATA = {};
|
|
Card.SEQ.DATA.PORT = 0x3C5;
|
|
Card.SEQ.RESET = {};
|
|
Card.SEQ.RESET.INDX = 0x00; // RESET
|
|
Card.SEQ.RESET.ASYNC = 0x01;
|
|
Card.SEQ.RESET.SYNC = 0x02;
|
|
Card.SEQ.CLK = {};
|
|
Card.SEQ.CLK.INDX = 0x01; // CLOCKING MODE
|
|
Card.SEQ.CLK.DOTS8 = 0x01; // 1: 8 dots; 0: 9 dots
|
|
Card.SEQ.CLK.BANDWIDTH = 0x02; // 0: CRTC has access 4 out of every 5 cycles (for high-res modes); 1: CRTC has access 2 out of 5
|
|
Card.SEQ.CLK.SHIFTLOAD = 0x04;
|
|
Card.SEQ.CLK.DOTCLOCK = 0x08; // 0: normal dot clock; 1: master clock divided by two (used for 320x200 modes: 0, 1, 4, 5, and D)
|
|
Card.SEQ.MAPMASK = {};
|
|
Card.SEQ.MAPMASK.INDX = 0x02; // MAP MASK
|
|
Card.SEQ.MAPMASK.PL0 = 0x01;
|
|
Card.SEQ.MAPMASK.PL1 = 0x02;
|
|
Card.SEQ.MAPMASK.PL2 = 0x04;
|
|
Card.SEQ.MAPMASK.PL3 = 0x08;
|
|
Card.SEQ.MAPMASK.MAPS = 0x0f;
|
|
Card.SEQ.CHARMAP = {};
|
|
Card.SEQ.CHARMAP.INDX = 0x03; // CHAR MAP SELECT
|
|
Card.SEQ.CHARMAP.SELB = 0x03; // 0x0: 1st 8Kb of plane 2; 0x1: 2nd 8Kb; 0x2: 3rd 8Kb; 0x3: 4th 8Kb
|
|
Card.SEQ.CHARMAP.SELA = 0x0C; // 0x0: 1st 8Kb of plane 2; 0x4: 2nd 8Kb; 0x8: 3rd 8Kb; 0xC: 4th 8Kb
|
|
Card.SEQ.MODE = {};
|
|
Card.SEQ.MODE.INDX = 0x04; // MEMORY MODE
|
|
Card.SEQ.MODE.ALPHA = 0x01; // 1: alphanumeric (A/N) mode active; 0: graphics (APA or "All Points Addressable") mode active
|
|
Card.SEQ.MODE.EXT = 0x02; // 1: memory expansion installed; 0: not installed
|
|
Card.SEQ.MODE.SEQUENTIAL = 0x04; // 1: memory is sequential; 0: even addresses mapped to planes 0/2, odd addresses to planes 1/3
|
|
Card.SEQ.TOTAL_REGS = 0x05;
|
|
|
|
if (DEBUGGER) Card.SEQ.REGS = ["RESET","CLK","MAPMASK","CHARMAP","MODE"];
|
|
|
|
/*
|
|
* EGA Graphics Controller (GRC) ports
|
|
*/
|
|
Card.GRC = {};
|
|
Card.GRC.POS1_PORT = 0x3CC;
|
|
Card.GRC.POS2_PORT = 0x3CA;
|
|
Card.GRC.INDX = {};
|
|
Card.GRC.INDX.PORT = 0x3CE;
|
|
Card.GRC.INDX.MASK = 0x0F;
|
|
Card.GRC.DATA = {};
|
|
Card.GRC.DATA.PORT = 0x3CF;
|
|
|
|
Card.GRC.SRESET = {};
|
|
Card.GRC.SRESET.INDX = 0x00; // SET/RESET (write-only; each bit used only if WRITE_MODE is 0 and corresponding ESR bit set)
|
|
Card.GRC.ESRESET = {};
|
|
Card.GRC.ESRESET.INDX = 0x01; // ENABLE SET/RESET
|
|
Card.GRC.COLRCMP = {};
|
|
Card.GRC.COLRCMP.INDX = 0x02; // COLOR COMPARE
|
|
Card.GRC.DATAROT = {};
|
|
Card.GRC.DATAROT.INDX = 0x03; // DATA ROTATE
|
|
Card.GRC.DATAROT.COUNT = 0x07;
|
|
Card.GRC.DATAROT.AND = 0x08;
|
|
Card.GRC.DATAROT.OR = 0x10;
|
|
Card.GRC.DATAROT.XOR = 0x18;
|
|
Card.GRC.DATAROT.FUNC = 0x18;
|
|
Card.GRC.DATAROT.MASK = 0x1F;
|
|
Card.GRC.READMAP = {};
|
|
Card.GRC.READMAP.INDX = 0x04; // READ MAP SELECT
|
|
Card.GRC.READMAP.NUM = 0x03;
|
|
Card.GRC.MODE = {};
|
|
Card.GRC.MODE.INDX = 0x05; // MODE REGISTER
|
|
Card.GRC.MODE.WRITE_MODE0 = 0x00; // write mode 0x0: each plane written with CPU data, rotated as needed, unless SR enabled
|
|
Card.GRC.MODE.WRITE_MODE1 = 0x01; // write mode 0x1: each plane written with contents of the processor latches (loaded by a read)
|
|
Card.GRC.MODE.WRITE_MODE2 = 0x02; // write mode 0x2: memory plane N is written with 8 bits matching data bit N
|
|
Card.GRC.MODE.WRITE_MODE3 = 0x03; // write mode 0x3: VGA only
|
|
Card.GRC.MODE.WRITE = 0x03;
|
|
Card.GRC.MODE.TEST = 0x04;
|
|
Card.GRC.MODE.READ_MODE0 = 0x00; // read mode 0x0: read map mode
|
|
Card.GRC.MODE.READ_MODE1 = 0x08; // read mode 0x1: color compare mode
|
|
Card.GRC.MODE.EVENODD = 0x10;
|
|
Card.GRC.MODE.SHIFT = 0x20;
|
|
Card.GRC.MISC = {};
|
|
Card.GRC.MISC.INDX = 0x06; // MISCELLANEOUS
|
|
Card.GRC.MISC.GRAPHICS = 0x01; // set for graphics mode addressing, clear for text mode addressing
|
|
Card.GRC.MISC.CHAIN = 0x02; // set for odd/even planes selected with odd/even values of the processor AO bit
|
|
Card.GRC.MISC.MAPMEM = 0x0C; //
|
|
Card.GRC.MISC.MAPA0128 = 0x00; //
|
|
Card.GRC.MISC.MAPA064 = 0x04; //
|
|
Card.GRC.MISC.MAPB032 = 0x08; //
|
|
Card.GRC.MISC.MAPB832 = 0x0C; //
|
|
Card.GRC.COLRDC = {};
|
|
Card.GRC.COLRDC.INDX = 0x07; // COLOR DON'T CARE
|
|
Card.GRC.BITMASK = {};
|
|
Card.GRC.BITMASK.INDX = 0x08; // BIT MASK
|
|
Card.GRC.TOTAL_REGS = 0x09;
|
|
|
|
if (DEBUGGER) Card.GRC.REGS = ["SRESET","ESRESET","COLRCMP","DATAROT","READMAP","MODE","MISC","COLRDC","BITMASK"];
|
|
|
|
/*
|
|
* EGA Memory Access Functions
|
|
*
|
|
* Here's where we define all the getMemoryAccess() functions that know how to deal with "planar" EGA memory,
|
|
* which consists of 32-bit values for every byte of address space, allowing us to internally store plane 0
|
|
* bytes in bits 0-7, plane 1 bytes in bits 8-15, plane 2 bytes in bits 16-23, and plane 3 bytes in bits 24-31.
|
|
*
|
|
* All our functions have slightly more overhead than the standard Bus memory access functions, because the
|
|
* offset (off) parameter is block-relative, which we must transform into a buffer-relative offset. Fortunately,
|
|
* all our Memory objects know this and have already recorded their buffer-relative offset in "this.offset".
|
|
*
|
|
* Also, the EGA includes a set of latches, one for each plane, which must be updated on most reads/writes;
|
|
* we rely on the Memory object's "this.controller" property to give us access to the Card's state.
|
|
*
|
|
* And we take a little extra time to conditionally set fDirty on writes, meaning if a write did not actually
|
|
* change the value of the memory, we will not set fDirty. The default write functions in mem.js don't take that
|
|
* performance hit, but here, it may be worthwhile, because if it results in fewer dirty blocks, display updates
|
|
* may be faster.
|
|
*
|
|
* Note that we don't have to worry about dealing with word accesses that straddle block boundaries, because
|
|
* the Bus component automatically breaks those accesses into separate byte requests. Similarly, byte and word
|
|
* values for the write functions have already been pre-masked by the Bus component to 8 and 16 bits, respectively.
|
|
*
|
|
* My motto: Be paranoid, but also be careful not to do any more work than you absolutely have to.
|
|
*
|
|
*
|
|
* CGA Emulation on the EGA
|
|
*
|
|
* Modes 4/5 (320x200 low-res graphics) emulate the same buffer format that the CGA uses. To recap: 1 byte contains
|
|
* 4 pixels (pixel 0 in bits 7-6, pixel 1 in bits 5-4, etc), and thus one row of pixels is 80 (0x50) bytes long.
|
|
* Moreover, all even rows are stored in the first 8K of the frame buffer (at 0xB8000), and all odd rows are stored
|
|
* in the second 8K (at 0xBA000). Of each 8K, only 8000 (0x1F40) bytes are used (80 bytes X 100 rows); the remaining
|
|
* 192 bytes of each 8K are unused.
|
|
*
|
|
* For these modes, the EGA's GRC.MODE is programmed with 0x30: Card.GRC.MODE.EVENODD and Card.GRC.MODE.SHIFT.
|
|
* The latter claims to work by forming each 2-bit pixel with even bits from plane 0 and odd bits from plane 1;
|
|
* however, I'm unclear how that works if even bytes are only written to plane 0 and odd bytes are only written to
|
|
* plane 1, as Card.GRC.MODE.EVENODD implies, because plane 0 would never have any bits for the odd bytes, and
|
|
* plane 1 would never have any bits for the even bytes. Clearly, I'm missing something.
|
|
*
|
|
*
|
|
* Even/Odd Memory Access Functions
|
|
*
|
|
* The "EVENODD" functions deal with the EGA's default text-mode addressing, where every EVEN address is mapped to
|
|
* plane 0 (and plane 2) and every ODD address is mapped to plane 1 (and plane 3). This occurs when SEQ.MODE.SEQUENTIAL
|
|
* is clear (and GRC.MODE.EVENODD is set), turning address bit 0 (A0) into a "plane select" bit. Whether A0 is
|
|
* also used as a memory address bit depends on CRTC.MODE_CTRL.BM: if it's set, then we're in "Byte Mode" and A0 is
|
|
* used as-is; if it's clear, then we're in "Word Mode", and either A15 (when CRTC.MODE_CTRL.AW is set) or A13
|
|
* (when CRTC.MODE_CTRL.AW is clear, typically when only 64Kb of EGA memory is installed) is substituted for A0.
|
|
*
|
|
* Note that A13 remains clear until addresses reach 8K, at which point we've spanned 32Kb of EGA memory, so it makes
|
|
* sense to propagate A13 to A0 at that point, so that the next 8K of addresses start using ODD instead of EVEN bytes,
|
|
* and no memory is wasted on a 64Kb EGA card.
|
|
*
|
|
* These functions, however, don't yet deal with all those subtleties: A0 is currently used only as a "plane select"
|
|
* bit and set to zero for addressing purposes, meaning that only the EVEN bytes in EGA memory will ever be used.
|
|
*/
|
|
|
|
Card.ACCESS = {};
|
|
|
|
/*
|
|
* Values returned by getAccess(); the low byte describes the current "read mode", while the high byte describes the
|
|
* current "write mode".
|
|
*/
|
|
Card.ACCESS.READ = {};
|
|
Card.ACCESS.READ.EVENODD = 0x0001; // this can also be OR'ed with the other read modes
|
|
Card.ACCESS.READ.MODE0 = 0x0002;
|
|
Card.ACCESS.READ.MODE1 = 0x0010;
|
|
Card.ACCESS.READ.MASK = 0x00ff;
|
|
Card.ACCESS.WRITE = {};
|
|
Card.ACCESS.WRITE.EVENODD = 0x0100; // this can also be OR'ed with the other write modes
|
|
Card.ACCESS.WRITE.MODE0 = 0x0200;
|
|
Card.ACCESS.WRITE.MODE0ROT = 0x0400;
|
|
Card.ACCESS.WRITE.MODE0AND = 0x0600;
|
|
Card.ACCESS.WRITE.MODE0OR = 0x0A00;
|
|
Card.ACCESS.WRITE.MODE0XOR = 0x0E00;
|
|
Card.ACCESS.WRITE.MODE1 = 0x1000;
|
|
Card.ACCESS.WRITE.MODE2 = 0x2000;
|
|
Card.ACCESS.WRITE.MODE2AND = 0x6000;
|
|
Card.ACCESS.WRITE.MODE2OR = 0xA000;
|
|
Card.ACCESS.WRITE.MODE2XOR = 0xE000;
|
|
Card.ACCESS.WRITE.MASK = 0xff00;
|
|
|
|
/**
|
|
* readWord(off)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @return {number}
|
|
*/
|
|
Card.ACCESS.readWord = function writeWord(off)
|
|
{
|
|
return this.readByte(off) | (this.readByte(off+1) << 8);
|
|
};
|
|
|
|
/**
|
|
* writeWord(off, w)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} w
|
|
*/
|
|
Card.ACCESS.writeWord = function writeWord(off, w)
|
|
{
|
|
this.writeByte(off, w & 0xff);
|
|
this.writeByte(off + 1, (w >> 8) & 0xff);
|
|
};
|
|
|
|
/**
|
|
* readByteMode0(off)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @return {number}
|
|
*/
|
|
Card.ACCESS.readByteMode0 = function readByteMode0(off)
|
|
{
|
|
off += this.offset;
|
|
var dw = this.controller.latches = this.adw[off];
|
|
return (dw >> this.controller.nReadMapShift) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* readByteMode0EvenOdd(off)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @return {number}
|
|
*/
|
|
Card.ACCESS.readByteMode0EvenOdd = function readByteMode0EvenOdd(off)
|
|
{
|
|
off += this.offset;
|
|
var idw = off & ~0x1;
|
|
return (!(off & 1)? this.adw[idw] : (this.adw[idw] >> 8)) & 0xff;
|
|
};
|
|
|
|
/**
|
|
* readByteMode1(off)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @return {number}
|
|
*/
|
|
Card.ACCESS.readByteMode1 = function readByteMode1(off)
|
|
{
|
|
off += this.offset;
|
|
var dw = this.adw[off];
|
|
var nColorCompare = this.controller.nColorCompare & this.controller.nColorDontCare;
|
|
var b = 0, bit = 0x80;
|
|
while (bit) {
|
|
if ((dw & nColorCompare) == nColorCompare) b |= bit;
|
|
nColorCompare >>>= 1;
|
|
bit >>= 1;
|
|
}
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0 = function writeByteMode0(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
dw = (this.adw[idw] & ~this.controller.nWriteMapMask) | (dw & this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0EvenOdd(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0EvenOdd = function writeByteMode0EvenOdd(off, b)
|
|
{
|
|
off += this.offset;
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
//
|
|
// When even/odd addressing is enabled, nWriteMapMask must be cleared for planes 1 and 3 if
|
|
// the address is even, and cleared for planes 0 and 2 if the address is odd.
|
|
//
|
|
var idw = off & ~0x1;
|
|
var maskMaps = this.controller.nWriteMapMask & (idw == off? 0x00ff00ff : 0xff00ff00);
|
|
dw = (dw & maskMaps) | (this.adw[idw] & ~maskMaps);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0Rot(off, b)
|
|
*
|
|
* Supporting Set/Reset means that for every plane for which Set/Reset is enabled, we must
|
|
* replace the corresponding byte in "dw" with a byte of zeros or ones. This is accomplished with
|
|
* nSetMapMask, nSetMapData, and nSetMapBits. nSetMapMask is the inverse of the ESRESET bits,
|
|
* because we use it to mask the processor data; nSetMapData records the desired SRESET bits; and
|
|
* nSetMapBits contains the bits to replace those that we masked in the processor data.
|
|
*
|
|
* We could have done this:
|
|
*
|
|
* dw = (dw & this.controller.nSetMapMask) | (this.controller.nSetMapData & ~this.controller.nSetMapMask)
|
|
*
|
|
* but by maintaining nSetMapBits equal to (nSetMapData & ~nSetMapMask), we are able to make the writes
|
|
* slightly more efficient.
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0Rot = function writeByteMode0Rot(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
b = ((b >> this.controller.nDataRotate) | (b << (8 - this.controller.nDataRotate)) & 0xff);
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
dw = (dw & this.controller.nSetMapMask) | this.controller.nSetMapBits;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0And(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0And = function writeByteMode0And(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
b = ((b >> this.controller.nDataRotate) | (b << (8 - this.controller.nDataRotate)) & 0xff);
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
dw = (dw & this.controller.nSetMapMask) | this.controller.nSetMapBits;
|
|
dw &= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0Or(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0Or = function writeByteMode0Or(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
b = ((b >> this.controller.nDataRotate) | (b << (8 - this.controller.nDataRotate)) & 0xff);
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
dw = (dw & this.controller.nSetMapMask) | this.controller.nSetMapBits;
|
|
dw |= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode0Xor(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode0Xor = function writeByteMode0Xor(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
b = ((b >> this.controller.nDataRotate) | (b << (8 - this.controller.nDataRotate)) & 0xff);
|
|
var dw = b | (b << 8) | (b << 16) | (b << 24);
|
|
dw = (dw & this.controller.nSetMapMask) | this.controller.nSetMapBits;
|
|
dw ^= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode1(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (ignored; the EGA latches provide the source data)
|
|
*/
|
|
Card.ACCESS.writeByteMode1 = function writeByteMode1(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = (this.adw[idw] & ~this.controller.nWriteMapMask) | (this.controller.latches & this.controller.nWriteMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode2(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode2 = function writeByteMode2(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = Video.aEGAByteToDW[b & 0xf];
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode2And(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode2And = function writeByteMode2And(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = Video.aEGAByteToDW[b & 0xf];
|
|
dw &= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode2Or(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode2Or = function writeByteMode2Or(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = Video.aEGAByteToDW[b & 0xf];
|
|
dw |= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* writeByteMode2Xor(off, b)
|
|
*
|
|
* @this {Memory}
|
|
* @param {number} off
|
|
* @param {number} b (which should already be pre-masked to 8 bits; see Bus.prototype.setByteDirect)
|
|
*/
|
|
Card.ACCESS.writeByteMode2Xor = function writeByteMode2Xor(off, b)
|
|
{
|
|
var idw = off + this.offset;
|
|
var dw = Video.aEGAByteToDW[b & 0xf];
|
|
dw ^= this.controller.latches;
|
|
dw = (dw & this.controller.nWriteMapMask) | (this.adw[idw] & ~this.controller.nWriteMapMask);
|
|
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
|
|
if (this.adw[idw] != dw) {
|
|
this.adw[idw] = dw;
|
|
this.fDirty = true;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Mappings from getAccess() values to access functions above
|
|
*/
|
|
Card.ACCESS.afn = [];
|
|
Card.ACCESS.afn[Card.ACCESS.READ.MODE0] = Card.ACCESS.readByteMode0;
|
|
Card.ACCESS.afn[Card.ACCESS.READ.MODE0 | Card.ACCESS.READ.EVENODD] = Card.ACCESS.readByteMode0EvenOdd;
|
|
Card.ACCESS.afn[Card.ACCESS.READ.MODE1] = Card.ACCESS.readByteMode1;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0] = Card.ACCESS.writeByteMode0;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0ROT] = Card.ACCESS.writeByteMode0Rot;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0AND] = Card.ACCESS.writeByteMode0And;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0OR] = Card.ACCESS.writeByteMode0Or;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0XOR] = Card.ACCESS.writeByteMode0Xor;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.EVENODD] = Card.ACCESS.writeByteMode0EvenOdd;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE1] = Card.ACCESS.writeByteMode1;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2] = Card.ACCESS.writeByteMode2;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2AND] = Card.ACCESS.writeByteMode2And;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2OR] = Card.ACCESS.writeByteMode2Or;
|
|
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2XOR] = Card.ACCESS.writeByteMode2Xor;
|
|
|
|
/**
|
|
* initEGA(data)
|
|
*
|
|
* Another one of my frustrations with JSON is that it encodes empty arrays with non-zero lengths as
|
|
* arrays of nulls, which means that any uninitialized register arrays whose elements were all originally
|
|
* undefined come back via the JSON round-trip as *initialized* arrays whose elements are now all null.
|
|
*
|
|
* I'm a bit surprised, because JavaScript purists want us to use the '===' operator to determine
|
|
* whether an element is initialized (eg, 'aReg[i] === undefined'), but because of this JSON stupidity,
|
|
* that would require all such tests to become 'aReg[i] === undefined || aReg[i] === null'. Great.
|
|
*
|
|
* The simple solution is to change such comparisons to 'aReg[i] == null', because undefined is coerced
|
|
* to null, whereas numeric values are not.
|
|
*
|
|
* Someday, perhaps a purist can explain to me why the coercion of '==' is evil, but JSON's coercion of
|
|
* 'undefined' values to 'null' values is not.
|
|
*
|
|
* [What do I mean by "another" frustration? Let me talk to you some day about disallowing hex constants,
|
|
* or insisting that property names be quoted, or refusing to allow comments. I think it's fine for
|
|
* JSON.stringify() to produce output that adheres to rules like that -- although some parameters to control
|
|
* the output would be nice -- but refusing to let JSON.parse() parse objects that are, in fact, perfectly
|
|
* parseable, is just JSON being a dick.]
|
|
*
|
|
* @this {Card}
|
|
* @param {Array|undefined} data
|
|
* @param {number} nMonitorType
|
|
*/
|
|
Card.prototype.initEGA = function(data, nMonitorType)
|
|
{
|
|
if (data === undefined) {
|
|
data = [
|
|
/* 0*/ false,
|
|
/* 1*/ 0,
|
|
/* 2*/ new Array(Card.ATC.TOTAL_REGS),
|
|
/* 3*/ 0,
|
|
/* 4*/ (nMonitorType == ChipSet.MONITOR.MONO? 0: Card.MISC.IO_SELECT),
|
|
/* 5*/ 0,
|
|
/* 6*/ 0,
|
|
/* 7*/ new Array(Card.SEQ.TOTAL_REGS),
|
|
/* 8*/ 0,
|
|
/* 9*/ 0,
|
|
/*10*/ 0,
|
|
/*11*/ new Array(Card.GRC.TOTAL_REGS),
|
|
/*12*/ 0,
|
|
/*13*/ [this.addrBuffer, this.sizeBuffer, this.cbMemory],
|
|
/*14*/ new Array(this.cbMemory >> 2), // divide cbMemory by 4 since this is an array of DWORDs (8 bits for each of 4 planes)
|
|
/*
|
|
* Card.ACCESS.WRITE.MODE0 by itself is a pretty good default, but if we choose to "randomize" the screen with
|
|
* text characters prior to starting the machine, defaulting to Card.ACCESS.WRITE.EVENODD is more faithful to how
|
|
* characters and attributes are typically stored (ie, in planes 0 and 1, respectively). As soon as the machine
|
|
* starts up and initializes the hardware itself, these defaults won't matter.
|
|
*/
|
|
/*15*/ Card.ACCESS.READ.MODE0 | Card.ACCESS.READ.EVENODD | Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.EVENODD,
|
|
/*16*/ 0,
|
|
/*17*/ 0xffffffff,
|
|
/*18*/ 0,
|
|
/*19*/ 0xffffffff,
|
|
/*20*/ 0,
|
|
/*21*/ 0xffffffff,
|
|
/*22*/ 0,
|
|
/*23*/ 0,
|
|
/*24*/ 0
|
|
];
|
|
}
|
|
|
|
this.fATCData = data[0];
|
|
this.iATCReg = data[1];
|
|
this.aATCRegs = data[2];
|
|
this.asATCRegs = DEBUGGER? Card.ATC.REGS : [];
|
|
this.status0 = data[3]; // aka STATUS0 (not to be confused with this.statusReg, which the EGA refers to as STATUS1)
|
|
this.miscReg = data[4];
|
|
this.featReg = data[5]; // for feature control bits, see Card.FEAT_CTRL.BITS; for feature status bits, see Card.STATUS0.FEAT
|
|
this.iSEQReg = data[6];
|
|
this.aSEQRegs = data[7];
|
|
this.asSEQRegs = DEBUGGER? Card.SEQ.REGS : [];
|
|
this.iGRCPos1 = data[8];
|
|
this.iGRCPos2 = data[9];
|
|
this.iGRCReg = data[10];
|
|
this.aGRCRegs = data[11];
|
|
this.asGRCRegs = DEBUGGER? Card.GRC.REGS : [];
|
|
this.latches = data[12];
|
|
|
|
/*
|
|
* Since we originally neglected to save/restore the card's active frame buffer address and length,
|
|
* we're now stashing all that information in data[13]. So if we're presented with an old data entry
|
|
* that contains only the card's memory size, fix it up.
|
|
*
|
|
* TODO: This code just creates the required array; the correct frame buffer address and length would
|
|
* still need to be calculated from the current GRC registers; checkMode() knows how to do that, but I'm
|
|
* not prepared to shoehorn in a call to checkMode() here, and potentially create more issues, for an
|
|
* old problem that will eventually disappear anyway.
|
|
*/
|
|
var a = data[13];
|
|
if (typeof a == "number") {
|
|
a = [this.addrBuffer, this.sizeBuffer, a];
|
|
}
|
|
this.addrBuffer = a[0];
|
|
this.sizeBuffer = a[1];
|
|
Component.assert(this.cbMemory === a[2]);
|
|
|
|
var cdw = this.cbMemory >> 2;
|
|
this.adwMemory = data[14];
|
|
if (this.adwMemory && this.adwMemory.length < cdw) {
|
|
this.adwMemory = State.decompressEvenOdd(this.adwMemory, cdw);
|
|
}
|
|
this.setMemoryAccess(data[15]);
|
|
|
|
/*
|
|
* nReadMapShift must perfectly track how the GRC.READMAP register is programmed, so that Card.ACCESS.READ.MODE0
|
|
* memory read functions read the appropriate plane. This default is not terribly critical, unless Card.ACCESS.WRITE.MODE0
|
|
* is chosen as our default AND you want the screen randomizer to work.
|
|
*/
|
|
this.nReadMapShift = data[16];
|
|
|
|
/*
|
|
* Similarly, nWriteMapMask must perfectly track how the SEQ.MAPMASK register is programmed, so that memory write
|
|
* functions write the appropriate plane(s). Again, this default is not terribly critical, unless Card.ACCESS.WRITE.MODE0
|
|
* is chosen as our default AND you want the screen randomizer to work.
|
|
*/
|
|
this.nWriteMapMask = data[17];
|
|
this.nDataRotate = data[18];
|
|
this.nBitMapMask = data[19];
|
|
this.nSetMapData = data[20];
|
|
this.nSetMapMask = data[21];
|
|
this.nSetMapBits = data[22];
|
|
this.nColorCompare = data[23];
|
|
this.nColorDontCare = data[24];
|
|
};
|
|
|
|
/**
|
|
* saveCard()
|
|
*
|
|
* @this {Card}
|
|
* @return {Array}
|
|
*/
|
|
Card.prototype.saveCard = function()
|
|
{
|
|
var data = [];
|
|
if (this.iCard !== undefined) {
|
|
data[0] = this.fActive;
|
|
data[1] = this.modeReg;
|
|
data[2] = this.colorReg;
|
|
data[3] = this.statusReg;
|
|
data[4] = this.iCRTCReg | (this.iCRTCPrev << 8);
|
|
data[5] = this.aCRTCRegs;
|
|
if (this.iCard == Video.CARDS.EGA) {
|
|
data[6] = this.saveEGA();
|
|
}
|
|
data[7] = this.nInitCycles;
|
|
}
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* saveEGA()
|
|
*
|
|
* @this {Card}
|
|
* @return {Array}
|
|
*/
|
|
Card.prototype.saveEGA = function()
|
|
{
|
|
var data = [];
|
|
data[0] = this.fATCData;
|
|
data[1] = this.iATCReg;
|
|
data[2] = this.aATCRegs;
|
|
data[3] = this.status0;
|
|
data[4] = this.miscReg;
|
|
data[5] = this.featReg;
|
|
data[6] = this.iSEQReg;
|
|
data[7] = this.aSEQRegs;
|
|
data[8] = this.iGRCPos1;
|
|
data[9] = this.iGRCPos2;
|
|
data[10] = this.iGRCReg;
|
|
data[11] = this.aGRCRegs;
|
|
data[12] = this.latches;
|
|
data[13] = [this.addrBuffer, this.sizeBuffer, this.cbMemory];
|
|
data[14] = State.compressEvenOdd(this.adwMemory);
|
|
data[15] = this.nAccess;
|
|
data[16] = this.nReadMapShift;
|
|
data[17] = this.nWriteMapMask;
|
|
data[18] = this.nDataRotate;
|
|
data[19] = this.nBitMapMask;
|
|
data[20] = this.nSetMapData;
|
|
data[21] = this.nSetMapMask;
|
|
data[22] = this.nSetMapBits;
|
|
data[23] = this.nColorCompare;
|
|
data[24] = this.nColorDontCare;
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* dumpCard()
|
|
*
|
|
* @this {Card}
|
|
*/
|
|
Card.prototype.dumpCard = function()
|
|
{
|
|
if (DEBUGGER) {
|
|
/*
|
|
* Start with registers that are common to all cards....
|
|
*/
|
|
this.dumpRegs("CRTC", this.iCRTCReg, this.aCRTCRegs, this.asCRTCRegs);
|
|
|
|
if (this.iCard == Video.CARDS.MDA || this.iCard == Video.CARDS.CGA) {
|
|
this.dumpRegs(" MODEREG", this.modeReg);
|
|
this.dumpRegs(" STATUS1", this.statusReg);
|
|
}
|
|
|
|
if (this.iCard == Video.CARDS.CGA) {
|
|
this.dumpRegs(" COLOR", this.colorReg);
|
|
}
|
|
|
|
if (this.iCard == Video.CARDS.EGA) {
|
|
this.dbg.message(" ATCDATA: " + this.fATCData);
|
|
this.dumpRegs(" ATC", this.iATCReg, this.aATCRegs, this.asATCRegs);
|
|
this.dumpRegs(" GRC", this.iGRCReg, this.aGRCRegs, this.asGRCRegs);
|
|
this.dumpRegs(" SEQ", this.iSEQReg, this.aSEQRegs, this.asSEQRegs);
|
|
this.dumpRegs(" FEAT", this.featReg);
|
|
this.dumpRegs(" MISC", this.miscReg);
|
|
this.dumpRegs(" STATUS0", this.status0);
|
|
this.dumpRegs(" LATCHES", this.latches);
|
|
this.dbg.message(" ACCESS: " + str.toHexWord(this.nAccess));
|
|
this.dbg.message("Use 'dump video buffer' to dump video memory");
|
|
/*
|
|
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
|
|
*/
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* dumpBuffer()
|
|
*
|
|
* @this {Card}
|
|
* @param {string} sParm
|
|
*/
|
|
Card.prototype.dumpBuffer = function(sParm)
|
|
{
|
|
if (DEBUGGER) {
|
|
if (!this.adwMemory) {
|
|
this.dbg.message("no buffer");
|
|
return;
|
|
}
|
|
var idw = str.parseInt(sParm);
|
|
idw = (idw !== undefined? idw - this.addrBuffer : (this.prevDump || 0));
|
|
if (idw < 0) idw = 0;
|
|
var cLines = 8, sDump = "";
|
|
for (var iLine = 0; iLine < cLines; iLine++) {
|
|
var sData = str.toHex(this.addrBuffer + idw) + ":";
|
|
for (var i = 0; i < 8 && idw < this.adwMemory.length; i++) {
|
|
var dw = this.adwMemory[idw++];
|
|
sData += " " + str.toHex(dw);
|
|
}
|
|
if (sDump) sDump += "\n";
|
|
sDump += sData;
|
|
}
|
|
if (sDump) this.dbg.message(sDump);
|
|
this.prevDump = idw;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* dumpRegs()
|
|
*
|
|
* Since we don't pre-allocate the register arrays (eg, ATC, CRTC, GRC, etc) on a Card, we can't
|
|
* rely on their array length, so we instead rely on the number of register names supplied in asRegs.
|
|
*
|
|
* @this {Card}
|
|
* @param {string} sName
|
|
* @param {number} iReg
|
|
* @param {Array} [aRegs]
|
|
* @param {Array} [asRegs]
|
|
*/
|
|
Card.prototype.dumpRegs = function(sName, iReg, aRegs, asRegs)
|
|
{
|
|
if (DEBUGGER) {
|
|
if (!aRegs) {
|
|
this.dbg.message(sName + ": " + str.toHexByte(iReg));
|
|
return;
|
|
}
|
|
var s = "", i, cchMax = 0;
|
|
for (i = 0; i < asRegs.length; i++) {
|
|
if (cchMax < asRegs[i].length) cchMax = asRegs[i].length;
|
|
}
|
|
cchMax++;
|
|
for (i = 0; i < asRegs.length; i++) {
|
|
if (s) s += '\n';
|
|
s += sName + "[" + str.toHexByte(i) + "]: " + str.pad(asRegs[i], cchMax) + str.toHexByte(aRegs[i]) + (i === iReg? "*" : "");
|
|
}
|
|
this.dbg.message(s);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getMemoryBuffer(addr)
|
|
*
|
|
* If we passed a controller object (ie, this card) to addMemory(), then each allocated Memory block
|
|
* will call this function to obtain a buffer.
|
|
*
|
|
* @this {Card}
|
|
* @param {number} addr
|
|
* @return {Array} containing the buffer (and the offset within that buffer that corresponds to the requested block)
|
|
*/
|
|
Card.prototype.getMemoryBuffer = function(addr)
|
|
{
|
|
return [this.adwMemory, addr - this.addrBuffer];
|
|
};
|
|
|
|
/**
|
|
* getMemoryAccess()
|
|
*
|
|
* Return the last set of memory access functions recorded by setMemoryAccess().
|
|
*
|
|
* @this {Card}
|
|
* @return {Array.<function()>}
|
|
*/
|
|
Card.prototype.getMemoryAccess = function()
|
|
{
|
|
return this.afnAccess;
|
|
};
|
|
|
|
/**
|
|
* setMemoryAccess(nAccess)
|
|
*
|
|
* This transforms the memory access value that getAccess() returns into the best available set of
|
|
* memory access functions, which are then returned via getMemoryAccess() to any memory blocks we allocate
|
|
* or modify.
|
|
*
|
|
* @this {Card}
|
|
* @param {number|undefined} nAccess
|
|
*/
|
|
Card.prototype.setMemoryAccess = function(nAccess)
|
|
{
|
|
if (nAccess != null && nAccess != this.nAccess) {
|
|
|
|
var nReadAccess = nAccess & Card.ACCESS.READ.MASK;
|
|
var fnReadByte = Card.ACCESS.afn[nReadAccess];
|
|
if (!fnReadByte) {
|
|
if (DEBUG && this.dbg) this.dbg.message("Card.setMemoryAccess(" + str.toHexWord(nAccess) + "): missing readByte handler");
|
|
if (nReadAccess & Card.ACCESS.READ.EVENODD) {
|
|
fnReadByte = Card.ACCESS.afn[Card.ACCESS.READ.EVENODD];
|
|
}
|
|
}
|
|
var nWriteAccess = nAccess & Card.ACCESS.WRITE.MASK;
|
|
var fnWriteByte = Card.ACCESS.afn[nWriteAccess];
|
|
if (!fnWriteByte) {
|
|
if (DEBUG && this.dbg) this.dbg.message("Card.setMemoryAccess(" + str.toHexWord(nAccess) + "): missing writeByte handler");
|
|
if (nWriteAccess & Card.ACCESS.WRITE.EVENODD) {
|
|
fnWriteByte = Card.ACCESS.afn[Card.ACCESS.WRITE.EVENODD];
|
|
}
|
|
}
|
|
if (!this.afnAccess) {
|
|
this.afnAccess = [null, Card.ACCESS.readWord, null, Card.ACCESS.writeWord];
|
|
}
|
|
this.afnAccess[0] = fnReadByte;
|
|
this.afnAccess[2] = fnWriteByte;
|
|
this.nAccess = nAccess;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Card Specifications
|
|
*
|
|
* We support dynamically switching between MDA and CGA cards by simply flipping switches on
|
|
* the virtual SW1 switch block and resetting the machine. However, I'm not sure I'll support
|
|
* dynamically switching the EGA card the same way; there's certainly no UI for it at this point.
|
|
*
|
|
* For each supported card, there is a cardSpec array that the Card class uses to initialize the
|
|
* card's defaults:
|
|
*
|
|
* [0]: card descriptor
|
|
* [1]: default CRTC port address
|
|
* [2]: default frame buffer address
|
|
* [3]: default frame buffer size
|
|
* [4]: total on-board memory (if no "memory" parm was specified)
|
|
* [5]: default monitor type
|
|
*
|
|
* If total on-board memory is zero, then addMemory() will simply add the specified frame buffer
|
|
* to the address space; otherwise, we will allocate an internal buffer (adwMemory) and tell addMemory()
|
|
* to map it to the frame buffer address. The latter approach gives us total control over the buffer;
|
|
* refer to getMemoryAccess().
|
|
*
|
|
* TODO: Consider allocating our own buffer for all video cards, not just EGA. For MDA/CGA, I'm not sure
|
|
* it would offer any benefits, other than allowing our internal update functions, like updateScreen(),
|
|
* to access the buffer directly, instead of going through the Bus memory interface.
|
|
*/
|
|
Video.cardSpecs = [];
|
|
Video.cardSpecs[Video.CARDS.MDA] = ["MDA", Card.MDA.CRTC.INDX.PORT, 0xB0000, 0x01000, 0, ChipSet.MONITOR.MONO];
|
|
Video.cardSpecs[Video.CARDS.CGA] = ["CGA", Card.CGA.CRTC.INDX.PORT, 0xB8000, 0x04000, 0, ChipSet.MONITOR.COLOR];
|
|
Video.cardSpecs[Video.CARDS.EGA] = ["EGA", Card.CGA.CRTC.INDX.PORT, 0xB8000, 0x04000, 0x10000, ChipSet.MONITOR.EGACOLOR];
|
|
|
|
/*
|
|
* BIOS video interrupts, modes, and other parameters
|
|
*/
|
|
Video.BIOS = {
|
|
INT_VIDEO: 0x10
|
|
};
|
|
|
|
/**
|
|
* initBus(cmp, bus, cpu, dbg)
|
|
*
|
|
* This is a notification issued by the Computer component, after all the other components (notably the CPU)
|
|
* have had a chance to initialize.
|
|
*
|
|
* @this {Video}
|
|
* @param {Computer} cmp
|
|
* @param {Bus} bus
|
|
* @param {X86CPU} cpu
|
|
* @param {Debugger} dbg
|
|
*/
|
|
Video.prototype.initBus = function(cmp, bus, cpu, dbg)
|
|
{
|
|
this.bus = bus;
|
|
this.cpu = cpu;
|
|
this.dbg = dbg;
|
|
|
|
bus.addPortInputTable(this, Video.aPortInput);
|
|
bus.addPortOutputTable(this, Video.aPortOutput);
|
|
|
|
if (this.model == "ega") {
|
|
bus.addPortInputTable(this, Video.aEGAPortInput);
|
|
bus.addPortOutputTable(this, Video.aEGAPortOutput);
|
|
}
|
|
|
|
if (DEBUGGER && dbg) {
|
|
var video = this;
|
|
dbg.messageInit(Video);
|
|
this.cpu.addIntNotify(Video.BIOS.INT_VIDEO, this, this.intBIOSVideo);
|
|
dbg.messageDump(Video.MESSAGE_VIDEO, function onDumpVideo(sParm) {
|
|
video.dumpVideo(sParm);
|
|
});
|
|
}
|
|
|
|
/*
|
|
* 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.getComponentByType("Keyboard");
|
|
if (this.kbd && this.canvasScreen) {
|
|
this.kbd.setBinding("input", this.textareaScreen? "textarea" : "canvas", "kbd", this.textareaScreen || this.canvasScreen);
|
|
}
|
|
|
|
this.bEGASW = 0x9; // our default "switches" setting (see aEGAMonitorSwitches)
|
|
this.chipset = cmp.getComponentByType("ChipSet");
|
|
if (this.chipset && this.sEGASW) {
|
|
this.bEGASW = this.chipset.parseSwitches(this.sEGASW, this.bEGASW);
|
|
}
|
|
|
|
if (this.kbd && this.fTouchScreen) this.captureTouch();
|
|
};
|
|
|
|
/**
|
|
* intBIOSVideo(addr)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} addr
|
|
* @return {boolean} true to proceed with the INT 0x10 software interrupt, false to skip
|
|
*/
|
|
Video.prototype.intBIOSVideo = function(addr)
|
|
{
|
|
if (DEBUGGER) {
|
|
if (this.dbg && this.dbg.messageEnabled(Video.MESSAGE_VIDEO | Video.MESSAGE_INT)) {
|
|
this.dbg.messageInt(Video.BIOS.INT_VIDEO, addr);
|
|
this.cpu.addIntReturn(addr, function (video, nCycles) {
|
|
return function onBIOSVideoReturn(nLevel) {
|
|
video.dbg.messageIntReturn(Video.BIOS.INT_VIDEO, nLevel, video.cpu.getCycles() - nCycles);
|
|
};
|
|
}(this, this.cpu.getCycles()));
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* setBinding(sHTMLClass, sHTMLType, sBinding, control)
|
|
*
|
|
* @this {Video}
|
|
* @param {string|null} sHTMLClass is the class of the HTML control (eg, "input", "output")
|
|
* @param {string|null} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
|
|
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "refresh")
|
|
* @param {Object} control is the HTML control DOM object (eg, HTMLButtonElement)
|
|
* @return {boolean} true if binding was successful, false if unrecognized binding request
|
|
*/
|
|
Video.prototype.setBinding = function(sHTMLClass, sHTMLType, sBinding, control)
|
|
{
|
|
var video = this;
|
|
switch (sBinding) {
|
|
case "refresh":
|
|
this.bindings[sBinding] = control;
|
|
control.onclick = function onClickRefresh() {
|
|
if (DEBUG) video.messageDebugger("refreshScreen()");
|
|
video.updateScreen(true);
|
|
};
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* setFocus()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.setFocus = function()
|
|
{
|
|
if (this.canvasScreen) this.canvasScreen.focus();
|
|
};
|
|
|
|
/**
|
|
* getCanvas()
|
|
*
|
|
* This is an interface used by the Mouse component, so that it can invoke capture/release mouse events from the screen element.
|
|
*
|
|
* @this {Video}
|
|
* @return {Object|undefined}
|
|
*/
|
|
Video.prototype.getCanvas = function()
|
|
{
|
|
return this.canvasScreen;
|
|
};
|
|
|
|
/**
|
|
* captureTouch()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.captureTouch = function()
|
|
{
|
|
var control = this.canvasScreen;
|
|
if (control) {
|
|
var video = this;
|
|
if (!this.fCaptured) {
|
|
control.addEventListener(
|
|
'touchstart',
|
|
function onTouchStartCanvas(event) { video.onTouchStart(event); },
|
|
false // we'll specify false for the 'useCapture' parameter for now...
|
|
);
|
|
control.addEventListener(
|
|
'touchmove',
|
|
function onTouchMoveCanvas(event) { video.onTouchMove(event); },
|
|
true
|
|
);
|
|
control.addEventListener(
|
|
'touchend',
|
|
function onTouchEndCanvas(event) { video.onTouchEnd(event); },
|
|
false // we'll specify false for the 'useCapture' parameter for now...
|
|
);
|
|
if (MAXDEBUG) {
|
|
control.addEventListener(
|
|
'mousedown',
|
|
function onMouseDownCanvas(event) { video.onTouchStart(event); },
|
|
false // we'll specify false for the 'useCapture' parameter for now...
|
|
);
|
|
control.addEventListener(
|
|
'mousemove',
|
|
function onMouseMoveCanvas(event) { video.onTouchMove(event); },
|
|
true
|
|
);
|
|
control.addEventListener(
|
|
'mouseup',
|
|
function onMouseUpCanvas(event) { video.onTouchEnd(event); },
|
|
false // we'll specify false for the 'useCapture' parameter for now...
|
|
);
|
|
}
|
|
// this.log("touch events captured");
|
|
this.fCaptured = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* onFocusChange(fFocus)
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} fFocus is true if gaining focus, false if losing it
|
|
*/
|
|
Video.prototype.onFocusChange = function(fFocus)
|
|
{
|
|
if (this.fHasFocus != fFocus && DEBUG) this.messageDebugger("onFocusChange(): focus is now " + fFocus);
|
|
this.fHasFocus = fFocus;
|
|
};
|
|
|
|
/*
|
|
Video.prototype.releaseTouch = function()
|
|
{
|
|
};
|
|
*/
|
|
|
|
/**
|
|
* onTouchStart(event)
|
|
*
|
|
* @this {Video}
|
|
* @param {Event} event object from a 'touch' event
|
|
*/
|
|
Video.prototype.onTouchStart = function(event)
|
|
{
|
|
if (DEBUG) this.messageDebugger("onTouchStart()");
|
|
this.processTouchEvent(event, true);
|
|
};
|
|
|
|
/**
|
|
* onTouchMove(event)
|
|
*
|
|
* @this {Video}
|
|
* @param {Event} event object from a 'touch' event
|
|
*/
|
|
Video.prototype.onTouchMove = function(event)
|
|
{
|
|
if (DEBUG) this.messageDebugger("onTouchMove()");
|
|
this.processTouchEvent(event, false);
|
|
};
|
|
|
|
/**
|
|
* onTouchEnd(event)
|
|
*
|
|
* @this {Video}
|
|
* @param {Event} event object from a 'touch' event
|
|
*/
|
|
Video.prototype.onTouchEnd = function(event)
|
|
{
|
|
if (DEBUG) this.messageDebugger("onTouchEnd()");
|
|
};
|
|
|
|
/**
|
|
* processTouchEvent(event, fStart)
|
|
*
|
|
* @this {Video}
|
|
* @param {Event} event object from a 'touch' event
|
|
* @param {boolean} fStart if this is a 'touchstart' event
|
|
*/
|
|
Video.prototype.processTouchEvent = function(event, fStart)
|
|
{
|
|
// if (!event) event = window.event;
|
|
/*
|
|
* My thinking here is that if the canvas does NOT yet have focus, then we should actually SKIP
|
|
* the usual preventDefault() call, so that everything the user has come to expect (eg, activation of
|
|
* the soft keyboard) will work as before.
|
|
*
|
|
* The process of touching the canvas means it should ultimately receive focus, and as long as it
|
|
* retains focus, preventDefault() will always be called.
|
|
*/
|
|
if (this.fHasFocus) event.preventDefault();
|
|
/*
|
|
* Touch coordinates (that is, the pageX and pageY properties) are relative to the page, so to make
|
|
* them relative to the canvas, we must subtract the canvas's left and top positions. This Apple web page:
|
|
*
|
|
* https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingMouseandTouchControlstoCanvas/AddingMouseandTouchControlstoCanvas.html
|
|
*
|
|
* makes it sound simple, but it turns out we have to walk the canvas' entire "parentage" of DOM elements
|
|
* to get the exact offsets.
|
|
*/
|
|
var xTouchOffset = 0;
|
|
var yTouchOffset = 0;
|
|
var eCurrent = this.canvasScreen;
|
|
do {
|
|
if (!isNaN(eCurrent.offsetLeft)) {
|
|
xTouchOffset += eCurrent.offsetLeft;
|
|
yTouchOffset += eCurrent.offsetTop;
|
|
}
|
|
} while ((eCurrent = eCurrent.offsetParent));
|
|
/*
|
|
* Due to the responsive nature of our pages, the displayed size of the canvas may be smaller than the allocated
|
|
* size, and the coordinates we receive from touch events are based on the currently displayed size.
|
|
*/
|
|
var xScale = this.cxScreen / this.canvasScreen.offsetWidth;
|
|
var yScale = this.cyScreen / this.canvasScreen.offsetHeight;
|
|
/**
|
|
* @name Event
|
|
* @property {Array} targetTouches
|
|
*/
|
|
var xTouch = ((event.targetTouches[0].pageX - xTouchOffset) * xScale);
|
|
var yTouch = ((event.targetTouches[0].pageY - yTouchOffset) * yScale);
|
|
var xThird = (xTouch / (this.cxScreen / 3)) | 0;
|
|
var yThird = (yTouch / (this.cyScreen / 3)) | 0;
|
|
/*
|
|
* At this point, xThird and yThird should both be one of 0, 1 or 2, indicating which horizontal and vertical
|
|
* third of the virtual screen the touch event occurred.
|
|
*/
|
|
if (/* xThird == 1 && */ yThird != 1) {
|
|
if (!yThird) {
|
|
this.kbd.keyPressSimulate(Keyboard.CHARCODE.U_ARROW, true);
|
|
} else {
|
|
this.kbd.keyPressSimulate(Keyboard.CHARCODE.D_ARROW, true);
|
|
}
|
|
} else if (/* yThird == 1 && */ xThird != 1) {
|
|
if (!xThird) {
|
|
this.kbd.keyPressSimulate(Keyboard.CHARCODE.L_ARROW, true);
|
|
} else {
|
|
this.kbd.keyPressSimulate(Keyboard.CHARCODE.R_ARROW, true);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
if (!fRepower) {
|
|
if (!data || !this.restore) {
|
|
this.reset();
|
|
} else {
|
|
if (!this.restore(data)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* powerDown(fSave)
|
|
*
|
|
* This is where we might add some method of blanking the display, without the disturbing the video
|
|
* buffer contents, and blocking all further updates to the display.
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} fSave
|
|
* @return {Object|boolean}
|
|
*/
|
|
Video.prototype.powerDown = function(fSave)
|
|
{
|
|
return fSave && this.save? this.save() : true;
|
|
};
|
|
|
|
/**
|
|
* reset()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.reset = function()
|
|
{
|
|
var fRandomize = true;
|
|
var nMonitorType = ChipSet.MONITOR.NONE;
|
|
|
|
/*
|
|
* We'll ask the ChipSet what SW1 indicates for monitor type, but we may override it if a specific
|
|
* video card model is set. For EGA, SW1 is supposed to be set to indicate NO monitor, and we rely
|
|
* on the EGA's own switch settings instead.
|
|
*/
|
|
if (this.chipset) {
|
|
nMonitorType = this.chipset.getSWVideoMonitor();
|
|
}
|
|
|
|
var fEGA = false;
|
|
if (this.model) {
|
|
switch (this.model) {
|
|
case "ega":
|
|
fEGA = true;
|
|
var aMonitors = Video.aEGAMonitorSwitches[this.bEGASW];
|
|
/*
|
|
* TODO: Figure out how to deal with aMonitors[2], the boolean which indicates
|
|
* whether the EGA is driving the primary monitor (true) or the secondary monitor (false).
|
|
*/
|
|
if (aMonitors) nMonitorType = aMonitors[0];
|
|
if (!nMonitorType) nMonitorType = ChipSet.MONITOR.EGACOLOR;
|
|
break;
|
|
case "mda":
|
|
nMonitorType = ChipSet.MONITOR.MONO;
|
|
break;
|
|
case "cga":
|
|
/* falls through */
|
|
default:
|
|
nMonitorType = ChipSet.MONITOR.COLOR;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (this.nMonitorType !== nMonitorType) {
|
|
this.nMonitorType = nMonitorType;
|
|
fRandomize = true;
|
|
}
|
|
|
|
this.cardActive = null;
|
|
this.cardMono = this.cardMDA = new Card(this, Video.CARDS.MDA);
|
|
this.cardColor = this.cardCGA = new Card(this, Video.CARDS.CGA);
|
|
|
|
if (!fEGA) {
|
|
/*
|
|
* Define a dummy (uninitialized) EGA card for now
|
|
*/
|
|
this.cardEGA = new Card();
|
|
} else {
|
|
this.cardEGA = new Card(this, Video.CARDS.EGA, null, this.cbMemory);
|
|
this.enableEGA();
|
|
}
|
|
|
|
/*
|
|
* We need to call buildFonts() *after* the card(s) are initialized but *before* setMode() is called.
|
|
*/
|
|
this.buildFonts();
|
|
|
|
this.nMode = null;
|
|
this.nModeDefault = (nMonitorType == ChipSet.MONITOR.MONO? Video.MODES.MDA_80X25 : Video.MODES.CGA_80X25);
|
|
|
|
this.iCellCursor = -1; // initially, there is no visible cursor cell
|
|
this.cBlinks = -1; // initially, blinking is not active
|
|
this.cBlinkVisible = 0; // no visible blinking characters (yet)
|
|
|
|
this.setMode(this.nModeDefault);
|
|
|
|
if (this.cardActive.addrBuffer && fRandomize) {
|
|
/*
|
|
* On the initial power-on, we initialize the video buffer to random characters,
|
|
* as a way of testing whether our font(s) were successfully loaded. It's assumed
|
|
* that our default display mode is a text mode, and that since this is a reset,
|
|
* the CRTC.START_ADDR registers are zero as well.
|
|
*
|
|
* If this is an MDA device, then the buffer should reside at 0xB0000 through 0xB0FFF,
|
|
* for a total length of 4Kb (0x1000), where every even byte contains a character code,
|
|
* and every odd byte contains an attribute code. See the ATTR bit definitions above for
|
|
* applicable color, intensity, and blink values. On a CGA device, the buffer resides
|
|
* at 0xB8000 through 0xBBFFF, for a total length of 16Kb.
|
|
*
|
|
* Note that the only valid MDA display mode (7) is the 80x25 text mode, which uses 4000
|
|
* bytes (2000 character bytes + 2000 attribute bytes), not all 4096 bytes; addrScreenLimit
|
|
* reflects the visible limit, not the physical limit. Also, as noted in updateScreen(),
|
|
* this simplistic calculation of the extent of visible screen memory is valid only for
|
|
* text modes; in general, it's safer to use cardActive.sizeBuffer as the extent.
|
|
*/
|
|
var addrScreenLimit = this.cardActive.addrBuffer + this.cbScreen;
|
|
for (var addrScreen = this.cardActive.addrBuffer; addrScreen < addrScreenLimit; addrScreen += 2) {
|
|
var dataRandom = Math.floor(Math.random() * 0x10000);
|
|
var bChar, bAttr;
|
|
if (this.nMonitorType == ChipSet.MONITOR.EGACOLOR) {
|
|
/*
|
|
* For the EGA, we choose sequential characters; for random characters, copy the MDA/CGA code below.
|
|
*/
|
|
bChar = (addrScreen >> 1) & 0xff;
|
|
bAttr = (dataRandom >> 8) & ~Video.ATTRS.BGND_BLINK; // TODO: turn blink attributes off unless we can ensure blinking is initially disabled
|
|
if ((bAttr >> 4) == (bAttr & 0xf)) {
|
|
bAttr ^= 0x0f; // if background matches foreground, invert foreground to ensure character visibility
|
|
}
|
|
} else {
|
|
bChar = dataRandom & 0xff;
|
|
bAttr = ((dataRandom & 0x100)? (Video.ATTRS.FGND_WHITE | Video.ATTRS.BGND_BLACK) : (Video.ATTRS.FGND_BLACK | Video.ATTRS.BGND_WHITE)) | ((Video.ATTRS.FGND_BRIGHT /* | Video.ATTRS.BGND_BLINK */) & (dataRandom >> 8));
|
|
}
|
|
this.bus.setWordDirect(addrScreen, bChar | (bAttr << 8));
|
|
}
|
|
this.updateScreen(true);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* enableEGA()
|
|
*
|
|
* Redirect cardMono or cardColor to cardEGA as appropriate.
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.enableEGA = function()
|
|
{
|
|
if (!(this.cardEGA.miscReg & Card.MISC.IO_SELECT)) {
|
|
this.cardMono = this.cardEGA;
|
|
this.cardColor = this.cardCGA; // this is done mainly to siphon away any CGA I/O
|
|
} else {
|
|
this.cardMono = this.cardMDA; // similarly, this is done to siphon away any MDA I/O
|
|
this.cardColor = this.cardEGA;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* save()
|
|
*
|
|
* This implements save support for the Video component.
|
|
*
|
|
* @this {Video}
|
|
* @return {Object}
|
|
*/
|
|
Video.prototype.save = function()
|
|
{
|
|
var state = new State(this);
|
|
state.set(0, this.cardMDA.saveCard());
|
|
state.set(1, this.cardCGA.saveCard());
|
|
state.set(2, [this.nMonitorType, this.nModeDefault, this.nMode]);
|
|
state.set(3, this.cardEGA.saveCard());
|
|
return state.data();
|
|
};
|
|
|
|
/**
|
|
* restore(data)
|
|
*
|
|
* This implements restore support for the Video component.
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} data
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Video.prototype.restore = function(data)
|
|
{
|
|
var a = data[2];
|
|
this.nMonitorType = a[0];
|
|
this.nModeDefault = a[1];
|
|
this.nMode = a[2];
|
|
|
|
this.cardActive = null;
|
|
this.cardMono = this.cardMDA = new Card(this, Video.CARDS.MDA, data[0]);
|
|
this.cardColor = this.cardCGA = new Card(this, Video.CARDS.CGA, data[1]);
|
|
|
|
/*
|
|
* If no EGA was originally initialized, then cardEGA will remain uninitialized.
|
|
*/
|
|
this.cardEGA = new Card(this, Video.CARDS.EGA, data[3], this.cbMemory);
|
|
if (this.cardEGA.fActive) this.enableEGA();
|
|
|
|
/*
|
|
* We need to call buildFonts() *after* the card(s) are initialized but *before* setMode() is called.
|
|
*/
|
|
this.buildFonts();
|
|
|
|
/*
|
|
* While I could restore the active card here, it's better for setMode() to do it, because
|
|
* setMode() will also take care of mapping the appropriate video buffer. So, after restore() has
|
|
* finished, we call checkMode(), because the current video mode (nMode) is determined by the
|
|
* active card state.
|
|
*
|
|
* Unfortunately, that creates a chicken-and-egg problem, since I just said I didn't want to select
|
|
* the active card here.
|
|
*
|
|
* So, we'll add some "cop-out" code to checkMode(): if there's no active card, then fall-back
|
|
* to the last known video mode (nMode) and force a call to setMode().
|
|
*
|
|
* this.cardActive = (this.cardMDA.fActive? this.cardMDA : (this.cardCGA.fActive? this.cardCGA : undefined));
|
|
*/
|
|
if (!this.checkMode()) return false;
|
|
|
|
this.checkCursor();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* onLoadSetFonts(sFontFile, sFontData, nErrorCode)
|
|
*
|
|
* @this {Video}
|
|
* @param {string} sFontFile
|
|
* @param {string} sFontData
|
|
* @param {number} nErrorCode (response from server if anything other than 200)
|
|
*/
|
|
Video.prototype.onLoadSetFonts = function(sFontFile, sFontData, nErrorCode)
|
|
{
|
|
if (nErrorCode) {
|
|
this.notice("Unable to load font ROM image (error " + nErrorCode + ")");
|
|
return;
|
|
}
|
|
try {
|
|
/*
|
|
* The most likely source of any exception will be right here, where we're parsing the JSON-encoded data.
|
|
*/
|
|
var abFontData = eval("(" + sFontData + ")");
|
|
|
|
if (!abFontData.length) {
|
|
Component.error("Empty font ROM image: " + sFontFile);
|
|
return;
|
|
}
|
|
else if (abFontData.length == 1) {
|
|
Component.error(abFontData[0]);
|
|
return;
|
|
}
|
|
/*
|
|
* Translate the character data into separate "fonts", each of which will be a separate canvas object, with all
|
|
* 256 characters arranged in a 16x16 grid.
|
|
*/
|
|
if (abFontData.length == 8192) {
|
|
/*
|
|
* Here are the first few rows of MDA font data, at the 0K and 2K boundaries:
|
|
*
|
|
* 00000000 00 00 00 00 00 00 00 00 00 00 7e 81 a5 81 81 bd |..........~.....|
|
|
* 00000010 00 00 7e ff db ff ff c3 00 00 00 36 7f 7f 7f 7f |..~........6....|
|
|
* ...
|
|
* 00000800 00 00 00 00 00 00 00 00 99 81 7e 00 00 00 00 00 |..........~.....|
|
|
* 00000810 e7 ff 7e 00 00 00 00 00 3e 1c 08 00 00 00 00 00 |..~.....>.......|
|
|
*
|
|
* 8 bytes of data from a row in each of the 2K chunks are combined to form a 8-bit wide character with
|
|
* a maximum height of 16 bits. Assembling the bits for character 0x01 (a happy face), we observe the following:
|
|
*
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x0008
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x0009
|
|
* 0 1 1 1 1 1 1 0 <== 7e from offset 0x000A
|
|
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000B
|
|
* 1 0 1 0 0 1 0 1 <== a5 from offset 0x000C
|
|
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000D
|
|
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x000E
|
|
* 1 0 1 1 1 1 0 1 <== bd from offset 0x000F
|
|
* 1 0 0 1 1 0 0 1 <== 99 from offset 0x0808
|
|
* 1 0 0 0 0 0 0 1 <== 81 from offset 0x0809
|
|
* 0 1 1 1 1 1 1 0 <== 7e from offset 0x080A
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080B
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080C
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080D
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080E
|
|
* 0 0 0 0 0 0 0 0 <== 00 from offset 0x080F
|
|
*
|
|
* In the second 2K chunk, we observe that the last two bytes of every font cell definition are zero;
|
|
* this confirms our understanding that MDA font cell size is 8x14.
|
|
*
|
|
* Finally, there's the issue of screen cell size, which is actually 9x14 on the MDA. We compensate for that
|
|
* by building a 9x14 font, even though there's only 8x14 bits of data. As http://www.seasip.info/VintagePC/mda.html
|
|
* explains:
|
|
*
|
|
* "For characters C0h-DFh, the ninth pixel column is a duplicate of the eighth; for others, it's blank."
|
|
*
|
|
* This last point is confirmed by "The IBM Personal Computer From The Inside Out", p.295:
|
|
*
|
|
* "Another unique feature of the monochrome adapter is a set of line-drawing and area-fill characters that give
|
|
* continuous lines and filled areas. This is unusual for a display with a 9x14 character box because the character
|
|
* generator provides a row only eight dots wide. On most displays, a blank 9th dot is then inserted between characters.
|
|
* On the monochrome display, there is circuitry that duplicates the 8th dot into the 9th dot position for characters
|
|
* whose ASCII codes are 0xB0 through 0xDF."
|
|
*
|
|
* The only question is: is the range actually 0xC0-0xDF, or 0xB0-0xDF??? I'll assume the latter, since 0xB0 is where
|
|
* the line-drawing/area-fill characters appear to begin.
|
|
*
|
|
* The CGA font is part of the same ROM. In fact, there are TWO CGA fonts in the ROM: a thin 5x7 "single dot" font
|
|
* located at offset 0x1000, and a thick 7x7 "double dot" font at offset 0x1800. The latter is the default font,
|
|
* unless overridden by a jumper setting on the CGA card, so it is our default CGA font as well (although someday we
|
|
* may provide a virtual jumper setting that allows you to select the thinner font).
|
|
*
|
|
* The first offset we pass to setFontData() is the offset of the MDA font. For the second (CGA) font offset,
|
|
* we choose the thicker "double dot" CGA font at 0x1800 (which was the PC's default font as well), instead
|
|
* of the thinner "single dot" font at 0x1000.
|
|
*/
|
|
this.setFontData(abFontData, [0x0000, 0x1800]);
|
|
}
|
|
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!
|
|
*/
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* onROMLoad(abRom)
|
|
*
|
|
* Called by ROM.prototype.copyImage() whenever a ROM with a 'notify' attribute set to our component ID
|
|
* has been loaded.
|
|
*
|
|
* If model is "ega", then we assume the associated ROM is the original IBM EGA ROM, which stores
|
|
* its 8x14 font data at 0x2230 (and unlike the MDA/CGA character generator ROM, which splits the first
|
|
* 8 rows and remaining 6 rows of each character across separate 2K chunks, the bytes for all the EGA
|
|
* character rows are contiguous); the total size of the 8x14 font is 0xE00 bytes.
|
|
*
|
|
* At 0x3030, there is an "ALPHA SUPPLEMENT" table, which contains 15 bytes per row instead of 14,
|
|
* because each row is preceded by one byte containing the corresponding ASCII code; there are 20 entries
|
|
* in the "ALPHA SUPPLEMENT" table, for a total size of 0x12C bytes.
|
|
*
|
|
* Finally, at 0x3160, we have the 8x8 font data (also known as the thicker "double dot" CGA font);
|
|
* the total size of the 8x8 font is 0x800 bytes. No other font data is present in the EGA ROM;
|
|
* the thin 5x7 "single dot" CGA font is notably absent, which is fine, because we never loaded it for
|
|
* the MDA/CGA either.
|
|
*
|
|
* TODO: Determine how the "ALPHA SUPPLEMENT" table is used and whether we need to add some "run-time"
|
|
* font generation to support it (as opposed to "init-time" generation, which is all we do now). There's
|
|
* probably a similar need for user-defined fonts; for now, they're just not supported.
|
|
*
|
|
* @this {Video}
|
|
* @param {Array.<number>} abROM
|
|
*/
|
|
Video.prototype.onROMLoad = function(abROM)
|
|
{
|
|
if (this.model == "ega") {
|
|
/*
|
|
* TODO: Unlike the MDA/CGA font data, we may want to hang onto this data, so that we can regenerate
|
|
* the color font(s) whenever the foreground and/or background colors have been changed.
|
|
*/
|
|
if (DEBUG) this.messageDebugger("onROMLoad(): EGA fonts loaded");
|
|
this.setFontData(abROM, [0x2230, 0x3160], 8);
|
|
}
|
|
this.setReady();
|
|
};
|
|
|
|
/**
|
|
* getCardColors(nBitsPerPixel)
|
|
*
|
|
* @param {number} [nBitsPerPixel]
|
|
* @returns {Array}
|
|
*/
|
|
Video.prototype.getCardColors = function(nBitsPerPixel)
|
|
{
|
|
if (nBitsPerPixel == 1) {
|
|
/*
|
|
* Only 2 total colors.
|
|
*/
|
|
this.aRGB[0] = Video.aCGAColors[Video.ATTRS.FGND_BLACK];
|
|
this.aRGB[1] = Video.aCGAColors[Video.ATTRS.FGND_WHITE];
|
|
return this.aRGB;
|
|
}
|
|
|
|
if (nBitsPerPixel == 2) {
|
|
/*
|
|
* Of the 4 colors returned, the first color comes from colorReg and the other 3 come from one of
|
|
* the two hard-coded CGA color sets:
|
|
*
|
|
* Color Set 1 Color Set 2
|
|
* ----------- -----------
|
|
* Background (0x00) Background (0x00)
|
|
* Green (0x12) Cyan (0x13)
|
|
* Red (0x14) Magenta (0x15)
|
|
* Brown (0x16) White (0x17)
|
|
*
|
|
* The numbers in parentheses are the EGA ATC palette register values that the EGA BIOS uses for each
|
|
* color set; on an EGA, I synthesize a fake CGA colorReg value, until I figure out exactly how the EGA
|
|
* simulates the CGA color palette. TODO: Figure it out.
|
|
*/
|
|
var colorReg = this.cardActive.colorReg;
|
|
if (this.cardActive === this.cardEGA) {
|
|
var bBackground = this.cardEGA.aATCRegs[0];
|
|
colorReg = bBackground & Card.CGA.COLOR.BORDER;
|
|
if (bBackground & Card.ATC.PALETTE.BRIGHT) colorReg |= Card.CGA.COLOR.BRIGHT;
|
|
if (this.cardEGA.aATCRegs[1] != 0x12) colorReg |= Card.CGA.COLOR.COLORSET2;
|
|
}
|
|
this.aRGB[0] = Video.aCGAColors[colorReg & (Card.CGA.COLOR.BORDER | Card.CGA.COLOR.BRIGHT)];
|
|
var aColorSet = (colorReg & Card.CGA.COLOR.COLORSET2)? Video.aCGAColorSet2 : Video.aCGAColorSet1;
|
|
for (var iColor = 0; iColor < aColorSet.length; iColor++) {
|
|
this.aRGB[iColor+1] = Video.aCGAColors[aColorSet[iColor]];
|
|
}
|
|
return this.aRGB;
|
|
}
|
|
|
|
if (this.cardColor === this.cardCGA) {
|
|
/*
|
|
* There's no need to update this.aRGB if we simply want to return a hard-coded set of 16 colors.
|
|
*/
|
|
return Video.aCGAColors;
|
|
}
|
|
|
|
Component.assert(this.cardColor === this.cardEGA);
|
|
|
|
var aRegs = (this.cardEGA.aATCRegs[15] != null? this.cardEGA.aATCRegs : Video.aEGAPalDef);
|
|
for (var i = 0; i < this.aRGB.length; i++) {
|
|
var b = aRegs[i] || 0;
|
|
var bRed = (((b & 0x04)? 0xaa : 0) | ((b & 0x20)? 0x55 : 0));
|
|
var bGreen = (((b & 0x02)? 0xaa : 0) | ((b & 0x10)? 0x55 : 0));
|
|
var bBlue = (((b & 0x01)? 0xaa : 0) | ((b & 0x08)? 0x55 : 0));
|
|
this.aRGB[i] = [bRed, bGreen, bBlue, 0xff];
|
|
}
|
|
return this.aRGB;
|
|
};
|
|
|
|
/**
|
|
* setFontData(abFontData, aFontOffsets, cxFontChar)
|
|
*
|
|
* To support partial font rebuilds (required for the EGA), we now preserve the original font data (abFontData),
|
|
* font offsets (aFontOffsets), and font character width (8 for the EGA, undefined for the MDA/CGA).
|
|
*
|
|
* TODO: Ultimately, we want to have exactly one dedicated font for the EGA, the data for which we'll read directly
|
|
* from plane 2 of video memory, instead of relying on the original font data in ROM. Relying on the ROM data was
|
|
* originally just a crutch to help get EGA support bootstrapped.
|
|
*
|
|
* Also, for the MDA/CGA, we should be discarding the font data after the first buildFonts() call, because we
|
|
* should not need to ever rebuild the fonts for those cards (both their font patterns and colors were hard-coded).
|
|
*
|
|
* @this {Video}
|
|
* @param {*} abFontData is the raw font data, from the ROM font file
|
|
* @param {Array.<number>} aFontOffsets contains offsets into abFontData: [0] for MDA, [1] for CGA
|
|
* @param {number} [cxFontChar] is a fixed character width to use for all fonts; undefined to use MDA/CGA defaults
|
|
*/
|
|
Video.prototype.setFontData = function(abFontData, aFontOffsets, cxFontChar)
|
|
{
|
|
this.abFontData = abFontData;
|
|
this.aFontOffsets = aFontOffsets;
|
|
this.cxFontChar = cxFontChar;
|
|
};
|
|
|
|
/**
|
|
* buildFonts()
|
|
*
|
|
* buildFonts() is called whenever the Video component is reset or restored; we used to build the fonts as soon
|
|
* as the ROM containing them was loaded, and then throw away the underlying font data, but with the EGA's ability
|
|
* to change the color of any font, font building must now be deferred until the reset or restore notifications,
|
|
* ensuring we have access to all the colors the card is currently programmed to use.
|
|
*
|
|
* We're also called whenever EGA palette registers are modified, since one or more fonts will likely need
|
|
* to be rebuilt (this is because our fonts contain pre-rendered images of all glyphs for all 16 active colors).
|
|
* Calls to buildFonts() should not be expensive though: the underlying createFont() function rebuilds a font only
|
|
* if its color has actually changed.
|
|
*
|
|
* TODO: We should avoid rebuilding fonts when palette registers change in graphics modes. More importantly, our
|
|
* font code is still written with the assumption that, like the MDA/CGA, the underlying font data never changes.
|
|
* The EGA, however, stores its fonts in plane 2, which means fonts are dynamic; this needs to be fixed.
|
|
*
|
|
* Supporting dynamic EGA fonts should not be hard though. We can get rid of abFontData and simply build a
|
|
* temporary snapshot of all the font bytes in plane 2 of the EGA's video buffer (adwMemory), and pass that on to
|
|
* buildFont() instead. We'll also need to either invalidate the existing font's color (to trigger a rebuild) or
|
|
* pass a new "force rebuild" flag.
|
|
*
|
|
* Once that's done, an added benefit will be that we can build just the font(s) that have been loaded into plane 2,
|
|
* instead of the multitude of fonts that we now build on a just-in-case basis (eg, the MDA font, the 8x8 CGA font
|
|
* for 43-line mode, and so on).
|
|
*
|
|
* @this {Video}
|
|
* @return {boolean} true if any or all fonts were (re)built, false if nothing changed
|
|
*/
|
|
Video.prototype.buildFonts = function()
|
|
{
|
|
var fChanges = false;
|
|
|
|
/*
|
|
* There's no point building any fonts if (a) we're in a non-windowed (eg, command-line) environment or
|
|
* (b) no font data was loaded.
|
|
*/
|
|
if (window && this.abFontData) {
|
|
|
|
var offSplit = this.cxFontChar? 0 : 0x0800;
|
|
var cxChar = this.cxFontChar? this.cxFontChar : 9;
|
|
|
|
if (this.buildFont(Video.FONTS.MDA, this.aFontOffsets[0], offSplit, cxChar, 14, this.abFontData, Video.aMDAColors, Video.aMDAColorMap)) {
|
|
fChanges = true;
|
|
}
|
|
|
|
var aRGBColors = this.getCardColors();
|
|
offSplit = 0x0000;
|
|
cxChar = this.cxFontChar? this.cxFontChar : 8;
|
|
if (this.buildFont(Video.FONTS.CGA, this.aFontOffsets[1], offSplit, cxChar, 8, this.abFontData, aRGBColors)) {
|
|
fChanges = true;
|
|
}
|
|
|
|
if (this.cxFontChar) {
|
|
if (this.buildFont(Video.FONTS.EGA, this.aFontOffsets[0], 0, this.cxFontChar, 14, this.abFontData, aRGBColors)) {
|
|
fChanges = true;
|
|
}
|
|
}
|
|
}
|
|
return fChanges;
|
|
};
|
|
|
|
/**
|
|
* buildFont(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)
|
|
*
|
|
* This is a wrapper for createFont() which also takes care loading double-size fonts when fDoubleFont is set.
|
|
*
|
|
* @this {Video}
|
|
* @param {number} nFont
|
|
* @param {number} offData is the offset of the font data
|
|
* @param {number} offSplit is the offset of any split font data, or zero if not split
|
|
* @param {number} cxChar is the width of the font characters
|
|
* @param {number} cyChar is the height of the font characters
|
|
* @param {*} abFontData is the raw font data, from the ROM font file
|
|
* @param {Array} aRGBColors is an array of color RGB variations, corresponding to supported FGND attribute values
|
|
* @param {Array} [aColorMap] contains color indexes corresponding to attribute values (if not supplied, the mapping is assumed to be 1-1)
|
|
* @return {boolean} true if any or all fonts were (re)built, false if nothing changed
|
|
*/
|
|
Video.prototype.buildFont = function(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)
|
|
{
|
|
var fChanges = false;
|
|
if (DEBUG) this.messageDebugger("buildFont(" + nFont + "): building " + Video.cardSpecs[nFont][0] + " font");
|
|
if (this.createFont(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)) fChanges = true;
|
|
|
|
/*
|
|
* If font-doubling is enabled, then load a double-size version of the font as well, as it provides
|
|
* sharper rendering, especially when the screen cell size is a multiple of the above font cell size;
|
|
* in the case of the CGA, this may also be useful for 40-column modes.
|
|
*/
|
|
if (this.fDoubleFont) {
|
|
nFont <<= 1;
|
|
if (DEBUG) this.messageDebugger("buildFont(" + nFont + "): building " + Video.cardSpecs[nFont >> 1][0] + " double-size font");
|
|
if (this.createFont(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)) fChanges = true;
|
|
}
|
|
return fChanges;
|
|
};
|
|
|
|
/**
|
|
* createFont(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)
|
|
*
|
|
* All color variations are stored on the same font canvas, arranged vertically as a series of grids, where each
|
|
* grid is a 16x16 character glyph array.
|
|
*
|
|
* Since every character must be drawn first with its background color and then with the foreground shape on top,
|
|
* I used to include a series of empty cells at the top every font canvas containing all supported background colors
|
|
* (ie, before the character grids). But now createFont() also creates an aCSSColors array that is saved alongside
|
|
* the font canvas, and updateChar() uses that array in conjunction with fillRect() to draw character backgrounds.
|
|
*
|
|
* @this {Video}
|
|
* @param {number} nFont
|
|
* @param {number} offData is the offset of the font data
|
|
* @param {number} offSplit is the offset of any split font data, or zero if not split
|
|
* @param {number} cxChar is the width of the font characters
|
|
* @param {number} cyChar is the height of the font characters
|
|
* @param {*} abFontData is the raw font data, from the ROM font file
|
|
* @param {Array} aRGBColors is an array of color RGB variations, corresponding to supported FGND attribute values
|
|
* @param {Array|undefined} aColorMap contains color indexes corresponding to attribute values (if not supplied, the mapping is assumed to be 1-1)
|
|
* @return {boolean} true if any or all fonts were (re)created, false if nothing changed
|
|
*/
|
|
Video.prototype.createFont = function(nFont, offData, offSplit, cxChar, cyChar, abFontData, aRGBColors, aColorMap)
|
|
{
|
|
var fChanges = false;
|
|
var nDouble = (nFont & 0x1)? 0 : 1;
|
|
var font = this.aFonts[nFont];
|
|
if (!font) {
|
|
font = {
|
|
cxCell: cxChar << nDouble,
|
|
cyCell: cyChar << nDouble,
|
|
aCSSColors: new Array(aRGBColors.length),
|
|
aRGBColors: aRGBColors.slice(), // using the Array slice() method to simply make a copy
|
|
aColorMap: aColorMap,
|
|
aCanvas: new Array(aRGBColors.length)
|
|
};
|
|
}
|
|
for (var iColor = 0; iColor < aRGBColors.length; iColor++) {
|
|
var rgbColor = aRGBColors[iColor];
|
|
var rgbColorOrig = font.aCSSColors[iColor]? font.aRGBColors[iColor] : [];
|
|
if (rgbColor[0] !== rgbColorOrig[0] || rgbColor[1] !== rgbColorOrig[1] || rgbColor[2] !== rgbColorOrig[2]) {
|
|
if (DEBUG) this.messageDebugger("creating font color " + iColor + " for font " + nFont);
|
|
this.createFontColor(font, iColor, rgbColor, nDouble, offData, offSplit, cxChar, cyChar, abFontData);
|
|
fChanges = true;
|
|
}
|
|
}
|
|
this.aFonts[nFont] = font;
|
|
return fChanges;
|
|
};
|
|
|
|
/**
|
|
* createFontColor(font, iColor, rgbColor, nDouble, offData, offSplit, cxChar, cyChar, abFontData)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} font
|
|
* @param {number} iColor
|
|
* @param {Array} rgbColor contains the RGB values for iColor
|
|
* @param {number} nDouble is 1 to double output font dimensions, 0 to match input dimensions
|
|
* @param {number} offData is the offset of the font data
|
|
* @param {number} offSplit is the offset of any split font data, or zero if not split
|
|
* @param {number} cxChar is the width of the font characters
|
|
* @param {number} cyChar is the height of the font characters
|
|
* @param {*} abFontData is the raw font data, from the ROM font file
|
|
*/
|
|
Video.prototype.createFontColor = function(font, iColor, rgbColor, nDouble, offData, offSplit, cxChar, cyChar, abFontData)
|
|
{
|
|
/*
|
|
* Now we're ready to create a 16x16 character grid for the specified color. Note that all
|
|
* the character bits are opaque (alpha=0xff) while all the surrounding bits are transparent
|
|
* (alpha=0x00, as specified in the 4th byte of rgbOff).
|
|
*
|
|
* Originally, I created 256 ImageData objects, using context.createImageData(cxChar,cyChar),
|
|
* then setting its pixels to match those of an individual character, and then drawing characters
|
|
* with contextFont.putImageData(). But putImageData() is relatively slow....
|
|
*
|
|
* Now I create a new canvas, with dimensions that allow me to arrange all 256 characters in an
|
|
* 16x16 grid -- much like the "chargen.png" bitmap used in the C1Pjs version of the Video component.
|
|
* Then drawing becomes much the same as before, because it turns out that drawImage() accepts either
|
|
* an image object OR a canvas object.
|
|
*
|
|
* This also yields better performance, since drawImage() is much faster than putImageData().
|
|
* We still have to use putImageData() to build the font canvas, but that's a one-time operation.
|
|
*/
|
|
var rgbOff = [0x00, 0x00, 0x00, 0x00];
|
|
var canvasFont = window.document.createElement("canvas");
|
|
canvasFont.width = font.cxCell << 4;
|
|
canvasFont.height = (font.cyCell << 4);
|
|
var contextFont = canvasFont.getContext("2d");
|
|
|
|
/*
|
|
* See notes above regarding ImageSmoothingEnabled....
|
|
*
|
|
contextFont['mozImageSmoothingEnabled'] = false;
|
|
contextFont['webkitImageSmoothingEnabled'] = false;
|
|
*/
|
|
|
|
var iChar, x, y;
|
|
var cyLimit = (cyChar < 8 || !offSplit)? cyChar : 8;
|
|
var imageChar = contextFont.createImageData(font.cxCell, font.cyCell);
|
|
|
|
for (iChar = 0; iChar < 256; iChar++) {
|
|
for (y = 0; y < cyChar; y++) {
|
|
/*
|
|
* fUnderline should be true only in the FONT_MDA case, and only for the odd color variations
|
|
* (1 and 3, out of variations 0 to 4), and only for the two bottom-most rows of the character cell
|
|
* (which I still need to confirm)
|
|
*/
|
|
var fUnderline = (font.aColorMap && (iColor & 0x1) && y >= cyChar - 2);
|
|
var offChar = (y < cyLimit? offData + iChar * cyLimit + y : offSplit + iChar * cyLimit + y - cyLimit);
|
|
var b = abFontData[offChar];
|
|
for (var nRowDoubler = 0; nRowDoubler <= nDouble; nRowDoubler++) {
|
|
for (x = 0; x < cxChar; x++) {
|
|
/*
|
|
* This "bit" of logic takes care of those characters (0xB0-0xDF) whose 9th bit must mirror the 8th bit;
|
|
* in all other cases, any bit past the 8th bit is automatically zero. It also takes care of embedding a solid
|
|
* row of bits whenever fUnderline is true.
|
|
*/
|
|
var bit = (fUnderline? 1 : (b & (0x80 >> (x >= 8 && iChar >= 0xB0 && iChar <= 0xDF? 7 : x))));
|
|
var xDst = (x << nDouble);
|
|
var yDst = (y << nDouble) + nRowDoubler;
|
|
var rgb = (bit? rgbColor : rgbOff);
|
|
this.setPixel(imageChar, xDst, yDst, rgb);
|
|
if (nDouble) this.setPixel(imageChar, xDst + 1, yDst, rgb);
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* (iChar >> 4) performs the integer equivalent of Math.floor(iChar / 16), and (iChar & 0xf) is the equivalent of (iChar % 16).
|
|
*/
|
|
contextFont.putImageData(imageChar, x = (iChar & 0xf) * font.cxCell, y = (iChar >> 4) * font.cyCell);
|
|
}
|
|
|
|
/*
|
|
* The colors for cell backgrounds and cursor elements must be converted to CSS color strings.
|
|
*/
|
|
font.aCSSColors[iColor] = "#" + str.toHexByte(rgbColor[0]) + str.toHexByte(rgbColor[1]) + str.toHexByte(rgbColor[2]);
|
|
font.aRGBColors[iColor] = rgbColor;
|
|
|
|
/*
|
|
* Enable this code if you want to see what the generated font looks like....
|
|
*
|
|
if (MAXDEBUG) {
|
|
var iSrcColor = (iColor == 15? 0 : iColor + 1);
|
|
this.contextScreen.fillStyle = aCSSColors[iSrcColor];
|
|
this.contextScreen.fillRect(iColor*(font.cxCell<<2), 0, canvasFont.width>>2, font.cyCell<<4);
|
|
this.contextScreen.drawImage(canvasFont, 0, iColor*(font.cyCell<<4), canvasFont.width>>2, font.cyCell<<4, iColor*(font.cxCell<<2), 0, canvasFont.width>>2, font.cyCell<<4);
|
|
}
|
|
*/
|
|
|
|
font.aCanvas[iColor] = canvasFont;
|
|
};
|
|
|
|
/**
|
|
* checkBlink()
|
|
*
|
|
* Called at the end of every updateScreen(), which may have updated cBlinkVisible to a non-zero value.
|
|
*
|
|
* Also called at the end of every checkCursor(); ie, whenever the CRT register(s) affecting the position or shape
|
|
* of the hardware cursor have been modified, and any of iCellCursor, yCursor or cyCursor have been modified as a result.
|
|
*
|
|
* Note that the cursor always blinks when it's ON; it can only be turned OFF, moved off-screen, or its rate set to half
|
|
* the normal blink rate (by default, it blinks at the normal blink rate). Bits 5-6 of the CRTC.CURSOR_START register can
|
|
* be set as follows:
|
|
*
|
|
* 00: Cursor blinks at normal blink rate
|
|
* 01: Cursor is off
|
|
* 10: (Same as 00)
|
|
* 11: Cursor blinks at half the normal blink rate
|
|
*
|
|
* According to documentation, the normal blink rate is 1/16 of the frame rate (8 frames on, 8 off).
|
|
*
|
|
* TODO: As an aside, I've observed in the "real world" that the MDA cursor cycles about 3 times per second, and by "cycle"
|
|
* I mean one full off-and-on-again cycle. I'm assuming that's the normal rate (00), not the slower "half rate" (11).
|
|
* Since that's faster than our current cursor blink rate, we should look into an option to boost our rate, without adversely
|
|
* affecting the attribute blink rate (which is currently hard-coded at half the cursor blink rate), and we should look into
|
|
* supporting "half rate" blinking, too.
|
|
*
|
|
* @this {Video}
|
|
* @return {boolean} true if there are things to blink, false if not
|
|
*/
|
|
Video.prototype.checkBlink = function()
|
|
{
|
|
if (this.cBlinkVisible > 0 || this.iCellCursor >= 0) {
|
|
if (this.cBlinks < 0) {
|
|
this.cBlinks = 0;
|
|
/*
|
|
* At this point, we can either fire up our own timer (doBlink), or rely on updateScreen()
|
|
* being called by the CPU at a regular rate (eg, CPU.VIDEO_UPDATES_PER_SECOND = 60) and advance
|
|
* cBlinks at the start of updateScreen() accordingly.
|
|
*
|
|
* doBlink() wants to increment cBlinks every 266ms. On the other hand, if updateScreen() is being
|
|
* called 60 times per second, that's about once every 16ms, so if every 16th updateScreen() increments
|
|
* cBlinks, cBlinks should advance at the same rate.
|
|
*
|
|
* The only downside to relying on the CPU driving our blink count is that whenever the CPU is halted
|
|
* (eg, by the PCjs debugger) all blinking stops -- all characters with the blink attribute AND the cursor.
|
|
*
|
|
* But we can simply say that when we halt, we mean "halt everything" (ie, call it a feature).
|
|
*
|
|
* this.doBlink(true);
|
|
*/
|
|
}
|
|
return true;
|
|
}
|
|
this.cBlinks = -1;
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* checkCursor()
|
|
*
|
|
* Called whenever a CRT data register is updated, since there are multiple registers that can affect the
|
|
* visibility of the cursor (more than these, actually, but I'm going to limit my initial support to standard
|
|
* ROM BIOS controller settings):
|
|
*
|
|
* CRTC.MAX_SCAN_LINE
|
|
* CRTC.CURSOR_START
|
|
* CRTC.CURSOR_END
|
|
* CRTC.START_ADDR_HI
|
|
* CRTC.START_ADDR_LO
|
|
* CRTC.CURSOR_ADDR_HI
|
|
* CRTC.CURSOR_ADDR_LO
|
|
*
|
|
* @this {Video}
|
|
* @return {boolean} true if the cursor is visible, false if not
|
|
*/
|
|
Video.prototype.checkCursor = function()
|
|
{
|
|
/*
|
|
* The "hardware cursor" is never visible in graphics modes.
|
|
*/
|
|
if (!this.nFont) return false;
|
|
|
|
for (var i = Card.CRTC.CURSOR_START.INDX; i <= Card.CRTC.CURSOR_ADDR_LO; i++) {
|
|
if (this.cardActive.aCRTCRegs[i] == null)
|
|
return false;
|
|
}
|
|
|
|
var bCursorFlags = this.cardActive.aCRTCRegs[Card.CRTC.CURSOR_START.INDX];
|
|
var bCursorStart = bCursorFlags & Card.CRTC.CURSOR_START.MASK;
|
|
var bCursorEnd = this.cardActive.aCRTCRegs[Card.CRTC.CURSOR_END.INDX] & Card.CRTC.CURSOR_END.MASK;
|
|
var bCursorMax = this.cardActive.aCRTCRegs[Card.CRTC.MAX_SCAN_LINE] & Card.CRTC.CURSOR_END.MASK;
|
|
|
|
/*
|
|
* HACK: The original EGA BIOS has a cursor emulation bug when 43-line mode is enabled, so we attempt to detect
|
|
* that particular combination of bad values and automatically fix them.
|
|
*/
|
|
var fEGA = false;
|
|
if (this.cardActive === this.cardEGA) {
|
|
fEGA = true;
|
|
if (bCursorMax == 7 && bCursorStart == 4 && !bCursorEnd) bCursorEnd = 7;
|
|
}
|
|
|
|
/*
|
|
* One way of disabling the cursor is to set bit 5 (Card.CRTC.CURSOR_START.BLINKOFF) of the CRTC.CURSOR_START flags;
|
|
* another way is setting bCursorStart > bCursorEnd (unless it's an EGA, in which case we must actually draw a
|
|
* "split block" cursor instead).
|
|
*
|
|
* TODO: Verify whether the second test (bCursorStart > bCursorMax) should also result in a hidden cursor;
|
|
* ThinkTank sets both start and end values to 0x0f, which doesn't make sense on a CGA, where the max is 0x07.
|
|
*/
|
|
if ((bCursorFlags & Card.CRTC.CURSOR_START.BLINKOFF) || bCursorStart > bCursorEnd && !fEGA || bCursorStart > bCursorMax) {
|
|
this.removeCursor();
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* The most compatible way of disabling the cursor is to simply move the cursor to an off-screen position.
|
|
*/
|
|
var iCellCursor = (this.cardActive.aCRTCRegs[Card.CRTC.CURSOR_ADDR_LO] + ((this.cardActive.aCRTCRegs[Card.CRTC.CURSOR_ADDR_HI] & Card.CRTC.ADDR_HI_MASK) << 8));
|
|
if (this.iCellCursor != iCellCursor) {
|
|
if (DEBUG) this.messageDebugger("checkCursor(): cursor moved from " + this.iCellCursor + " to " + iCellCursor);
|
|
this.removeCursor();
|
|
this.iCellCursor = iCellCursor;
|
|
}
|
|
|
|
/*
|
|
* yCursor and cyCursor are no longer scaled at this point, because the necessary scaling will depend on whether we're
|
|
* drawing the cursor to the on-screen or off-screen buffer, and updateChar() is in the best position to determine that.
|
|
*
|
|
* We also record cyCursorCell, the hardware cell height, since we'll need to know what the yCursor and cyCursor values
|
|
* are relative to when it's time to scale them.
|
|
*/
|
|
var bCursorSize = bCursorEnd - bCursorStart + 1;
|
|
if (this.yCursor != bCursorStart || this.cyCursor != bCursorSize) {
|
|
this.yCursor = bCursorStart;
|
|
this.cyCursor = bCursorSize;
|
|
}
|
|
this.cyCursorCell = bCursorMax + 1;
|
|
|
|
this.checkBlink();
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* removeCursor()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.removeCursor = function()
|
|
{
|
|
if (this.iCellCursor >= 0) {
|
|
if (this.aCellCache !== undefined) {
|
|
var drawCursor = (Video.ATTRS.DRAW_CURSOR << 8);
|
|
var data = this.aCellCache[this.iCellCursor];
|
|
if (data & drawCursor) {
|
|
data &= ~drawCursor;
|
|
var col = this.iCellCursor % this.nCols;
|
|
var row = Math.floor(this.iCellCursor / this.nCols);
|
|
if (this.nFont && this.aFonts[this.nFont]) {
|
|
/*
|
|
* 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);
|
|
}
|
|
/*
|
|
* While updating the on-screen canvas directly could open us up to potential subpixel artifacts again,
|
|
* I'm hopeful that won't be the case, since removeCursor() is called only during certain well-defined
|
|
* events. The alternative to this simple updateChar() call is unappealing: redrawing the ENTIRE off-screen
|
|
* buffer to the on-screen canvas, just as updateScreen() does.
|
|
*/
|
|
this.updateChar(col, row, data);
|
|
}
|
|
if (DEBUG) this.messageDebugger("removeCursor(): removed from " + row + "," + col);
|
|
this.aCellCache[this.iCellCursor] = data;
|
|
}
|
|
}
|
|
this.iCellCursor = -1;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* getAccess()
|
|
*
|
|
* @this {Video}
|
|
* @return {number|undefined} current memory access setting, or undefined if unknown
|
|
*/
|
|
Video.prototype.getAccess = function()
|
|
{
|
|
var nAccess;
|
|
var card = this.cardActive;
|
|
|
|
var regGRCMode = card.aGRCRegs[Card.GRC.MODE.INDX];
|
|
if (regGRCMode != null) {
|
|
var nReadAccess = Card.ACCESS.READ.MODE0;
|
|
var nWriteAccess = Card.ACCESS.WRITE.MODE0;
|
|
var nWriteMode = regGRCMode & Card.GRC.MODE.WRITE;
|
|
var regDataRotate = card.aGRCRegs[Card.GRC.DATAROT.INDX] & Card.GRC.DATAROT.MASK;
|
|
switch (nWriteMode) {
|
|
case Card.GRC.MODE.WRITE_MODE0:
|
|
if (regDataRotate) {
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE0ROT;
|
|
switch (regDataRotate & Card.GRC.DATAROT.FUNC) {
|
|
case Card.GRC.DATAROT.AND:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE0AND;
|
|
break;
|
|
case Card.GRC.DATAROT.OR:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE0OR;
|
|
break;
|
|
case Card.GRC.DATAROT.XOR:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE0XOR;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
card.nDataRotate = regDataRotate & Card.GRC.DATAROT.COUNT;
|
|
}
|
|
break;
|
|
case Card.GRC.MODE.WRITE_MODE1:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE1;
|
|
break;
|
|
case Card.GRC.MODE.WRITE_MODE2:
|
|
switch (regDataRotate & Card.GRC.DATAROT.FUNC) {
|
|
default:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE2;
|
|
break;
|
|
case Card.GRC.DATAROT.AND:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE2AND;
|
|
break;
|
|
case Card.GRC.DATAROT.OR:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE2OR;
|
|
break;
|
|
case Card.GRC.DATAROT.XOR:
|
|
nWriteAccess = Card.ACCESS.WRITE.MODE2XOR;
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
if (DEBUG) this.messageDebugger("getAccess(): invalid GRC mode (" + str.toHexByte(regGRCMode) + ")");
|
|
break;
|
|
}
|
|
if (regGRCMode & Card.GRC.MODE.READ_MODE1) {
|
|
nReadAccess = Card.ACCESS.READ.MODE1;
|
|
}
|
|
if (regGRCMode & Card.GRC.MODE.EVENODD) {
|
|
nReadAccess |= Card.ACCESS.READ.EVENODD;
|
|
nWriteAccess |= Card.ACCESS.WRITE.EVENODD;
|
|
}
|
|
nAccess = nReadAccess | nWriteAccess;
|
|
}
|
|
return nAccess;
|
|
};
|
|
|
|
/**
|
|
* setAccess(nAccess)
|
|
*
|
|
* @this {Video}
|
|
* @param {number|undefined} nAccess (one of the Card.ACCESS.* constants)
|
|
*/
|
|
Video.prototype.setAccess = function(nAccess)
|
|
{
|
|
var card = this.cardActive;
|
|
if (nAccess != null && card && nAccess != card.nAccess) {
|
|
|
|
if (DEBUG) this.messageDebugger("setAccess(0x" + str.toHexWord(nAccess) + ")");
|
|
|
|
card.setMemoryAccess(nAccess);
|
|
|
|
/*
|
|
* Note that setMemoryAccess() can fail, in which case it will an report error, indicating either a
|
|
* misconfiguration or some sort of internal inconsistency; in any case, there's not much we can do about
|
|
* it at this point, other than possibly reverting the current access setting. There's probably not much
|
|
* point, however, because there's no guarantee that setMemoryAccess() didn't modify one or more blocks
|
|
* before choking.
|
|
*/
|
|
this.bus.setMemoryAccess(card.addrBuffer, card.sizeBuffer, card.getMemoryAccess());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* setDimensions()
|
|
*
|
|
* @this {Video}
|
|
*/
|
|
Video.prototype.setDimensions = function()
|
|
{
|
|
this.nFont = 0;
|
|
this.nCols = this.nDefaultCols;
|
|
this.nRows = this.nDefaultRows;
|
|
this.nCellsPerWord = Video.aModeParms[Video.MODES.MDA_80X25][2];
|
|
|
|
var cbPadding = 0;
|
|
var modeParms = Video.aModeParms[this.nMode];
|
|
if (modeParms) {
|
|
this.nCols = modeParms[0];
|
|
this.nRows = modeParms[1];
|
|
this.nCellsPerWord = modeParms[2];
|
|
cbPadding = modeParms[3] || 0;
|
|
this.nFont = modeParms[4]; // this will be undefined for graphics modes
|
|
if (this.nMonitorType == ChipSet.MONITOR.EGACOLOR) {
|
|
/*
|
|
* When an EGA is connected to a CGA monitor, the old aModeParms table is correct: we must
|
|
* use the hard-coded 8x8 "CGA_80" font. But when it's connected to an EGA monitor, we want
|
|
* 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.
|
|
*/
|
|
if (this.cardActive === this.cardEGA && this.nFont == Video.FONTS.CGA) {
|
|
if (this.cardEGA.aCRTCRegs[Card.CRTC.MAX_SCAN_LINE] == 7) {
|
|
/*
|
|
* Vertical resolution of 350 divided by 8 (ie, scan lines 0-7) yields 43 whole rows.
|
|
*/
|
|
this.nRows = 43;
|
|
}
|
|
/*
|
|
* Since we can also be called before any hardware registers have been initialized,
|
|
* it may be best to not perform the following test (which is why it's commented out).
|
|
*/
|
|
else /* if (this.cardEGA.aCRTCRegs[Card.CRTC.MAX_SCAN_LINE] == 13) */ {
|
|
/*
|
|
* Vertical resolution of 350 divided by 14 (ie, scan lines 0-13) yields exactly 25 rows.
|
|
*/
|
|
this.nFont = Video.FONTS.EGA;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.nCells = this.nCols * this.nRows;
|
|
this.nCellCache = (this.nCells / this.nCellsPerWord);
|
|
this.cbScreen = (this.nCellCache << 1) + cbPadding;
|
|
this.cbSplit = (cbPadding? ((this.cbScreen + cbPadding) >> 1) : 0);
|
|
if (this.nMode >= Video.MODES.EGA_320X200) this.nCellCache <<= 1;
|
|
|
|
/*
|
|
* If no fonts were successfully loaded, there's no point in initializing the remaining drawing parameters.
|
|
*/
|
|
if (!this.aFonts.length) return;
|
|
|
|
this.cxScreenCell = Math.floor(this.cxScreen / this.nCols);
|
|
this.cyScreenCell = Math.floor(this.cyScreen / this.nRows);
|
|
|
|
/*
|
|
* Now we make the all-important scaling determination: if the font cell dimensions (cxCell, cyCell)
|
|
* don't match the physical screen cell dimensions (cxCell, cyCell), then we look at the caller's
|
|
* fScaleFont setting: if it's false, we draw the characters as-is, with a border if the characters
|
|
* are smaller than the cells; and if fScaleFont is true, we simply tell drawImage to draw the
|
|
* characters to fit.
|
|
*
|
|
* WARNING: The only problem with fScaleFont is that any stretching or shrinking tends to be accompanied
|
|
* by subpixel artifacts along the boundaries of the font images. Definitely annoying, and apparently
|
|
* there are no standard mechanisms for turning that behavior off. So, for now, I've "neutered" the
|
|
* fScaleFont test slightly, by adding the "nCols == 80" test that prevents scaling from kicking in for
|
|
* 40-column modes.
|
|
*
|
|
* Also, whether scaling or not, if it makes sense to use a "doubled" font, we'll switch the font as
|
|
* well. Note that the doubled font for any existing font also has an ID that is double the existing ID,
|
|
* making it easy to check for the existence of a font's "double" (shift the ID left by 1).
|
|
*
|
|
* TODO: Since we now use an off-screen buffer for ALL modes, both text and graphics, we should
|
|
* revisit changes that were made to work around subpixel artifacts; those should no longer be an issue.
|
|
*/
|
|
if (this.nFont) {
|
|
var font = this.aFonts[this.nFont];
|
|
var fontDoubled = this.aFonts[this.nFont << 1];
|
|
|
|
if (this.fScaleFont && this.nCols == 80) {
|
|
if (fontDoubled) {
|
|
if (this.cxScreenCell >= (fontDoubled.cxCell * 3) >> 2) { // && this.cyScreenCell > (fontDoubled.cyCell * 3) >> 2) {
|
|
this.nFont <<= 1;
|
|
font = fontDoubled;
|
|
if (DEBUG) this.log("setDimensions(): switching to double-size font, scaled");
|
|
}
|
|
}
|
|
} else {
|
|
if (fontDoubled) {
|
|
if (this.cxScreenCell >= fontDoubled.cxCell) { // && this.cyScreenCell == fontDoubled.cyCell) {
|
|
this.nFont <<= 1;
|
|
font = fontDoubled;
|
|
if (DEBUG) this.log("setDimensions(): switching to double-size font, unscaled");
|
|
}
|
|
}
|
|
if (font) {
|
|
this.cxScreenCell = font.cxCell;
|
|
this.cyScreenCell = font.cyCell;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* In text modes, we have the option of setting all the *ScreenBuffer 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.cxBuffer = this.cyBuffer = 0;
|
|
if (font) {
|
|
this.cxBuffer = this.nCols * font.cxCell;
|
|
this.cyBuffer = this.nRows * font.cyCell;
|
|
}
|
|
} else {
|
|
/*
|
|
* CGA graphics modes have their "cells" (pixels) split evenly across two halves of the video buffer, with
|
|
* EVEN scan lines in the first half and ODD scan lines in the second half, so unlike text modes, we can't set a
|
|
* limit of what's visible on-screen to "columns * rows", so the screen limit is set to match the buffer limit.
|
|
*
|
|
* In addition, updateScreen() requires an off-screen imageData buffer that matches the size of the entire screen,
|
|
* so that updateScreen() can set all pixels that have changed and then update the screen with a single drawImage().
|
|
*
|
|
* An alternative approach, with a smaller footprint, would be to allocate an off-screen buffer large enough for a
|
|
* single scan line, and redraw one scan line at a time, but given how EVEN and ODD scan lines are spread across the
|
|
* entire buffer, it's not clear there would be enough unchanged scan lines on average to make that approach faster.
|
|
*/
|
|
this.cxScreenCell = this.cyScreenCell = 1; // in graphics mode, a cell is exactly one pixel
|
|
this.cxBuffer = this.nCols;
|
|
this.cyBuffer = this.nRows;
|
|
}
|
|
|
|
/*
|
|
* Allocate the off-screen buffers
|
|
*/
|
|
this.imageScreenBuffer = this.contextScreen.createImageData(this.cxBuffer, this.cyBuffer);
|
|
this.canvasScreenBuffer = window.document.createElement("canvas");
|
|
this.canvasScreenBuffer.width = this.cxBuffer;
|
|
this.canvasScreenBuffer.height = this.cyBuffer;
|
|
this.contextScreenBuffer = this.canvasScreenBuffer.getContext("2d");
|
|
|
|
/*
|
|
* Since cxCell and cyCell were originally defined in terms of cxScreen/nCols and cyScreen/nRows, you might think
|
|
* these border calculations would always be zero, but that would mean you overlooked the code above which tries to
|
|
* avoid stretching 40-column modes into an unpleasantly wide shape.
|
|
*/
|
|
this.xScreenOffset = this.yScreenOffset = 0;
|
|
this.cxScreenOffset = this.cxScreen;
|
|
this.cyScreenOffset = this.cyScreen;
|
|
|
|
var cxBorder = this.cxScreen - (this.nCols * this.cxScreenCell);
|
|
var cyBorder = this.cyScreen - (this.nRows * this.cyScreenCell);
|
|
if (cxBorder > 0) {
|
|
this.xScreenOffset = (cxBorder >> 1);
|
|
this.cxScreenOffset -= cxBorder;
|
|
}
|
|
if (cyBorder > 0) {
|
|
this.yScreenOffset = (cyBorder >> 1);
|
|
this.cyScreenOffset -= cyBorder;
|
|
}
|
|
if (cxBorder || cyBorder) {
|
|
this.contextScreen.fillStyle = this.canvasScreen.style.backgroundColor;
|
|
this.contextScreen.fillRect(0, 0, this.cxScreen, this.cyScreen);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* checkMode(fForce)
|
|
*
|
|
* Called whenever the MDA/CGA's mode register (eg, Card.MDA.MODE.PORT, Card.CGA.MODE.PORT) is updated,
|
|
* or whenever the EGA's GRC Misc register is updated, or when we've just finished a restore().
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} [fForce] is used to force a mode update, if we recognize the current mode
|
|
* @return {boolean} true if successful, false if not
|
|
*/
|
|
Video.prototype.checkMode = function(fForce)
|
|
{
|
|
var nAccess;
|
|
var nMode = this.nMode;
|
|
var card = this.cardActive;
|
|
|
|
if (!card) {
|
|
/*
|
|
* We are likely being called after a restore(), which needs us to call setMode() to insure the proper video
|
|
* buffer is mapped in. So we unset this.nMode to guarantee that setMode() will be called, and if it wasn't set
|
|
* to anything before, then we fall-back to the default mode.
|
|
*/
|
|
this.nMode = null;
|
|
if (nMode == null) nMode = this.nModeDefault;
|
|
}
|
|
else {
|
|
if (card.iCard == Video.CARDS.MDA) {
|
|
nMode = Video.MODES.MDA_80X25;
|
|
}
|
|
else if (card.iCard == Video.CARDS.EGA) {
|
|
/*
|
|
* The sizeBuffer we choose reflects the amount of physical address space that all 4 planes
|
|
* of EGA memory normally span, NOT the total amount of EGA memory. So for a 64Kb EGA card,
|
|
* we would set card.sizeBuffer to 16Kb (0x4000).
|
|
*
|
|
* TODO: Need to take into account modes that "chain" planes together (eg, mode 0x0F, and
|
|
* presumably mode 0x10, on an EGA card with only 64Kb).
|
|
*/
|
|
nMode = null;
|
|
var cbBuffer = card.cbMemory >> 2;
|
|
var cbBufferText = (cbBuffer > 0x8000? 0x8000 : cbBuffer);
|
|
|
|
var regGRCMisc = card.aGRCRegs[Card.GRC.MISC.INDX];
|
|
if (regGRCMisc != null) {
|
|
|
|
switch(regGRCMisc & Card.GRC.MISC.MAPMEM) {
|
|
case Card.GRC.MISC.MAPA0128:
|
|
card.addrBuffer = 0xA0000;
|
|
card.sizeBuffer = cbBuffer; // 0x20000
|
|
nMode = Video.MODES.UNKNOWN; // no BIOS mode uses this mapping, but we don't want to leave nMode null if we've come this far
|
|
break;
|
|
case Card.GRC.MISC.MAPA064:
|
|
card.addrBuffer = 0xA0000;
|
|
card.sizeBuffer = cbBuffer; // 0x10000
|
|
nMode = (this.nMonitorType == ChipSet.MONITOR.MONO? Video.MODES.EGA_640X350_MONO : Video.MODES.EGA_640X350);
|
|
break;
|
|
case Card.GRC.MISC.MAPB032:
|
|
card.addrBuffer = 0xB0000;
|
|
card.sizeBuffer = cbBufferText;
|
|
nMode = Video.MODES.MDA_80X25;
|
|
break;
|
|
case Card.GRC.MISC.MAPB832:
|
|
card.addrBuffer = 0xB8000;
|
|
card.sizeBuffer = cbBufferText;
|
|
nMode = (this.nMonitorType == ChipSet.MONITOR.MONO? Video.MODES.CGA_80X25_BW : Video.MODES.CGA_80X25);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
var fSEQDotClock = (card.aSEQRegs[Card.SEQ.CLK.INDX] & Card.SEQ.CLK.DOTCLOCK);
|
|
var nCRTCVertTotal = card.aCRTCRegs[Card.CRTC.EGA.VERT_TOTAL] | ((card.aCRTCRegs[Card.CRTC.EGA.OVERFLOW.INDX] & Card.CRTC.EGA.OVERFLOW.VERT_TOTAL) << 8);
|
|
|
|
if (nMode != Video.MODES.UNKNOWN) {
|
|
if (!(regGRCMisc & Card.GRC.MISC.GRAPHICS)) {
|
|
if (fSEQDotClock) nMode -= 2;
|
|
} else {
|
|
if (card.addrBuffer == 0xB8000) {
|
|
//
|
|
// Since nMode will have been assigned a default of either 0x02 or 0x03, convert that to either
|
|
// 0x05 or 0x04 if we're in a low-res graphics mode, 0x06 otherwise.
|
|
//
|
|
nMode = fSEQDotClock? (7 - nMode) : Video.MODES.CGA_640X200;
|
|
} else {
|
|
//
|
|
// card.addrBuffer must be 0xA0000, so we need to discriminate between modes 0x0D through 0x10;
|
|
// we've already defaulted to 0x0F or 0x10, so determine if it's 0x0D or 0x0E (ie, a 200-row mode)
|
|
// and then which one (ie, 320 wide or 640 wide).
|
|
//
|
|
if (nCRTCVertTotal < 350) nMode = (fSEQDotClock? Video.MODES.EGA_320X200 : Video.MODES.EGA_640X200);
|
|
}
|
|
}
|
|
}
|
|
|
|
nAccess = this.getAccess();
|
|
}
|
|
}
|
|
else if (card.modeReg & Card.CGA.MODE.VIDEO_ENABLE) {
|
|
/*
|
|
* NOTE: For the CGA, we precondition any mode change on CGA.MODE.VIDEO_ENABLE being set, otherwise
|
|
* we'll get spoofed by the ROM BIOS scroll code, which waits for vertical retrace and then turns CGA.MODE.VIDEO_ENABLE
|
|
* off, using a hard-coded mode value (0x25) that does NOT necessarily match the the CGA video mode currently in effect.
|
|
*/
|
|
if (!(card.modeReg & Card.CGA.MODE.GRAPHIC_SEL)) {
|
|
nMode = ((card.modeReg & Card.CGA.MODE._80X25)? Video.MODES.CGA_80X25 : Video.MODES.CGA_40X25);
|
|
if (card.modeReg & Card.CGA.MODE.BW_SEL) nMode -= 1;
|
|
} else {
|
|
nMode = ((card.modeReg & Card.CGA.MODE.HIRES_BW)? Video.MODES.CGA_640X200 : Video.MODES.CGA_320X200_BW);
|
|
if (!(card.modeReg & Card.CGA.MODE.BW_SEL)) nMode -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* NOTE: If setMode() remaps the video memory, that will trigger calls to getMemoryAccess() to also update the
|
|
* memory's access functions. However, if the memory access setting (nAccess) is about to change as well, those
|
|
* changes will be moot until the setAccess() call that follows. Basically, whenever both memory mapping AND access
|
|
* functions are changing, the memory will be in an inconsistent state until both setMode() and setAccess() are
|
|
* finished.
|
|
*
|
|
* The setMode() call takes precedence; if we called setAccess() first, it might attempt to modify memory access
|
|
* functions based on the card's addrBuffer setting, and if that doesn't match what's currently mapped, assertions
|
|
* will be triggered (probably not fatal, but it would defeat the point of the assertions).
|
|
*/
|
|
if (!this.setMode(nMode, fForce)) return false;
|
|
|
|
this.setAccess(nAccess);
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* 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().
|
|
*
|
|
* @this {Video}
|
|
* @param {number|null} nMode
|
|
* @param {boolean|undefined} [fForce] is set when checkMode() wants to force a mode update
|
|
* @return {boolean} true if successful, false if failure
|
|
*/
|
|
Video.prototype.setMode = function(nMode, fForce)
|
|
{
|
|
if (nMode != null && (nMode != this.nMode || fForce)) {
|
|
|
|
if (DEBUG) this.messageDebugger("setMode(0x" + str.toHexWord(nMode) + (fForce? ",force" : "") + ")");
|
|
|
|
this.cUpdates = 0; // count updateScreen() calls as a means of driving blink updates
|
|
this.nMode = nMode;
|
|
|
|
/*
|
|
* On an EGA, it's CRITICAL that a reset() invalidate cardActive, to ensure that the code below
|
|
* releases the previous frame buffer and installs a new one, even if there was no change in the
|
|
* frame buffer address or size, because otherwise the Memory blocks installed at the frame buffer
|
|
* address may still be using blocks of the EGA's previous memory buffer.
|
|
*
|
|
* When the EGA is reinitialized, a new memory buffer (adwMemory) is allocated (see initEGA()), and
|
|
* this is where the mapping of that EGA memory buffer to the frame buffer occurs. Other cards
|
|
* (MDA or CGA) don't allocate/manage their own memory buffer, but even then, it's still a good idea
|
|
* to always force this operation (eg, in case a switch setting changed the active video card).
|
|
*/
|
|
var card = this.cardActive || (nMode == Video.MODES.MDA_80X25? this.cardMono : this.cardColor);
|
|
|
|
if (card != this.cardActive || card.addrBuffer != this.addrBuffer || card.sizeBuffer != this.sizeBuffer) {
|
|
|
|
this.removeCursor();
|
|
|
|
if (this.addrBuffer) {
|
|
|
|
if (DEBUG) this.messageDebugger("setMode(" + nMode + "): removing 0x" + str.toHex(this.sizeBuffer) + " bytes from 0x" + str.toHex(this.addrBuffer));
|
|
|
|
if (!this.bus.removeMemory(this.addrBuffer, this.sizeBuffer)) {
|
|
/*
|
|
* TODO: Force this failure case and see how well the Video component deals with it.
|
|
*/
|
|
return false;
|
|
}
|
|
if (this.cardActive) this.cardActive.fActive = false;
|
|
}
|
|
|
|
this.cardActive = card;
|
|
card.fActive = true;
|
|
|
|
this.addrBuffer = card.addrBuffer;
|
|
this.sizeBuffer = card.sizeBuffer;
|
|
|
|
if (DEBUG) this.messageDebugger("setMode(" + nMode + "): adding 0x" + str.toHex(this.sizeBuffer) + " bytes to 0x" + str.toHex(this.addrBuffer));
|
|
|
|
var controller = (card === this.cardEGA? card : null);
|
|
|
|
if (!this.bus.addMemory(card.addrBuffer, card.sizeBuffer, false, controller)) {
|
|
/*
|
|
* TODO: Force this failure case and see how well the Video component deals with it.
|
|
*/
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.setDimensions();
|
|
|
|
if (fForce !== false) {
|
|
this.updateScreen(true);
|
|
} else {
|
|
this.initCellCache(true);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* setPixel(imageData, x, y, rgb)
|
|
*
|
|
* Worker function used by createFontColor() and updateScreen() (graphics modes only).
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} imageData
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {Array.<number>} rgb is a 4-element array containing the red, green, blue and alpha values
|
|
*/
|
|
Video.prototype.setPixel = function(imageData, x, y, rgb)
|
|
{
|
|
var index = (x + y * imageData.width) * rgb.length;
|
|
imageData.data[index + 0] = rgb[0];
|
|
imageData.data[index + 1] = rgb[1];
|
|
imageData.data[index + 2] = rgb[2];
|
|
imageData.data[index + 3] = rgb[3];
|
|
};
|
|
|
|
/**
|
|
* initCellCache(fNew)
|
|
*
|
|
* Invalidates the contents of our internal cell cache.
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} fNew is true to reallocate/resize the cell cache; in any case, it's still reinitialized
|
|
*/
|
|
Video.prototype.initCellCache = function(fNew)
|
|
{
|
|
var nCells;
|
|
if (!fNew) {
|
|
if (this.aCellCache === undefined)
|
|
return;
|
|
nCells = this.aCellCache.length;
|
|
} else {
|
|
nCells = this.nCellCache;
|
|
if (this.aCellCache === undefined || this.aCellCache.length != nCells) {
|
|
this.aCellCache = new Array(nCells);
|
|
}
|
|
}
|
|
for (var iCell = 0; iCell < nCells; iCell++) {
|
|
this.aCellCache[iCell] = -1; // invalidate every cell of our internal cell cache (-1 is an invalid cell value)
|
|
}
|
|
this.cBlinkVisible = -1; // also invalidate the visible blinking character count, to force updateScreen() to recount
|
|
};
|
|
|
|
/**
|
|
* doBlink()
|
|
*
|
|
* This function is obsolete, now that the checkBlink() function is called on every updateScreen()
|
|
* and checkCursor() call. updateScreen() is driven by the CPU timer, so piggy-backing on that to
|
|
* drive blink updates seems preferable to having another active timer in the system.
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} [fStart]
|
|
*
|
|
Video.prototype.doBlink = function(fStart)
|
|
{
|
|
if (this.cBlinks >= 0) {
|
|
this.cBlinks++;
|
|
if (this.cBlinkVisible || this.iCellCursor >= 0) {
|
|
if (!fStart && !this.cpu.isRunning()) {
|
|
this.updateScreen();
|
|
}
|
|
setTimeout(function(video) { return function onBlinkTimeout() {video.doBlink();}; }(this), 266);
|
|
return;
|
|
}
|
|
this.cBlinks = -1;
|
|
}
|
|
},
|
|
*/
|
|
|
|
/**
|
|
* updateChar(col, row, data, context)
|
|
*
|
|
* Updates a particular character cell (row,col) in the associated window.
|
|
*
|
|
* The data parameter is the attribute byte from the display buffer (fgnd attribute in the low nibble,
|
|
* bgnd attribute in the high nibble), but updateScreen() supplements data with a couple internal attribute bits:
|
|
*
|
|
* ATTRS.DRAW_FGND: set for every cell whose fgnd element is currently on (ie, non-blinking, or whenever blink is on)
|
|
* ATTRS.DRAW_CURSOR: set only for the cell containing the cursor, if any
|
|
*
|
|
* To make a character blink, we alternately draw its cell with ATTRS.DRAW_FGND set, and then again with
|
|
* ATTRS.DRAW_FGND clear (meaning only the cell background is drawn).
|
|
*
|
|
* To make the cursor blink, we must alternately draw its entire cell with ATTRS.DRAW_CURSOR set, and then
|
|
* draw it again with ATTRS.DRAW_CURSOR clear.
|
|
*
|
|
* @this {Video}
|
|
* @param {number} col
|
|
* @param {number} row
|
|
* @param {number} data (if text mode, character code in low byte, attribute code in high byte)
|
|
* @param {Object} [context]
|
|
*/
|
|
Video.prototype.updateChar = function(col, row, data, context)
|
|
{
|
|
/*
|
|
* The caller MUST promise this.nFont is defined, and that the font in this.aFonts[this.nFont] has been loaded.
|
|
*/
|
|
var bChar = data & 0xff;
|
|
var bAttr = data >> 8;
|
|
var iFgnd = bAttr & 0xf;
|
|
var font = this.aFonts[this.nFont];
|
|
if (font.aColorMap) iFgnd = font.aColorMap[iFgnd];
|
|
|
|
/*
|
|
* Just as aColorMap maps the foreground attribute to the appropriate foreground character grid,
|
|
* it also maps the background attribute to the appropriate background color.
|
|
*/
|
|
var xDst, yDst;
|
|
var iBgnd = (bAttr >> 4) & 0xf;
|
|
if (font.aColorMap) iBgnd = font.aColorMap[iBgnd];
|
|
|
|
if (context) {
|
|
xDst = col * font.cxCell;
|
|
yDst = row * font.cyCell;
|
|
context.fillStyle = font.aCSSColors[iBgnd];
|
|
context.fillRect(xDst, yDst, font.cxCell, font.cyCell);
|
|
} else {
|
|
xDst = col * this.cxScreenCell + this.xScreenOffset;
|
|
yDst = row * this.cyScreenCell + this.yScreenOffset;
|
|
this.contextScreen.fillStyle = font.aCSSColors[iBgnd];
|
|
this.contextScreen.fillRect(xDst, yDst, this.cxScreenCell, this.cyScreenCell);
|
|
}
|
|
|
|
if (MAXDEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled(Video.MESSAGE_VIDEO | Video.MESSAGE_LOG)) {
|
|
this.log("updateCharBgnd(" + col + "," + row + "," + bChar + "): filled " + xDst + "," + yDst);
|
|
}
|
|
|
|
if (bAttr & Video.ATTRS.DRAW_FGND) {
|
|
/*
|
|
* (bChar & 0xf) is the equivalent of (bChar % 16), and (bChar >> 4) is the equivalent of Math.floor(bChar / 16)
|
|
*/
|
|
var xSrcFgnd = (bChar & 0xf) * font.cxCell;
|
|
var ySrcFgnd = (bChar >> 4) * font.cyCell;
|
|
|
|
if (MAXDEBUG && DEBUGGER && this.dbg && this.dbg.messageEnabled(Video.MESSAGE_VIDEO | Video.MESSAGE_LOG)) {
|
|
this.log("updateCharFgnd(" + col + "," + row + "," + bChar + "): draw from " + xSrcFgnd + "," + ySrcFgnd + " (" + font.cxCell + "," + font.cyCell + ") to " + xDst + "," + yDst);
|
|
}
|
|
|
|
if (context) {
|
|
context.drawImage(font.aCanvas[iFgnd], xSrcFgnd, ySrcFgnd, font.cxCell, font.cyCell, xDst, yDst, font.cxCell, font.cyCell);
|
|
} else {
|
|
this.contextScreen.drawImage(font.aCanvas[iFgnd], xSrcFgnd, ySrcFgnd, font.cxCell, font.cyCell, xDst, yDst, this.cxScreenCell, this.cyScreenCell);
|
|
}
|
|
}
|
|
|
|
if (bAttr & Video.ATTRS.DRAW_CURSOR) {
|
|
/*
|
|
* Drawing the cursor with lineTo() seemed logical, but it was complicated by the fact that the
|
|
* TOP of the line must appear at "yDst + this.yCursor", whereas lineTo() wants to know the CENTER
|
|
* of the line. So it's simpler to draw the cursor with another fillRect(). Here's the old code:
|
|
*
|
|
* this.contextScreen.strokeStyle = font.aCSSColors[iFgnd];
|
|
* this.contextScreen.lineWidth = this.cyCursor;
|
|
* this.contextScreen.beginPath();
|
|
* this.contextScreen.moveTo(xDst, yDst + this.yCursor);
|
|
* this.contextScreen.lineTo(xDst + this.cxScreenCell, yDst + this.yCursor);
|
|
* this.contextScreen.stroke();
|
|
*
|
|
* Also, note that we're scaling the yCursor and cyCursor values here, instead of in checkCursor(), because
|
|
* this is where we have all the required information: in the first case (off-screen buffer), the scaling must
|
|
* be based on the font cell size (cxCell, cyCell), whereas in the second case (on-screen buffer), the scaling
|
|
* must be based on the screen cell size (cxScreenCell,cyScreenCell).
|
|
*
|
|
* yCursor and cyCursor are actual hardware values, both relative to another hardware value: cyCursorCell.
|
|
*/
|
|
var yCursor = this.yCursor;
|
|
var cyCursor = this.cyCursor;
|
|
if (context) {
|
|
if (this.cyCursorCell && this.cyCursorCell !== font.cyCell) {
|
|
yCursor = Math.floor((yCursor * font.cyCell) / this.cyCursorCell);
|
|
cyCursor = Math.floor((cyCursor * font.cyCell) / this.cyCursorCell);
|
|
}
|
|
context.fillStyle = font.aCSSColors[iFgnd];
|
|
context.fillRect(xDst, yDst + yCursor, font.cxCell, cyCursor);
|
|
} else {
|
|
if (this.cyCursorCell && this.cyCursorCell !== this.cyScreenCell) {
|
|
yCursor = Math.floor((yCursor * this.cyScreenCell) / this.cyCursorCell);
|
|
cyCursor = Math.floor((cyCursor * this.cyScreenCell) / this.cyCursorCell);
|
|
}
|
|
this.contextScreen.fillStyle = font.aCSSColors[iFgnd];
|
|
this.contextScreen.fillRect(xDst, yDst + yCursor, this.cxScreenCell, cyCursor);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* updateScreen(fForce)
|
|
*
|
|
* Propagates the video buffer to the cell cache and updates the window with any changes. Forced updates
|
|
* are generally internal updates triggered by an I/O operation or other state change, while non-forced updates
|
|
* are the periodic updates coming from the CPU.
|
|
*
|
|
* For every cell in the video buffer, compare it to the cell stored in the cell cache, render if it differs,
|
|
* and then update the cell cache to match. Since initCellCache() sets every cell in the cell cache to an
|
|
* invalid value, we're assured that the next call to updateScreen() will redraw the entire (visible) video buffer.
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} [fForce] is used by setMode() to reset the cell cache and force a redraw
|
|
*/
|
|
Video.prototype.updateScreen = function(fForce)
|
|
{
|
|
/*
|
|
* The Computer component maintains an fPowered setting on our behalf, so we use it.
|
|
*/
|
|
if (!this.fPowered) return;
|
|
|
|
/*
|
|
* If the card's video signal is disabled (eg, during a mode change), then skip the update,
|
|
* unless fForce is set.
|
|
*/
|
|
var fEnabled = false;
|
|
if (this.cardActive) {
|
|
if (this.cardActive === this.cardEGA) {
|
|
if (this.cardEGA.iATCReg & Card.ATC.INDX_PAL_ENABLE) fEnabled = true;
|
|
}
|
|
else {
|
|
if (this.cardActive.modeReg & Card.CGA.MODE.VIDEO_ENABLE) fEnabled = true;
|
|
}
|
|
}
|
|
|
|
if (!fEnabled && !fForce) return;
|
|
|
|
if (fForce) {
|
|
this.initCellCache(true);
|
|
}
|
|
else {
|
|
/*
|
|
* This should never happen, but since updateScreen() is also called by CPU.displayVideo(),
|
|
* better safe than sorry.
|
|
*/
|
|
if (this.aCellCache === undefined) return;
|
|
}
|
|
|
|
/*
|
|
* If cBlinks is "enabled" (ie, >= 0), then advance it once every 16 updateScreen() calls
|
|
* (assuming an updateScreen() frequency of 60 per second; see CPU.VIDEO_UPDATES_PER_SECOND).
|
|
*
|
|
* We assume that the CPU is calling us whenever fForce is undefined.
|
|
*/
|
|
var fBlinkUpdate = false;
|
|
if (!fForce && !(++this.cUpdates & 0xf) && this.cBlinks >= 0) {
|
|
this.cBlinks++;
|
|
fBlinkUpdate = true;
|
|
}
|
|
|
|
var iCell = 0;
|
|
var nCells = this.nCells;
|
|
|
|
/*
|
|
* Calculate the VISIBLE start of screen memory (addrScreen), not merely the PHYSICAL start,
|
|
* as well as the extent of it (cbScreen) and use those values for all addressing operations
|
|
* to follow. FYI, in these calculations, offScreen does not refer to "off-screen" memory,
|
|
* but rather the "offset" of the start of visible screen memory.
|
|
*/
|
|
var addrScreen = this.cardActive.addrBuffer;
|
|
var addrScreenLimit = addrScreen + this.cardActive.sizeBuffer;
|
|
var offScreen = (this.cardActive.aCRTCRegs[Card.CRTC.START_ADDR_HI] << 8) + this.cardActive.aCRTCRegs[Card.CRTC.START_ADDR_LO];
|
|
|
|
/*
|
|
* Any screen (aka "page") offset must be doubled for text modes, due to the attribute bytes.
|
|
*
|
|
* TODO: Come up with a more robust method of deciding when any screen offset should be doubled.
|
|
*/
|
|
if (this.nFont) offScreen <<= 1;
|
|
|
|
addrScreen += offScreen;
|
|
var cbScreen = this.cbScreen;
|
|
if (addrScreen + cbScreen > addrScreenLimit) {
|
|
cbScreen = addrScreenLimit - addrScreen;
|
|
if (cbScreen < 0) cbScreen = 0;
|
|
}
|
|
/*
|
|
* addrScreenLimit was initially the limit of the entire frame buffer, but we now adjust it
|
|
* to the limit of what's visible, since that's all we want to draw.
|
|
*/
|
|
addrScreenLimit = addrScreen + cbScreen;
|
|
|
|
/*
|
|
* This next bit of code can be completely disabled if we discover problems with the dirty
|
|
* memory block tracking feature, or if we need to remove or disable that feature in the future.
|
|
*
|
|
* We use cleanMemory() to check the video buffer's dirty state. If the buffer is clean
|
|
* AND there are no visible blinking characters (as of the last updateScreen) AND there is
|
|
* no visible cursor, then we're done; simply return. Otherwise, if there's only a blinking
|
|
* cursor, then update JUST that one cell.
|
|
*
|
|
* When dealing with blinking characters, note that we need to run through the entire buffer
|
|
* ONLY if the low bits of the blink count just transitioned to 2 or 0; hence, we could return if
|
|
* the blink count was ODD. But we'd still have to worry about the cursor, so it's simpler to blow
|
|
* that small optimization off. Further optimizations are certainly possible, such as a hash table
|
|
* of all blinking character locations, but all those optimizations are saved for a rainy day.
|
|
*/
|
|
if (!fForce && this.bus.cleanMemory(addrScreen, cbScreen)) {
|
|
if (!fBlinkUpdate) return;
|
|
if (!this.cBlinkVisible) {
|
|
if (this.iCellCursor < 0)
|
|
return;
|
|
iCell = this.iCellCursor;
|
|
nCells = iCell + 1;
|
|
}
|
|
// else if (this.cBlinks & 0x1) return;
|
|
}
|
|
|
|
if (this.nFont) {
|
|
/*
|
|
* This is the text-mode update case. We're required to FIRST verify that the current font
|
|
* has been successfully loaded, because we're not allowed to call updateChar() if there's no font.
|
|
*/
|
|
if (this.aFonts[this.nFont]) {
|
|
this.updateScreenText(addrScreen, addrScreenLimit, iCell, nCells);
|
|
this.checkBlink();
|
|
}
|
|
}
|
|
else if (this.cbSplit) {
|
|
this.updateScreenGraphicsCGA(addrScreen, addrScreenLimit);
|
|
}
|
|
else {
|
|
this.updateScreenGraphicsEGA(addrScreen, addrScreenLimit);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* updateScreenText(addrScreen, addrScreenLimit, iCell, nCells)
|
|
*
|
|
* @param addrScreen
|
|
* @param addrScreenLimit
|
|
* @param iCell
|
|
* @param nCells
|
|
*/
|
|
Video.prototype.updateScreenText = function(addrScreen, addrScreenLimit, iCell, nCells)
|
|
{
|
|
var addr, data, dataCache, cUpdated = 0;
|
|
|
|
/*
|
|
* If MDA.MODE.BLINK_ENABLE is set and a cell's blink bit is set, then if (cBlinks & 0x2) != 0,
|
|
* we want the foreground element of the cell to be drawn; otherwise we don't. So every 16-bit
|
|
* data word we pull from the video buffer will be supplemented with our own special attribute bit
|
|
* (ATTRS.DRAW_FGND = 0x100) accordingly; and to simplify the drawing code, we will also mask the
|
|
* blink bit from the cell's attribute bits.
|
|
*
|
|
* If MDA.MODE.BLINK_ENABLE is clear, then we always set ATTRS.DRAW_FGND and never mask the blink
|
|
* bit in a cell's attributes bits, since it's actually an intensity bit in that case.
|
|
*/
|
|
this.cBlinkVisible = 0;
|
|
var dataBlink = 0;
|
|
var dataDraw = (Video.ATTRS.DRAW_FGND << 8);
|
|
var dataMask = 0xfffff;
|
|
if (this.cardActive.modeReg & Card.MDA.MODE.BLINK_ENABLE) {
|
|
dataBlink = (Video.ATTRS.BGND_BLINK << 8);
|
|
dataMask &= ~dataBlink;
|
|
if (!(this.cBlinks & 0x2)) dataMask &= ~dataDraw;
|
|
}
|
|
addr = addrScreen + (iCell << 1);
|
|
while (addr < addrScreenLimit && iCell < nCells) {
|
|
data = this.bus.getWordDirect(addr);
|
|
data |= dataDraw;
|
|
if (data & dataBlink) {
|
|
this.cBlinkVisible++;
|
|
data &= dataMask;
|
|
}
|
|
if (iCell == this.iCellCursor) {
|
|
data |= ((this.cBlinks & 0x1)? (Video.ATTRS.DRAW_CURSOR << 8) : 0);
|
|
}
|
|
Component.assert(iCell < this.aCellCache.length);
|
|
dataCache = this.aCellCache[iCell];
|
|
if (dataCache != data) {
|
|
var col = iCell % this.nCols;
|
|
var row = Math.floor(iCell / this.nCols);
|
|
this.updateChar(col, row, data, this.contextScreenBuffer);
|
|
this.aCellCache[iCell] = data;
|
|
cUpdated++;
|
|
}
|
|
addr += 2;
|
|
iCell++;
|
|
}
|
|
if (cUpdated && this.contextScreenBuffer) {
|
|
this.contextScreen.drawImage(this.canvasScreenBuffer, 0, 0, this.cxBuffer, this.cyBuffer, this.xScreenOffset, this.yScreenOffset, this.cxScreenOffset, this.cyScreenOffset);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* updateScreenGraphicsCGA(addrScreen, addrScreenLimit)
|
|
*
|
|
* @param addrScreen
|
|
* @param addrScreenLimit
|
|
*/
|
|
Video.prototype.updateScreenGraphicsCGA = function(addrScreen, addrScreenLimit)
|
|
{
|
|
var addr, data, dataCache;
|
|
|
|
/*
|
|
* This is the CGA graphics-mode update case, where cells are pixels spread across two halves of the buffer.
|
|
*/
|
|
addr = addrScreen;
|
|
this.cBlinkVisible = 0;
|
|
var iCell = 0, nPixelsPerCell = this.nCellsPerWord;
|
|
var wPixelMask = (nPixelsPerCell == 16? 0x10000 : 0x30000);
|
|
var nPixelShift = (nPixelsPerCell == 16? 1 : 2);
|
|
var aPixelColors = this.getCardColors(nPixelShift);
|
|
|
|
var x = 0, y = 0;
|
|
var xDirty = this.nCols, xMaxDirty = 0, yDirty = this.nRows, yMaxDirty = 0;
|
|
while (addr < addrScreenLimit) {
|
|
data = this.bus.getWordDirect(addr);
|
|
Component.assert(iCell < this.aCellCache.length);
|
|
dataCache = this.aCellCache[iCell];
|
|
if (dataCache === data) {
|
|
x += nPixelsPerCell;
|
|
} else {
|
|
this.aCellCache[iCell] = data;
|
|
var wPixels = (data >> 8) | ((data & 0xff) << 8);
|
|
var wMask = wPixelMask, nShift = 16;
|
|
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]);
|
|
}
|
|
if (x > xMaxDirty) xMaxDirty = x;
|
|
if (y < yDirty) yDirty = y;
|
|
if (y >= yMaxDirty) yMaxDirty = y + 1;
|
|
}
|
|
addr += 2;
|
|
iCell++;
|
|
if (x >= this.nCols) {
|
|
x = 0;
|
|
y += 2;
|
|
if (y > this.nRows)
|
|
break;
|
|
if (y == this.nRows) {
|
|
y = 1;
|
|
addr = addrScreen + this.cbSplit;
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* 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
|
|
* 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);
|
|
/*
|
|
* 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
|
|
* 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
|
|
* on every change.
|
|
*
|
|
var xScreen = ((xDirty * this.cxScreen) / this.nCols);
|
|
var yScreen = ((yDirty * this.cyScreen) / this.nRows);
|
|
var cxScreen = ((cxDirty * this.cxScreen) / this.nCols);
|
|
var cyScreen = ((cyDirty * this.cyScreen) / this.nRows);
|
|
this.contextScreen.drawImage(this.canvasScreenBuffer, 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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* updateScreenGraphicsEGA(addrScreen, addrScreenLimit)
|
|
*
|
|
* @param addrScreen
|
|
* @param addrScreenLimit
|
|
*/
|
|
Video.prototype.updateScreenGraphicsEGA = function(addrScreen, addrScreenLimit)
|
|
{
|
|
var addr, data, dataCache;
|
|
|
|
addr = addrScreen;
|
|
this.cBlinkVisible = 0;
|
|
var iCell = 0, nPixelsPerCell = 8;
|
|
var aPixelColors = this.getCardColors();
|
|
var adwMemory = this.cardActive.adwMemory;
|
|
|
|
var x = 0, y = 0;
|
|
var xDirty = this.nCols, xMaxDirty = 0, yDirty = this.nRows, yMaxDirty = 0;
|
|
while (addr < addrScreenLimit) {
|
|
var idw = addr++ - this.addrBuffer;
|
|
Component.assert(idw >= 0 && idw < adwMemory.length);
|
|
data = adwMemory[idw];
|
|
Component.assert(iCell < this.aCellCache.length);
|
|
dataCache = this.aCellCache[iCell];
|
|
if (dataCache === data) {
|
|
x += nPixelsPerCell;
|
|
} else {
|
|
this.aCellCache[iCell] = data;
|
|
if (x < xDirty) xDirty = x;
|
|
for (var iPixel = 0; iPixel < nPixelsPerCell; iPixel++) {
|
|
var dwPixel = data & 0x80808080;
|
|
/*
|
|
* JavaScript Alert: if adwMemory contains a 32-bit value such as -1526726656, and then we mask it
|
|
* with 0x80808080, we end up with -2147483648, which in a perfect 32-bit world, would be equivalent
|
|
* to 0x80000000, which means that when we look up "Video.aEGADWToByte[0x80000000]", we should get
|
|
* the entry containing 0x8. But no, in JavaScript, since the original value was negative, the
|
|
* masked value is still negative, because there are 52 "significand" bits in JavaScript numbers,
|
|
* whereas bit-wise operations operate ONLY on the low 32 bits.
|
|
*
|
|
* This can be confirmed by looking at dwPixel.toString(16), which returns "-80000000". The solution
|
|
* is to add 4294967296 (0x100000000) to any negative 32-bit value for which you need the positive
|
|
* representation instead.
|
|
*/
|
|
if (dwPixel < 0) dwPixel += 0x100000000;
|
|
/*
|
|
* Since assertions don't fix problems (only catch them, and only in DEBUG builds), I'm also insuring
|
|
* that bPixel will always default to 0 if an undefined value ever slips through again.
|
|
*/
|
|
Component.assert(Video.aEGADWToByte[dwPixel] !== undefined);
|
|
var bPixel = Video.aEGADWToByte[dwPixel] || 0;
|
|
this.setPixel(this.imageScreenBuffer, x++, y, aPixelColors[bPixel]);
|
|
data <<= 1;
|
|
}
|
|
if (x > xMaxDirty) xMaxDirty = x;
|
|
if (y < yDirty) yDirty = y;
|
|
if (y >= yMaxDirty) yMaxDirty = y + 1;
|
|
}
|
|
iCell++;
|
|
if (x >= this.nCols) {
|
|
x = 0;
|
|
if (++y > this.nRows) break;
|
|
}
|
|
}
|
|
/*
|
|
* For a fascinating discussion of the best way to update the screen canvas at this point, see updateScreenGraphicsCGA().
|
|
*/
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inMDAIndx(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B4)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inMDAIndx = function(port, addrFrom)
|
|
{
|
|
return this.inCRTCIndx(this.cardMono, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outMDAIndx(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B4)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outMDAIndx = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCRTCIndx(this.cardMono, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inMDAData(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B5)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number|undefined}
|
|
*/
|
|
Video.prototype.inMDAData = function(port, addrFrom)
|
|
{
|
|
return this.inCRTCData(this.cardMono, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outMDAData(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B5)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outMDAData = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCRTCData(this.cardMono, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inMDAMode(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B8)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inMDAMode = function(port, addrFrom)
|
|
{
|
|
return this.inCardMode(this.cardMono, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outMDAMode(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3B8)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outMDAMode = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCardMode(this.cardMono, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inMDAStatus(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3BA)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inMDAStatus = function(port, addrFrom)
|
|
{
|
|
return this.inCardStatus(this.cardMono, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outFeat(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3BA or 0x3DA)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*
|
|
* NOTE: While this port also existed on the MDA and CGA, it existed only as an INPUT port, not an OUTPUT port.
|
|
*/
|
|
Video.prototype.outFeat = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.featReg = (this.cardEGA.featReg & ~Card.FEAT_CTRL.BITS) | (bOut & Card.FEAT_CTRL.BITS);
|
|
this.messagePort(port, bOut, addrFrom, "FEAT");
|
|
};
|
|
|
|
/**
|
|
* inATC(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C0)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inATC = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.fATCData? this.cardEGA.aATCRegs[this.cardEGA.iATCReg & Card.ATC.INDX_MASK] : this.cardEGA.iATCReg;
|
|
this.messagePort(Card.ATC.PORT, null, addrFrom, "ATC." + (this.cardEGA.fATCData? this.cardEGA.asATCRegs[this.cardEGA.iATCReg & Card.ATC.INDX_MASK] : "INDX"), b);
|
|
this.cardEGA.fATCData = !this.cardEGA.fATCData;
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outATC(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C0)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outATC = function(port, bOut, addrFrom)
|
|
{
|
|
var fPalEnabled = (this.cardEGA.iATCReg & Card.ATC.INDX_PAL_ENABLE);
|
|
if (!this.cardEGA.fATCData) {
|
|
this.cardEGA.iATCReg = bOut;
|
|
this.messagePort(port, bOut, addrFrom, "ATC.INDX");
|
|
this.cardEGA.fATCData = true;
|
|
if ((bOut & Card.ATC.INDX_PAL_ENABLE) && !fPalEnabled) {
|
|
if (!this.buildFonts()) {
|
|
if (DEBUG) this.messageDebugger("outATC(" + str.toHexByte(bOut) + "): no font changes required");
|
|
} else {
|
|
if (DEBUG) this.messageDebugger("outATC(" + str.toHexByte(bOut) + "): redraw screen for font changes");
|
|
this.updateScreen(true);
|
|
}
|
|
}
|
|
} else {
|
|
var iReg = this.cardEGA.iATCReg & Card.ATC.INDX_MASK;
|
|
if (iReg >= Card.ATC.PALETTE_REGS || !fPalEnabled) {
|
|
if (Video.TRAPALL || this.cardEGA.aATCRegs[iReg] !== bOut) {
|
|
this.messagePort(port, bOut, addrFrom, "ATC." + this.cardEGA.asATCRegs[iReg]);
|
|
this.cardEGA.aATCRegs[iReg] = bOut;
|
|
}
|
|
}
|
|
this.cardEGA.fATCData = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inStatus0(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C2)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inStatus0 = function(port, addrFrom)
|
|
{
|
|
var iBit = 3 - ((this.cardEGA.miscReg & Card.MISC.CLK_SELECT) >> 2); // this is the desired SW # (0-3)
|
|
var bSWBit = (this.bEGASW & (1 << iBit)) << (Card.STATUS0.SWSENSE_SHIFT - iBit);
|
|
var b = ((this.cardEGA.status0 & ~Card.STATUS0.SWSENSE) | bSWBit);
|
|
/*
|
|
* TODO: Figure out where Card.STATUS0.FEAT bits should come from....
|
|
*/
|
|
this.cardEGA.status0 = b;
|
|
this.messagePort(Card.STATUS0.PORT, null, addrFrom, "STATUS0", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* @this {Video}
|
|
* @param {number} port (0x3C2)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outMisc = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.miscReg = bOut;
|
|
this.enableEGA();
|
|
this.messagePort(Card.MISC.PORT, bOut, addrFrom, "MISC");
|
|
};
|
|
|
|
/**
|
|
* inSEQIndx(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C4)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inSEQIndx = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.iSEQReg;
|
|
this.messagePort(Card.SEQ.INDX.PORT, null, addrFrom, "SEQ.INDX", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outSEQIndx(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C4)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outSEQIndx = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.iSEQReg = bOut;
|
|
this.messagePort(Card.SEQ.INDX.PORT, bOut, addrFrom, "SEQ.INDX");
|
|
};
|
|
|
|
/**
|
|
* inSEQData(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C5)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inSEQData = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.aSEQRegs[this.cardEGA.iSEQReg];
|
|
this.messagePort(Card.SEQ.DATA.PORT, null, addrFrom, "SEQ" + this.cardEGA.asSEQRegs[this.cardEGA.iSEQReg], b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outSEQData(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3C5)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outSEQData = function(port, bOut, addrFrom)
|
|
{
|
|
if (Video.TRAPALL || this.cardEGA.aSEQRegs[this.cardEGA.iSEQReg] !== bOut) {
|
|
this.messagePort(Card.SEQ.DATA.PORT, bOut, addrFrom, "SEQ." + this.cardEGA.asSEQRegs[this.cardEGA.iSEQReg]);
|
|
this.cardEGA.aSEQRegs[this.cardEGA.iSEQReg] = bOut;
|
|
}
|
|
if (this.cardEGA.iSEQReg == Card.SEQ.MAPMASK.INDX) {
|
|
this.cardEGA.nWriteMapMask = Video.aEGAByteToDW[bOut & Card.SEQ.MAPMASK.MAPS];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inGRCPos1(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CC)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inGRCPos1 = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.iGRCPos1;
|
|
this.messagePort(Card.GRC.POS1_PORT, null, addrFrom, "GRC1", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outGRCPos1(port, bOut, addrFrom)
|
|
*
|
|
* "The EGA was originally implemented by IBM using two Graphics Controller Chips. It was necessary to program
|
|
* each to respond to a different set of two consecutive bits of the 8-bit host data bus. In the IBM EGA implementation,
|
|
* a 0 must be loaded into this register. In the VGA, there is no analogous register."
|
|
*
|
|
* "A zero should be loaded into this location to map host data bus bits 0 and 1 to display planes 0 and 1 respectively."
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CC)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outGRCPos1 = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.iGRCPos1 = bOut;
|
|
this.messagePort(Card.GRC.POS1_PORT, bOut, addrFrom, "GRC1");
|
|
};
|
|
|
|
/**
|
|
* inGRCPos2(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CA)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inGRCPos2 = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.iGRCPos2;
|
|
this.messagePort(Card.GRC.POS2_PORT, null, addrFrom, "GRC2", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outGRCPos2(port, bOut, addrFrom)
|
|
*
|
|
* "The EGA was originally implemented by IBM using two Graphics Controller Chips. This register is used to program
|
|
* the Graphics #2 chip. See the Graphics #1 Position Register for details."
|
|
*
|
|
* "A one should be loaded into this location to map host data bus bits 2 and 3 to display planes 2 and 3, respectively."
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CA)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outGRCPos2 = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.iGRCPos2 = bOut;
|
|
this.messagePort(Card.GRC.POS2_PORT, bOut, addrFrom, "GRC2");
|
|
};
|
|
|
|
/**
|
|
* inGRCIndx(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CE)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inGRCIndx = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.iGRCReg;
|
|
this.messagePort(Card.GRC.INDX.PORT, null, addrFrom, "GRC.INDX", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outGRCIndx(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CE)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outGRCIndx = function(port, bOut, addrFrom)
|
|
{
|
|
this.cardEGA.iGRCReg = bOut;
|
|
this.messagePort(Card.GRC.INDX.PORT, bOut, addrFrom, "GRC.INDX");
|
|
};
|
|
|
|
/**
|
|
* inGRCData(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CF)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inGRCData = function(port, addrFrom)
|
|
{
|
|
var b = this.cardEGA.aGRCRegs[this.cardEGA.iGRCReg];
|
|
this.messagePort(Card.GRC.DATA.PORT, null, addrFrom, "GRC." + this.cardEGA.asGRCRegs[this.cardEGA.iGRCReg], b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outGRCData(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3CF)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outGRCData = function(port, bOut, addrFrom)
|
|
{
|
|
if (Video.TRAPALL || this.cardEGA.aGRCRegs[this.cardEGA.iGRCReg] !== bOut) {
|
|
this.messagePort(Card.GRC.DATA.PORT, bOut, addrFrom, "GRC." + this.cardEGA.asGRCRegs[this.cardEGA.iGRCReg]);
|
|
this.cardEGA.aGRCRegs[this.cardEGA.iGRCReg] = bOut;
|
|
}
|
|
switch(this.cardEGA.iGRCReg) {
|
|
case Card.GRC.SRESET.INDX:
|
|
this.cardEGA.nSetMapData = Video.aEGAByteToDW[bOut & 0xf];
|
|
this.cardEGA.nSetMapBits = this.cardEGA.nSetMapData & ~this.cardEGA.nSetMapMask;
|
|
break;
|
|
case Card.GRC.ESRESET.INDX:
|
|
this.cardEGA.nSetMapMask = ~Video.aEGAByteToDW[bOut & 0xf];
|
|
this.cardEGA.nSetMapBits = this.cardEGA.nSetMapData & ~this.cardEGA.nSetMapMask;
|
|
break;
|
|
case Card.GRC.COLRCMP.INDX:
|
|
this.cardEGA.nColorCompare = Video.aEGAByteToDW[bOut & 0xf] & 0x80808080;
|
|
break;
|
|
case Card.GRC.DATAROT.INDX:
|
|
case Card.GRC.MODE.INDX:
|
|
this.setAccess(this.getAccess());
|
|
break;
|
|
case Card.GRC.READMAP.INDX:
|
|
this.cardEGA.nReadMapShift = (bOut & Card.GRC.READMAP.NUM) << 3;
|
|
break;
|
|
case Card.GRC.MISC.INDX:
|
|
this.checkMode(false);
|
|
break;
|
|
case Card.GRC.COLRDC.INDX:
|
|
this.cardEGA.nColorDontCare = Video.aEGAByteToDW[(bOut & 0xf) ^ 0xf] & 0x80808080;
|
|
break;
|
|
case Card.GRC.BITMASK.INDX:
|
|
this.cardEGA.nBitMapMask = bOut | (bOut << 8) | (bOut << 16) | (bOut << 24);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inCGAIndx(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D4)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCGAIndx = function(port, addrFrom)
|
|
{
|
|
return this.inCRTCIndx(this.cardColor, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outCGAIndx(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D4)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCGAIndx = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCRTCIndx(this.cardColor, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inCGAData(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D5)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number|undefined}
|
|
*/
|
|
Video.prototype.inCGAData = function(port, addrFrom)
|
|
{
|
|
return this.inCRTCData(this.cardColor, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outCGAData(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D5)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCGAData = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCRTCData(this.cardColor, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inCGAMode(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D8)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCGAMode = function(port, addrFrom)
|
|
{
|
|
return this.inCardMode(this.cardColor, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* outCGAMode(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D8)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCGAMode = function(port, bOut, addrFrom)
|
|
{
|
|
this.outCardMode(this.cardColor, bOut, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inCGAColor(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D9)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCGAColor = function(port, addrFrom)
|
|
{
|
|
var b = this.cardColor.colorReg;
|
|
this.messagePort(this.cardColor.port + 5, null, addrFrom, this.cardColor.type + ".COLOR", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outCGAColor(port, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3D9)
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCGAColor = function(port, bOut, addrFrom)
|
|
{
|
|
this.messagePort(this.cardColor.port + 5, bOut, addrFrom, this.cardColor.type + ".COLOR");
|
|
if (this.cardColor.colorReg !== bOut) {
|
|
this.cardColor.colorReg = bOut;
|
|
/*
|
|
* When this color register changes, it can automatically change the appearance of any number of cells, so we make
|
|
* a special call to initCellCache() to invalidate every cell, forcing all cells to be redrawn on the next updateScreen().
|
|
*/
|
|
this.initCellCache(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inCGAStatus(port, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port (0x3DA)
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCGAStatus = function(port, addrFrom)
|
|
{
|
|
return this.inCardStatus(this.cardColor, addrFrom);
|
|
};
|
|
|
|
/**
|
|
* inCRTCIndx(card, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCRTCIndx = function(card, addrFrom)
|
|
{
|
|
var b = card.iCRTCReg;
|
|
this.messagePort(card.port, null, addrFrom, "CRTC.INDX", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outCRTCIndx(card, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCRTCIndx = function(card, bOut, addrFrom)
|
|
{
|
|
card.iCRTCPrev = card.iCRTCReg;
|
|
card.iCRTCReg = bOut & Card.CGA.CRTC.INDX.MASK;
|
|
this.messagePort(card.port, bOut, addrFrom, "CRTC.INDX");
|
|
};
|
|
|
|
/**
|
|
* inCRTCData(card, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number|undefined}
|
|
*/
|
|
Video.prototype.inCRTCData = function(card, addrFrom)
|
|
{
|
|
var b;
|
|
if (card.iCRTCReg < card.nCRTCRegs) b = card.aCRTCRegs[card.iCRTCReg];
|
|
this.messagePort(card.port + 1, null, addrFrom, "CRTC." + card.asCRTCRegs[card.iCRTCReg], b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outCRTCData(card, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCRTCData = function(card, bOut, addrFrom)
|
|
{
|
|
if (card.iCRTCReg < card.nCRTCRegs) {
|
|
if (Video.TRAPALL || card.aCRTCRegs[card.iCRTCReg] !== bOut) {
|
|
this.messagePort(card.port + 1, bOut, addrFrom, "CRTC." + card.asCRTCRegs[card.iCRTCReg]);
|
|
card.aCRTCRegs[card.iCRTCReg] = bOut;
|
|
}
|
|
/*
|
|
* During mode changes on the EGA, all the CRTC regs are typically programmed in sequence,
|
|
* and if that's all that's happening with Card.CRTC.MAX_SCAN_LINE, then we don't want to treat
|
|
* it special; let the mode change be detected normally (eg, when the GRC regs are written later).
|
|
*
|
|
* On the other hand, if this was an out-of-sequence write to Card.CRTC.MAX_SCAN_LINE, then
|
|
* yes, we want to force setMode() to call setDimensions(), which is key to setting the proper
|
|
* number of screen rows.
|
|
*/
|
|
if (card.iCRTCReg == Card.CRTC.MAX_SCAN_LINE && card.iCRTCPrev != Card.CRTC.MAX_SCAN_LINE-1) {
|
|
this.checkMode(true);
|
|
}
|
|
this.checkCursor();
|
|
} else {
|
|
if (DEBUG) this.messageDebugger("outCRTCData(): ignoring unexpected write to CRTC[" + str.toHexByte(card.iCRTCReg) + "]: " + str.toHexByte(bOut));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* inCardMode(card, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCardMode = function(card, addrFrom)
|
|
{
|
|
var b = card.modeReg;
|
|
this.messagePort(card.port + 4, null, addrFrom, "MODE", b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* outCardMode(card, bOut, addrFrom)
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} bOut
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
*/
|
|
Video.prototype.outCardMode = function(card, bOut, addrFrom)
|
|
{
|
|
this.messagePort(card.port + 4, bOut, addrFrom, "MODE");
|
|
card.modeReg = bOut;
|
|
this.checkMode(false);
|
|
};
|
|
|
|
/**
|
|
* inCardStatus(card, addrFrom)
|
|
*
|
|
* On an EGA, this register is called "Status Register One" (0x3BA/0x3DA aka STATUS1), to distinguish it from
|
|
* "Status Register Zero" (0x3C2 aka STATUS0). One of the side-effects of reading STATUS1 is that it resets the
|
|
* ATC address/data flip-flop to "address" mode, which we emulate by setting cardEGA.fATCData to false, indicating
|
|
* that the ATC is not in "data" mode.
|
|
*
|
|
* @this {Video}
|
|
* @param {Object} card
|
|
* @param {number} [addrFrom] (not defined whenever the Debugger tries to read the specified port)
|
|
* @return {number}
|
|
*/
|
|
Video.prototype.inCardStatus = function(card, addrFrom)
|
|
{
|
|
var b = 0;
|
|
|
|
/*
|
|
* NOTE: The CGA bits CGA.STATUS.DISP_ENABLE (0x01) and CGA.STATUS.VERT_RETRACE (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.
|
|
*
|
|
* 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.
|
|
*/
|
|
var nCycles = this.cpu.getCycles();
|
|
var nElapsedCycles = nCycles - card.nInitCycles;
|
|
if (nElapsedCycles < 0) nElapsedCycles = 0; // TODO: Determine if this ever happens
|
|
var nCyclesHorzRemain = nElapsedCycles % card.nCyclesHorzPeriod;
|
|
if (nCyclesHorzRemain > card.nCyclesHorzActive) b |= Card.CGA.STATUS.DISP_ENABLE;
|
|
var nCyclesVertRemain = nElapsedCycles % card.nCyclesVertPeriod;
|
|
if (nCyclesVertRemain > card.nCyclesVertActive) b |= Card.CGA.STATUS.VERT_RETRACE;
|
|
/*
|
|
* This is optional: the number of CPU cycles that remain in the current vertical period is all we need to keep
|
|
* track of (the number of cycles since the card was initialized is fine, too, but that delta can become extremely
|
|
* large after a while).
|
|
*/
|
|
card.nInitCycles = nCycles - nCyclesVertRemain;
|
|
|
|
if (card === this.cardEGA) {
|
|
/*
|
|
* STATUS1 diagnostic bits 5 and 4 are set according to the Card.ATC.PLANES.MUX bits:
|
|
*
|
|
* MUX Bit 5 Bit 4
|
|
* --- ---- ----
|
|
* 00: Red Blue
|
|
* 01: SecBlue Green
|
|
* 10: SecRed SecGreen
|
|
* 11: unused unused
|
|
*
|
|
* Depending on where we are in the horizontal and vertical periods (which can be inferred from the
|
|
* same elapsed cycle count that we used to simulate the retrace bits above), we could extract 4 bits
|
|
* from a corresponding region of the video buffer, "and" them with Card.ATC.PLANES.MASK, use
|
|
* that to index into the palette registers (cardEGA.aATCRegs), and use the resulting palette register
|
|
* bits to set these diagnostics bits. However, that's all rather tedious, and the process of extracting
|
|
* 4 appropriate bits from the video buffer varies depending on the video mode.
|
|
*
|
|
* Why are we even considering this? Because the EGA BIOS diagnostic code draws a bright reverse-video
|
|
* line of text blocks across the top of the screen, writes 0x3F to palette register 0x0f, and then
|
|
* monitors the STATUS1 diagnostic bits, waiting for those palette bits to show up. It turns out, however,
|
|
* that we can easily fool the EGA BIOS by simply toggling the diagnostic bits. So we take the easy way out.
|
|
*
|
|
* TODO: Faithful emulation of these bits is certainly doable, so consider doing it at some point.
|
|
*/
|
|
b |= ((card.statusReg & Card.STATUS1.DIAGNOSTIC) ^ Card.STATUS1.DIAGNOSTIC);
|
|
|
|
/*
|
|
* Last but not least, we must reset the EGA's ATC flip-flop whenever this register is read.
|
|
*/
|
|
card.fATCData = false;
|
|
}
|
|
else {
|
|
/*
|
|
* On the MDA/CGA, to satisfy ROM BIOS testing ("TEST.10"), it's sufficient to do a simple toggle of
|
|
* bits 0 and 3 on every read.
|
|
*
|
|
* Also, according to http://www.seasip.info/VintagePC/mda.html, on an MDA, bits 7-4 are always ON and bits 2-1
|
|
* are always OFF, hence the "OR" of 0xf0.
|
|
*/
|
|
b = (card.statusReg ^= (Card.CGA.STATUS.DISP_ENABLE | Card.CGA.STATUS.VERT_RETRACE)) | 0xf0;
|
|
}
|
|
card.statusReg = b;
|
|
this.messagePort(card.port + 6, null, addrFrom, (card === this.cardEGA? "STATUS1" : "STATUS"), b);
|
|
return b;
|
|
};
|
|
|
|
/**
|
|
* dumpVideo(sParm)
|
|
*
|
|
* @this {Video}
|
|
* @param {string|undefined} sParm
|
|
*/
|
|
Video.prototype.dumpVideo = function(sParm)
|
|
{
|
|
if (DEBUGGER) {
|
|
if (!this.cardActive) {
|
|
this.dbg.message("no active video card");
|
|
return;
|
|
}
|
|
if (sParm) {
|
|
this.cardActive.dumpBuffer(sParm);
|
|
return;
|
|
}
|
|
this.dbg.message("BIOSMODE: " + str.toHexByte(this.nMode));
|
|
this.cardActive.dumpCard();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* messageDebugger(sMessage)
|
|
*
|
|
* This is a combination of the Debugger's messageEnabled(MESSAGE_VIDEO) and message() functions, for convenience.
|
|
*
|
|
* @this {Video}
|
|
* @param {boolean} [fForce] to display the message regardless of the MESSAGE_VIDEO setting, provided the Debugger is loaded
|
|
* @param {string} sMessage is any caller-defined message string
|
|
*/
|
|
Video.prototype.messageDebugger = function(sMessage, fForce)
|
|
{
|
|
if (DEBUGGER && this.dbg) {
|
|
if (fForce || this.dbg.messageEnabled(Video.MESSAGE_VIDEO)) {
|
|
this.dbg.message(sMessage);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* messagePort(port, bOut, addrFrom, name, bIn)
|
|
*
|
|
* This is an internal version of the Debugger's messagePort() function, for convenience.
|
|
*
|
|
* @this {Video}
|
|
* @param {number} port
|
|
* @param {number|null} bOut if an output operation
|
|
* @param {number|null} [addrFrom]
|
|
* @param {string|null} [name] of the port, if any
|
|
* @param {number} [bIn] is the input value, if known, on an input operation
|
|
*/
|
|
Video.prototype.messagePort = function(port, bOut, addrFrom, name, bIn)
|
|
{
|
|
if (DEBUGGER && this.dbg) {
|
|
this.dbg.messagePort(this, port, bOut, addrFrom, name, Video.MESSAGE_VIDEO, bIn);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Port input/output notification tables
|
|
*
|
|
* TODO: I added some "duplicate" entries for the MDA because, according to docs I'd read, MDA ports are
|
|
* decoded at multiple addresses. However, if this is important, then it should be verified and implemented
|
|
* consistently (eg, for CGA as well). For now, I'm decoding only the standard port addresses.
|
|
*/
|
|
Video.aPortInput = {
|
|
// 0x3B1: Video.prototype.inMDAData, // duplicate
|
|
// 0x3B3: Video.prototype.inMDAData, // duplicate
|
|
0x3B4: Video.prototype.inMDAIndx, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3B5: Video.prototype.inMDAData, // technically, the only Data registers that are readable are R14-R17
|
|
// 0x3B7: Video.prototype.inMDAData, // duplicate
|
|
0x3B8: Video.prototype.inMDAMode, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3BA: Video.prototype.inMDAStatus,
|
|
0x3D4: Video.prototype.inCGAIndx, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3D5: Video.prototype.inCGAData, // technically, the only Data registers that are readable are R14-R17
|
|
0x3D8: Video.prototype.inCGAMode, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3D9: Video.prototype.inCGAColor, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3DA: Video.prototype.inCGAStatus
|
|
};
|
|
|
|
Video.aPortOutput = {
|
|
// 0x3B0: Video.prototype.outMDAIndx, // duplicate
|
|
// 0x3B1: Video.prototype.outMDAData, // duplicate
|
|
// 0x3B2: Video.prototype.outMDAIndx, // duplicate
|
|
// 0x3B3: Video.prototype.outMDAData, // duplicate
|
|
0x3B4: Video.prototype.outMDAIndx, // 0x3B4 is decoded at 0x3B0, 0x3B2 and 0x3B6 as well (at least on an MDA), hence the duplicate mappings
|
|
0x3B5: Video.prototype.outMDAData, // 0x3B5 is decoded at 0x3B1, 0x3B3 and 0x3B7 as well (at least on an MDA), hence the duplicate mappings
|
|
// 0x3B6: Video.prototype.outMDAIndx, // duplicate
|
|
// 0x3B7: Video.prototype.outMDAData, // duplicate
|
|
0x3B8: Video.prototype.outMDAMode,
|
|
0x3D4: Video.prototype.outCGAIndx,
|
|
0x3D5: Video.prototype.outCGAData,
|
|
0x3D8: Video.prototype.outCGAMode,
|
|
0x3D9: Video.prototype.outCGAColor
|
|
};
|
|
|
|
Video.aEGAPortInput = {
|
|
0x3C0: Video.prototype.inATC, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3C1: Video.prototype.inATC, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3C2: Video.prototype.inStatus0,
|
|
0x3C4: Video.prototype.inSEQIndx, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3C5: Video.prototype.inSEQData, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3CA: Video.prototype.inGRCPos2, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3CC: Video.prototype.inGRCPos1, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3CE: Video.prototype.inGRCIndx, // technically, not actually readable, but I want the Debugger to be able to read this
|
|
0x3CF: Video.prototype.inGRCData // technically, not actually readable, but I want the Debugger to be able to read this
|
|
};
|
|
|
|
Video.aEGAPortOutput = {
|
|
0x3BA: Video.prototype.outFeat,
|
|
0x3C0: Video.prototype.outATC,
|
|
0x3C1: Video.prototype.outATC, // the EGA BIOS writes to this port (see C000:0416), implying that 0x3C0 and 0x3C1 both decode the same register
|
|
0x3C2: Video.prototype.outMisc, // FYI, since this overlaps with STATUS0.PORT, there's currently no way for the Debugger to read the Misc register
|
|
0x3C4: Video.prototype.outSEQIndx,
|
|
0x3C5: Video.prototype.outSEQData,
|
|
0x3CA: Video.prototype.outGRCPos2,
|
|
0x3CC: Video.prototype.outGRCPos1,
|
|
0x3CE: Video.prototype.outGRCIndx,
|
|
0x3CF: Video.prototype.outGRCData,
|
|
0x3DA: Video.prototype.outFeat
|
|
};
|
|
|
|
/**
|
|
* Video.init()
|
|
*
|
|
* This function operates on every element (e) of class "video", and initializes
|
|
* all the necessary HTML to construct every Video module as spec'ed.
|
|
*
|
|
* Note that each element (e) of class "video" is expected to have a "data-value"
|
|
* attribute containing the same JSON-encoded parameters that the Video constructor
|
|
* expects.
|
|
*/
|
|
Video.init = function()
|
|
{
|
|
var aeVideo = Component.getElementsByClass(window.document, PCJSCLASS, "video");
|
|
for (var iVideo = 0; iVideo < aeVideo.length; iVideo++) {
|
|
var eVideo = aeVideo[iVideo];
|
|
var parmsVideo = Component.getComponentParms(eVideo);
|
|
|
|
var eCanvas = window.document.createElement("canvas");
|
|
if (eCanvas === undefined) {
|
|
eVideo.innerHTML = "<br/>Missing <canvas> support; try a new web browser.";
|
|
return;
|
|
}
|
|
|
|
eCanvas.setAttribute("class", PCJSCLASS + "-canvas");
|
|
eCanvas.setAttribute("width", parmsVideo['screenWidth']);
|
|
eCanvas.setAttribute("height", parmsVideo['screenHeight']);
|
|
|
|
/*
|
|
* As noted in keyboard.js, the keyboard on an iOS device pops up with the SHIFT key depressed,
|
|
* which is not the initial keyboard state that the Keyboard component expects. I originally tried
|
|
* to fix that by adding an 'autocapitalize="off"' attribute alongside the 'contenteditable="true"'
|
|
* attribute on the <canvas> element, but apparently Safari honors that only inside certain elements
|
|
* (eg, <input>). However, I'm still optimistic that it'll be supported someday....
|
|
*/
|
|
eCanvas.setAttribute("contenteditable", "true");
|
|
eCanvas.setAttribute("autocapitalize", "off");
|
|
eCanvas.setAttribute("autocorrect", "off");
|
|
eCanvas.style.backgroundColor = parmsVideo['screenColor'];
|
|
|
|
/*
|
|
* HACK: A canvas style of "auto" provides for excellent responsive canvas scaling in EVERY browser
|
|
* except IE9/IE10, so I recalculate the appropriate CSS height every time the parent DIV is resized;
|
|
* IE11 works without this hack, so we take advantage of the fact that IE11 doesn't report itself as "MSIE".
|
|
*
|
|
* Also, make sure the parent DIV also has a style of "auto"; normally, it has no explicit height, but
|
|
* sometimes we'll preset it to a height (eg, "350px") for design purposes.
|
|
*/
|
|
eCanvas.style.height = eVideo.style.height = "auto";
|
|
|
|
if (web.getUserAgent().indexOf("MSIE") >= 0) {
|
|
eCanvas.style.height = (((eVideo.clientWidth * parmsVideo['screenHeight']) / parmsVideo['screenWidth']) | 0) + "px";
|
|
eVideo.onresize = function(eParent, eChild, cx, cy) {
|
|
return function onResizeVideo() {
|
|
eChild.style.height = (((eParent.clientWidth * cy) / cx) | 0) + "px";
|
|
};
|
|
}(eVideo, eCanvas, parmsVideo['screenWidth'], parmsVideo['screenHeight']); // jshint ignore:line
|
|
}
|
|
eVideo.appendChild(eCanvas);
|
|
|
|
/*
|
|
* HACK: Android-based browsers (eg, the Kindle Fire browser, the Chrome browser) don't honor the
|
|
* "contenteditable" attribute; that is, when the canvas receives focus, they don't activate the on-screen
|
|
* keyboard. So my fallback is to create a transparent textarea on top of the canvas.
|
|
*
|
|
* We depend upon the containing DIV (and/or its parent DIV) to have a style of "position:relative" (which
|
|
* all elements of class "pcjs-container" should have) so that we can position the textarea using absolute
|
|
* coordinates. Also, we don't want the textarea to be visible, but we must use "opacity:0" instead of
|
|
* "visibility:hidden", because the latter prevents the element from receiving events.
|
|
*
|
|
* UPDATE: Unfortunately, Android keyboards like to compose whole words before transmitting any of the
|
|
* intervening characters; our textarea's keyDown/keyUp event handlers DO receive intervening key events,
|
|
* but their keyCode and charCode properties are ZERO. Virtually the only usable key event we receive is
|
|
* the Enter key, which makes this hack useless. Android users will have to use machines that display
|
|
* their own on-screen keyboard, or use an external keyboard.
|
|
*
|
|
* See this Chromium issue for more information: https://code.google.com/p/chromium/issues/detail?id=118639
|
|
*
|
|
var eTextArea = window.document.createElement("textarea");
|
|
eTextArea.setAttribute("style", "position:absolute; left:0; top:0; width:100%; height:100%; opacity:0");
|
|
eVideo.appendChild(eTextArea);
|
|
*
|
|
* The following test failed as well. You can clearly see the overlaid semi-transparent password-enabled
|
|
* input field, but none of the input characters are passed along, with the exception of the "Go" (Enter) key.
|
|
*
|
|
var eInput = window.document.createElement("input");
|
|
eInput.setAttribute("type", "password");
|
|
eInput.setAttribute("style", "position:absolute; left:0; top:0; width:100%; height:100%; opacity:0.5");
|
|
eVideo.appendChild(eInput);
|
|
*/
|
|
|
|
/*
|
|
* Now we can create the Video object, record it, and wire it up to the associated document elements.
|
|
*/
|
|
var eContext = eCanvas.getContext("2d");
|
|
var video = new Video(parmsVideo, eCanvas, eContext /*, eTextArea || eInput */);
|
|
|
|
/*
|
|
* Bind any video-specific controls (eg, the Refresh button). There are no essential controls, however;
|
|
* even the "Refresh" button is just a diagnostic tool, to ensure that the screen contents are up-to-date.
|
|
*/
|
|
Component.bindComponentControls(video, eVideo, PCJSCLASS);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Initialize every Video module on the page.
|
|
*/
|
|
web.onInit(Video.init);
|
|
|
|
if (typeof APP_PCJS !== 'undefined') {APP_PCJS.Card = Card; APP_PCJS.Video = Video;}
|
|
|
|
if (typeof module !== 'undefined') module.exports = Video;
|