VOGONS


First post, by riplin

User metadata
Rank Newbie
Rank
Newbie

Hi folks, I'm working on some VGA/SVGA programming (in particular, Matrox video cards).

I'm trying to understand the workings of all the registers in the CRT, Sequencer, etc. I figured that trying to translate those values back to human readable form that make sense when looking at timing documentation would be a good start.

So far, I've gotten most of the values to work, but I'm having some problems with the horizontal start / end blanking values. Here's an example for the blank start using 1600x1200@60Hz:

Standard VGA data from BIOS (in BDA format):

{
0x50, // Number of character columns
0x0B, // Number of screen rows - 1
0x08, // Character height in pixels
0x2000, // Video buffer size
{ 0x01, 0x0F, 0x00, 0x0E }, // Sequencer controller registers, starting at SR01
0x2F, //Miscellaneous output register
{ 0x09, 0xC7, 0xC7, 0x0D, 0xCF, 0x07, 0xE0, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0, 0x23, 0xAF, 0xC8, 0x00, 0xAF, 0xE1, 0xC3, 0xFF }, //CRT controller registers starting at CR00
{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x41, 0x02, 0x0F, 0x00 }, //Attribute controller registers starting at AR00
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x05, 0x0F, 0xFF } //Graphics controller registers starting at GR00
};

Matrox specific registers:
Horizontal counter extensions (CREXT01): 0x01
Vertical counter extensions (CREXT02): 0x2D

All the horizontal timings are in character clocks, so we need to know how many pixels there are to a character clock. This can be either 8 or 9 pixels.
The dot clock select is located in SR01<0> (Sequencer register 1, bit 0). SR01 = 0x01. Bit 0 is set to 1 and according to the docs that means a dot clock of 8. 8 pixels per character. Great.

I'm going to need horizontal display end to show the issue. This one lives in CR01, with a value of 0xC7. The docs say we need to add 1 to this to get the actual value used, so 0xC8 it is. In decimal that's 200. Multiply by the dot clock of 8 and we get a grand total of 1600 pixels.

Start horizontal blank lives in CR02 with a value of 0xC7, with an extension bit in CREXT01<1>. CREXT01 = 0x01, so bit 1 is 0, which leaves our value at 0xC7, which is 199 decimal and times 8 = 1592 pixels... Wait. Less than 1600? Was I supposed to add 1 here as well? Not according to the docs, so this is a bit confusing. Does Horizontal display end supersede this value? Why not just make them the same value?

The annoying part is, that it's not all modes. Some of them do have the same horizontal display end value and horizontal blank start value after conversion.

Here's the issue I have with horizontal blank end. This is the only mode that has a weird value.

80x60@59Hz (rendered as 640x480):

{
0x50, // Number of character columns
0x3B, // Number of screen rows - 1
0x08, // Character height in pixels
0x2580, // Video buffer size
{ 0x01, 0x03, 0x00, 0x02 }, // Sequencer controller registers, starting at SR01
0xEF, //Miscellaneous output register
{ 0x60, 0x4F, 0x50, 0x83, 0x52, 0x9E, 0x0B, 0x3E, 0x00, 0x47, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, 0xEB, 0x8C, 0xDF, 0x28, 0x1F, 0xE6, 0x06, 0xA3, 0xFF }, //CRT controller registers starting at CR00
{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x14, 0x07, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x0C, 0x00, 0x0F, 0x00 }, //Attribute controller registers starting at AR00
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x0E, 0x00, 0xFF } //Graphics controller registers starting at GR00
};

Matrox specific registers:
Horizontal counter extensions (CREXT01): 0x00
Vertical counter extensions (CREXT02): 0x00

Horizontal total is stored in CR00, which is 0x60, an extra bit lives in CREXT01<0>, but the entire register is 0x00, so nothing to add. Result ix 0x60 + 5 due to register quirk, decimal that's 101, times 8, is 808 pixels.

Horizontal start blank, again, lives in CR02 value is 0x50, with an extension bit in CREXT01<1>, which is 0, so final value is 80 decimal, or 640 in pixels. (see how this one matches the vertical resolution instead of 8 less?)

Horizontal blank end lives in CR03, which is 0x83. Only the lowest 5 bits count, so we and with 0x1F, giving us 0x03. CR05<7>, value 0x9E has bit 5, which is 1, so we get 0x23. Finally, bit 6 lives in the extension register CREXT01<6>, which is 0x00, so nothing to gain there. We have 7 bits total, making our correction factor 0x80 (lowest bit above our value). With a value of 0x23, it is less than the hblank start value of 0x50, (since we are looking for hblank start + hblank duration) so we add 0x80 to our value, giving 0xA3, which is 163 in decimal, or 1304 in pixels... which is waaaay, to0 high compared to the 808 horizontal total.

What am I missing?

Any help would be greatly appreciated.

Reply 1 of 3, by bakemono

User metadata
Rank Oldbie
Rank
Oldbie
riplin wrote on 2025-05-28, 01:19:

Horizontal blank end lives in CR03, which is 0x83. Only the lowest 5 bits count, so we and with 0x1F, giving us 0x03. CR05<7>, value 0x9E has bit 5, which is 1, so we get 0x23. Finally, bit 6 lives in the extension register CREXT01<6>, which is 0x00, so nothing to gain there. [...] With a value of 0x23,

I think the key to interpreting the H-blank End value is that it is compared to only the low bits of the character count. So in your example, the counter reaches 0x50 (pixel 640) where the active display ends and blanking begins. Then it precedes to 0x52 where H-sync begins. Then it will hit 0x5E, where the low 6 bits match the H-sync End value of 0x1E (from register 5). Finally, it will hit 0x63, where the low 6 bits match the H-blank End value of 0x23.

From what you said, it sounds like Matrox has added a 7th bit for H-blank End in some other register. My guess is that when it displays standard VGA resolutions, this extended bit remains inactive, and only the low 6 bits are compared.

GBAJAM 2024 submission on itch: https://90soft90.itch.io/wreckage

Reply 2 of 3, by riplin

User metadata
Rank Newbie
Rank
Newbie
bakemono wrote on 2025-05-28, 15:24:
riplin wrote on 2025-05-28, 01:19:

From what you said, it sounds like Matrox has added a 7th bit for H-blank End in some other register. My guess is that when it displays standard VGA resolutions, this extended bit remains inactive, and only the low 6 bits are compared.

That makes a whole lot of sense actually, since it's a text mode and not an MGA mode. Awesome, thank you! I'll adjust my code to check if mga mode is activated and adjust accordingly.

Reply 3 of 3, by riplin

User metadata
Rank Newbie
Rank
Newbie

And here's the result!

Every number is calculated off the register settings in the BIOS. I tried to make it look like the mode listings in the VESA spec.

Here's the ones i mentioned in my first post:

Timing Name         = 1600 x 1200 @ 60Hz;

Hor Pixels = 1600; // Pixels
Ver Pixels = 1200; // Pixels

Hor Frequency = 75.000; // kHz = 13.3 usec / line
Ver Frequency = 60.000; // Hz = 16.7 msec / frame

Pixel Clock = 162.000; // MHz = 6.2 nsec ± 0.5
Character Width = 8; // Pixels = 49.4 nsec
Scan Type = NONINTERLACED; // H Phase = ?
Hor Sync Polarity = POSITIVE; // HBlank = 25.9% of HTotal
Ver Sync Polarity = POSITIVE; // VBlank = 4.0% of VTotal

Hor Total Time = 13.333; // (usec) = 270 chars = 2160 Pixels
Hor Addr Time = 9.877; // (usec) = 200 chars = 1600 Pixels
Hor Blank Start = 9.877; // (usec) = 200 chars = 1600 Pixels
Hor Blank Time = 3.457; // (usec) = 70 chars = 560 Pixels
Hor Sync Start = 10.222; // (usec) = 207 chars = 1656 Pixels

// H Right Border = 0.000; // (usec) = 0 chars = 0 Pixels
// H Front Porch = 0.346; // (usec) = 7 chars = 56 Pixels
Hor Sync Time = 1.185; // (usec) = 24 chars = 192 Pixels
// H Back Porch = 1.926; // (usec) = 39 chars = 312 Pixels
// H Left Border = 0.000; // (usec) = 0 chars = 0 Pixels

Ver Total Time = 16.667; // (msec) = 1250 lines HT - (1.06xHA)
Ver Addr Time = 16.000; // (msec) = 1200 lines = 2.86
Ver Blank Start = 16.000; // (msec) = 1200 lines
Ver Blank Time = 0.667; // (msec) = 50 lines
Ver Sync Start = 16.000; // (msec) = 1200 lines

// V Bottom Border = 0.000; // (msec) = 0 lines
// V Front Porch = 0.000; // (msec) = 0 lines
Ver Sync Time = 0.040; // (msec) = 3 lines
// V Back Porch = 0.627; // (msec) = 47 lines
// V Top Border = 0.000; // (msec) = 0 lines

And the text mode that was giving me so much grief:

Timing Name         = 640 x 480 @ 59Hz;

Hor Pixels = 640; // Pixels
Ver Pixels = 480; // Pixels

Hor Frequency = 31.188; // kHz = 32.1 usec / line
Ver Frequency = 59.406; // Hz = 16.8 msec / frame

Pixel Clock = 25.200; // MHz = 39.7 nsec ± 0.5
Character Width = 8; // Pixels = 317.5 nsec
Scan Type = NONINTERLACED; // H Phase = ?
Hor Sync Polarity = NEGATIVE; // HBlank = 18.8% of HTotal
Ver Sync Polarity = NEGATIVE; // VBlank = 5.9% of VTotal

Hor Total Time = 32.063; // (usec) = 101 chars = 808 Pixels
Hor Addr Time = 25.397; // (usec) = 80 chars = 640 Pixels
Hor Blank Start = 25.714; // (usec) = 81 chars = 648 Pixels
Hor Blank Time = 6.032; // (usec) = 19 chars = 152 Pixels
Hor Sync Start = 26.032; // (usec) = 82 chars = 656 Pixels

// H Right Border = 0.317; // (usec) = 1 chars = 8 Pixels
// H Front Porch = 0.317; // (usec) = 1 chars = 8 Pixels
Hor Sync Time = 3.810; // (usec) = 12 chars = 96 Pixels
// H Back Porch = 1.905; // (usec) = 6 chars = 48 Pixels
// H Left Border = 0.317; // (usec) = 1 chars = 8 Pixels

Ver Total Time = 16.833; // (msec) = 525 lines HT - (1.06xHA)
Ver Addr Time = 15.390; // (msec) = 480 lines = 5.14
Ver Blank Start = 15.615; // (msec) = 487 lines
Ver Blank Time = 0.994; // (msec) = 31 lines
Ver Sync Start = 15.743; // (msec) = 491 lines

// V Bottom Border = 0.224; // (msec) = 7 lines
// V Front Porch = 0.128; // (msec) = 4 lines
Ver Sync Time = 0.032; // (msec) = 1 lines
// V Back Porch = 0.834; // (msec) = 26 lines
// V Top Border = 0.224; // (msec) = 7 lines

If anyone spots any errors, let me know. 😀

Edit: I updated the output after putting in some +1's here and there to make the numbers symmetric. Borders are now all the same. I think these tweaks are most likely Matrox specific though.