--->also available in English.
TITEL: Win32 Assembler Tutorial Kapitel 2.718
AUTOR: T$

Win32 Assembler Tutorial Kapitel 2.718
(Wer diese Nummer nicht kennt hat was verpennt)

Hallo erstmal! Nachdem es in den ersten 2 Teilen um das Fundament eines Win32-Programmes ging, kommt jetzt ein richtiges Programm heraus (soll heißen es sieht nicht so aus wie ein Beispielprogramm :). Wie im letzten Teil versprochen gibts endlich auch Multimedia, genauer: Grafik + Gedröhn.

Der allgemeine Aufbau eines Win32-Programmes ist ja seit Teil 1.2beta bekannt. Damit der Code besser lesbar ist hab ich ihn zerteilt: Die *.asm-Datei ist wie im letzten Teil aufgebaut. Die Initialisation, die Aufräumarbeiten, die Hauptschleife und -da Windows īne Menge mit Messages macht - die Verarbeitung der Messages stecken jetzt in eigenen *.inc - Dateien. Eine eingedeutschte Version des Quellcodes gibt es diesmal nicht, der englische tut es ja auch.

Inhalt:

Ach so: Das Beispielprogramm sollte bei einer Farbtiefe von 32 Bit und ohne Soundausgabe (DAC) im Hintergrund gestarted werden, da die Fehlerbehandlung zugunsten der Übersichtlichkeit stark vereinfacht ist. Außerdem sollte man die Soundkarte mit CD-Audio, Line-In oder MIDI füttern, da man sonst praktisch nichts sieht.

1. Grafik mit Win32

Normalerweise benützt Windows für die Grafikausgabe GDI (ääh... das hat nichts mit Kane & Co zu tun ;-)). GDI ist auf Kompatibilität, nicht auf Geschwindigkeit ausgelegt, so daß die gesamte Grafikverarbeitung im Systemspeicher erfolgt. Die Pixel werden dann vom OS auf den Monitor gebracht, die Anwendung hat damit nichts zu tun.

Jetzt gibt es dazu DirectDraw, mit dem man wie mit VESA 2.0 den Grafikspeicher direkt ansprechen kann. Dazu kommen eine Menge weiterer Funktionen, vom Verändern der Palette über Hardware-Sprites in beliebiger Größe und mit Transparenz bis hin zu dem 3D-Kruscht.

1.1. DirectX und ASM

Klar, man braucht *.LIBīs und Header/Include - Dateien um DirectX zu benützen. Jedoch wird ASM von den Softwareherstellern kaum unterstützt. Und weil jeder Assembler seine eigenen Extrawürste bratet, paßt eine inc-Datei nicht zu allen. Im ZIP steckt eines für NASM und eines für TASM/MASM. Beide benennen die Funktionen und Konstanten ebenso wie in den SDK - Dateien, so daß es recht einfach ist, sie zu verwenden. Eine brauchbare Datei für DirectSound habe ich nicht gefunden, deshalb stecken die paar Definitionen und Structuren dafür direkt im Quellcode. So wie es aussieht wird Win32 mit Assembler immer beliebter, so daß sich das Problem mit den Includes mit der Zeit auflöst :-D

Da ein statisches Linken der DLL-Einsprungpunkte bei den vielen DirectX-Versionen Probleme erzeugte werden sie hier zur Laufzeit ermittelt. Ganz nebenbei braucht man dann keine zusätzlichen DirectX *.libīs mehr!

1.2. Bestimmen der Einsprungpunkte

Dieses Verfahren ist nicht so ungewöhnlich wie man denken könnte. Jeder kennt ein Programm das dies macht: Die WinAmp-Plugins haben alle dieselben Funktionsnamen, da die Funktionen aber in jeder DLL anders liegen kann man sie nicht statisch linken.

Der folgende Code lädt eine DLL (solange sie nicht schon geladen ist) und liefert ein Handle dafür zurück (in EAX):

          push offset NameDerZuLadendenDLL
          call LoadLibraryA
Der NameDerZuLadendenDLL ist eine nullterminierte Zeichenkette.

Mit dem Handle kann man die Eintrittspuntkte in EAX erhalten:
         push offset NameDerFunktionDieManAufrufenWill
         push HandleDerGeladenenDLL
         call [GetProcAddress]
Um die Funktion endlich aufzurufen reicht ein simples
         call eax
Man kann auch andere Dateien als DLLs verwenden, solange sie nur geeignete Funktionen enthalten. Statt dem Funktionsname kann man auch ein sog. Ordinal verwenden, eine Zahl die die Funktion eindeutig identifiziert. Es kann jedoch passieren, daß in einer anderen DLL-Version diese Zahlen durcheinander geraten, was mit den Namen wohl kaum passieren würde.

1.3. DirectX verwendet das COM

COM heißt Component Object Model, die neue API die in vielen neuen Windows-Funktionen verwendet wird. Es ist flexibler als die herkömmliche Variante. Die Parameterübergabe funktioniert wie gewohnt, nur der Aufruf funktioniert anders:

Wie der Name schon sagt ist das Ganze objektorientiert. Ein Objekt kann, z.B., die Grafikkarte repräsentieren. So ein Objekt hat mehrere Eigenschaften sowie eine Menge dazugehöriger Funktionen um es zu verwenden. Diese Funktionen stecken in einem sogenannten Interface. Ein Interface zeigt auf eine Tabelle (vtable genannt) welche mit den Eintrittsadressen seiner Funktionen gefüllt ist. Eine Funktion aufzurufen (die Funktionen heißen hier Methoden weil sie je nach Objekt und seiner Version sich anders verhalten) sieht nun so aus:

Das warīs. Es handelt sich in der Praxis um ein Zeigerkette. Hier ist ein Makro damit man das nicht ständig neu eintippen muß:
      DXfunction macro  interface , method
       mov edi,[interface]    ;edi = COM-Object (address)
       mov edi,[edi]          ;edi = VTable (address)
       mov edi,[edi+method]   ;edi = call-Ziel
       push [interface]
       call edi
      endm
Wie man sieht benötigt eine COM-Funktion auch den eigenen Interface - Wert. Das Makro erledigt dies nebenbei mit. Dies trifft auch auf C/C++ zu, deshalb sind die Parameter wie im SDK.

Da ein Objekt bzw. Interface von mehreren Programmen (oder mehrmals vom eigenen Code) benützt werden kann besitzen alle Interfaces zwei Methoden um einen internen Zähler zu bearbeiten. Jedesmal wenn ein Objekt benötigt wird erhöht AddRef den Zähler um 1, und jedesmal wenn es nicht mehr gebraucht wird, wird er mit Release erniedrigt. Wenn der Zähler 0 erreicht wird das Objekt vernichtet (mitsamt all seinen Unterobjekten).

2. Genug gelabert, ab die Post

Eines fehlt noch: Wie das allererste Objekt erschaffen wird. Am einfachsten geht dies mit der DirectDrawCreate Function von DDRAW.DLL (diese Funktion wird zur Laufzeit eingebunden, siehe oben).

Wenn das ganze geklappt hat bekommen wir den Pointer mit der Objektadresse versehen zurück.

So ein DirectDraw Objekt ist ziemlich öde. Es steht nur für die Grafikkarte. Interessanter ist jedoch der Grafikkartenspeicher. Ein Block des Grafikspeichers wird Surface genannt. Eine Surface bezeichnet nicht nur den Speicher, sondern es ist ein vollständiges Objekt mit Funktionen um es zu bearbeiten.

Mit einem Aufruf von IDirectDraw:CreateSurface (wobei IDirectDraw durch unser DirectDraw Objekt ersetzt wird) erzeugen wir eine Surface:

     push 0
     push offset ZeigerDerMitDerAdresseDesSurfaceObjektsGefülltWerdenSoll
     push offset ZeigerAufEineStrukturDieDieGewünschteArtDerSurfaceBeschreibt
     DXfunction UnserDirectDrawObjekt, CreateSurface
nun wird die Surface erzeugt und unser Zeiger ausgefüllt.

Der interessanteste Teil des Grafikspeichers ist der FrontBuffer, in DirectDraw PrimarySurface genannt. Die PrimarySurface ist der gesamte sichtbare Bereich auf dem Monitor, egal ob wir DirectDraw im Vollbild- oder im Fenstermodus verwenden. Deshalb entsprechen die Eigenschaften der PrimarySurface immer dem momentanen Grafikmodus.

Wenn sowohl im Fenstermodus als auch im Vollbild derselbe Speicher verwendet wird fragt man sich wo denn der Unterschied liegt. Es gibt nur einen kleinen, aber wichtigen Unterschied: Man kann die PrimarySurface mit einer anderen Surface mit denselben Eigenschaften vertauschen (sprich: Page Flipping) und man darf den Videomodus ändern (und somit auch die Eigenschaften der PrimarySurface).

Statt vom Fenster- und Vollbildmodus ist es sinnvoller von exklusiven und nichtexklusivem (kooperativen) Modus zu sprechen.

Die meisten Programme bereiten die Grafik in einem nicht sichtbaren Puffer vor anstatt direkt den Front Buffer zu modifizieren. Das Beispielprogramm benützt solch eine Backbuffer Surface indem eine Offscreen Surface mit der selben Farbtiefe wie die Primary Surface, aber einer anderen Größe verwendet wird: 256*256 Pixel.

Surfaces kann man auch für Texturen, Video, Lightmaps,... verwenden.

2.2 Ab mit dem Inhalt der Offscreen Surface auf die primäre: Blitten

Blitten (eine auch in GDI langbekannte Funktion) ist nicht mehr als eine Sprite-Animation. Mit Blitten bekommt man den Inhalt einer Surface in eine andere (oder dieselbe). Man kann auch nur einen Teil einer Surface kopieren, oder nur in einen bestimmten Teil einer anderen. Transparenz wird über eine Schlüsselfarbe ermöglicht. Ein Blit kann auch nur dazu dienen, eine Fläche einzufärben. Der Vorteil des Blittens gegenüber Software ist daß das meiste in Hardware möglich ist (die sch... Voodoo2 macht aber das meiste in Software).

Skalieren? Kopieren in einen bestimmten Teil einer Surface? Damit bekommt man ideal den Backbuffer auf die Primary Surface, da wir die Grafik nur in unserem Fenster sehen wollen. Dazu muß man der Blitterfunktion nur die Abmessung und die Größe des Fensters angeben.

Größenanpassung und Transparenz kann im Beispielprogramm per Menü an- und ausgeschaltet werden.

2.3 Ansprechen des Grafikspeichers

Schön, wir haben den Grafikspeicher angefordert. Nur kann man damit nichts anfangen solange man die Position des Grafikspeichers im linearen 32Bit-Adressbereich nicht kennt. Ein simpler Aufruf der Lock-Funktion der Surface gibt die Adresse aus und erlaubt dem Programm, den Speicherbereich zu beschreiben. Ein weiterer Wert wird zurückgeliefert, der sogenannte Pitch. Er gibt die Anzahl von Bytes an, um die jede Bildschirmzeile verlängert ist bevor die neue Zeile anfängt. Den Pitch sollte man nicht beschreiben, da er ein Teil einer anderen Surface oder als Cache,... verwendet werden könnte.

Achtung: Die Lock-Funktion sperrt nicht nur den Speicherbereich, sondern verhindert auch, daß andere Anwendungen und GDI darauf zugreifen können. Der Code zwischen Lock und den dazugehörigen Unlock ist nicht unkritisch und sollte möglichst kurz sein. Jeder Fehler hier kann schwerwiegende Probleme verursachen.

2.4. Clipper

Ein Clipper verhindert, daß ein oder mehrere rechteckige Felder einer Surface verändert werden. Das Hauptanwendungsgebiet liegt sowohl darin, Blits, die über eine Kante einer Surface hinausgehen passend zu beschneiden oder einen Blit auf den Bereich eines Programmfensters zu begrenzen. Das Beispielprogramm macht das zweite indem dem Clipper-Objekt das Handle des Programmfensters übergeben wird. Der Rest geht dann von selbst, inklusive Verschieben und Größenveränderungen. Schaut mal an wie das ganze aussieht, wenn der Code für den Clipper auskommentiert wird.

3. Grafik ist ein Medium - Audio ein anderes

DirectSound funktioniert ähnlich wie DirectDraw:
DirectSoundCreate aufrufen oder DirectSoundCaptureCreate anstelle von DirectDrawCreate und statt GraphicSurfaces erzeugt man SoundBuffer. Diese werden wie in DirectDraw mit Lock gesperrt.

Das besondere an SoundBuffern ist (was denn sonst :-)) daß sie sich von selbst verändern da die Soundkarte mit ihrer eigenen Geschwindigkeit darauf zugreift (geht ja kaum anders, wenn man keine Störungen im Audiodatenstrom haben will).

Ein SoundBuffer braucht nur bis zu seinem Ende abgespielt oder bespielt werden. Meist benötigt man jedoch einen permanenten Puffer, der, wenn die Endposition erreicht wurde, wieder von vorne anfängt.

Das bedeutet, daß man aufpassen muß welchen Teil des Puffers man sperrt. Wenn sowohl das Programm als auch die Soundkarte auf denselben Speicher zugreifen gibt es natürlich Störungen. Das Beispielprogramm ermittelt eine geeignete Leseposition, indem die aktuelle Aufnahmeposition ermittelt wird und nur der Bereich dahinter gesperrt wird. Eine andere Möglichkeit wäre, DirectSound unser Programm jedesmal darüber zu informieren wenn eine bestimmte Pufferposition erreicht wird (die Notify-Funktion erledigt dies).

Jedes DirectSound-Objekt verfügt über einen PrimaryBuffer, jeder besitzt soviele SecondaryBuffer wie man möchte. Die SecondaryBuffers werden von DirectSound in Hardware oder in Software in den primären abgemischt. Es gibt jedoch nur einen einzigen Aufnahmepuffer für jedes DirectSoundCapture-Objekt (hmmm... villeicht erfindet mal jemand einen Entmischalgorithmus).

4. Der Main Loop im Beispielprogramm...

...funktioniert so:

5. Was man noch wissen sollte

5.1. Cursor und Blitten

Wahrscheinlich ist einigen aufgefallen, daß der Mauszeiger auf einigen Systemen nicht zu sehen ist wenn er das Fenster des Beispielprogrammes durchquert. Dies ist normal, da der Cursor so oft wie möglich durch den Blit überschrieben wird. Und der Clipper entfernt den Cursor während des Blittens um Darstellungsfehler zu verhindern. Eine mögliche Lösung wäre, z.B., weniger häufig zu blitten.

5.2. Fehlerbehandlung

Das Beispielprogramm verfügt nur über rudimentäre Fehlerbehandlungsroutinen: Es zeigt einfach an, wenn ein Fehler erfolgt und beendet sich dann. Die Lock-Funktionen sind die einzige Ausnahme, da es durchaus wahrscheinlich ist, daß der Speicherbereich momentan nicht für uns gesperrt werden kann, so daß hier einfach ein neuer Versuch im nächsten Schleifendurchgang gemacht wird.

Wer mit dem Beispiel herumexperimentiert wird feststellen, daß unter bestimmten Umständen Fehler auftreten: Wechsel des Grafikmoduses (auch durch Bildschirmschoner),... Wenn der Grafikmodus wechselt verschwindet der gesamte Speicher der Surfaces, und ein DDSERR_SURFACELOST wird zurückgegeben. Um weiterzumachen muß man nun IDirectDraw:Restore aufrufen, um sie zurückzuerhalten. Das kann auch im exklusiven Modus vorkommen: Man denke nur an Alt-Tab...

Wenn man DirectX effektiver nützt als hier gezeigt, werden einem viele unkritische Fehlermeldungen begegnen: Ein Blit ist noch nicht fertig, eine andere Funktion läuft noch ab,... Man könnte hier warten, bis die Fehlermeldung verschwindet, sinnvoller ist es jedoch die Rechnezeit für andere Dinge nützen. In Verbindung mit Hardwarebeschleunigung kann das den Code eine Menge optimieren.

5.3. Verbesserungen im Main Loop

Der Main Loop ist nicht optimal: Er schluckt die gesamte Rechenzeit, die er kriegen kann, selbst wenn man das Fenster nicht sieht oder wenn es minimiert ist. Manchmal frißt es dermaßen Rechenzeit, daß andere Programme wie aufgehängt wirken, besonders wenn mehrere Programme die PrimarySurface beschreiben wollen. Deshalb sollte noch die Darstellungsrate daran angepaßt werden, ob das Fenster aktiv ist oder im Hintergrund oder gar minimiert. Die optimale Lösung wäre, den Graphiccode von der Message-Bearbeitung in einem Extrathread zu trennen und im Hauptthread nur GetMessage zu verwenden.

P.S.: DirectSound erzeugt immer einen eigenen Thread für die Aufnahme, da das Hauptprogramm die DirectSound-Funktionen nicht unbedingt regelmäßig aufrufen muß (etwa während Änderungen an der Fenstergröße und Position stattfinden).

5.4. Unterstützung mehrerer Geräte

Größere Programme sollten den Benützer zwischen seinen verschiedenen Sound- und Grafikkarten auswählen lassen bevor das DirectDraw- oder DirectSound-Objekt erzeugt wird. Falls sich jemand fragt, wer 2 Grafikkarten hat: Man denke nur an die 3D-Zusatzkarten oder TwinView...

Der Parameter für die DirectXXXCreate-Funktionen wird mit DirectDrawEnumerate und DirectSoundEnumerate bestimmt. Dies sind übrigens keine COM-, sondern Standardfunktionen.

5.5. Hardwarebeschleunigung, Emulation und nichtunterstützte Funktionen

Kann ein Treiber eine gewisse Funktion nicht ausführen, emuliert ihn DirectX in Software. Viele weniger gebräuchliche Funktionen werden jedoch nicht emuliert. Deshalb gibt es Funktionen, die auf allen Systemen vorhanden sind (die, welche auch in Software vorhanden sind) und andere, auf die man sich nicht verlassen sollte. Entweder schreibt man für sie eine eigene Emulation oder man läßt sie einfach bleiben, wenn es keine Hardware dafür gibt. Interpolation ist z.B. eine Funktion, die nicht immer verfügbar ist. Da sie nur die Qualität verbessert kann man das ganze auch pixelig anzeigen...

5.6. Wie man neue DirectX-Versionen benützt

Möchte man die neuen Möglichkeiten neuerer DirectX-Versionen nützen braucht man ein neues Interface das diese unterstützt. Jedes Interface besitzt eine Zahl die es identifiziert: Die IID - Werte. Sie stecken in den DirectX-Header-Dateien. Der folgende Codeschnipsel zeigt wie man eine neueres Interface erhält:

    push offset ZeigerDerMitDerAdresseEinerNeuerenVersionDesObjektsGefülltWerdenSoll
    push offset IIDderNeuerenVersionDesInterfacesDesObjekts
    DXfunction ZeigerDerDieältereVersionDesObjektsBeinhaltet , QueryInterface
Wenn EAX auf 0 gesetzt wurde hat alles geklappt. Das alte Objekt kann man jetzt wegwerfen, wir haben ja ein neues:
    DXfunction ZeigerDerDieältereVersionDesObjektsBeinhaltet , Release
Solange das alte Interface nicht noch irgendwo verwendet wird erreicht sein Referenzzähler den Wert 0 und es wird automatisch vernichtet.

Es gibt noch einen anderen Weg, Objekte zu erschaffen als DirectDrawCreate, DirectSoundCreate, etc, zu benützen (welche immer die älteste Version des Objekts liefern):

Mit den grundlegenden COM-Funktionen kann man auch DirectX-Objekte erschaffen (diese Funktionen stecken in ole32.dll). Hier ist der Quellcode:
       push 0
       call CoInitialize
Diese Funktion muß man nur einmal aufrufen. Am Ende des Programmes ruft man passend dazu CoUninitialize auf (Parameter braucht man hier keine).

Um ein Objekt mit einem bestimmten Interface zu erschaffen nimmt man:
     push offset ZeigerDerMitDerAdresseDesErzeugtenObjektsGefülltWerdenSoll
     push offset IIDdesGewünschtenInterfaces ;steht in den DirectX Headern
     push CLSCTX_ALL      ;laufe in allen Kontexten, ist hier nicht so wichtig
     push 0               ;kein "controlling unknown" verfügbar, deshalb einfach nur 0
     push offset CLSIDdesObjekts         ;steht in den DirectX Headern
     call CoCreateInstance
Letzter Schritt: Initialisieren des Objekts:
     push 0
     DXfunction ZeigerDerMitDerAdresseDesErzeugtenObjektsGefülltWerdenSoll, Initialize
Wenn man auf diese Art Objekte erzeugt braucht man keine älteren Versionen mehr wegzuwerfen. Einige Teile von DirectX wie DirectMusic können nur auf diese Weise initialisiert werden. Bei anderen, wie z.B. bei Direct3D8/Direct3D9, kann man aber auch direkt per Create-Funktion die aktuelle Version verwenden.

6. Jetzt reichts, Schluß für Heute

Auch wenn DirectX auf den ersten Blick etwas ungewohnt aussieht: Sobald man sich dran gewöhnt hat ist es einfacher als das Standard API. Bis im nächsten Teil - codet bis dahin bis die Tastatur kracht!

Mail an den Autor: webmeister@deinmeister.de

Hauptseite Programmieren Win32Asm Downloads Software Hardware Cartoons+Co Texte Sitemap