Framebuffer

Meine OpenGL Experimente und die gategames Demo-App sind zwar toll für Windows, Linux und Android, jedoch kann ich diese Form der Grafikausgabe nie auf EFI, FreeRTOS oder DOS portieren.

Wenn man aber ein abstraktes Modell eines primitiven 2D-Bildspeichers hätte, in dem man Pixel setzen und Grafik-Untermengen hinein und herauskopieren kann, dann hätte man einen Ansatz, den man viel leichter auf alle Formen von “Bildschirmen” portieren kann. (Mikrocontroller inklusive).


Vorgeschichte

Der Aufbau von UIs ist in jedem Betriebssystem grundlegend anders (trotz einiger Ähnlichkeiten). Ich behaupte mal, dass der Erfolg von HTML und der Webbrowser auch damit begründet ist, dass UI-Designer nur einmal die Arbeit haben, ihr Werk abzutippen und die Browser kümmern sich um die korrekte Übersetzung in die Welt des Ziel-OS.

Früher in den DOS Zeiten baute sich jede Anwendung ihr eigenes UI zusammen, und zeichnete mehr oder weniger direkt in den Grafikkartenspeicher.

Man stelle sich also vor, man hätte einen “abstrakten” Grafikspeicher (genannt Framebuffer) in seiner Bibliothek, der unter Windows ein Fenster mit GDI bemalt, unter Linux die XLib befehligt und auf exotischen Plattformen wie früher direkt in den Video-RAM schreibt.

Dann hätte man die Möglichkeit grafische Oberflächen universell portabel zu gestalten.

Windows und GDI

In Windows erzeugt man einfach ein Fenster mit CreateWindowEx und eine “32 Bit pro Pixel” DIB-Section.
Die DIB-Section ist dann unser Framebuffer, wo wir direkt auf die Bilddaten zugreifen können.

 1HWND hwnd_desktop = GetDesktopWindow();
 2HDC hdc_desktop = GetDC(hwnd_desktop);
 3HDC hdc_framebuffer = CreateCompatibleDC(hdc_desktop);
 4BITMAPINFO bitmapinfo;
 5
 6bitmapinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
 7bitmapinfo.bmiHeader.biWidth = (LONG)framebuffer_width;
 8bitmapinfo.bmiHeader.biHeight = (LONG)framebuffer_height;
 9bitmapinfo.bmiHeader.biPlanes = 1;
10bitmapinfo.bmiHeader.biBitCount = 32;
11bitmapinfo.bmiHeader.biCompression = BI_RGB;
12bitmapinfo.bmiHeader.biSizeImage = 0;
13bitmapinfo.bmiHeader.biXPelsPerMeter = 0;
14bitmapinfo.bmiHeader.biYPelsPerMeter = 0;
15bitmapinfo.bmiHeader.biClrImportant = 0;
16bitmapinfo.bmiHeader.biClrUsed = 0;
17
18DWORD* ptr_framebuffer;
19HBITMAP hbmp_framebuffer = CreateDIBSection(hdc_framebuffer, 
20  &bitmapinfo, DIB_RGB_COLORS, (void**)&ptr_framebuffer, NULL, 0);
21
22HGDIOBJ old_dc = SelectObject(hdc_framebuffer, hbmp_framebuffer);
23
24ReleaseDC(hwnd_desktop, hdc_desktop);

Man erzeugt also einen Device-Context kompatibel zum Desktop-Fenster (hdc_framebuffer) und die DIB-Section (hbmp_framebuffer) durch einen BITMAP-Info-Header mit den Größenangaben. Dabei fällt einem der Daten-Pointer (ptr_framebuffer) in die Hände.

Die größte Challenge ist der Aufbau der DIB-Section, die wie eine Windows Bitmap aussieht. Die Bildzeilen verlaufen dort von unten nach oben, und damit ist Koordinate (0, 0) (bzw. Pixel-Offset 0) die linke untere Ecke und (0, Y-max) die linke obere Ecke.
Viele andere Framebuffer (und auch mein GATE Framework) definieren (0, 0) als links-oben und verlaufen mit steigendem Y nach unten.

1DWORD* ptr_pixel = ptr_framebuffer + (height - y - 1) * width + x;
2*ptr_pixel = (((DWORD)r) << 16)
3           | (((DWORD)g) << 8)
4           | (((DWORD)b));

Und am Ende muss man per
RedrawWindow(hwnd, NULL, NULL, RDW_INTERNALPAINT | RDW_INVALIDATE) nur das Neuzeichnen des gesamten Fensters auslösen.

Im Fenster wird im WM_PAINT Event dann einfach per BitBlt() der aktuell “ungültige” Teil des Fenster mit den Daten aus der DIB-Section übermalt.

XLIB und Pixmaps

Mein erster Versuch mit der XLib und dem Zeichnen auf X11-Pixmaps lief über die Funktion XPutPixel() in einer Schleife. Das funktionierte zwar, doch für eine 640x480 Pixel Grafik brauchte mein Rechner mehrere Sekunden.
Oder anders gesagt: Noch ineffizienter geht es nicht mehr.

Doch auch bei Pixmaps kann man den Datenpuffer selbst allokieren und dann die RGB-Werte selbst in den Puffer schreiben. Womit man schon in wenigen Millisekunden eine ganze Szene in den Puffer gerendert hat.
So kann man dann die gesamte Pixmap in einem Schritt dann auf sein Fenster am Monitor zeichnen lassen.

Das funktioniert natürlich nur bei einer Farbtiefe von 24 oder 32 bit, wo in beiden Fällen ein 32 Bit breites RGB-Array im Speicher liegt. (Alle anderen historischen Bitmusterformen sind komplizierter umzurechnen.)

Rein exemplarisch und vereinfacht, sieht das dann so aus:

 1#include <X11/Xlib.h>
 2
 3void set_pixmap_pixel(void* image_data, 
 4  unsigned x, unsigned y, rgb_t const* color)
 5{
 6  uint32_t* ptr_pixels = (uint32_t)image_data;
 7  uint32_t* target_pixel = ptr_pixels[y * width + x];
 8  *target_pixel = (((uint32_t)color->r) << 16)
 9                | (((uint32_t)color->g) << 8)
10                | (((uint32_t)color->b))
11                ;
12}
13
14XImage* create_x11_pixmap(...)
15{
16  unsigned int bytes_per_pixel = 4;
17  unsigned int padding = XBitmapPad(display);
18  unsigned int bytes_per_line = width * bytes_per_pixel;
19  if(bytes_per_line % padding != 0)
20  {
21    bytes_per_line += (padding - (bytes_per_line % padding));
22  }
23  size_t image_size bytes_per_line * height;
24  void* image_data = malloc(image_size);
25  XImage* image = XCreateImage(display, visual, depth, ZPixmap, 
26    0, image_data, width, height, padding, 0);
27  return image;
28}
29
30void create_x11_window()
31{
32  Display* display = XOpenDisplay(NULL);
33  int screen = XDefaultScreen(display);
34  Window root_window = XDefaultRootWindow(display);
35  int depth = XDefaultDepth(display, screen);
36  Visual* visual = XDefaultVisual(display, screen);
37  Colormap colmap = XDefaultColormap(display, screen);
38  XSetWindowAttributes win_attribs = { 0 };
39
40  win_attribs.colormap = colmap;
41  win_attribs.event_mask = StructureNotifyMask;
42  win_attribs.border_pixel = XBlackPixel(display, screen);
43  win_attribs.background_pixel = XWhitePixel(display, screen);
44  unsigned long attr_mask = CWBackPixel | CWBorderPixel 
45                          | CWEventMask | CWColormap;
46  
47  Window window = XCreateWindow(display, root_window, 
48    0, 0, width, height, 0, depth, InputOutput, visual, 
49    attr_mask, &win_attribs);
50  XMapWindow(display, window);
51  XFlush(display);
52  ...
53}
54
55void render_x11_scene(...)
56{
57  ...
58  XImage* image = create_x11_pixmap(...)
59  ...
60  for(...)
61  {
62    ...
63    set_pixmap_pixel(image_data, x, y, &color);
64    ...
65  }
66  ...
67  GC gc = XCreateGC(display, window, 0, 0);
68  XSetGraphicsExposures(display, gc, 0);
69  XPutImage(display, window, gc, image, 0, 0, 0, 0, width, height);
70}

Fazit

Man sieht wieder einmal, dass Windows und Linux bzw. X11 gar nicht so weit auseinander liegen, was den Aufbau von GUIs intern anbelangt. Denn in beiden kann man einen Framebuffer so einrichten, dass man ihn mit einfachen 32-bit RGB Werten abfüllen kann.

Wenn man jetzt noch Maus und Tastaturbehandlung hinzufügt, dann kann mit einfachsten Mitteln GUIs für beide Plattformen bauen. Dafür müsste man sich natürlich alle “üblichen” Controls wie Buttons, Listen und Eingabefelder selbst zusammenstellen, doch das Ergebnis wäre eine komplett Plattform-unabhängige UI.

Einfache Apps (wie z.B.: zum Anzeigen von Fotos) kann man auf diese Weise in wenigen Kilobytes selbst erstellen, ohne auf Megabyte große Frameworks zurückgreifen zu müssen.