EGL, OpenGL-ES und Linux

Der kleinste gemeinsame Nenner aller Linux Systeme in Sachen OpenGL ist - so mein aktueller Stand - OpenGL ES 1.0 (OpenGL for Embedded Systems, auch GLES oder EGL genannt), welches ein Subset von OpenGL 1.3 ist. Genau hier schließt sich der Kreis, wie man eine GL-Codebase ohne (große) Anpassungen auf allen Plattformen zum Laufen bekommen kann.
(Selbstverständlich geht das nur ohne Shader einzusetzen.)

Sowohl unsere Desktop und Embedded Linux Varianten bis hin zu Android bieten EGL Support, also ist klar, dass diese Anbindung ins GATE Projekt muss.


Da OpenGL ES eine Teilmenge von OpenGL 1.3 ist, kann man sagen:

Ein GLES-API basiertes Programm läuft auch unter OpenGL ohne Anpassungen. OpenGL-API basierte Programme laufen jedoch nicht unter GLES Umgebungen, wenn sie OpenGL-only APIs benutzen.

Die Sache ist also einfach: Wir müssen bzw. dürfen nur jene APIs nutzen, die in GLES und OpenGL gemeinsam vorkommen. Dann laden wir die entsprechenden GL Libraries zur Laufzeit und holen nur die notwendigen Funktionszeiger heraus.
Naja … leider nicht ganz … aber grundsätzlich doch …

Kein 64 Bit auf GLES

Es ist ein bisschen kompliziert. Die GL APIs sind oft mehrfach vorhanden und zwar für die Datentypen: int, float und double. Bei int handelt es sich um Festkommazahlen, die in einen Integer abgebildet sind, und dann noch 32 bit und 64 bit Gleitkomma-Varianten.

Bei GLES gibt es mehrere Profile, wobei double auf meinen Plattformen nie implementiert war.
Die Integer-Variante ist im Zahlenbereich recht limitiert und somit sind die float Funktionen das einzige, was man plattformübergreifend nutzen kann.

Also wenn man eine Farbe setzen möchte, wählt man von den verfügbaren glColor4d (double), glColor4f (float), glColor4i (int), glColor4s (short) einfach nur glColor4f und kann sich recht sicher sein, dass man damit am weitesten kommen wird (außer man hat wirklich eine Plattform erwischt, die gar keine Gleitkommazahlen kann).

Ein bisschen “blöd” wird es mit einer Hand voll Funktionen, die unter OpenGL nur eine double-only und unter GLES nur eine float-only Implementierung haben. Bei diesen musste ich bei den Funktionsdeklarationen “#ifdef“-en und einmal float und einmal double als Parameter wählen. In der Anwendung selbst nutze ich dann nur float welches unter OpenGL automatisch in einen double gecastet wird.
Folgende Funktionen sind in meinem Anwendungsbereich betroffen und haben unter GLES ein f am Ende angefügt, während OpenGL sie ohne Suffix mit double füttert:

Richtige Lib laden

Unter Windows lädt man seit NT 4.0 einfach opengl32.dll und alles ist gut. Linux und Android machen einem das etwas schwerer, indem nicht überall alle Links gesetzt sind, oder die Libs grundsätzlich anders heißen.
Mit:

  • libGLESv1_CM.so
  • libGL.so.1.2
  • libGL.so.1
  • libGL.so

habe ich aber eine Möglichkeitensammlung, wo meist ein Treffer dabei ist. Aber ich wette, dass mit weiteren Plattformen weitere Libnamen möglich sein werden.

GLES Ablauf

Es kommt natürlich darauf an, ob man auf UI Fenster oder in Bitmaps zeichnet, aber folgender Ablauf ist notwendig, um per GLES den Bildschirm zu erhellen:

  • egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY)
    Display-Handle holen
  • eglInitialize(egl_display, &egl_version_major, &egl_version_minor)
    EGL/GLES initialisieren (und Versionsnummer abholen)
  • eglGetConfigs(egl_display, all_configs_array, all_configs_count, &configs_returned)
    Alle unterstützten Konfigurationsprofile auslesen
  • eglChooseConfig(egl_display, attrib_list, configs_array, configs_array_capacity, &configs_returned)
    Zu einer Liste von gewünschten Attributen alle dazu passenden Konfigurationsprofile auslesen. Ich nutze für Fenstergrafik folgendes:
     1static EGLint gate_egl_attribs_window[] =
     2{
     3  EGL_SURFACE_TYPE,    EGL_WINDOW_BIT,
     4  EGL_RENDERABLE_TYPE, EGL_OPENGL_ES_BIT,
     5  EGL_RED_SIZE,        (EGLint)5,
     6  EGL_GREEN_SIZE,      (EGLint)5,
     7  EGL_BLUE_SIZE,       (EGLint)5,
     8  EGL_ALPHA_SIZE,      (EGLint)0,
     9  EGL_DEPTH_SIZE,      (EGLint)16,
    10  //EGL_STENCIL_SIZE,  (EGLint)0,
    11  EGL_NONE
    12};
    

    Das sind zwar nur 16-bit Farbtiefe aber funktionieren zumindest immer.

  • egl_context = eglCreateContext(egl_display, configs_array[0], NULL, NULL)
    Gewünschtes Profil laden, das erste gelieferte Profil von eglChooseConfig hat bei mir immer auf Anhieb funktioniert.
  • egl_surface = eglCreateWindowSurface(egl_display, configs_array[0], native_window, NULL)
    Nun wird die Zeichenoberfläche vorbereitet. Dafür braucht man das native_window der Plattform. Bei X11 wäre das ein Fenster, das mit XCreateWindow() erzeugt wurde und bei Android NDK nutzt man das ANativeWindow welches man über den Event-Callback onNativeWindowCreated empfangen hat.
  • eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context)
    Am Ende werden “display”, “surface” und “context” gemeinsam aktiviert und nun kann man die üblichen glXYZ() Funktionen nutzen um darauf zu zeichnen.

Tja und wenn man genug von GLES hat, muss man die Bindung und die Objekte einfach nur freigeben:

  • eglMakeCurrent(egl_display, NULL, NULL, EGL_NO_CONTEXT)
  • eglDestroySurface(egl_display, egl_surface)
  • eglDestroyContext(egl_display, egl_context)

Fazit

Im Großen und Ganzen war es das auch schon. Ein offenere Brocken ist natürlich das “Event-Handling”, also wie eine Mausbewegung oder ein Tastendruck da noch hineinspielt und das ist natürlich wieder extrem plattformspezifisch.
In Linux lässt man eine X11 XEvent Schleife um das native_window kreisen und im Android NDK darf man die entsprechenden AInputEvents empfangen um z.B. AKeyEvent und AMotionEvent herauszupicken.

Das Schöne dabei ist jedoch, dass man nach der Initialisierung nicht mehr an die Plattform denken muss. Windows, Linux und Android können mit den gleichen Zeichenbefehlen gefüttert werden, die (solange alles float ist) brav abgearbeitet und auf dem Anzeigegerät sichtbar werden.