BetterOS.org : An attempt to make computer machines run better


HOME | BETTER LINUX | GAMES | SOFTWARE | TUTORIALS | ABOUT | REFERENCES | FORUM | WEB LOG |

Web Log: INDEX |

Web Log

April 14, 2014 - Programming the Framebuffer

April 14, 2014

Programming the Framebuffer

A few days ago I wrote a small program to read from my old dusty Hanvon Graphicpal tablet. The program is not perfect, and I want to improve it. In order to facilitate this improvement, I had the idea to plot the output on the screen instead of just printing the x,y coorinates as numbers.
This is usually a trivial task, but I had written my program to use nothing but indirect system calls, which I could then implement in assembly language to avoid linking to the standard C library. Drawing graphics on the screen within these constraints is non-trivial, but I decided to accept the challenge.

So, the first step is to decide exactly how to draw on the screen. My only criteria was that it had to be done without calling any libraries, including libc. This greatly limits the available options, no SDL, no X11, no XCB, and of course, no GTK or QT.
There was one option though: The Linux Framebuffer.
Now there are a few problems with this. First of all, I have never found any documentation on how to make this work exactly. Second, I wrote my Hanvon data interpreter in FreeBSD, and FreeBSD doesn't have the Linux Framebuffer (obviously).
The second problem is easy, just switch to Linux and rewrite the hanvon code on Linux. Once I get everything working in Linux then I can try to figure out how to do something similar in FreeBSD. The first problem is a bit more difficult and required some serious late night research. Still, haven't found documentation, but I found the next best thing: working code.
The code I found didn't work perfectly, but it was close enough to get me pointed in the right direction. Seems that the only thing you can do is open the fbdev device created by Linux. Then, using the ioctl syscall, you can get information about the screen, such as it's height and width. Then, you can mmap the screen and write to the screen pixel-by-pixel.

So all I essentially did was take the working code I found, strip out the stuff I didn't care about, and then convert everything to indirect system calls.
This step was very easy and left me with this code:

#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>

int main (int argc, char *argv[])
{
        int fbfd = 0, tty = 0;;
        struct fb_var_screeninfo vinfo;
        struct fb_fix_screeninfo finfo;
        long int screensize = 0;
        char *fbp = 0;
        int x = 0, y = 0, i = 0;
        uint8_t pixel = 0;
        long int location = 0;

        if (__syscall(SYS_getuid) != 0)
        {
                __syscall(SYS_write,0,"Not Root\n",9);
                __syscall(SYS_exit,1);
        }

        /* open framebuffer */
        fbfd = __syscall(SYS_open,"/dev/fb0", O_RDWR);

        if (fbfd!=0)
                __syscall(SYS_write,0,"Framebuffer opened.\n",20);

        tty = __syscall(SYS_open,"/dev/tty1", O_RDWR);

        // Get fixed screen information
        __syscall(SYS_ioctl,fbfd, FBIOGET_FSCREENINFO, &finfo);

        // Get variable screen information
        __syscall(SYS_ioctl,fbfd, FBIOGET_VSCREENINFO, &vinfo);

        // Figure out the size of the screen in bytes
        screensize = vinfo.yres_virtual * finfo.line_length;

        // Map the device to memory
        fbp = (uint8_t *)__syscall(SYS_mmap, 0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);

        if (fbp!=0)
                __syscall(SYS_write,0,"Mapped\n",7);

        while (1)
        {
                __syscall(SYS_write,0,".\n",2);

                /* blit the screen */
                for (y = 0; y<vinfo.yres; y++)
                {
                        for (x = 0; x<finfo.line_length; x++)
                        {
                                location = (y*finfo.line_length)+x;
                                pixel = 128;
                                *((uint8_t*)(fbp + location)) = pixel;
                        }
                }

//                __syscall(SYS_nanosleep,(struct timespec[]){{0, 50000000}}, NULL);
        }

        __syscall(SYS_munmap,fbp, screensize);
        __syscall(SYS_close,fbfd);

        return 0;
}

Now, it is certainly not a beautiful piece of code, but it does draw to the screen, and it does use only indirect syscalls.
I used __syscall() instead of syscall() so that I could redefine it in assembly language. I reused the asm shim I wrote in FreeBSD since the Linux calling convention is the same in amd64 as the FreeBSD calling convention.
However, for some reason, the program doesn't work. If I compile and link to libc using:
gcc fbhack.c -D__syscall=syscall
Then everything runs perfectly. However, when linking with the asm shim, the program exits silently at the point where it should place the pixel in video memory.
I have not yet discovered the reason for this failure, but there must be a subtle difference between my implementation of __syscall() and the glibc implementation of syscall(). I looked at a copy of the glibc sources, but was unable to find it in the source tree. However, in the Musl libc sources, __syscall() is defined almost exactly the same way that I defined it, but in GNU as syntax. Also in Musl libc syscall() is defined in C as a vararg function which calls __syscall() and then passes the return value through __syscall_ret() which appears to do nothing useful except interpret error codes correctly.
In an attempt to figure out the issue, I tried a number of different methods of compilation and linking:
Linked with glibc - Works
Linked with shim - Fails
Linked with musl shim - Fails
Linked with musl libc - Works
The fact that it works with the full Musl libc has me baffled. The only difference should be that errors are interpreted correctly, but if the program works, then there should be no error code to convert. Yet something is different.

I guess maybe the next step is to implement the entire program in pure assembly language.
For now, that's all.