/** * @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 [HRETRACE_END], 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 [VRETRACE_START], must be a minimum of one horizontal scan line greater * than register R18 [VDISP_END]. 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 * [LINE_COMPARE] 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]: HDISP_END 0x27 0x27 0x4F 0x4F 0x27 0x27 0x4F 0x27 0x4F 0x4F 0x4F 0x4F * CRTC[0x02]: HBLANK_START 0x28 0x28 0x50 0x50 0x28 0x28 0x50 0x28 0x50 0x50 0x50 0x50 * CRTC[0x03]: HBLANK_END 0x90 0x90 0x82 0x82 0x90 0x90 0x82 0x90 0x82 0x82 0x82 0x82 * CRTC[0x04]: HRETRACE_START 0x2B 0x2B 0x55 0x55 0x2B 0x2B 0x54 0x2B 0x54 0x54 0x54 0x54 * CRTC[0x05]: HRETRACE_END 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]: PRESET_ROW 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 * CRTC[0x09]: MAX_SCAN 0x4F 0x4F 0x4F 0x4F 0xC1 0xC1 0xC1 0xC0 0xC0 0x40 0x40 0x41 * CRTC[0x0A]: CURSOR_START 0x0D 0x0D 0x0D 0x0D 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 * CRTC[0x0B]: CURSOR_END 0x0E 0x0E 0x0E 0x0E 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 * CRTC[0x0C]: START_ADDR_HI 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 * CRTC[0x0D]: START_ADDR_LO 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 * CRTC[0x0E]: CURSOR_ADDR_HI 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x00 * CRTC[0x0F]: CURSOR_ADDR_LO 0x19 0x19 0x41 0x41 0x19 0x19 0x41 0x19 0x41 0x41 0xE1 0xA2 * CRTC[0x10]: VRETRACE_START 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x9C 0x83 0xEA 0x9C * CRTC[0x11]: VRETRACE_END 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x8E 0x85 0x8C 0x8E * CRTC[0x12]: VDISP_END 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]: VBLANK_START 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x96 0x63 0xE7 0x96 * CRTC[0x16]: VBLANK_END 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xB9 0xBA 0x04 0xB9 * CRTC[0x17]: MODE_CTRL 0xA3 0xA3 0xA3 0xA3 0xA2 0xA2 0xC2 0xE3 0xE3 0xE3 0xE3 0xA3 * CRTC[0x18]: LINE_COMPARE 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 : []; if (nCard >= Video.CARD.EGA) { this.nCRTCRegs = Card.CRTC.EGA.TOTAL_REGS; this.asCRTCRegs = DEBUGGER? Card.CRTC.EGA_REGS : []; this.initEGA(data[6], nMonitorType); } var monitorSpecs = Video.monitorSpecs[nMonitorType] || Video.monitorSpecs[ChipSet.MONITOR.MONO]; var nCyclesPerSecond = video.cpu.getCyclesPerSecond(); // eg, 4772727 this.nCyclesHorzPeriod = (nCyclesPerSecond / monitorSpecs.nHorzPeriodsPerSec)|0; this.nCyclesHorzActive = (this.nCyclesHorzPeriod * monitorSpecs.percentHorzActive / 100)|0; this.nCyclesVertPeriod = (this.nCyclesHorzPeriod * monitorSpecs.nHorzPeriodsPerFrame)|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.START_ADDR_HI,CRTC.START_ADDR_LO 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 bOvrflowBit8 = 0, bOvrflowBit9 = 0, bMaxScanBit9 = 0; switch(iReg) { case Card.CRTC.EGA.VTOTAL: // 0x06 bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT8; // 0x01 if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VTOTAL_BIT9; break; case Card.CRTC.EGA.CURSOR_START.INDX: // 0x0A if (this.nCard == Video.CARD.EGA) bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.CURSOR_START_BIT8; break; case Card.CRTC.EGA.VRETRACE_START: // 0x10 bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VRETRACE_START_BIT8; // 0x04 if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VRETRACE_START_BIT9; break; case Card.CRTC.EGA.VDISP_END: // 0x12 bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VDISP_END_BIT8; // 0x02 if (this.nCard == Video.CARD.VGA) bOvrflowBit9 = Card.CRTC.EGA.OVERFLOW.VDISP_END_BIT9; break; case Card.CRTC.EGA.VBLANK_START: // 0x15 bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.VBLANK_START_BIT8; // 0x08 if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAX_SCAN.VBLANK_START_BIT9; break; case Card.CRTC.EGA.LINE_COMPARE: // 0x18 bOvrflowBit8 = Card.CRTC.EGA.OVERFLOW.LINE_COMPARE_BIT8; // 0x10 if (this.nCard == Video.CARD.VGA) bMaxScanBit9 = Card.CRTC.EGA.MAX_SCAN.LINE_COMPARE_BIT9; break; } if (bOvrflowBit8) { reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOvrflowBit8)? 0x100 : 0); reg |= ((this.regCRTData[Card.CRTC.EGA.OVERFLOW.INDX] & bOvrflowBit9)? 0x200 : 0); reg |= ((this.regCRTData[Card.CRTC.EGA.MAX_SCAN.INDX] & bMaxScanBit9)? 0x200 : 0); } } return reg; } } /* * 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 (CURSOR_ADDR_HI and CURSOR_ADDR_LO), * we end up making ALL the registers readable, otherwise we would have to explicitly block any register marked write-only. I don't * think making the CRT registers fully readable presents any serious compatibility issues, and it actually offers some benefits * (eg, improved debugging). * * However, some things are broken: the (readable) light pen registers on the EGA are overloaded as (writable) vertical retrace * registers, so the vertical retrace registers cannot actually be read that way. I'm sure the VGA solved that problem, but I haven't * looked into it yet. */ Card.CRTC = { HTOTAL: 0x00, HDISP: 0x01, HSYNC_POS: 0x02, HSYNC_WIDTH: 0x03, VTOTAL: 0x04, VTOTAL_ADJ: 0x05, VDISP_TOTAL: 0x06, VSYNC_POS: 0x07, INTERLACE_POS: 0x08, MAX_SCAN: { INDX: 0x09, MASK: 0x1F }, CURSOR_START: { INDX: 0x0A, MASK: 0x1F, /* * I don't entirely understand these cursor blink control bits. Here's what the MC6845 datasheet says: * * Bit 5 is the blink timing control. When bit 5 is low, the blink frequency is 1/16 of the vertical field rate, * and when bit 5 is high, the blink frequency is 1/32 of the vertical field rate. Bit 6 is used to enable a blink. */ BLINKON: 0x00, // (supposedly, 0x04 has the same effect as 0x00) BLINKOFF: 0x20, // if blinking is disabled, the cursor is effectively hidden BLINKFAST: 0x60 // default is 1/16 of the frame rate; this switches to 1/32 of the frame rate }, CURSOR_END: { INDX: 0x0B, MASK: 0x1F }, START_ADDR_HI: 0x0C, START_ADDR_LO: 0x0D, CURSOR_ADDR_HI: 0x0E, CURSOR_ADDR_LO: 0x0F, LIGHT_PEN_HI: 0x10, LIGHT_PEN_LO: 0x11, TOTAL_REGS: 0x12, // total CRT registers on MDA/CGA EGA: { HDISP_END: 0x01, HBLANK_START: 0x02, HBLANK_END: 0x03, HRETRACE_START: 0x04, HRETRACE_END: 0x05, VTOTAL: 0x06, OVERFLOW: { INDX: 0x07, VTOTAL_BIT8: 0x01, // bit 8 of register 0x06 VDISP_END_BIT8: 0x02, // bit 8 of register 0x12 VRETRACE_START_BIT8:0x04, // bit 8 of register 0x10 VBLANK_START_BIT8: 0x08, // bit 8 of register 0x15 LINE_COMPARE_BIT8: 0x10, // bit 8 of register 0x18 CURSOR_START_BIT8: 0x20, // bit 8 of register 0x0A (EGA only) VTOTAL_BIT9: 0x20, // bit 9 of register 0x06 (VGA only) VDISP_END_BIT9: 0x40, // bit 9 of register 0x12 (VGA only, unused on EGA) VRETRACE_START_BIT9:0x80 // bit 9 of register 0x10 (VGA only, unused on EGA) }, PRESET_SCAN: 0x08, /* * NOTE: EGA/VGA CRTC registers 0x09-0x0F are the same as the MDA/CGA CRTC registers defined above */ MAX_SCAN: { INDX: 0x09, SCAN_LINE: 0x1f, VBLANK_START_BIT9: 0x20, // (VGA only) LINE_COMPARE_BIT9: 0x40, // (VGA only) CONVERT400: 0x80 // 200-to-400 scan-line conversion is in effect (VGA only) }, CURSOR_START: { INDX: 0x0A, MASK: 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) }, CURSOR_END: { INDX: 0x0B, MASK: 0x1F }, START_ADDR_HI: 0x0C, START_ADDR_LO: 0x0D, CURSOR_ADDR_HI: 0x0E, CURSOR_ADDR_LO: 0x0F, VRETRACE_START: 0x10, VRETRACE_END: 0x11, VDISP_END: 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, COUNTBY4: 0x20, // (VGA only) DWORD: 0x40 // (VGA only) }, VBLANK_START: 0x15, VBLANK_END: 0x16, MODE_CTRL: { 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 COUNTBY2: 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 }, LINE_COMPARE: 0x18, TOTAL_REGS: 0x19 // total CRT registers on EGA/VGA }, ADDR_HI_MASK: 0x3F }; if (DEBUGGER) { Card.CRTC.REGS = ["HTOTAL","HDISP","HSYNC_POS","HSYNC_WIDTH","VTOTAL","VTOTAL_ADJ", "VDISP","VSYNC_POS","INTERLACE_POS","MAX_SCAN","CURSOR_START","CURSOR_END", "START_ADDR_HI","START_ADDR_LO","CURSOR_ADDR_HI","CURSOR_ADDR_LO","LIGHT_PEN_HI","LIGHT_PEN_LO"]; Card.CRTC.EGA_REGS = ["HTOTAL","HDISP_END","HBLANK_START","HBLANK_END","HRETRACE_START","HRETRACE_END", "VTOTAL","OVERFLOW","PRESET_SCAN","MAX_SCAN","CURSOR_START","CURSOR_END", "START_ADDR_HI","START_ADDR_LO","CURSOR_ADDR_HI","CURSOR_ADDR_LO","VRETRACE_START","VRETRACE_END", "VDISP_END","OFFSET","UNDERLINE","VBLANK_START","VBLANK_END","MODE_CTRL","LINE_COMPARE"]; } /* * 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.MODE_CTRL.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.MODE_CTRL.ADDR_WRAP is set) or A13 * (when CRTC.MODE_CTRL.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) * 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. * * The CPU periodically calls updateVideo(), which in turn calls updateScreen() for each Video * instance. These updates should occur at a rate of 60 times/second, to update any blinking * elements (the cursor and any cells with the blink attribute), to compare/update the contents * of our internal buffer with the video buffer, and to render any differences between the two * buffers into the associated screen canvas, via either updateChar() or setPixel(). * * Thanks to the 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 {Object} [canvas] * @param {Object} [context] * @param {Object} [textarea] * @param {Object} [container] */ constructor(parmsVideo, canvas, context, textarea, container) { super("Video", parmsVideo, Messages.VIDEO); var video = this; this.fGecko = Web.isUserAgent("Gecko/"); var i, sEvent, asWebPrefixes = ['', 'moz', 'ms', 'webkit']; /* * 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.textareaScreen = textarea; this.inputScreen = textarea || canvas || null; /* * 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) { for (i = 0; i < asWebPrefixes.length; i++) { sEvent = asWebPrefixes[i]; if (!sEvent) { sEvent = 'imageSmoothingEnabled'; } else { sEvent += 'ImageSmoothingEnabled'; } if (this.contextScreen[sEvent] !== undefined) { this.contextScreen[sEvent] = fSmoothing; break; } } } /* * 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 'full' or 'Full, 'request' or 'Request', 'screen' or 'Screen', and * while some browsers honor other browser prefixes, most browsers don't. */ this.container = container; if (this.container) { this.container.doFullScreen = container['requestFullscreen'] || container['msRequestFullscreen'] || container['mozRequestFullScreen'] || container['webkitRequestFullscreen']; if (this.container.doFullScreen) { for (i = 0; i < asWebPrefixes.length; i++) { sEvent = asWebPrefixes[i] + 'fullscreenchange'; if ('on' + sEvent in document) { var onFullScreenChange = function() { var fFullScreen = (document['fullscreenElement'] || document['msFullscreenElement'] || document['mozFullScreenElement'] || document['webkitFullscreenElement']); video.notifyFullScreen(!!fFullScreen); }; document.addEventListener(sEvent, onFullScreenChange, false); break; } } for (i = 0; i < asWebPrefixes.length; i++) { sEvent = asWebPrefixes[i] + 'fullscreenerror'; if ('on' + sEvent in document) { var onFullScreenError = function() { video.notifyFullScreen(null); }; document.addEventListener(sEvent, onFullScreenError, false); break; } } } } /* * More gross code to handle pointer-locking support across all supported browsers. * * TODO: Consider "upgrading" this code to use the same asWebPrefixes array as above, especially once Microsoft * finally releases a browser that supports pointer-locking (post-Windows 10?) */ if (this.inputScreen) { this.inputScreen.onfocus = function onFocusScreen() { return video.onFocusChange(true); }; this.inputScreen.onblur = function onBlurScreen() { return video.onFocusChange(false); }; this.inputScreen.lockPointer = this.inputScreen['requestPointerLock'] || this.inputScreen['mozRequestPointerLock'] || this.inputScreen['webkitRequestPointerLock']; this.inputScreen.unlockPointer = this.inputScreen['exitPointerLock'] || this.inputScreen['mozExitPointerLock'] || this.inputScreen['webkitExitPointerLock']; if (this.inputScreen.lockPointer) { var onPointerLockChange = function() { var fLocked = ( document['pointerLockElement'] === video.inputScreen || document['mozPointerLockElement'] === video.inputScreen || document['webkitPointerLockElement'] === video.inputScreen); video.notifyPointerLocked(fLocked); }; if ('onpointerlockchange' in document) { document.addEventListener('pointerlockchange', onPointerLockChange, false); } else if ('onmozpointerlockchange' in document) { document.addEventListener('mozpointerlockchange', onPointerLockChange, false); } else if ('onwebkitpointerlockchange' in document) { document.addEventListener('webkitpointerlockchange', onPointerLockChange, false); } } } /* * As far as overall image quality of scaled fonts, these options don't seem necessary for Safari (and * don't have any discernible effect anyway). Turning 'webkitImageSmoothingEnabled' off DOES have an effect * on Chrome, but it's not really a positive effect overall, so I'm leaving these off for now. * * if (this.contextScreen) { * this.contextScreen['mozImageSmoothingEnabled'] = false; * this.contextScreen['webkitImageSmoothingEnabled'] = false; * } */ var sFileURL = parmsVideo['fontROM']; if (sFileURL) { var sFileExt = Str.getExtension(sFileURL); if (sFileExt != "json") { sFileURL = Web.getHost() + DumpAPI.ENDPOINT + '?' + DumpAPI.QUERY.FILE + '=' + sFileURL + '&' + DumpAPI.QUERY.FORMAT + '=' + DumpAPI.FORMAT.BYTES; } Web.getResource(sFileURL, null, true, function(sURL, sResponse, nErrorCode) { video.doneLoad(sURL, sResponse, nErrorCode); }); } } /** * 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) { 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) { var video = this; 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.textareaScreen? "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