DOS Programmierung Tutorial – VGA
Ein- und Ausgabe ist ja schon ganz nett. Die Konsole ist aber als Ausgabe recht fad, DOS bietet uns viel mehr Möglichkeiten. Ein Highlight ist die VGA Ausgabe am Monitor, das was jedes bessere Spiel leistet. In diesem Tutorial schalten wir vom Text- in den Grafikmodus und zeichnen auf dem Bildschirm.
DOS Programmierung Tutorial – VGA
Die meisten C++ Lehrbücher kommen nicht weiter als bis zur Konsolenausgabe. Von Hello World weg wird nur noch die Programmiersprache gelehrt und im besten Fall noch wie man mit dem Speicher umgeht. Bereits das primitive DOS Betriebssystem bietet viel mehr. Man kann frei auf dem Bildschirm zeichnen, es gibt viele unterschiedliche Grafikmodi. In den Jahren bis zur letzten DOS Version 6 kamen immer neue, höhere Auflösungen mit immer mehr Farben dazu. DOS hat die Entwicklung von monochromen Bildschirmen bis zu den ersten 3D Visualisierungen mit VGA Grafik durchlebt. Ich zeige euch wie das mit C++ funktioniert.
vga.h?
Als C++ Programmierer ist man gewohnt für alles eine Header Datei zu haben. Können wir einfach die vga.h Datei verwenden? Nein, es gibt keine solche Datei! Wir brauchen die auch nicht, man kann von C++ aus direkt mit den Mitteln des Betriebssystems auf dem VGA Bildschirm zeichnen. Das Stichwort lautet Interrupts. Mit einem Interrupt hält man die Ausführung des aktuellen Programms am Prozessor an und führt eine kurze andere Tätigkeit durch.
Eine Übersicht möglicher Interrupts zeigt eine lange Liste von Interrupts, das für die VGA Grafik relevante ist Interrupt 10. Dort gibt es die Definitionen von für uns relevanten Interrupts:
- schalte den Grafikmodus um
- zeichne einfarbiges Pixel an einer bestimmten Koordinate
- ändere die aktuelle Farbpalette
Mit diesen Interrupts führt der Prozessor fix im BIOS definierte Routinen hardwarenahe aus. Das sind Basisfunktionalitäten, die schon zur Verfügung stehen bevor DOS geladen wird (z.B. für die Anzeige des BIOS).
Videomodus umschalten
Das mit den Interrupts klingt im ersten Moment furchtbar kompliziert. Tatsächlich ist das alles super einfach. Wir müssen nur die Nummer des Interrupts herausfinden den wir aufrufen wollen und die korrekten zusätzlichen Daten (sofern nötig) übergeben. Die Detailseite vom „Set Video Mode“ Interrupt zeigt folgende Detailinformation:
1 2 3 4 5 | AH = 00 AL = 00 40x25 B/W text (CGA,EGA,MCGA,VGA) = 01 40x25 16 color text (CGA,EGA,MCGA,VGA) = 02 80x25 16 shades of gray text (CGA,EGA,MCGA,VGA) ... |
Was genau ist AH und AL? Eine CPU hat Register um damit zu Rechnen, dort werden die Informationen zwischengespeichert. Seit dem 8086er sind diese Register 16 Bit groß und bestehen aus 2 Byte, so gesehen aus einem High-Byte und einem Low-Byte. Und genau das sagt AH und AL aus. Das High-Byte muss Hexadezimal 00 enthalten und das Low-Byte 01, dann wird bei einem „Set Video Mode“ Interrupt der Grafikmodus der Grafikkarte auf den monochronen 40×25 Zeilenmodus umgeschaltet. Andere Kombinationen bewirken andere Grafikmodi wie in der Liste ersichtlich. Ein einfaches Beispiel sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <conio.h> #include <dos.h> #define VIDEO_INT 0x10 /* grafik mode interrupt number */ #define SET_MODE 0x00 /* high byte */ #define VGA_320_200_256 0x13 /* low byte 320x200 256 colors */ #define VGA_640_480_16 0x14 /* low byte 640x480 16 colors */ typedef unsigned char byte; /* changes video mode with given low byte*/ void set_mode(byte mode) { union REGS regs; regs.h.ah = SET_MODE; /* set high byte of register */ regs.h.al = mode; /* set low byte of register */ int86(VIDEO_INT, &regs, &regs); } int main() { set_mode(VGA_320_200_256); getch(); return 0; } |
Das Programm schaltet auf einen bestimmten Video Mode und wartet auf einen Tastendruck. In der Funktion set_mode wird anhand eines übergebenen Bytes ein Register Objekt (REGS) gesetzt. Dabei wird wie in der Dokumentation erläutert das High- und das Low-Byte gesetzt. Die Funktion int86 aus dem dos.h Header führt den Interrupt VIDEO_INT mit den Daten im Register aus. Recht einfach oder?
Grafikmodus wählen
Welcher Grafikmodus hat für meinen Anwendungsfall am meisten Sinn? In Zeiten von DOS war der Speicher für die Grafikausgabe stark begrenzt. Man hatte im Prinzip die Wahl zwischen vielen Farben und geringerer Auflösung oder weniger Farben, dafür höhere Auflösung. Zwei mögliche Grafikmodi zeigen das Dilemma:
1 2 | = 12 640x480 16 color graphics (VGA) = 13 320x200 256 color graphics (MCGA,VGA) |
Ich möchte ein möglichst hochauflösende grafische Ausgabe entwickeln. Ich möchte aber auch viele Farben verwenden. Man muss sich für einen der beiden Modi entscheiden. Die Ausgabe muss Pixel x Pixel x Farbtiefe im RAM abgespeichert werden. Die Beispiele brauchen:
- 320x200x8 = 512 kB
- 640x480x4 = 1228 kB
Für 640×480 und 256 Farben würde man über 2,5 MB Speicher brauchen. Welche Grafikkarte hatte das damals schon? Heute bei 8 GB+ ist das witzig, aber bei so stark begrenzten Ressourcen ist die Wahl von einer bestimmten Farbtiefe bereits eine starke Limitierung der Auflösung und umgekehrt.
Zeichnen
Das Umschalten des Grafikmodus ist erst die halbe Geschichte. Wir wollen doch auch Zeichnen und genau dafür verwenden wir weitere Interrupts. Wir beginnen damit einige weitere Dinge anzulegen bzw. zu definieren:
1 2 3 4 5 6 7 | #define SCREEN_HEIGHT 200 #define SCREEN_WIDTH 320 byte far *VGA = (byte far *)0xA0000000L; #define SETPIX(x,y,c) *(VGA+(x)+(y)*SCREEN_WIDTH)=c #define GETPIX(x,y) *(VGA+(x)+(y)*SCREEN_WIDTH) |
Die Dimensionen des Bildschirms werden wir noch viele Male benötigen. Mit den Konstanten für WIDTH und HEIGHT kann man in 2 Schleifen alle Pixel des Bildes durchgehen. Die als Makro definierten Funktionen SETPIX und GETPIX setzen die Farbe eines Pixels bzw. fragt dessen Farbe ab. Spannend ist das Konstrukt *(VGA+(x)+(y)*SCREEN_WIDTH). Bevor ich das näher beschreibe müssen wir erst einmal die Variable VGA verstehen. Es handelt sich in diesem Fall um einen Pointer. VGA zeigt auf den Speicherbereich des Grafikkarte. Der beginnt bei A000 0000. Das ist eine 2 mal 16 Bit große Adresse. Ein normaler Pointer ist nur 16 Bit groß, weshalb wir den DOS proprietären far Pointer verwenden.
Nun sollte auch der Code von SETPIX verständlicher sein. Wir starten bei der Adresse 0 vom VGA Speicherbereich und hüpfen zum übergebenen Pixel. Wobei x+y*SCREENWIDTH die 1d Repräsentation eines Pointers eines 2d Arrays entspricht. SETPIX weist den Wert der Farbe zu, GETPIX gibt den dort stehenden Wert zurück. Die draw_background Funktion ist fast selbsterklärend. Anstatt einer Farbe übergeben wir die Zählvariable y, damit wir in jeder Zeile eine andere Farbe haben.
1 2 3 4 5 6 7 8 9 10 11 12 | void draw_background() { int x,y; for(y=0;y<SCREEN_HEIGHT;++y) { for(x=0;x<SCREEN_WIDTH;++x) { SETPIX(x,y,y); } } } |
Das Ergebnis verblüfft uns etwas…warum hat jede Zeile eine andere Farbe? Wenn man an das Farbenrad denkt sollte doch ein Farbverlauf entstehen der von blau nach rot geht oder so ähnlich.
Tatsächlich handelt es sich bei der übergebenen Farbe nicht um einen RGB Wert, sondern um einen Index auf eine Farbpalette. Die Standard Farbpalette für diesen Grafikmodus sieht eben so aus. Es gibt einen Farbverlauf, der mehrmals hintereinander in unterschiedlichen Sättigungen im Speicher steht. Dazwischen findet man auch mal einen Verlauf von schwarz nach weiß. Was bedeutet für uns nun: wir haben zwar eine begrenzte Auswahl an Farbtöpfen, wir dürfen diese aber mit beliebigen Farben füllen. Spiele machen davon exzessiv Gebrauch und schaffen so selbst durch begrenzte Anzahl von Farben eine schöne Darstellung.
Fazit
Dieser Artikel zu VGA unter MS DOS war etwas umfangreicher. Wir haben gesehen, dass es relativ simpel ist auf den Bildschirm zu zeichnen. In weiterer Folge sehen wir uns an wie man die Farbpalette mit neuen Farben ausstattet.
Hallo,
das sind Fehler im Artikel:
zu:
— snip —
Die Beispiele brauchen:
320x200x8 = 512 kB
640x480x4 = 1228 kB
Für 640×480 und 256 Farben würde man über 2,5 MB Speicher brauchen. Welche Grafikkarte hatte das damals schon?
— snip —
Nein. 320×200 in 8Bit Farbtiefe belegen 64kBytes und nicht 512kBytes.
Und 640×480 in 4Bit Farbtiefe belegen 150kBytes und nicht 1228kBytes.
Und 640×480 in 8Bit Farbtiefe belegen 300kBytes und nicht 2,5MBytes.
Die meisten Full VGA-fähigen ISA-Grafikkarten hatten doch damals nur eine RAM-Speicher von 512kB.
Hier wurden Bits mit Bytes verwechselt. 8Bits = 1Byte.