Fix EGA scrolling for modes 0x0D and 0x0E

This commit is contained in:
Jeff Parsons 2015-06-05 17:26:08 -07:00
commit 38f16fcc50
13 changed files with 3322 additions and 3077 deletions

180
blog/2015/06/05/README.md Normal file
View file

@ -0,0 +1,180 @@
The Strange Case of the EGA Graphics Scroll Bug
---
For reasons I've forgotten, I was playing with different video modes in this
[IBM PC AT w/EGA](/devices/pc/machine/5170/ega/640kb/rev1/) machine, and I discovered some odd problems.
For example, run this code:
A>b:debug
-a
0CE0:0100 mov ax,e
0CE0:0103 int 10
0CE0:0105 int 3
0CE0:0106
-g
and the following "text" will appear at the top of the screen, in 640x200 16-color graphics mode 0x0E:
AX=0B01 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0CE0 ES=0CE0 SS=0CE0 CS=0CE0 IP=0105 NV UP EI PL NZ NA PO NC
0CE0:0105 CC INT 3
-
If I typed "q", then "cls" and finally "dir", the screen would properly fill with DOS directory contents.
However, as soon as the screen started to scroll, the screen contents became garbled. Other EGA graphics modes,
like 640x350 16-color mode 0x10, didn't have this problem.
To investigate, I set a breakpoint in the IBM EGA ROM where the scrolling starts, at 0xC000:12EA (see p.130 of the
"IBM Enhanced Graphics Adapter" Technical Reference document):
CRANK_A:
PUSH CX
MOV CL,DL
SUB CH,CH
PUSH SI
PUSH DI
REP MOVSB
POP DI
POP SI
ADD SI,BX
ADD DI,BX
POP CX
LOOP CRANK_A
CX arrives as 0xC0 (192), which is the number of scan-lines to move up, and BX is 0x50 (80), the number
of bytes per scan-line.
When the breakpoint was hit, I dumped the video hardware state, using the Debugger's "*d video*" command.
For reference purposes, I've pasted the corresponding video state for mode 0x10 on the right-hand side.
breakpoint hit: C000:12EA (exec)
stopped (175707689 ops, 800060264 cycles, 133470 ms, 5994308 hz)
AX=020F BX=0050 CX=00C0 DX=1950 SP=0B58 BP=0511 SI=0280 DI=0000
SS=011F DS=A000 ES=A000 PS=0246 V0 D0 I1 T0 S0 Z1 A0 P1 C0
C000:12EA 51 PUSH CX
BIOSMODE: 0x0E BIOSMODE: 0x10
CRTC[0x00]: HORZ_TOTAL 0x70 CRTC[0x00]: HORZ_TOTAL 0x5B
CRTC[0x01]: HORZ_DISP_END 0x4F CRTC[0x01]: HORZ_DISP_END 0x4F
CRTC[0x02]: HORZ_BLANK_START 0x59 CRTC[0x02]: HORZ_BLANK_START 0x53
CRTC[0x03]: HORZ_BLANK_END 0x2D CRTC[0x03]: HORZ_BLANK_END 0x37
CRTC[0x04]: HORZ_RETRACE_START 0x5E CRTC[0x04]: HORZ_RETRACE_START 0x52
CRTC[0x05]: HORZ_RETRACE_END 0x06 CRTC[0x05]: HORZ_RETRACE_END 0x00
CRTC[0x06]: VERT_TOTAL 0x04 CRTC[0x06]: VERT_TOTAL 0x6C
CRTC[0x07]: OVERFLOW 0x11 CRTC[0x07]: OVERFLOW 0x1F
CRTC[0x08]: PRESET_ROW_SCAN 0x00 CRTC[0x08]: PRESET_ROW_SCAN 0x00
CRTC[0x09]: MAX_SCAN_LINE 0x00 CRTC[0x09]: MAX_SCAN_LINE 0x00
CRTC[0x0A]: CURSOR_START 0x00 CRTC[0x0A]: CURSOR_START 0x00
CRTC[0x0B]: CURSOR_END 0x01 CRTC[0x0B]: CURSOR_END 0x01
CRTC[0x0C]: START_ADDR_HI 0x00 CRTC[0x0C]: START_ADDR_HI 0x00
CRTC[0x0D]: START_ADDR_LO 0x00 CRTC[0x0D]: START_ADDR_LO 0x00
CRTC[0x0E]: CURSOR_ADDR_HI 0x07 CRTC[0x0E]: CURSOR_ADDR_HI 0x01
CRTC[0x0F]: CURSOR_ADDR_LO 0x80* CRTC[0x0F]: CURSOR_ADDR_LO 0x41*
CRTC[0x10]: VERT_RETRACE_START 0xE0 CRTC[0x10]: VERT_RETRACE_START 0x5E
CRTC[0x11]: VERT_RETRACE_END 0x23 CRTC[0x11]: VERT_RETRACE_END 0x2B
CRTC[0x12]: VERT_DISP_END 0xC7 CRTC[0x12]: VERT_DISP_END 0x5D
CRTC[0x13]: OFFSET 0x28 CRTC[0x13]: OFFSET 0x28
CRTC[0x14]: UNDERLINE 0x00 CRTC[0x14]: UNDERLINE 0x0F
CRTC[0x15]: VERT_BLANK_START 0xDF CRTC[0x15]: VERT_BLANK_START 0x5F
CRTC[0x16]: VERT_BLANK_END 0xEF CRTC[0x16]: VERT_BLANK_END 0x0A
CRTC[0x17]: MODE_CTRL 0xE3 CRTC[0x17]: MODE_CTRL 0xE3
CRTC[0x18]: LINE_COMPARE 0xFF CRTC[0x18]: LINE_COMPARE 0xFF
STATUS1: 0x01 STATUS1: 0x01
ATCDATA: true ATCDATA: true
ATC[0x00]: PAL00 0x00 ATC[0x00]: PAL00 0x00
ATC[0x01]: PAL01 0x01 ATC[0x01]: PAL01 0x01
ATC[0x02]: PAL02 0x02 ATC[0x02]: PAL02 0x02
ATC[0x03]: PAL03 0x03 ATC[0x03]: PAL03 0x03
ATC[0x04]: PAL04 0x04 ATC[0x04]: PAL04 0x04
ATC[0x05]: PAL05 0x05 ATC[0x05]: PAL05 0x05
ATC[0x06]: PAL06 0x06 ATC[0x06]: PAL06 0x14
ATC[0x07]: PAL07 0x07 ATC[0x07]: PAL07 0x07
ATC[0x08]: PAL08 0x10 ATC[0x08]: PAL08 0x38
ATC[0x09]: PAL09 0x11 ATC[0x09]: PAL09 0x39
ATC[0x0A]: PAL0A 0x12 ATC[0x0A]: PAL0A 0x3A
ATC[0x0B]: PAL0B 0x13 ATC[0x0B]: PAL0B 0x3B
ATC[0x0C]: PAL0C 0x14 ATC[0x0C]: PAL0C 0x3C
ATC[0x0D]: PAL0D 0x15 ATC[0x0D]: PAL0D 0x3D
ATC[0x0E]: PAL0E 0x16 ATC[0x0E]: PAL0E 0x3E
ATC[0x0F]: PAL0F 0x17 ATC[0x0F]: PAL0F 0x3F
ATC[0x10]: MODE 0x01 ATC[0x10]: MODE 0x01
ATC[0x11]: OVERSCAN 0x00 ATC[0x11]: OVERSCAN 0x00
ATC[0x12]: PLANES 0x0F ATC[0x12]: PLANES 0x0F
ATC[0x13]: HORZPAN 0x00 ATC[0x13]: HORZPAN 0x00
GRC[0x00]: SRESET 0x00 GRC[0x00]: SRESET 0x00
GRC[0x01]: ESRESET 0x00 GRC[0x01]: ESRESET 0x00
GRC[0x02]: COLORCMP 0x00 GRC[0x02]: COLORCMP 0x00
GRC[0x03]: DATAROT 0x00 GRC[0x03]: DATAROT 0x00*
GRC[0x04]: READMAP 0x00 GRC[0x04]: READMAP 0x00
GRC[0x05]: MODE 0x11* GRC[0x05]: MODE 0x00
GRC[0x06]: MISC 0x05 GRC[0x06]: MISC 0x05
GRC[0x07]: COLORDC 0x0F GRC[0x07]: COLORDC 0x0F
GRC[0x08]: BITMASK 0xFF GRC[0x08]: BITMASK 0xFF
SEQ[0x00]: RESET 0x03 SEQ[0x00]: RESET 0x03
SEQ[0x01]: CLOCKING 0x01 SEQ[0x01]: CLOCKING 0x01
SEQ[0x02]: MAPMASK 0x0F* SEQ[0x02]: MAPMASK 0x0F*
SEQ[0x03]: CHARMAP 0x00 SEQ[0x03]: CHARMAP 0x00
SEQ[0x04]: MEMMODE 0x06 SEQ[0x04]: MEMMODE 0x06
FEAT: 0x02 FEAT: 0x02
MISC: 0x23 MISC: 0xA7
STATUS0: 0x10 STATUS0: 0x10
LATCHES: 0x00000000 LATCHES: 0x00000000
ACCESS: 0x1411 ACCESS: 0x0400
One of the apparent oddities is that, for mode 0x0E, GRC[MODE] has been programmed with 0x11, whereas
for mode 0x10, it was programmed with 0x00. Why would mode 0x0E want to set the ODDEVEN bit during the scroll,
when it hadn't been set during any other writes to the screen?
At this point, I dumped the instruction history buffer a bit ("*dh 100*"), and noticed this GRC write:
C000:1582 8BC5 MOV AX,BP ;history=33
C000:1584 B603 MOV DH,03 ;history=32
C000:1586 B2CE MOV DL,CE ;history=31
C000:1588 E88AF7 CALL 0D15 ;history=30
C000:0D15 86C4 XCHG AL,AH ;history=29
C000:0D17 EE OUT DX,AL ;history=28
C000:0D18 42 INC DX ;history=27
C000:0D19 86C4 XCHG AL,AH ;history=26
C000:0D1B EE OUT DX,AL ;history=25
C000:0D1C 4A DEC DX ;history=24
So I looked back farther and saw where BP was set:
C000:1522 BA00A0 MOV DX,A000 ;history=97
C000:1525 BD1105 MOV BP,0511 ;history=96
Here's the complete function:
GR_ST_1:
MOV DX,A000
MOV BP,0511
CMP AH,0F
JC 1535
CALL 14F7
JNC 1535
MOV BP,0501
RET
OK, so any (EGA) graphics mode below 0x0F is going to the trigger the use of Write Mode 1 with the ODDEVEN bit set.
And sure enough, the scrolling bug also occurs when using 320x200 16-color mode 0x0D.
It's also worth noting that, whenver the ODDEVEN bit of the GRC Mode Register is set, the SEQUENTIAL bit in the Sequencer
Memory Mode Register is supposed to be clear (and vice versa -- those two bits are always supposed to be oppositely set).
But here, the IBM EGA BIOS hasn't done that. One wonders if that was a mistake....
Another bit of trivia: while dumping the frame buffer in a VGA text mode in a different emulator, I discovered that
if I turned off the ODDEVEN bit in the GRC Mode Register, odd bytes would still appear from plane 1; it wasn't until I
*also* turned off the CHAIN bit in the GRC Miscellaneous Register that the odd bytes would no longer appear. But,
that could have just been an idiosyncrasy of that particular emulator.
As a result of all these observations, and more importantly, to make EGA scrolling work properly in modes 0x0D and 0x0E,
I've changed the Video component to use odd/even memory functions *only* when the SEQUENTIAL bit (bit 2) of the
Sequencer's Memory Mode Register is clear, instead of relying on the ODDEVEN bit (bit 4) of the Graphics Controller's Mode
Register.
To be continued.... because I've barely scratched the surface of all the side-effects of EGA/VGA odd/even addressing,
and I hope to do some testing on real hardware in the near future.
*[@jeffpar](http://twitter.com/jeffpar)*
*June 5, 2015*

View file

@ -1,12 +1,18 @@
IBM PC Machine Configurations
---
Here you'll find sample Machine Configurations for all the IBM PC models that PCjs currently supports.
PCjs supports the following IBM PC models:
* [Model 5150](/devices/pc/machine/#model-5150-machine-configurations)
* [Model 5160](/devices/pc/machine/#model-5160-machine-configurations)
* [Model 5170](/devices/pc/machine/#model-5170-machine-configurations)
Configurations for the [Challenger 1P](/devices/c1p/machine/) and other non-IBM machines are available [here](/devices/).
Support is also being added for a few IBM PC-compatible machines, starting with the [Compaq DeskPro 386](compaq/deskpro386/) series.
---
### Model 5150 Machine Configurations
* [IBM PC, MDA, 64K](/devices/pc/machine/5150/mda/64kb/)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -240,6 +240,8 @@ var asFilesNonServed = [
"web.config" // Azure/IISNode-specific
];
var nBlogExcerpts = 20;
/**
* HTMLOut()
*
@ -1167,10 +1169,10 @@ HTMLOut.prototype.getBlog = function(sToken, sIndent, aParms)
* strings) that can hold the excerpts. We'll plug the excerpts into aExcerpts as they
* come in, and then we'll assemble them all at the end.
*
* We also take this opportunity to cap the number of (most recent) excerpts at 10.
* We also take this opportunity to cap the number of (most recent) excerpts.
*/
var i;
var cExcerpts = Math.min(asPaths.length, 10);
var cExcerpts = Math.min(asPaths.length, nBlogExcerpts);
var aExcerpts = new Array(cExcerpts);
var cExcerptsRemaining = aExcerpts.length;
for (i = 0; i < cExcerpts; i++) {

View file

@ -4499,7 +4499,7 @@ if (DEBUGGER) {
this.println("\ninput commands:");
this.println("\ti [p]\tread port [p]");
/*
* NOTE: Regarding this warning, it might be nice if we had an "unchecked" version of
* TODO: Regarding this warning, consider adding an "unchecked" version of
* bus.checkPortInputNotify(), since all Debugger memory accesses are unchecked, too.
*
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),
@ -4796,7 +4796,7 @@ if (DEBUGGER) {
this.println("\noutput commands:");
this.println("\to [p] [b]\twrite byte [b] to port [p]");
/*
* NOTE: Regarding this warning, it might be nice if we had an "Unchecked" version of
* TODO: Regarding this warning, consider adding an "unchecked" version of
* bus.checkPortOutputNotify(), since all Debugger memory accesses are unchecked, too.
*
* All port I/O handlers ARE aware when the Debugger is calling (addrFrom is undefined),

View file

@ -1342,7 +1342,7 @@ Card.SEQ = {
SELB_HIGH: 0x10, // VGA only
SELA_HIGH: 0x20 // VGA only
},
MODE: {
MEMMODE: {
INDX: 0x04, // Sequencer Memory Mode Register
ALPHA: 0x01, // set for alphanumeric (A/N) mode, clear for graphics (APA or "All Points Addressable") mode (EGA only)
EXT: 0x02, // set if memory expansion installed, clear if not installed
@ -1352,7 +1352,7 @@ Card.SEQ = {
TOTAL_REGS: 0x05
};
if (DEBUGGER) Card.SEQ.REGS = ["RESET","CLOCKING","MAPMASK","CHARMAP","MODE"];
if (DEBUGGER) Card.SEQ.REGS = ["RESET","CLOCKING","MAPMASK","CHARMAP","MEMMODE"];
/*
* VGA Digital-to-Analog Converter (DAC) Registers (regDACMask, regDACState, regDACAddr, and regDACData)
@ -1507,8 +1507,8 @@ if (DEBUGGER) Card.GRC.REGS = ["SRESET","ESRESET","COLORCMP","DATAROT","READMAP"
*
* 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
* The "EVENODD" functions deal with the EGA's default text-mode addressing, where EVEN addresses are mapped to
* plane 0 (and 2) and ODD addresses are mapped to plane 1 (and 3). This occurs when SEQ.MEMMODE.SEQUENTIAL
* is clear (and GRC.MODE.EVENODD is set), turning address bit 0 (A0) into a "plane select" bit. Whether A0 is
* also used as a memory address bit depends on CRTC.MODE_CTRL.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
@ -1599,6 +1599,11 @@ Card.ACCESS.readByteMode0 = function readByteMode0(off, addr)
*/
Card.ACCESS.readByteMode0EvenOdd = function readByteMode0EvenOdd(off, addr)
{
/*
* TODO: As discussed in getAccess(), we need to run some tests on real EGA/VGA hardware to determine
* exactly what gets latched (ie, from which address) when EVENODD is in effect. Whatever we learn may
* also dictate a special EVENODD function for Read Mode 1 as well.
*/
off += this.offset;
var idw = off & ~0x1;
var dw = this.controller.latches = this.adw[idw];
@ -1821,6 +1826,10 @@ Card.ACCESS.writeByteMode1 = function writeByteMode1(off, b, addr)
*/
Card.ACCESS.writeByteMode1EvenOdd = function writeByteMode1EvenOdd(off, b, addr)
{
/*
* TODO: As discussed in getAccess(), we need to run some tests on real EGA/VGA hardware to determine
* exactly where latches are written (ie, to which address) when EVENODD is in effect.
*/
off += this.offset;
//
// When even/odd addressing is enabled, nWriteMapMask must be cleared for planes 1 and 3 if
@ -2225,9 +2234,9 @@ Card.prototype.dumpCard = function()
this.dumpRegs(" FEAT", this.regFeat);
this.dumpRegs(" MISC", this.regMisc);
this.dumpRegs(" STATUS0", this.regStatus0);
this.dumpRegs(" LATCHES", this.latches);
this.dbg.println(" LATCHES: 0x" + str.toHex(this.latches));
this.dbg.println(" ACCESS: " + str.toHexWord(this.nAccess));
this.dbg.println("Use 'dump video buffer' to dump video memory");
this.dbg.println("Use 'dump video [addr]' to dump video memory");
/*
* There are few more EGA regs we could dump, like GRCPos1, GRCPos2, but does anyone care?
*/
@ -3836,9 +3845,38 @@ Video.prototype.getAccess = function()
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;
/*
* I discovered that when the IBM EGA ROM scrolls the screen in graphics modes 0x0D and 0x0E, it
* reprograms this register for WRITE_MODE1 (which is fine) *and* EVENODD (which is, um, very odd).
* Moreover, it does NOT make the complementary change to the SEQ.MEMMODE.SEQUENTIAL bit; under
* "normal" circumstances, those two bits are always supposed to programmed oppositely.
*
* Until I can perform some tests on real hardware, I have to assume that the EGA scroll operation
* is supposed to actually WORK in modes 0x0D and 0x0E, so I've decided to tie the trigger for my own
* EVENODD functions to SEQ.MEMMODE.SEQUENTIAL being clear, instead of GRC.MODE.EVENODD being set.
*
* It's also possible that my EVENODD read/write functions are not implemented properly; when EVENODD
* is in effect, which addresses get latched by a read, and to which addresses are latches written?
* If EVENODD has no effect on the effective address used with the latches, then I should change the
* EVENODD read/write functions accordingly.
*
* However, I've also done some limited testing with an emulated VGA running in text mode, and I've
* discovered that toggling the GRC.MODE.EVENODD bit *alone* doesn't seem to affect the delivery of
* text mode attributes from plane 1. So maybe this is the wiser change after all.
*
* TODO: Perform some tests on actual EGA/VGA hardware, to determine the proper course of action.
*
* if (regGRCMode & Card.GRC.MODE.EVENODD) {
* nReadAccess |= Card.ACCESS.READ.EVENODD;
* nWriteAccess |= Card.ACCESS.WRITE.EVENODD;
* }
*/
var regSEQMode = card.regSEQData[Card.SEQ.MEMMODE.INDX];
if (regSEQMode != null) {
if (!(regSEQMode & Card.SEQ.MEMMODE.SEQUENTIAL)) {
nReadAccess |= Card.ACCESS.READ.EVENODD;
nWriteAccess |= Card.ACCESS.WRITE.EVENODD;
}
}
nAccess = nReadAccess | nWriteAccess;
}
@ -5157,8 +5195,15 @@ Video.prototype.outSEQData = function(port, bOut, addrFrom)
}
this.cardEGA.regSEQData[this.cardEGA.regSEQIndx] = bOut;
}
if (this.cardEGA.regSEQIndx == Card.SEQ.MAPMASK.INDX) {
switch(this.cardEGA.regSEQIndx) {
case Card.SEQ.MAPMASK.INDX:
this.cardEGA.nWriteMapMask = Video.aEGAByteToDW[bOut & Card.SEQ.MAPMASK.MAPS];
break;
case Card.SEQ.MEMMODE.INDX:
this.setAccess(this.getAccess());
break;
default:
break;
}
};

View file

@ -22,7 +22,7 @@
<li><a href="/">Home</a></li>
<li><a href="/apps/pc/">Apps</a></li>
<li><a href="/disks/pc/">Disks</a></li>
<li><a href="/devices/">Machines</a></li>
<li><a href="/devices/pc/machine/">Machines</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/pubs/">Pubs</a></li>
<li><a href="/blog/">Blog</a></li>

View file

@ -22,7 +22,7 @@
<li><a href="/">Home</a></li>
<li><a href="/apps/pc/">Apps</a></li>
<li><a href="/disks/pc/">Disks</a></li>
<li><a href="/devices/">Machines</a></li>
<li><a href="/devices/pc/machine/">Machines</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/pubs/">Pubs</a></li>
<li><a href="/blog/">Blog</a></li>

View file

@ -20,7 +20,7 @@
<li><a href="/">Home</a></li>
<li><a href="/apps/pc/">Apps</a></li>
<li><a href="/disks/pc/">Disks</a></li>
<li><a href="/devices/">Machines</a></li>
<li><a href="/devices/pc/machine/">Machines</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/pubs/">Pubs</a></li>
<li><a href="/blog/">Blog</a></li>

View file

@ -20,7 +20,7 @@
<li><a href="/">Home</a></li>
<li><a href="/apps/pc/">Apps</a></li>
<li><a href="/disks/pc/">Disks</a></li>
<li><a href="/devices/">Machines</a></li>
<li><a href="/devices/pc/machine/">Machines</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/pubs/">Pubs</a></li>
<li><a href="/blog/">Blog</a></li>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff