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