/**
* @fileoverview Implements the PCx86 Video component.
* @author Jeff Parsons
* @copyright © Jeff Parsons 2012-2017
*
* This file is part of PCjs, a computer emulation software project at .
*
* 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 .
*
* You are required to include the above copyright notice in every modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* .
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
if (NODE) {
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 State = require("../../shared/lib/state");
var PCX86 = require("./defines");
var Memory = require("./memory");
var Messages = require("./messages");
var ChipSet = require("./chipset");
var Keyboard = require("./keyboard");
var Mouse = require("./mouse");
}
/*
* MDA/CGA Support
*
* Since there's a lot of similarity between the MDA and CGA (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 indiscriminately
* initializes both, regardless which is actually installed), this same component emulates both
* devices.
*
* When no model is specified, this component supports the ability to dynamically switch between
* MDA and CGA emulation, by simply toggling the SW1 motherboard "monitor type" switch settings
* and resetting the machine. In that model-less configuration, we install I/O port handlers for
* both MDA and CGA cards, regardless which monitor type is initially selected.
*
* To simulate an IBM PC containing both an MDA and CGA (ie, a "dual display" system), the machine
* configuration simply defines two video components, one with model "mda" and the other with model
* "cga", resulting in two displays; setting a specific model forces each instance of this component
* to register only those I/O ports belonging to that model.
*
* In a single-display system, dynamically switching cards (ie, between MDA and CGA) creates some
* visual challenges. For one, the MDA prefers a native screen 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 screen, 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 screen 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 screen size. Pixels are captured
* in an off-screen buffer, which is then drawn to match the size of the virtual screen.
*
* 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 border 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.
*/
/*
* VGA Support
*
* More will be said here about PCjs VGA support later. But first, a word from IBM: "Video Graphics Array [VGA]
* Programming Considerations":
*
* Certain internal timings must be guaranteed by the user, in order to have the CRTC perform properly.
* This is due to the physical design of the chip. These timings can be guaranteed by ensuring that the
* rules listed below are followed when programming the CRTC.
*
* 1. The Horizontal Total [HTOTAL] register (R0) must be greater than or equal to a value of
* 25 decimal.
*
* 2. The minimum positive pulse width of the HSYNC output must be four character clock units.
*
* 3. Register R5, Horizontal Sync End [HREND], must be programmed such that the HSYNC
* output goes to a logic 0 a minimum of one character clock time before the 'horizontal display enable'
* signal goes to a logical 1.
*
* 4. Register R16, Vsync Start [VRSTART], must be a minimum of one horizontal scan line greater
* than register R18 [VDEND]. Register R18 defines where the 'vertical display enable' signal ends.
*
* When bit 5 of the Attribute Mode Control register equals 1, a successful line compare (see Line Compare
* [LINECOMP] register) in the CRT Controller forces the output of the PEL Panning register to 0's until Vsync
* occurs. When Vsync occurs, the output returns to the programmed value. This allows the portion of the screen
* indicated by the Line Compare register to be operated on by the PEL Panning register.
*
* A write to the Character Map Select register becomes valid on the next whole character line. No deformed
* characters are displayed by changing character generators in the middle of a character scan line.
*
* For 256-color 320 x 200 graphics mode hex 13, the attribute controller is configured so that the 8-bit attribute
* stored in video memory for each PEL becomes the 8-bit address (P0 - P7) into the integrated DAC. The user should
* not modify the contents of the internal Palette registers when using this mode.
*
* The following sequence should be followed when accessing any of the Attribute Data registers pointed to by the
* Attribute Index register:
*
* 1. Disable interrupts
* 2. Reset read/write flip/flop
* 3. Write to Index register
* 4. Read from or write to a data register
* 5. Enable interrupts
*
* The Color Select register in the Attribute Controller section may be used to rapidly switch between sets of colors
* in the video DAC. When bit 7 of the Attribute Mode Control register equals 0, the 8-bit color value presented to the
* video DAC is composed of 6 bits from the internal Palette registers and bits 2 and 3 from the Color Select register.
* When bit 7 of the Attribute Mode Control register equals 1, the 8-bit color value presented to the video DAC is
* composed of the lower four bits from the internal Palette registers and the four bits in the Color Select register.
* By changing the value in the Color Select register, software rapidly switches between sets of colors in the video DAC.
* Note that BIOS does not support multiple sets of colors in the video DAC. The user must load these colors if this
* function is to be used. Also see the Attribute Controller block diagram on page 4-26. Note that the above discussion
* applies to all modes except 256 Color Graphics mode. In this mode the Color Select register is not used to switch
* between sets of colors.
*
* An application that saves the "Video State" must store the 4 bytes of information contained in the system microprocessor
* latches in the graphics controller subsection. These latches are loaded with 32 bits from video memory (8 bits per map)
* each time the system microprocessor does a read from video memory. The application needs to:
*
* 1. Use write mode 1 to write the values in the latches to a location in video memory that is not part of
* the display buffer. The last location in the address range is a good choice.
*
* 2. Save the values of the latches by reading them back from video memory.
*
* Note: If in a chain 4 or odd/even mode, it will be necessary to reconfigure the memory organization as four
* sequential maps prior to performing the sequence above. BIOS provides support for completely saving and
* restoring video state. See the IBM Personal System/2 and Personal Computer BIOS Interface Technical Reference
* for more information.
*
* The description of the Horizontal PEL Panning register includes a figure showing the number of PELs shifted left
* for each valid value of the PEL Panning register and each valid video mode. Further panning beyond that shown in
* the figure may be accomplished by changing the start address in the CRT Controller registers, Start Address High
* and Start Address Low. The sequence involved in further panning would be as follows:
*
* 1. Use the PEL Panning register to shift the maximum number of bits to the left. See Figure 4-103 on page
* 4-106 for the appropriate values.
*
* 2. Increment the start address.
*
* 3. If you are not using Modes 0 + , 1 + , 2 + , 3 + ,7, or7 + , set the PEL Panning register to 0. If you
* are using these modes, set the PEL Panning register to 8. The screen will now be shifted one PEL left
* of the position it was in at the end of step 1. Step 1 through Step 3 may be repeated as desired.
*
* The Line Compare register (CRTC register hex 18) should be programmed with even values in 200 line modes when
* used in split screen applications that scroll a second screen on top of a first screen. This is a requirement
* imposed by the scan doubling logic in the CRTC.
*
* If the Cursor Start register (CRTC register hex 0A) is programmed with a value greater than that in the Cursor End
* register (CRTC register hex 0B), then no cursor is displayed. A split cursor is not possible.
*
* In 8-dot character modes, the underline attribute produces a solid line across adjacent characters, as in the IBM
* Color/Graphics Monitor Adapter, Monochrome Display Adapter and the Enhanced Graphics Adapter. In 9-dot modes, the
* underline across adjacent characters is dashed, as in the IBM 327X display terminals. In 9-dot modes, the line
* graphics characters (C0 - DF character codes) have solid underlines.
*
* For compatibility with the IBM Enhanced Graphics Adapter (EGA), the internal VGA palette is programmed the same
* as the EGA. The video DAC is programmed by BIOS so that the compatible values in the internal VGA palette produce
* a color compatible with what was produced by EGA. Mode hex 13 (256 colors) is programmed so that the first 16
* locations in the DAC produce compatible colors.
*
* Summing: When BIOS is used to load the video DAC palette for a color mode and a monochrome display is connected
* to the system unit, the color palette is changed. The colors are summed to produce shades of gray that allow
* color applications to produce a readable screen.
*
* There are 4 bits that should not be modified unless the sequencer is reset by setting bit 1 of the Reset register
* to 0. These bits are:
*
* • Bit 3, or bit 0 of the Clocking Mode register
* • Bit 3, or bit 2 of the Miscellaneous Output register
*
* Also, for quick reference, IBM VGA register values for the standard VGA modes (from http://www.pcjs.org/blog/2015/06/01/):
*
* INT 0x10 Mode Requested: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x0D 0x0E 0x10 0x12 0x13
*
* BIOSMODE: 0x01 0x01 0x03 0x03 0x04 0x04 0x06 0x0D 0x0E 0x10 0x12 0x13
* CRTC[0x00]: HTOTAL 0x2D 0x2D 0x5F 0x5F 0x2D 0x2D 0x5F 0x2D 0x5F 0x5F 0x5F 0x5F
* CRTC[0x01]: HDEND 0x27 0x27 0x4F 0x4F 0x27 0x27 0x4F 0x27 0x4F 0x4F 0x4F 0x4F
* CRTC[0x02]: HBSTART 0x28 0x28 0x50 0x50 0x28 0x28 0x50 0x28 0x50 0x50 0x50 0x50
* CRTC[0x03]: HBEND 0x90 0x90 0x82 0x82 0x90 0x90 0x82 0x90 0x82 0x82 0x82 0x82
* CRTC[0x04]: HRSTART 0x2B 0x2B 0x55 0x55 0x2B 0x2B 0x54 0x2B 0x54 0x54 0x54 0x54
* CRTC[0x05]: HREND 0xA0 0xA0 0x81 0x81 0x80 0x80 0x80 0x80 0x80 0x80 0x80 0x80
* CRTC[0x06]: VTOTAL 0xBF 0xBF 0xBF 0xBF 0xBF 0xBF 0xBF 0xBF 0xBF 0xBF 0x0B 0xBF
* CRTC[0x07]: OVERFLOW 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x1F 0x3E 0x1F
* CRTC[0x08]: PRESCAN 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* CRTC[0x09]: MAXSCAN 0x4F 0x4F 0x4F 0x4F 0xC1 0xC1 0xC1 0xC0 0xC0 0x40 0x40 0x41
* CRTC[0x0A]: CURSTART 0x0D 0x0D 0x0D 0x0D 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* CRTC[0x0B]: CUREND 0x0E 0x0E 0x0E 0x0E 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* CRTC[0x0C]: STARTHIGH 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* CRTC[0x0D]: STARTLOW 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* CRTC[0x0E]: CURHIGH 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x00
* CRTC[0x0F]: CURLOW 0x19 0x19 0x41 0x41 0x19 0x19 0x41 0x19 0x41 0x41 0xE1 0xA2
* CRTC[0x10]: VRSTART 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x83 0xEA 0x9C
* CRTC[0x11]: VREND 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x85 0x8C 0x8E
* CRTC[0x12]: VDEND 0x8F 0x8F 0x8F 0x8F 0x8F 0x8F 0x8F 0x8F 0x8F 0x5D 0xDF 0x8F
* CRTC[0x13]: OFFSET 0x14 0x14 0x28 0x28 0x14 0x14 0x28 0x14 0x28 0x28 0x28 0x28
* CRTC[0x14]: UNDERLINE 0x1F 0x1F 0x1F 0x1F 0x00 0x00 0x00 0x00 0x00 0x0F 0x00 0x40
* CRTC[0x15]: VBSTART 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x63 0xE7 0x96
* CRTC[0x16]: VBEND 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xBA 0x04 0xB9
* CRTC[0x17]: MODECTRL 0xA3 0xA3 0xA3 0xA3 0xA2 0xA2 0xC2 0xE3 0xE3 0xE3 0xE3 0xA3
* CRTC[0x18]: LINECOMP 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
* GRC[0x00]: SRESET 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* GRC[0x01]: ESRESET 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* GRC[0x02]: COLORCMP 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* GRC[0x03]: DATAROT 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* GRC[0x04]: READMAP 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* GRC[0x05]: MODE 0x10 0x10 0x10 0x10 0x30 0x30 0x00 0x00 0x00 0x00 0x00 0x40
* GRC[0x06]: MISC 0x0E 0x0E 0x0E 0x0E 0x0F 0x0F 0x0D 0x05 0x05 0x05 0x05 0x05
* GRC[0x07]: COLORDC 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0F 0x0F 0x0F 0x0F 0x0F
* GRC[0x08]: BITMASK 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
* SEQ[0x00]: RESET 0x03 0x03 0x03 0x03 0x03 0x03 0x03 0x03 0x03 0x03 0x03 0x03
* SEQ[0x01]: CLOCKING 0x08 0x08 0x00 0x00 0x09 0x09 0x01 0x09 0x01 0x01 0x01 0x01
* SEQ[0x02]: MAPMASK 0x03 0x03 0x03 0x03 0x03 0x03 0x01 0x0F 0x0F 0x0F 0x0F 0x0F
* SEQ[0x03]: CHARMAP 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* SEQ[0x04]: MEMMODE 0x03 0x03 0x03 0x03 0x02 0x02 0x06 0x06 0x06 0x06 0x06 0x0E
* ATC[0x00]: PAL00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* ATC[0x01]: PAL01 0x01 0x01 0x01 0x01 0x13 0x13 0x17 0x01 0x01 0x01 0x01 0x01
* ATC[0x02]: PAL02 0x02 0x02 0x02 0x02 0x15 0x15 0x17 0x02 0x02 0x02 0x02 0x02
* ATC[0x03]: PAL03 0x03 0x03 0x03 0x03 0x17 0x17 0x17 0x03 0x03 0x03 0x03 0x03
* ATC[0x04]: PAL04 0x04 0x04 0x04 0x04 0x02 0x02 0x17 0x04 0x04 0x04 0x04 0x04
* ATC[0x05]: PAL05 0x05 0x05 0x05 0x05 0x04 0x04 0x17 0x05 0x05 0x05 0x05 0x05
* ATC[0x06]: PAL06 0x14 0x14 0x14 0x14 0x06 0x06 0x17 0x06 0x06 0x14 0x14 0x06
* ATC[0x07]: PAL07 0x07 0x07 0x07 0x07 0x07 0x07 0x17 0x07 0x07 0x07 0x07 0x07
* ATC[0x08]: PAL08 0x38 0x38 0x38 0x38 0x10 0x10 0x17 0x10 0x10 0x38 0x38 0x08
* ATC[0x09]: PAL09 0x39 0x39 0x39 0x39 0x11 0x11 0x17 0x11 0x11 0x39 0x39 0x09
* ATC[0x0A]: PAL0A 0x3A 0x3A 0x3A 0x3A 0x12 0x12 0x17 0x12 0x12 0x3A 0x3A 0x0A
* ATC[0x0B]: PAL0B 0x3B 0x3B 0x3B 0x3B 0x13 0x13 0x17 0x13 0x13 0x3B 0x3B 0x0B
* ATC[0x0C]: PAL0C 0x3C 0x3C 0x3C 0x3C 0x14 0x14 0x17 0x14 0x14 0x3C 0x3C 0x0C
* ATC[0x0D]: PAL0D 0x3D 0x3D 0x3D 0x3D 0x15 0x15 0x17 0x15 0x15 0x3D 0x3D 0x0D
* ATC[0x0E]: PAL0E 0x3E 0x3E 0x3E 0x3E 0x16 0x16 0x17 0x16 0x16 0x3E 0x3E 0x0E
* ATC[0x0F]: PAL0F 0x3F 0x3F 0x3F 0x3F 0x17 0x17 0x17 0x17 0x17 0x3F 0x3F 0x0F
* ATC[0x10]: MODE 0x0C 0x0C 0x0C 0x0C 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x41
* ATC[0x11]: OVERSCAN 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
* ATC[0x12]: PLANES 0x0F 0x0F 0x0F 0x0F 0x03 0x03 0x01 0x0F 0x0F 0x0F 0x0F 0x0F
* ATC[0x13]: HPAN 0x08 0x08 0x08 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
*
* TODO: Build a similar table for the IBM EGA, and then work on rationalizing the mode detection logic in checkMode().
*/
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Card {
/**
* Card(video, nCard, data, cbMemory)
*
* Creates an object representing an initial video card state;
* can also restore a video card from state data created by saveCard().
*
* WARNING: Since Card objects are low-level objects that have no UI requirements,
* they do not inherit from the Component class, so you should only use class methods
* of Component, such as Component.assert(), or methods of the parent (video) object.
*
* @this {Card}
* @param {Video} [video]
* @param {number} [nCard] (see Video.CARD.*)
* @param {Array|null} [data]
* @param {number} [cbMemory] is specified if the card must allocate its own memory buffer
*/
constructor(video, nCard, data, cbMemory)
{
/*
* If a card was originally not present (eg, EGA), then the state will be empty,
* so we need to detect that case and continue indicating that the card is not present.
*/
if (nCard !== undefined && (!data || data.length)) {
this.video = video;
var specs = Video.cardSpecs[nCard];
var nMonitorType = video.nMonitorType || specs[5];
if (!data || data.length < 6) {
data = [false, 0, null, null, 0, new Array(nCard < Video.CARD.EGA? Card.CRTC.TOTAL_REGS : Card.CRTC.EGA.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.nCard = nCard;
this.addrBuffer = specs[2]; // default (physical) video buffer address
this.sizeBuffer = specs[3]; // default video buffer length (this is the total size, not the current visible size; this.cbScreen is calculated on the fly to reflect the latter)
/*
* If no memory size is specified, then setMode() will use addMemory() to automatically add enough
* memory blocks to cover the video 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 video buffer as needed, as well as giving it total control over
* the underlying memory.
*/
this.cbMemory = cbMemory || specs[4];
/*
* All of our cardSpec video 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 video 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.regMode = 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.regColor = data[2]; // see CGA.COLOR.* (undefined on MDA)
this.regStatus = data[3]; // see MDA.STATUS.* or CGA.STATUS.*
this.regCRTIndx = data[4] & 0xff;
this.regCRTPrev = (data[4] >> 8) & 0xff;
this.regCRTData = data[5];
this.nCRTCRegs = Card.CRTC.TOTAL_REGS;
this.asCRTCRegs = DEBUGGER? Card.CRTC.REGS : [];
this.offStartAddr = this.regCRTData[Card.CRTC.STARTLOW] | (this.regCRTData[Card.CRTC.STARTHIGH] << 8);
this.addrMaskHigh = 0x3F; // card-specific mask for the high (bits 8 and up) of CRTC address registers
if (nCard >= Video.CARD.EGA) {
this.addrMaskHigh = 0xFF;
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 nCyclesDefault = video.cpu.getBaseCyclesPerSecond(); // eg, 4772727
this.nCyclesHorzPeriod = (nCyclesDefault / monitorSpecs.nHorzPeriodsPerSec)|0;
this.nCyclesHorzActive = (this.nCyclesHorzPeriod * monitorSpecs.percentHorzActive / 100)|0;
this.nCyclesVertPeriod = (this.nCyclesHorzPeriod * monitorSpecs.nHorzPeriodsPerFrame)|0;
this.nCyclesVertActive = (this.nCyclesVertPeriod * monitorSpecs.percentVertActive / 100)|0;
this.nInitCycles = (data[7] || 0);
}
}
/**
* 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 tell us to always use the '===' operator (eg, use
* 'aReg[i] === undefined' to determine if an element is initialized), but because of this JSON stupidity,
* that would require all such tests to become 'aReg[i] === undefined || aReg[i] === null'. I'm puzzled
* why the coercion of '==' is considered evil but JSON's coercion of undefined to null is perfectly fine.
*
* The simple solution is to change such comparisons to 'aReg[i] == null', because undefined is coerced
* to null, whereas numeric values are 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 it's completely unnecessary for JSON.parse() to refuse to parse objects
* that are perfectly valid.]
*
* @this {Card}
* @param {Array|undefined} data
* @param {number} nMonitorType
*/
initEGA(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 | Card.ACCESS.V2,
/*16*/ 0,
/*17*/ 0xffffffff|0,
/*18*/ 0,
/*19*/ 0xffffffff|0,
/*20*/ 0,
/*21*/ 0xffffffff|0,
/*22*/ 0,
/*23*/ 0,
/*24*/ 0,
/*25*/ 0,
/*26*/ Card.VGA_ENABLE.ENABLED,
/*27*/ Card.DAC.MASK.DEFAULT,
/*28*/ 0,
/*29*/ 0,
/*30*/ Card.DAC.STATE.MODE_WRITE,
/*31*/ new Array(Card.DAC.TOTAL_REGS)
];
}
this.fATCData = data[0];
this.regATCIndx = data[1];
this.regATCData = data[2];
this.asATCRegs = DEBUGGER? Card.ATC.REGS : [];
this.regStatus0 = data[3]; // aka STATUS0 (not to be confused with this.regStatus, which the EGA refers to as STATUS1)
this.regMisc = data[4];
this.regFeat = data[5]; // for feature control bits, see Card.FEAT_CTRL.BITS; for feature status bits, see Card.STATUS0.FEAT
this.regSEQIndx = data[6];
this.regSEQData = data[7];
this.asSEQRegs = DEBUGGER? Card.SEQ.REGS : [];
this.regGRCPos1 = data[8];
this.regGRCPos2 = data[9];
this.regGRCIndx = data[10];
this.regGRCData = data[11];
this.asGRCRegs = DEBUGGER? Card.GRC.REGS : [];
this.latches = data[12];
/*
* Since we originally neglected to save/restore the card's active video 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 video 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];
this.video.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);
}
var nAccess = data[15];
if (nAccess) {
if (nAccess & Card.ACCESS.V2) {
nAccess &= ~Card.ACCESS.V2;
} else {
this.video.assert(Card.ACCESS.V1[nAccess & 0xff00] !== undefined && Card.ACCESS.V1[nAccess & 0xff] !== undefined);
nAccess = Card.ACCESS.V1[nAccess & 0xff00] | Card.ACCESS.V1[nAccess & 0xff];
}
}
this.setMemoryAccess(nAccess);
/*
* 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, nSeqMapMask 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.nSeqMapMask = 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];
this.offStartAddr = data[25]; // this is the last CRTC start address latched from CRTC.STARTHIGH,CRTC.STARTLOW
this.nVertPeriods = this.nVertPeriodsStartAddr = 0;
if (this.nCard == Video.CARD.VGA) {
this.regVGAEnable = data[26];
this.regDACMask = data[27];
this.regDACAddr = data[28];
this.regDACShift = data[29];
this.regDACState = data[30];
this.regDACData = data[31];
}
}
/**
* saveCard()
*
* @this {Card}
* @return {Array}
*/
saveCard()
{
var data = [];
if (this.nCard !== undefined) {
data[0] = this.fActive;
data[1] = this.regMode;
data[2] = this.regColor;
data[3] = this.regStatus;
data[4] = this.regCRTIndx | (this.regCRTPrev << 8);
data[5] = this.regCRTData;
if (this.nCard >= Video.CARD.EGA) {
data[6] = this.saveEGA();
}
data[7] = this.nInitCycles;
}
return data;
}
/**
* saveEGA()
*
* @this {Card}
* @return {Array}
*/
saveEGA()
{
var data = [];
data[0] = this.fATCData;
data[1] = this.regATCIndx;
data[2] = this.regATCData;
data[3] = this.regStatus0;
data[4] = this.regMisc;
data[5] = this.regFeat;
data[6] = this.regSEQIndx;
data[7] = this.regSEQData;
data[8] = this.regGRCPos1;
data[9] = this.regGRCPos2;
data[10] = this.regGRCIndx;
data[11] = this.regGRCData;
data[12] = this.latches;
data[13] = [this.addrBuffer, this.sizeBuffer, this.cbMemory];
data[14] = State.compressEvenOdd(this.adwMemory);
data[15] = this.nAccess | Card.ACCESS.V2;
data[16] = this.nReadMapShift;
data[17] = this.nSeqMapMask;
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;
data[25] = this.offStartAddr;
if (this.nCard == Video.CARD.VGA) {
data[26] = this.regVGAEnable;
data[27] = this.regDACMask;
data[28] = this.regDACAddr;
data[29] = this.regDACShift;
data[30] = this.regDACState;
data[31] = this.regDACData;
}
return data;
}
/**
* 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]
*/
dumpRegs(sName, iReg, aRegs, asRegs)
{
if (DEBUGGER) {
if (!aRegs) {
this.dbg.println(sName + ": " + Str.toHex(iReg, 2));
return;
}
var i, cchMax = 18, s = "";
for (i = 0; i < asRegs.length; i++) {
var reg = (aRegs === this.regCRTData)? this.getCRTCReg(i) : aRegs[i];
if (s) s += '\n';
s += sName + "[" + Str.toHex(i, 2) + "]: " + Str.pad(asRegs[i], cchMax) + (i === iReg? '*' : ' ') + Str.toHex(reg, reg > 0xff? 4 : 2);
if (reg != null) s += " (" + reg + ".)"
}
this.dbg.println(s);
}
}
/**
* dumpVideoCard()
*
* @this {Card}
*/
dumpVideoCard()
{
if (DEBUGGER) {
/*
* Start with registers that are common to all cards....
*/
this.dumpRegs("CRTC", this.regCRTIndx, this.regCRTData, this.asCRTCRegs);
if (this.nCard >= Video.CARD.EGA) {
this.dumpRegs(" GRC", this.regGRCIndx, this.regGRCData, this.asGRCRegs);
this.dumpRegs(" SEQ", this.regSEQIndx, this.regSEQData, this.asSEQRegs);
this.dumpRegs(" ATC", this.regATCIndx, this.regATCData, this.asATCRegs);
this.dumpRegs(" ATCINDX", this.regATCIndx);
this.dbg.println(" ATCDATA: " + this.fATCData);
this.dumpRegs(" FEAT", this.regFeat);
this.dumpRegs(" MISC", this.regMisc);
this.dumpRegs(" STATUS0", this.regStatus0);
/*
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
*/
}
/*
* TODO: This simply dumps the last value read from the STATUS1 register, not necessarily
* its current state; consider dumping getRetraceBits() instead of (or in addition to) this.
*/
this.dumpRegs(" STATUS1", this.regStatus);
if (this.nCard == Video.CARD.MDA || this.nCard == Video.CARD.CGA) {
this.dumpRegs(" MODEREG", this.regMode);
}
if (this.nCard == Video.CARD.CGA) {
this.dumpRegs(" COLOR", this.regColor);
}
if (this.nCard >= Video.CARD.EGA) {
this.dbg.println(" LATCHES: " + Str.toHex(this.latches));
this.dbg.println(" ACCESS: " + Str.toHex(this.nAccess, 4));
this.dbg.println("Use 'dump video [addr]' to dump video memory");
/*
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
*/
}
}
}
/**
* dumpVideoBuffer(asArgs)
*
* Rather than requiring the first parameter to ALWAYS be a frame buffer address OR a frame buffer
* offset, we'll just make a guess as to what the user intended and support BOTH; basically, if the
* value is less than the frame buffer address, we'll assume it's an offset.
*
* Also, we allow some special options to be encoded in asArgs: 'l' followed by a number means
* print that many rows of data. 'n' followed by a number (1-8) means print only that number of
* memory locations per row, and then adjust the starting address of the next row by the number
* of bytes per row (or whatever is specified by the 'w' option) so that the dump reflects a
* rectangular chunk of video data. Finally, if asArgs contains 'p' followed by a number (0-3),
* we display only the bits from that plane for each memory location, in binary instead of hex.
*
* For example, assuming a standard VGA frame buffer with 640x480 pixels across 38400 (0x9600) memory
* locations, the following command will dump a vertical swath of bits from plane 0 that is 32 (0x20)
* rows tall and 8 columns wide, from roughly the center of the screen (0x4B00 + 0x28 - 2 = 0x4B26).
*
* d video 4b26 l20 n8 p0
*
* Subsequent commands that omit a starting address or offset will continue where the last dump
* left off; eg:
*
* d video n8 p0
*
* To dump a chunk of off-screen memory starting at 0x9600, where the Windows VGA driver typically
* stores a copy of the video memory containing the current mouse pointer:
*
* d video 9600 l20 n5 w5 p0
*
* Alternatively, you could use decimal values:
*
* d video 9600 l32. n5. w5. p0.
*
* NOTE: If these commands look suspiciously like weird Hayes modem command strings, trust me,
* that is ENTIRELY coincidental (but mildly amusing).
*
* TODO: Make these options more general-purpose (it currently assumes a conventional VGA planar layout).
*
* @this {Card}
* @param {Array.} asArgs (all numeric arguments default to base 16 unless otherwise specified)
*/
dumpVideoBuffer(asArgs)
{
if (DEBUGGER) {
if (!this.adwMemory) {
this.dbg.println("no buffer");
return;
}
var i, j, idw, fColAdjust = false;
var l = 8, n = 8, p = -1, w = this.video.nCols >> 3;
for (i = 0; i < asArgs.length; i++) {
var s = asArgs[i];
if (!i) {
idw = Str.parseInt(s, 16);
continue;
}
var ch = s.charAt(0);
j = Str.parseInt(s.substr(1), 16);
switch(ch) {
case 'l':
l = j;
break;
case 'n':
if (j >= 1 && j <= 8) {
n = j;
fColAdjust = true;
}
break;
case 'p':
if (j >= 0 && j <= 3) p = j;
break;
case 'w':
if (j < w) w = j;
break;
default:
this.dbg.println("unrecognized argument: " + s);
break;
}
}
if (idw === undefined) {
idw = this.prevDump || 0;
} else if (idw >= this.addrBuffer) {
idw -= this.addrBuffer;
}
var sDump = "";
for (i = 0; i < l; i++) {
var sData = Str.toHex(this.addrBuffer + idw) + ":";
for (j = 0; j < n && idw < this.adwMemory.length; j++) {
var dw = this.adwMemory[idw++];
sData += ' ' + ((p < 0)? Str.toHex(dw) : Str.toBin((dw >> (p << 3)), 8));
}
if (fColAdjust) idw += w - n;
if (sDump) sDump += "\n";
sDump += sData;
}
if (sDump) this.dbg.println(sDump);
this.prevDump = idw;
}
}
/**
* 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)
*/
getMemoryBuffer(addr)
{
return [this.adwMemory, addr - this.addrBuffer];
}
/**
* getMemoryAccess()
*
* WARNING: This is a public method, whereas most Card methods are private to the Video component;
* because a Card also acts as a Memory controller, it must provide getMemoryAccess() to the Memory component.
*
* Return the last set of memory access functions recorded by setMemoryAccess().
*
* @this {Card}
* @return {Array.}
*/
getMemoryAccess()
{
return this.afnAccess;
}
/**
* setMemoryAccess(nAccess)
*
* This transforms the memory access value that getCardAccess() 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
*/
setMemoryAccess(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.messageEnabled(Messages.VIDEO)) {
this.dbg.message("Card.setMemoryAccess(" + Str.toHexWord(nAccess) + "): missing readByte handler");
/*
* I've taken a look, and the cases I've seen so far stem from the order in which the IBM VGA BIOS
* reprograms registers during a mode change: it reprograms the Sequencer registers BEFORE the Graphics
* Controller registers, so if GRC.MODE was set to READ.MODE1 prior to the mode change and the new mode
* clears SEQ.MEMMODE.SEQUENTIAL, we will briefly be in an "odd" (unsupported) state.
*
* This didn't used to occur when we relied on the GRC.MODE register instead of the SEQ.MEMMODE for
* determining the EVENODD state. But, as explained in getCardAccess(), we've run into inconsistencies in
* how GRC.MODE.EVENODD is programmed, so we must live with this warning.
*
* The ultimate solution is to provide a EVENODD handler for READ.MODE1, since there is the remote
* possibility of third-party software that relies on that "odd" combination.
*
* this.dbg.stopCPU(); // let's take a look
*/
}
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.messageEnabled(Messages.VIDEO)) {
this.dbg.message("Card.setMemoryAccess(" + Str.toHexWord(nAccess) + "): missing writeByte handler");
/*
* I've taken a look, and the cases I've seen so far stem from the order in which the IBM VGA BIOS
* reprograms registers during a mode change: it reprograms the Sequencer registers BEFORE the Graphics
* Controller registers, so if GRC.MODE was set to WRITE.MODE2 prior to the mode change and the new mode
* clears SEQ.MEMMODE.SEQUENTIAL, we will briefly be in an "odd" (unsupported) state.
*
* This didn't used to occur when we relied on the GRC.MODE register instead of the SEQ.MEMMODE for
* determining the EVENODD state. But, as explained in getCardAccess(), we've run into inconsistencies in
* how GRC.MODE.EVENODD is programmed, so we must live with this warning.
*
* The ultimate solution is to provide EVENODD handlers for all modes other than WRITE.MODE0, since there
* is the remote possibility of third-party software that relies on one of those "odd" combinations.
*
* this.dbg.stopCPU(); // let's take a look
*/
}
if (nWriteAccess & Card.ACCESS.WRITE.EVENODD) {
fnWriteByte = Card.ACCESS.afn[Card.ACCESS.WRITE.EVENODD];
}
}
if (!this.afnAccess) this.afnAccess = new Array(6);
this.afnAccess[0] = fnReadByte;
this.afnAccess[1] = fnWriteByte;
this.nAccess = nAccess;
}
}
/**
* getCRTCReg()
*
* @this {Card}
* @param {number} iReg
* @return {number}
*/
getCRTCReg(iReg)
{
var reg = this.regCRTData[iReg];
if (reg != null && this.nCard >= Video.CARD.EGA) {
var bOverflowBit8 = 0, bOverflowBit9 = 0, bMaxScanBit9 = 0;
switch(iReg) {
case Card.CRTC.EGA.VTOTAL: // 0x06
bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT8; // 0x01
if (this.nCard == Video.CARD.VGA) bOverflowBit9 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT9;
break;
case Card.CRTC.EGA.CURSTART.INDX: // 0x0A
if (this.nCard == Video.CARD.EGA) bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.CURSTART_BIT8;
break;
case Card.CRTC.EGA.VRSTART: // 0x10
bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.VRSTART_BIT8; // 0x04
if (this.nCard == Video.CARD.VGA) bOverflowBit9 = Card.CRTC.EGA.OVERFLOW.VRSTART_BIT9;
break;
case Card.CRTC.EGA.VDEND: // 0x12
bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.VDEND_BIT8; // 0x02
if (this.nCard == Video.CARD.VGA) bOverflowBit9 = Card.CRTC.EGA.OVERFLOW.VDEND_BIT9;
break;
case Card.CRTC.EGA.VBSTART: // 0x15
bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.VBSTART_BIT8; // 0x08
if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAXSCAN.VBSTART_BIT9;
break;
case Card.CRTC.EGA.LINECOMP: // 0x18
bOverflowBit8 = Card.CRTC.EGA.OVERFLOW.LINECOMP_BIT8; // 0x10
if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAXSCAN.LINECOMP_BIT9;
break;
}
if (bOverflowBit8) {
reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOverflowBit8)? 0x100 : 0);
reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOverflowBit9)? 0x200 : 0);
reg |= ((this.regCRTData[Card.CRTC.EGA.MAXSCAN.INDX] & bMaxScanBit9)? 0x200 : 0);
}
}
return reg;
}
}
/*
* MDA Registers (ports 0x3B4, 0x3B5, 0x3B8, and 0x3BA)
*/
Card.MDA = {
CRTC: {
INDX: {
PORT: 0x3B4, // NOTE: the low byte of this port address (0xB4) is mirrored at 40:0063 (0x0463)
MASK: 0x1F
},
DATA: {
PORT: 0x3B5
}
},
MODE: {
PORT: 0x3B8, // Mode Select Register, aka CRT Control Port 1 (write-only); the BIOS mirrors this register at 40:0065 (0x0465)
HIRES: 0x01,
VIDEO_ENABLE: 0x08,
BLINK_ENABLE: 0x20
},
STATUS: {
PORT: 0x3BA,
HDRIVE: 0x01,
BWVIDEO: 0x08
},
/*
* TODO: Add support for parallel port(s) someday....
*/
PRT_DATA: {
PORT: 0x3BC
},
PRT_STATUS: {
PORT: 0x3BD
},
PRT_CTRL: {
PORT: 0x3BE
}
};
/*
* CGA Registers (ports 0x3D4, 0x3D5, 0x3D8, 0x3D9, and 0x3DA)
*/
Card.CGA = {
CRTC: {
INDX: {
PORT: 0x3D4, // NOTE: the low byte of this port address (0xB4) is mirrored at 40:0063 (0x0463)
MASK: 0x1F
},
DATA: {
PORT: 0x3D5
}
},
MODE: {
PORT: 0x3D8, // Mode Select Register (write-only); the BIOS mirrors this register at 40:0065 (0x0465)
_80X25: 0x01,
GRAPHIC_SEL: 0x02,
BW_SEL: 0x04,
VIDEO_ENABLE: 0x08, // same as MDA.MODE.VIDEO_ENABLE
HIRES_BW: 0x10,
BLINK_ENABLE: 0x20 // same as MDA.MODE.BLINK_ENABLE
},
COLOR: {
PORT: 0x3D9, // write-only
BORDER: 0x07,
BRIGHT: 0x08,
BGND_ALT: 0x10, // alternate, intensified background colors in text mode
COLORSET2: 0x20 // selects aCGAColorSet2 colors for 320x200 graphics mode; aCGAColorSet1 otherwise
},
STATUS: {
PORT: 0x3DA, // read-only; same for EGA (although the EGA calls this STATUS1, to distinguish it from STATUS0)
RETRACE: 0x01,
PEN_TRIGGER: 0x02,
PEN_ON: 0x04,
VRETRACE: 0x08 // when set, this indicates the CGA is performing a vertical retrace
},
/*
* TODO: Add support for light pen port(s) someday....
*/
CLEAR_PEN: {
PORT: 0x3DB
},
PRESET_PEN: {
PORT: 0x3DC
}
};
/*
* Common CRT hardware registers (ports 0x3B4/0x3B5 or 0x3D4/0x3D5)
*
* NOTE: In this implementation, because we have to make at least two of the registers readable (CURHIGH and CURLOW),
* 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 = {
HTOTAL: 0x00, // Horizontal Total
HDISP: 0x01, // Horizontal Displayed
HSPOS: 0x02, // Horizontal Sync Position
HSWIDTH: 0x03, // Horizontal Sync Width
VTOTAL: 0x04, // Vertical Total
VTOTADJ: 0x05, // Vertical Total Adjust
VDISP: 0x06, // Vertical Displayed
VSPOS: 0x07, // Vertical Sync Position
ILMODE: 0x08, // Interlace Mode
MAXSCAN: 0x09, // Max Scan Line Address
CURSTART: 0x0A, // Cursor Start
/*
* I don't entirely understand the 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.
*/
CURSTART_SLMASK: 0x1F, // Scan Line Mask
CURSTART_BLINKON: 0x00, // (supposedly, 0x04 has the same effect as 0x00)
CURSTART_BLINKOFF: 0x20, // if blinking is disabled, the cursor is effectively hidden
CURSTART_BLINKFAST: 0x60, // default is 1/16 of the frame rate; this switches to 1/32 of the frame rate
CUREND: 0x0B,
STARTHIGH: 0x0C,
STARTLOW: 0x0D,
CURHIGH: 0x0E,
CURLOW: 0x0F,
PENHIGH: 0x10,
PENLOW: 0x11,
TOTAL_REGS: 0x12, // total CRT registers on MDA/CGA
EGA: {
HDEND: 0x01,
HBSTART: 0x02,
HBEND: 0x03,
HRSTART: 0x04,
HREND: 0x05,
VTOTAL: 0x06,
OVERFLOW: {
INDX: 0x07,
VTOTAL_BIT8: 0x01, // bit 8 of register 0x06
VDEND_BIT8: 0x02, // bit 8 of register 0x12
VRSTART_BIT8: 0x04, // bit 8 of register 0x10
VBSTART_BIT8: 0x08, // bit 8 of register 0x15
LINECOMP_BIT8: 0x10, // bit 8 of register 0x18
CURSTART_BIT8: 0x20, // bit 8 of register 0x0A (EGA only)
VTOTAL_BIT9: 0x20, // bit 9 of register 0x06 (VGA only)
VDEND_BIT9: 0x40, // bit 9 of register 0x12 (VGA only, unused on EGA)
VRSTART_BIT9: 0x80 // bit 9 of register 0x10 (VGA only, unused on EGA)
},
PRESCAN: 0x08,
/*
* NOTE: EGA/VGA CRTC registers 0x09-0x0F are the same as the MDA/CGA CRTC registers defined above
*/
MAXSCAN: {
INDX: 0x09,
SLMASK: 0x1F, // Scan Line Mask
VBSTART_BIT9: 0x20, // (VGA only)
LINECOMP_BIT9: 0x40, // (VGA only)
CONVERT400: 0x80 // 200-to-400 scan-line conversion is in effect (VGA only)
},
CURSTART: {
INDX: 0x0A,
SLMASK: 0x1F,
BLINKON: 0x00, // (VGA only; supposedly, 0x04 has the same effect as 0x00)
BLINKOFF: 0x20, // if blinking is disabled, the cursor is effectively hidden (VGA only)
BLINKFAST: 0x60 // default is 1/16 of the frame rate; this switches to 1/32 of the frame rate (VGA only)
},
CUREND: 0x0B,
STARTHIGH: 0x0C,
STARTLOW: 0x0D,
CURHIGH: 0x0E,
CURLOW: 0x0F,
VRSTART: 0x10,
VREND: 0x11,
VDEND: 0x12,
/*
* The OFFSET register (bits 0-7) specifies the logical line width of the screen. The starting memory address
* for the next character row is larger than the current character row by two or four times this amount.
* The OFFSET register is programmed with a word address. Depending on the method of clocking the CRT Controller,
* this word address is [effectively] either a word or double-word address. #IBMVGATechRef
*/
OFFSET: 0x13,
UNDERLINE: {
INDX: 0x14,
ROWSCAN: 0x1F,
COUNT_BY_4: 0x20, // (VGA only)
DWORD: 0x40 // (VGA only)
},
VBSTART: 0x15,
VBEND: 0x16,
MODECTRL: {
INDX: 0x17,
COMPAT_MODE: 0x01, // Compatibility Mode Support (CGA A13 control)
SEL_ROW_SCAN: 0x02, // Select Row Scan Counter
SEL_HRETRACE: 0x04, // Horizontal Retrace Select
COUNT_BY_2: 0x08, // Count By Two
OUTPUT_CTRL: 0x10, // Output Control
ADDR_WRAP: 0x20, // Address Wrap (in Word mode, 1 maps A15 to A0 and 0 maps A13; use the latter when only 64Kb is installed)
BYTE_MODE: 0x40, // Byte Mode (1 selects Byte Mode; 0 selects Word Mode)
HARD_RESET: 0x80 // Hardware Reset
},
LINECOMP: 0x18,
TOTAL_REGS: 0x19 // total CRT registers on EGA/VGA
}
};
/*
* TODO: These mask tables need to be card-specific. For example, the STARTHIGH and CURHIGH registers used to be
* limited to 0x3F, because the MC6845 controller used with the original MDA and CGA cards was limited to 16Kb of RAM,
* whereas later cards like the EGA and VGA had anywhere from 64Kb to 256Kb, so all the bits of those registers were
* significant. Currently, I'm doing very little masking, which means most CRTC registers are treated as full 8-bit
* registers (and fully readable as well), which might cause some compatibility problems for any MDA/CGA apps that
* were sloppy about how they programmed registers.
*
* I do make an exception, however, in the case of STARTHIGH and CURHIGH, due to the way the MC6845 controller wraps
* addresses around to the beginning of the buffer, because that seems like a high-risk case. See the card-specific
* variable addrMaskHigh.
*/
Card.CRTCMASKS = {
[Card.CRTC.HTOTAL]: 0xFF, // R0
[Card.CRTC.HDISP]: 0xFF, // R1
[Card.CRTC.HSPOS]: 0xFF, // R2
[Card.CRTC.HSWIDTH]: 0x0F, // R3
[Card.CRTC.VTOTAL]: 0x7F, // R4
[Card.CRTC.VTOTADJ]: 0x1F, // R5
[Card.CRTC.VDISP]: 0x7F, // R6
[Card.CRTC.VSPOS]: 0x7F, // R7
[Card.CRTC.ILMODE]: 0x03, // R8
[Card.CRTC.MAXSCAN]: 0x1F, // R9
[Card.CRTC.CURSTART]: 0x7F, // R10
[Card.CRTC.CUREND]: 0x1F, // R11
[Card.CRTC.STARTHIGH]: 0x3F, // R12
[Card.CRTC.STARTLOW]: 0xFF, // R13
[Card.CRTC.CURHIGH]: 0x3F, // R14
[Card.CRTC.CURLOW]: 0xFF, // R15
[Card.CRTC.PENHIGH]: 0x3F, // R16
[Card.CRTC.PENLOW]: 0xFF // R17
};
if (DEBUGGER) {
Card.CRTC.REGS = [
"HTOTAL","HDISP","HSPOS","HSWIDTH","VTOTAL","VTOTADJ",
"VDISP","VSPOS","ILMODE","MAXSCAN","CURSTART","CUREND",
"STARTHIGH","STARTLOW","CURHIGH","CURLOW","PENHIGH","PENLOW"];
Card.CRTC.EGA_REGS = [
"HTOTAL","HDEND","HBSTART","HBEND","HRSTART","HREND",
"VTOTAL","OVERFLOW","PRESCAN","MAXSCAN","CURSTART","CUREND",
"STARTHIGH","STARTLOW","CURHIGH","CURLOW","VRSTART","VREND",
"VDEND","OFFSET","UNDERLINE","VBSTART","VBEND","MODECTRL","LINECOMP"];
}
/*
* EGA/VGA Input Status 1 Register (port 0x3DA)
*
* STATUS1 bit 0 has confusing documentation: the EGA Tech Ref says "Logical 0 indicates the CRT raster is in a
* horizontal or vertical retrace interval", whereas the VGA Tech Ref says "Logical 1 indicates a horizontal or
* vertical retrace interval," but then clarifies: "This bit is the real-time status of the INVERTED display enable
* signal". So, instead of calling bit 0 DISP_ENABLE (or more precisely, DISP_ENABLE_INVERTED), it's simply RETRACE.
*
* 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 = {
PORT: 0x3DA,
RETRACE: 0x01, // bit 0: logical OR of horizontal and vertical retrace
VRETRACE: 0x08, // bit 3: set during vertical retrace interval
DIAGNOSTIC: 0x30, // bits 5,4 are controlled by the Card.ATC.PLANES.MUX bits
RESERVED: 0xC6
};
/*
* EGA/VGA Attribute Controller Registers (port 0x3C0: regATCIndx and regATCData)
*
* The current ATC INDX value is stored in cardEGA.regATCIndx (including the Card.ATC.INDX_ENABLE bit), and the
* ATC DATA values are stored in cardEGA.regATCData. 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 = {
PORT: 0x3C0, // ATC Index/Data Port
INDX_MASK: 0x1F,
INDX_PAL_ENABLE: 0x20, // must be clear when loading palette registers
PALETTE: {
INDX: 0x00, // 16 registers: 0x00 - 0x0F
MASK: 0x3f,
BLUE: 0x01,
GREEN: 0x02,
RED: 0x04,
SECBLUE: 0x08,
BRIGHT: 0x10, // NOTE: The IBM EGA manual (p.56) also calls this the "intensity" bit
SECGREEN: 0x10,
SECRED: 0x20
},
PALETTE_REGS: 0x10, // 16 total palette registers
MODE: {
INDX: 0x10, // ATC Mode Control Register
GRAPHICS: 0x01, // bit 0: set for graphics mode, clear for alphanumeric mode
MONOEM: 0x02, // bit 1: set for monochrome emulation mode, clear for color emulation
TEXT_9DOT: 0x04, // bit 2: set for 9-dot replication in character codes 0xC0-0xDF
BLINK_ENABLE: 0x08, // bit 3: set for text/graphics blink, clear for background intensity
RESERVED: 0x10, // bit 4: reserved
PANCOMPAT: 0x20, // bit 5: set for pixel-panning compatibility
PELWIDTH: 0x40, // bit 6: set for 256-color modes, clear for all other modes
COLORSEL_ALL: 0x80 // bit 7: set to enable all COLORSEL bits (ie, COLORSEL.DAC_BIT5 and COLORSEL.DAC_BIT4)
},
OVERSCAN: {
INDX: 0x11 // ATC Overscan Color Register
},
PLANES: {
INDX: 0x12, // ATC Color Plane Enable Register
MASK: 0x0F,
MUX: 0x30,
RESERVED: 0xC0
},
HPAN: {
INDX: 0x13, // ATC Horizontal PEL Panning Register
SHIFT_LEFT: 0x0F // bits 0-3 indicate # of pixels to shift left
},
COLORSEL: {
INDX: 0x14, // ATC Color Select Register (VGA only)
DAC_BIT7: 0x08, // specifies bit 7 of DAC values (ignored in 256-color modes)
DAC_BIT6: 0x04, // specifies bit 6 of DAC values (ignored in 256-color modes)
DAC_BIT5: 0x02, // specifies bit 5 of DAC values (if ATC.MODE.COLORSEL_ALL is set; ignored in 256-color modes)
DAC_BIT4: 0x01 // specifies bit 4 of DAC values (if ATC.MODE.COLORSEL_ALL is set; ignored in 256-color modes)
},
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","OVERSCAN","PLANES","HPAN"];
}
/*
* EGA/VGA Feature Control Register (port 0x3BA or 0x3DA: regFeat)
*
* 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 = {
PORT_MONO: 0x3BA, // write port address (other than the two bits below, the rest are reserved and/or unused)
PORT_COLOR: 0x3DA, // write port address (other than the two bits below, the rest are reserved and/or unused)
PORT_READ: 0x3CA, // read port address (VGA only)
BITS: 0x03 // feature control bits
};
/*
* EGA/VGA Miscellaneous Output Register (port 0x3C2: regMisc)
*/
Card.MISC = {
PORT_WRITE: 0x3C2, // write port address (EGA and VGA)
PORT_READ: 0x3CC, // read port addresss (VGA only)
IO_SELECT: 0x01, // 0 sets CRT ports to 0x3Bn, 1 sets CRT ports to 0x3Dn
ENABLE_RAM: 0x02, // 0 disables video RAM, 1 enables
CLOCK_SELECT: 0x0C, // 0x0: 14Mhz I/O clock, 0x4: 16Mhz on-board clock, 0x8: external clock, 0xC: unused
DISABLE_DRV: 0x10, // 0 activates internal video drivers, 1 activates feature connector direct drive outputs
PAGE_ODD_EVEN: 0x20, // 0 selects the low 64Kb page of video RAM for text modes, 1 selects the high page
HPOLARITY: 0x40, // 0 selects positive horizontal retrace
VPOLARITY: 0x80 // 0 selects positive vertical retrace
};
/*
* EGA/VGA Input Status 0 Register (port 0x3C2: regStatus0)
*/
Card.STATUS0 = {
PORT: 0x3C2, // read-only (aka STATUS0, to distinguish it from PORT_CGA_STATUS)
RESERVED: 0x0F,
SWSENSE: 0x10,
SWSENSE_SHIFT: 4,
FEAT: 0x60, // VGA: reserved
INTERRUPT: 0x80 // 1: video is being displayed; 0: vertical retrace is occurring
};
/*
* VGA Subsystem Enable Register (port 0x3C3: regVGAEnable)
*/
Card.VGA_ENABLE = {
PORT: 0x3C3,
ENABLED: 0x01, // when set, all VGA I/O and memory decoding is enabled; otherwise disabled (TODO: Implement)
RESERVED: 0xFE
};
/*
* EGA/VGA Sequencer Registers (ports 0x3C4/0x3C5: regSEQIndx and regSEQData)
*/
Card.SEQ = {
INDX: {
PORT: 0x3C4, // Sequencer Index Port
MASK: 0x07
},
DATA: {
PORT: 0x3C5 // Sequencer Data Port
},
RESET: {
INDX: 0x00, // Sequencer Reset Register
ASYNC: 0x01,
SYNC: 0x02
},
CLOCKING: {
INDX: 0x01, // Sequencer Clocking Mode Register
DOTS8: 0x01, // 1: 8 dots; 0: 9 dots
BANDWIDTH: 0x02, // 0: CRTC has access 4 out of every 5 cycles (for high-res modes); 1: CRTC has access 2 out of 5 (VGA: reserved)
SHIFTLOAD: 0x04,
DOTCLOCK: 0x08, // 0: normal dot clock; 1: master clock divided by two (used for 320x200 modes: 0, 1, 4, 5, and D)
SHIFT4: 0x10, // VGA only
SCREEN_OFF: 0x20, // VGA only
RESERVED: 0xC0
},
MAPMASK: {
INDX: 0x02, // Sequencer Map Mask Register
PL0: 0x01,
PL1: 0x02,
PL2: 0x04,
PL3: 0x08,
MAPS: 0x0F,
RESERVED: 0xF0
},
CHARMAP: {
INDX: 0x03, // Sequencer Character Map Select Register
SELB: 0x03, // 0x0: 1st 8Kb of plane 2; 0x1: 2nd 8Kb; 0x2: 3rd 8Kb; 0x3: 4th 8Kb
SELA: 0x0C, // 0x0: 1st 8Kb of plane 2; 0x4: 2nd 8Kb; 0x8: 3rd 8Kb; 0xC: 4th 8Kb
SELB_HIGH: 0x10, // VGA only
SELA_HIGH: 0x20 // VGA only
},
MEMMODE: {
INDX: 0x04, // Sequencer Memory Mode Register
ALPHA: 0x01, // set for alphanumeric (A/N) mode, clear for graphics (APA or "All Points Addressable") mode (EGA only)
EXT: 0x02, // set if memory expansion installed, clear if not installed
SEQUENTIAL: 0x04, // set for sequential memory access, clear for mapping even addresses to planes 0/2, odd addresses to planes 1/3
CHAIN4: 0x08 // VGA only: set to select memory map (plane) based on low 2 bits of address
},
TOTAL_REGS: 0x05
};
if (DEBUGGER) Card.SEQ.REGS = ["RESET","CLOCKING","MAPMASK","CHARMAP","MEMMODE"];
/*
* VGA Digital-to-Analog Converter (DAC) Registers (regDACMask, regDACState, regDACAddr, and regDACData)
*
* To write DAC data, write an address to DAC.ADDR.PORT_WRITE, then write 3 bytes to DAC.DATA.PORT; the low 6 bits
* of each byte will be concatenated to form an 18-bit DAC value (red is least significant, followed by green, then blue).
* When the final byte is received, the 18-bit DAC value is updated and regDACAddr is auto-incremented.
*
* To read DAC data, the process is similar, but the initial address is written to DAC.ADDR.PORT_READ instead.
*
* DAC.STATE.PORT and DAC.ADDR.PORT_WRITE can be read at any time and will not interfere with a read or write operation
* in progress. To prevent "snow", reading or writing DAC values should be limited to retrace intervals (see regStatus1),
* or by using the SCREEN_OFF bit in the SEQ.CLOCKING register.
*/
Card.DAC = {
MASK: {
PORT: 0x3C6, // initialized to 0xFF and should not be changed
DEFAULT: 0xFF
},
STATE: {
PORT: 0x3C7,
MODE_WRITE: 0x00, // the DAC is in write mode if bits 0 and 1 are clear
MODE_READ: 0x03 // the DAC is in read mode if bits 0 and 1 are set
},
ADDR: {
PORT_READ: 0x3C7, // write to initiate a read
PORT_WRITE: 0x3C8 // write to initiate a write; read to determine the current ADDR
},
DATA: {
PORT: 0x3C9
},
TOTAL_REGS: 0x100
};
/*
* EGA/VGA Graphics Controller Registers (ports 0x3CE/0x3CF: regGRCIndx and regGRCData)
*
* The VGA added Write Mode 3, which is described as follows:
*
* "Each map is written with 8 bits of the value contained in the Set/Reset register for that map
* (the Enable Set/Reset register has no effect). Rotated system microprocessor data is ANDed with the
* Bit Mask register data to form an 8-bit value that performs the same function as the Bit Mask register
* does in write modes 0 and 2."
*/
Card.GRC = {
POS1_PORT: 0x3CC, // EGA only, write-only
POS2_PORT: 0x3CA, // EGA only, write-only
INDX: {
PORT: 0x3CE, // GRC Index Port
MASK: 0x0F
},
DATA: {
PORT: 0x3CF // GRC Data Port
},
SRESET: {
INDX: 0x00 // GRC Set/Reset Register (write-only; each bit used only if WRITE.MODE0 and corresponding ESR bit set)
},
ESRESET: {
INDX: 0x01 // GRC Enable Set/Reset Register
},
COLORCMP: {
INDX: 0x02 // GRC Color Compare Register
},
DATAROT: {
INDX: 0x03, // GRC Data Rotate Register
COUNT: 0x07,
AND: 0x08,
OR: 0x10,
XOR: 0x18,
FUNC: 0x18,
MASK: 0x1F
},
READMAP: {
INDX: 0x04, // GRC Read Map Select Register
NUM: 0x03
},
MODE: {
INDX: 0x05, // GRC Mode Register
WRITE: {
MODE0: 0x00, // write mode 0: each plane written with CPU data, rotated as needed, unless SR enabled
MODE1: 0x01, // write mode 1: each plane written with contents of the processor latches (loaded by a read)
MODE2: 0x02, // write mode 2: memory plane N is written with 8 bits matching data bit N
MODE3: 0x03, // write mode 3: VGA only
MASK: 0x03
},
TEST: 0x04,
READ: {
MODE0: 0x00, // read mode 0: read map mode
MODE1: 0x08, // read mode 1: color compare mode
MASK: 0x08
},
EVENODD: 0x10,
SHIFT: 0x20,
COLOR256: 0x40 // VGA only
},
MISC: {
INDX: 0x06, // GRC Miscellaneous Register
GRAPHICS: 0x01, // set for graphics mode addressing, clear for text mode addressing
CHAIN: 0x02, // set for odd/even planes selected with odd/even values of the processor AO bit
MAPMEM: 0x0C, //
MAPA0128: 0x00, //
MAPA064: 0x04, //
MAPB032: 0x08, //
MAPB832: 0x0C //
},
COLORDC: {
INDX: 0x07 // GRC Color "Don't Care" Register
},
BITMASK: {
INDX: 0x08 // GRC Bit Mask Register
},
TOTAL_REGS: 0x09
};
if (DEBUGGER) Card.GRC.REGS = ["SRESET","ESRESET","COLORCMP","DATAROT","READMAP","MODE","MISC","COLORDC","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 memory.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 video 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. TODO: Figure this out.
*
*
* Even/Odd Memory Access Functions
*
* The "EVENODD" functions deal with the EGA's default text-mode addressing, where EVEN addresses are mapped to
* plane 0 (and 2) and ODD addresses are mapped to plane 1 (and 3). This occurs when SEQ.MEMMODE.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.MODECTRL.BYTE_MODE: 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.MODECTRL.ADDR_WRAP is set) or A13
* (when CRTC.MODECTRL.ADDR_WRAP 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.
* TODO: Implement the subtleties.
*/
/*
* Values returned by getCardAccess(); the high byte describes the read mode, and the low byte describes the write mode.
*
* V2 should never appear in any values used by getCardAccess() or setCardAccess(); the sole purpose of V2 is to
* distinguish newer (V2) access values from older (V1) access values in saved contexts. It's set when the context
* is saved, and cleared when the context is restored. Thus, if V2 is not set on restore, we assume we're dealing with
* a V1 value, so we run it through the V1 table (below) to produce a V2 value. Hopefully at some point V1 contexts
* can be deprecated, and the V2 bit can be eliminated/repurposed.
*/
Card.ACCESS = {
READ: { // READ values are designed to be OR'ed with WRITE values
MODE0: 0x0400,
MODE1: 0x0500,
EVENODD: 0x1000,
CHAIN4: 0x4000,
MASK: 0xFF00
},
WRITE: { // and WRITE values are designed to be OR'ed with READ values
MODE0: 0x0000,
MODE1: 0x0001,
MODE2: 0x0002,
MODE3: 0x0003, // VGA only
CHAIN4: 0x0004,
EVENODD: 0x0010,
ROT: 0x0020,
AND: 0x0060,
OR: 0x00A0,
XOR: 0x00E0,
MASK: 0x00FF
},
V2: (0x80000000|0) // this is a signature bit used ONLY to differentiate V2 access values from V1
};
/*
* Table of older (V1) access values and their corresponding new values; the new values are similar but more orthogonal
*/
Card.ACCESS.V1 = [];
Card.ACCESS.V1[0x0002] = Card.ACCESS.READ.MODE0;
Card.ACCESS.V1[0x0003] = Card.ACCESS.READ.MODE0 | Card.ACCESS.READ.EVENODD;
Card.ACCESS.V1[0x0010] = Card.ACCESS.READ.MODE1;
Card.ACCESS.V1[0x0200] = Card.ACCESS.WRITE.MODE0;
Card.ACCESS.V1[0x0400] = Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.ROT;
Card.ACCESS.V1[0x0600] = Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.AND;
Card.ACCESS.V1[0x0A00] = Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.OR;
Card.ACCESS.V1[0x0E00] = Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.XOR;
Card.ACCESS.V1[0x0300] = Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.EVENODD;
Card.ACCESS.V1[0x1000] = Card.ACCESS.WRITE.MODE1;
Card.ACCESS.V1[0x2000] = Card.ACCESS.WRITE.MODE2;
Card.ACCESS.V1[0x6000] = Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.AND;
Card.ACCESS.V1[0xA000] = Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.OR;
Card.ACCESS.V1[0xE000] = Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.XOR;
/**
* readByteMode0(off, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} [addr]
* @return {number}
*/
Card.ACCESS.readByteMode0 = function readByteMode0(off, addr)
{
off += this.offset;
var dw = this.controller.latches = this.adw[off];
return (dw >> this.controller.nReadMapShift) & 0xff;
};
/**
* readByteMode0Chain4(off, addr)
*
* See writeByteMode0Chain4 for a description of how writes are distributed across planes.
*
* @this {Memory}
* @param {number} off
* @param {number} [addr]
* @return {number}
*/
Card.ACCESS.readByteMode0Chain4 = function readByteMode0Chain4(off, addr)
{
var idw = (off & ~0x3) + this.offset;
var shift = (off & 0x3) << 3;
return ((this.controller.latches = this.adw[idw]) >> shift) & 0xff;
};
/**
* readByteMode0EvenOdd(off, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} [addr]
* @return {number}
*/
Card.ACCESS.readByteMode0EvenOdd = function readByteMode0EvenOdd(off, addr)
{
/*
* TODO: As discussed in getCardAccess(), we need to run some tests on real EGA/VGA hardware to determine
* exactly what gets latched (ie, from which address) when EVENODD is in effect. Whatever we learn may
* also dictate a special EVENODD function for READ.MODE1 as well.
*/
off += this.offset;
var idw = off & ~0x1;
var dw = this.controller.latches = this.adw[idw];
return (!(off & 1)? dw : (dw >> 8)) & 0xff;
};
/**
* readByteMode1(off, addr)
*
* This mode requires us to step through each of the 8 sets of 4 bits in the specified DWORD of video memory,
* returning a 1 wherever all 4 match the Color Compare (COLORCMP) Register and a 0 otherwise. An added wrinkle
* is that the Color Don't Care (COLORDC) Register can specify that any/all/none of the 4 bits must be ignored.
*
* We perform the comparison from most to least significant bit, because that matches how the nColorCompare and
* nColorDontCare masks are initialized; we could have gone either way, but this is more consistent with the rest
* of the component (eg, pixels are drawn across the screen from left to right, starting with the most significant
* bit of each byte).
*
* Also note that, while not well-documented, this mode also affects the internal latches, so we make sure those
* are updated as well.
*
* @this {Memory}
* @param {number} off
* @param {number} [addr]
* @return {number}
*/
Card.ACCESS.readByteMode1 = function readByteMode1(off, addr)
{
off += this.offset;
var dw = this.controller.latches = this.adw[off];
/*
* Minor optimization: we could pre-mask nColorCompare with nColorDontCare, whenever either register
* is updated, but that's a drop in the bucket compared to all the other work this function must do.
*/
var mask = this.controller.nColorDontCare;
var color = this.controller.nColorCompare & mask;
var b = 0, bit = 0x80;
while (bit) {
if ((dw & mask) == color) b |= bit;
color >>>= 1; mask >>>= 1; bit >>= 1;
}
return b;
};
/**
* writeByteMode0(off, b, addr)
*
* 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 cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0 = function writeByteMode0(off, b, addr)
{
var idw = off + this.offset;
var dw = b | (b << 8) | (b << 16) | (b << 24);
dw = (dw & this.controller.nSetMapMask) | this.controller.nSetMapBits;
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0Chain4(off, b, addr)
*
* This is how we distribute writes of 0xff across the address space to the planes (assuming that all
* planes are enabled by the Sequencer's MAPMASK register):
*
* off idw adw[idw]
* ------ ------ ----------
* 0x0000: 0x0000 0x000000ff
* 0x0001: 0x0000 0x0000ff00
* 0x0002: 0x0000 0x00ff0000
* 0x0003: 0x0000 0xff000000
* 0x0004: 0x0004 0x000000ff
* 0x0005: 0x0004 0x0000ff00
* 0x0006: 0x0004 0x00ff0000
* 0x0007: 0x0004 0xff000000
* ...
*
* Some VGA emulations calculate the video buffer index (idw) by shifting the offset (off) right 2 bits,
* instead of simply masking off the low 2 bits, as we do here. That would be a more "pleasing" arrangement,
* because we would be using sequential video buffer locations, instead of multiples of 4, and would match how
* pixels are stored in "Mode X". However, I don't think that's how CHAIN4 modes operate (although that still
* needs to be confirmed, because multiple sources conflict on this point). TODO: Confirm CHAIN4 operation on
* actual VGA hardware, including the extent to which ALU and other writeByteMode0() functionality needs to
* be folded into this.
*
* Address decoding may not matter that much, as long as both the read and write CHAIN4 functions decode their
* addresses in exactly the same manner; we'd only get into trouble with software that "unchained" or otherwise
* reconfigured the planes and then made assumptions about existing data in the video buffer.
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0Chain4 = function writeByteMode0Chain4(off, b, addr)
{
var idw = (off & ~0x3) + this.offset;
var shift = (off & 0x3) << 3;
/*
* TODO: Consider adding a separate "unmasked" version of this CHAIN4 write function when nSeqMapMask is -1
* (or removing nSeqMapMask from the equation altogether, if CHAIN4 is never used with any planes disabled).
*/
var dw = ((b << shift) & this.controller.nSeqMapMask) | (this.adw[idw] & ~((0xff << shift) & this.controller.nSeqMapMask));
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0Chain4(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0EvenOdd(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0EvenOdd = function writeByteMode0EvenOdd(off, b, addr)
{
off += this.offset;
var dw = b | (b << 8) | (b << 16) | (b << 24);
/*
* When even/odd addressing is enabled, nSeqMapMask 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;
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
var maskMaps = this.controller.nSeqMapMask & (idw == off? 0x00ff00ff : (0xff00ff00|0));
dw = (dw & maskMaps) | (this.adw[idw] & ~maskMaps);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0EvenOdd(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0Rot(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0Rot = function writeByteMode0Rot(off, b, addr)
{
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.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0Rot(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0And(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0And = function writeByteMode0And(off, b, addr)
{
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.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0And(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0Or(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0Or = function writeByteMode0Or(off, b, addr)
{
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.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0Or(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode0Xor(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode0Xor = function writeByteMode0Xor(off, b, addr)
{
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.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode0Xor(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode1(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (ignored; the EGA latches provide the source data)
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode1 = function writeByteMode1(off, b, addr)
{
var idw = off + this.offset;
var dw = (this.adw[idw] & ~this.controller.nSeqMapMask) | (this.controller.latches & this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode1(" + Str.toHexLong(addr) + "): " + Str.toHexLong(dw));
}
};
/**
* writeByteMode1EvenOdd(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (ignored; the EGA latches provide the source data)
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode1EvenOdd = function writeByteMode1EvenOdd(off, b, addr)
{
/*
* TODO: As discussed in getCardAccess(), we need to run some tests on real EGA/VGA hardware to
* determine exactly where latches are written (ie, to which address) when EVENODD is in effect.
*/
off += this.offset;
//
// When even/odd addressing is enabled, nSeqMapMask 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.nSeqMapMask & (idw == off? 0x00ff00ff : (0xff00ff00|0));
var dw = (this.adw[idw] & ~maskMaps) | (this.controller.latches & maskMaps);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode1EvenOdd(" + Str.toHexLong(addr) + "): " + Str.toHexByte(dw));
}
};
/**
* writeByteMode2(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode2 = function writeByteMode2(off, b, addr)
{
var idw = off + this.offset;
var dw = Video.aEGAByteToDW[b & 0xf];
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode2(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode2And(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode2And = function writeByteMode2And(off, b, addr)
{
var idw = off + this.offset;
var dw = Video.aEGAByteToDW[b & 0xf];
dw &= this.controller.latches;
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode2And(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode2Or(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode2Or = function writeByteMode2Or(off, b, addr)
{
var idw = off + this.offset;
var dw = Video.aEGAByteToDW[b & 0xf];
dw |= this.controller.latches;
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode2Or(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode2Xor(off, b, addr)
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode2Xor = function writeByteMode2Xor(off, b, addr)
{
var idw = off + this.offset;
var dw = Video.aEGAByteToDW[b & 0xf];
dw ^= this.controller.latches;
dw = (dw & this.controller.nBitMapMask) | (this.controller.latches & ~this.controller.nBitMapMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode2Xor(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/**
* writeByteMode3(off, b, addr)
*
* In MODE3, Set/Reset is always enabled, so the ESRESET bits (and therefore nSetMapMask and nSetMapBits)
* are ignored; we look only at the SRESET bits, which are stored in nSetMapData.
*
* Unlike MODE0, we currently have no non-rotate function for MODE3. If performance dictates, we can add one;
* ditto for other features like the Sequencer's MAPMASK register (nSeqMapMask).
*
* @this {Memory}
* @param {number} off
* @param {number} b (which should already be pre-masked to 8 bits; see cpu.setByte())
* @param {number} [addr]
*/
Card.ACCESS.writeByteMode3 = function writeByteMode3(off, b, addr)
{
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);
var dwMask = (dw & this.controller.nBitMapMask);
dw = (this.controller.nSetMapData & dwMask) | (this.controller.latches & ~dwMask);
dw = (dw & this.controller.nSeqMapMask) | (this.adw[idw] & ~this.controller.nSeqMapMask);
if (this.adw[idw] != dw) {
this.adw[idw] = dw;
this.fDirty = true;
}
if (DEBUG && this.controller.video.messageEnabled(Messages.MEM | Messages.VIDEO)) {
this.controller.video.printMessage("writeByteMode3(" + Str.toHexLong(addr) + "): " + Str.toHexByte(b) + " -> " + Str.toHexLong(dw));
}
};
/*
* Mappings from getCardAccess() 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.CHAIN4] = Card.ACCESS.readByteMode0Chain4;
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.MODE0 | Card.ACCESS.WRITE.ROT] = Card.ACCESS.writeByteMode0Rot;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.AND] = Card.ACCESS.writeByteMode0And;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.OR] = Card.ACCESS.writeByteMode0Or;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.XOR] = Card.ACCESS.writeByteMode0Xor;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE0 | Card.ACCESS.WRITE.CHAIN4] = Card.ACCESS.writeByteMode0Chain4;
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.MODE1 | Card.ACCESS.WRITE.EVENODD] = Card.ACCESS.writeByteMode1EvenOdd;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2] = Card.ACCESS.writeByteMode2;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.AND] = Card.ACCESS.writeByteMode2And;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.OR] = Card.ACCESS.writeByteMode2Or;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE2 | Card.ACCESS.WRITE.XOR] = Card.ACCESS.writeByteMode2Xor;
Card.ACCESS.afn[Card.ACCESS.WRITE.MODE3] = Card.ACCESS.writeByteMode3;
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @unrestricted
*/
class Video extends Component {
/**
* Video(parmsVideo, canvas, context, textarea, container)
*
* The Video component can be configured with the following (parmsVideo) properties:
*
* model: model (eg, "mda" for Monochrome Display Adapter)
* mode: initial video mode (default is null, which selects a mode based on model)
* screenWidth: width of the screen canvas, in pixels
* screenHeight: height of the screen canvas, in pixels
* screenColor: background color of the screen canvas (default is black)
* flicker: 1 enables screen flicker, 0 disables (default is 0)
* 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) containing the character set
* touchScreen: string specifying desired touch-screen support (default is none)
* autoLock: true to (attempt to) auto-lock the mouse to the canvas (default is false)
*
* An EGA/VGA 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 Bus 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.
*
* Our initBus() handler defines a timer that periodically calls updateScreen() for each Video
* instance. These updates should occur at a rate of 60 times/second, to update any blinking
* elements (the cursor and any cells with the blink attribute), to compare/update the contents
* of our internal buffer with the video buffer, and to render any differences between the two
* buffers into the associated screen canvas, via either updateChar() or setPixel().
*
* Thanks to the Bus' new block-based memory manager that allows us to sparse-allocate memory
* (in 4Kb increments on 20-bit buses, 16Kb increments on 24-bit buses), updateScreen()
* can also ask the CPU for the "dirty" state of all the blocks underlying the video buffer,
* bypassing the update completely if the buffer is still clean.
*
* Sadly, that optimization is defeated if the count of active blink elements is non-zero,
* because we must rescan the entire buffer to locate and redraw them all; I'm assuming for now
* that, more often than not, very few (if any) blink attributes will be present, and therefore
* they're not worth a separate caching mechanism. If the only blinking element is the cursor,
* that's no problem, as we redraw only the one cell containing the cursor (assuming the buffer
* is otherwise clean).
*
* @this {Video}
* @param {Object} parmsVideo
* @param {HTMLCanvasElement} [canvas]
* @param {CanvasRenderingContext2D} [context]
* @param {HTMLTextAreaElement} [textarea]
* @param {HTMLElement} [container]
*/
constructor(parmsVideo, canvas, context, textarea, container)
{
super("Video", parmsVideo, Messages.VIDEO);
var video = this, sProp, sEvent;
this.fGecko = Web.isUserAgent("Gecko/");
/*
* This records the model specified (eg, "mda", "cga", "ega", "vga" 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'];
var aModelDefaults = Video.MODEL[this.model] || Video.MODEL['mda'];
this.nCard = aModelDefaults[0];
this.cbMemory = parmsVideo['memory'] || 0; // zero means fallback to the cardSpec's default size
this.sSwitches = parmsVideo['switches'];
/*
* powerUp() uses the default mode ONLY if ChipSet doesn't give us a default.
*/
this.nModeDefault = parmsVideo['mode'];
if (this.nModeDefault == null || Video.aModeParms[this.nModeDefault] == null) {
this.nModeDefault = aModelDefaults[1];
}
/*
* setDimensions() uses these values ONLY if it doesn't recognize the video mode.
*/
this.nColsDefault = parmsVideo['charCols'];
this.nRowsDefault = parmsVideo['charRows'];
if (this.nColsDefault === undefined || this.nRowsDefault === undefined) {
this.nColsDefault = Video.aModeParms[this.nModeDefault][0];
this.nRowsDefault = 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.nColsDefault) >= 12;
this.canvasScreen = canvas;
this.contextScreen = context;
this.inputTextArea = textarea;
this.inputScreen = textarea || canvas || null;
/*
* We now ensure that a colorScreen property is always set (to "black" if nothing else), and
* set BOTH the canvas element's AND the container element's backgroundColor to match that color.
*
* This gives us option of doing "cute" things like flipping the canvas element's opacity from
* 1 to 0 briefly, alternately revealing and hiding the underlying container element, to simulate
* screen "flicker".
*/
this.colorScreen = parmsVideo['screenColor'] || "black";
this.opacityFlicker = (1 - (Web.getURLParm('flicker') || parmsVideo['flicker'] || 0)).toString();
this.fOpacityReduced = false;
if (canvas) canvas.style.backgroundColor = this.colorScreen;
if (container) container.style.backgroundColor = this.colorScreen;
/*
* Support for disabling (or, less commonly, enabling) image smoothing, which all browsers
* seem to support now (well, OK, I still have to test the latest MS Edge browser), despite
* it still being labelled "experimental technology". Let's hope the browsers standardize
* on this. I see other options emerging, like the CSS property "image-rendering: pixelated"
* that's apparently been added to Chrome. Sigh.
*/
var fSmoothing = parmsVideo['smoothing'];
var sSmoothing = Web.getURLParm('smoothing');
if (sSmoothing) fSmoothing = (sSmoothing == "true");
if (fSmoothing != null) {
sProp = Web.findProperty(this.contextScreen, 'imageSmoothingEnabled');
if (sProp) this.contextScreen[sProp] = fSmoothing;
}
/*
* initBus() will determine touch-screen support; for now, just record values and set defaults.
*/
this.sTouchScreen = parmsVideo['touchScreen'];
this.nTouchConfig = Video.TOUCH.NONE;
/*
* If a Mouse exists, we'll be notified when it requests our canvas, and we make a note of it
* so that if lockPointer() is ever invoked, we can notify the Mouse.
*/
this.mouse = null;
this.fAutoLock = parmsVideo['autoLock'];
/*
* 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 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 and simply update the entries as needed. Note that for an EGA (or a VGA operating in an
* EGA-compatible mode), only the first 16 entries get used (derived from the ATC); only when a VGA
* is operating in an 8bpp mode are 256 entries used (derived from the DAC rather than the ATC).
*/
this.aRGB = new Array(this.nCard == Video.CARD.VGA? 256 : 16);
this.fRGBValid = false; // whenever this is false, it signals getCardColors() to rebuild aRGB
/*
* 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 screen to maintain my own focus state.
*/
this.fHasFocus = false;
/*
* Here's the gross code to handle full-screen support across all supported browsers. The lack of standards
* is exasperating; browsers can't agree on 'Fullscreen' (most common) or 'FullScreen' (least common), and while
* some browsers honor other browser prefixes, most don't. Event handlers tend to be more consistent (ie, all
* lower-case).
*/
this.container = container;
if (this.container) {
sProp = Web.findProperty(container, 'requestFullscreen') || Web.findProperty(container, 'requestFullScreen');
if (sProp) {
this.container.doFullScreen = container[sProp];
sEvent = Web.findProperty(document, 'on', 'fullscreenchange');
if (sEvent) {
var sFullScreen = Web.findProperty(document, 'fullscreenElement') || Web.findProperty(document, 'fullScreenElement');
document.addEventListener(sEvent, function onFullScreenChange() {
video.notifyFullScreen(!!sFullScreen);
}, false);
}
sEvent = Web.findProperty(document, 'on', 'fullscreenerror');
if (sEvent) {
document.addEventListener(sEvent, function onFullScreenError() {
video.notifyFullScreen(null);
}, false);
}
}
}
/*
* More gross code to handle pointer-locking support across all supported browsers.
*/
if (this.inputScreen) {
this.inputScreen.onfocus = function onFocusScreen() {
return video.onFocusChange(true);
};
this.inputScreen.onblur = function onBlurScreen() {
return video.onFocusChange(false);
};
this.inputScreen.lockPointer = (sProp = Web.findProperty(this.inputScreen, 'requestPointerLock')) && this.inputScreen[sProp];
this.inputScreen.unlockPointer = (sProp = Web.findProperty(this.inputScreen, 'exitPointerLock')) && this.inputScreen[sProp];
if (this.inputScreen.lockPointer) {
sEvent = Web.findProperty(document, 'on', 'pointerlockchange');
if (sEvent) {
var sPointerLock = Web.findProperty(document, 'pointerLockElement');
document.addEventListener(sEvent, function onPointerLockChange() {
var fLocked = !!(sPointerLock && document[sPointerLock] === video.inputScreen);
video.notifyPointerLocked(fLocked);
}, false);
}
}
}
this.sFileURL = parmsVideo['fontROM'];
if (this.sFileURL) {
var sFileExt = Str.getExtension(this.sFileURL);
if (sFileExt != "json") {
this.sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + this.sFileURL + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES;
}
}
}
/**
* 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 {DebuggerX86} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
var video = this;
this.bus = bus;
this.cpu = cpu;
this.dbg = dbg;
/*
* nCard will be undefined if no model was explicitly set (whereas this.nCard is ALWAYS defined).
*/
var aModel = Video.MODEL[this.model], nCard = aModel && aModel[0];
/*
* The only time we do NOT want to trap MDA ports is when the model has been explicitly set to CGA.
*/
if (nCard !== Video.CARD.CGA) {
bus.addPortInputTable(this, Video.aMDAPortInput);
bus.addPortOutputTable(this, Video.aMDAPortOutput);
}
/*
* Similarly, the only time we do NOT want to trap CGA ports is when the model is explicitly set to MDA.
*/
if (nCard !== Video.CARD.MDA) {
bus.addPortInputTable(this, Video.aCGAPortInput);
bus.addPortOutputTable(this, Video.aCGAPortOutput);
}
/*
* Note that in the case of EGA and VGA models, the above code ensures that we will trap both MDA and CGA
* port ranges -- which is good, because both the EGA and VGA can be reprogrammed to respond to those ports,
* but also potentially bad if you want to simulate a "dual display" system, where one of the displays is
* driven by either an MDA or CGA.
*
* However, you should still be able to make that work by loading the MDA or CGA video component first, because
* components should be initialized in the order they appear in the machine configuration file. Any attempt
* by another component to trap the same ports should be ignored.
*/
if (this.nCard >= Video.CARD.EGA) {
bus.addPortInputTable(this, Video.aEGAPortInput);
bus.addPortOutputTable(this, Video.aEGAPortOutput);
}
if (this.nCard == Video.CARD.VGA) {
bus.addPortInputTable(this, Video.aVGAPortInput);
bus.addPortOutputTable(this, Video.aVGAPortOutput);
}
if (DEBUGGER && dbg) {
dbg.messageDump(Messages.VIDEO, function onDumpVideo(asArgs) {
video.dumpVideo(asArgs);
});
}
/*
* If we have an associated keyboard, then ensure that the keyboard will be notified
* whenever the canvas gets focus and receives input.
*/
this.kbd = cmp.getMachineComponent("Keyboard");
if (this.kbd && this.canvasScreen) {
for (var s in this.bindings) {
if (s.indexOf("lock") > 0) this.kbd.setBinding("led", s, this.bindings[s]);
}
this.kbd.setBinding(this.inputTextArea? "textarea" : "canvas", "screen", this.inputScreen);
}
this.bEGASwitches = 0x09; // our default "switches" setting (see aEGAMonitorSwitches)
this.chipset = cmp.getMachineComponent("ChipSet");
if (this.chipset && this.sSwitches) {
if (this.nCard == Video.CARD.EGA) {
this.bEGASwitches = this.chipset.parseDIPSwitches(this.sSwitches, this.bEGASwitches);
}
}
/*
* The default value for the 'touchScreen' parameter is an empty string; machine configs must explicitly
* select one of the following values, via the 'touchscreen' attribute in the