Framebuffer
« | 09 Jan 2022 | »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.
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.