“Fonts, cursors and colour gradients, this week we look at the framebuffer code”
Jay explains some of the kernel framebuffer code
Reckless Guide
Part 3
In this week's installment of our series, Jay decides to dive into the kernel, tweak some of the framebuffer code, and hope not to break his system.
We won't be doing anything groundbreaking, but if you're a C programmer who is new to, and perhaps intimidated by kernel land, come along and join us.
You'll quickly see that the bar to entry isn't really that high, and that hacking at least some parts of the OpenBSD kernel internals is certainly not out of reach for anybody with some programming experience.
This article is part of a series - check out the index.
Website themes
The Exotic Silicon website is available in ten themes, but you haven't chosen one yet!
A new environment
Simple kernel hacking for the uninitiated C programmer
Leaving the comfort of userland...
Starting simple - changing the console font
Starting with OpenBSD 6.5, the default font for the framebuffer kernel on most architectures changed from ‘gallant’, to a new font called, ‘spleen’. Whilst I'm sure that many hours of work went into the creation of the new spleen font, and I do agree that on small displays it's quite a reasonable choice, I simply don't like the look of it on my large desktop monitors.
The code that loads and initializes the console font is in /usr/src/sys/dev/wsfont/wsfont.c, but if you didn't know that, it's fairly simple to grep across the entire source tree, and use a bit of intelligence to find it:
# grep -r font /usr/src/sys/
Using grep to find the code we're interested in
As well as simply finding a way to switch the default font back to gallant, by reading the code in wsfont.c we can start to learn how the font selection algorithm works. The code here is fairly well commented, making it a good starting point for anybody cautiously dipping their toes into the kernel sources.
We can immediately see that the actual fonts themselves are stored in C header files, and included by wsfont.c when enabled with a #define pre-processor directive. Several fonts are enabled by default, and they will all be compiled into the kernel. The one that is actually used for the console display will be selected later, once the display resolution is known.
Spleen 8x16 is always included, other sizes of the spleen font are included based on architecture and whether SMALL_KERNEL is defined. The gallant font is included only on Sparc64.
The easiest way to ensure that we end up with gallant, is to make it the only font loaded. This is easily done by changing the FONT_SPLEEN8x16 define to FONT_GALLANT12x22, and commenting out the four lines of code below that conditionally enable FONT_SPLEEN16x32 and FONT_SPLEEN32x64.
When making changes to the kernel code, it's useful to keep a copy the original file around and add some comments to the modified version noting your local changes:
# mv wsfont.c wsfont.c.dist
# cp -p wsfont.c.dist wsfont.c
Backing up files before modifying them
Note that we use an extension of ‘.dist’, (for the version as distributed), rather than ‘.orig’, so as to clearly distinguish local changes from ‘.orig’ files that are created by the patch utility, such as you might get after applying an errata patch.
Handy hint!
A word about timestamps
If you don't run a ‘make clean’ before the kernel compile, the make process only re-compiles files that have changed. More specifically, it only re-compiles files that have a more recent timestamp. This is a good way to compile a slightly modified kernel more quickly, and if you're editing files interactively with an editor, or copying them using cp, you'll be fine.
However, be aware that if you decide to revert some local changes that you've made by renaming a backup file back to it's original filename, then the timestamp will almost certainly be older than the current version. In this case, the restored file will not be re-compiled, (and no error or warning about this will be displayed during the make process).
To avoid this, be sure to touch any files that you rename in this way. For example, if we edit wsfont.c and later decide to remove our local changes by restoring the wsfont.c.dist file, we could do something like this:
# mv wsfont.c.dist wsfont.c
# touch wsfont.c
Now we just need to re-compile, and reboot into the new kernel. We covered kernel compilation in the previous installment on custom kernel configurations, so if you haven't read that yet, now would be a good time to do so. As soon as the framebuffer console is initialized, we should see gallant in place of spleen.
More about fonts and the kernel framebuffer code
Having read the code in wsfont.c, together with the included header files containing the actual font data, we can clearly see the format used for the fonts. With this knowledge, we could potentially design our own font from scratch. See how we've learned something useful already by actually looking at the code, instead of just searching on-line for ‘how to change the console font in OpenBSD’?
Programming exercise
Suggested programming exercise:
Many 8-bit home computers natively used ROM-based fonts based on an 8 x 8 pixel grid, with each character represented by 8 bytes, one for each row. Write a program to convert a file containing this raw data at a certain offset, such as might be obtained from a dump of the machine's internal ROMs, into a header file suitable to include in the OpenBSD kernel.
You might have noticed when we ran a recursive grep over the source tree for ‘font’, that there is also some interesting code in /usr/src/sys/dev/rasops/. This shouldn't come as much of a surprise, given that ‘rasops’ is short for ‘raster operations’.
In rasops.c, we find the code that selects an appropriate font based on screen resolution. It's the function rasops_init(), and it uses the function wsfont_find() from the code we were just looking at in wsfont.c. Although the code for wsfont_find() is quite self-explanatory, some useful commentary explaining it's usage can be found at the beginning of wsfont.h.
Whilst the font selection code is interesting, the default behaviour makes sense for the majority of displays. There probably isn't much to be gained from changing it, because if we ever needed a particularly large or small font for a specific machine, as we could as simply hard-code it as the only font compiled into the kernel by modifying wsfont.c.
Handy hint!
A larger scrollback buffer!
Elsewhere in rasops.c, we can find a define for RS_SCROLLBACK_SCREENS, which is by default set to 5. As you can probably guess, this controls how much memory is allocated to the scroll-back buffer, the ability to see text that's already scrolled off the top of the display by pressing shift and page-up.
Have you ever found five screens to be too few and wished for more? With a simple change to rasops.c and a kernel re-compile, this shouldn't be a problem any longer!
In fact, scrolling back up through rasops.c we can quickly find something else that's quite interesting, this time for a cosmetic change. Near the top of the file, we find the defines for the sixteen colors used on the console. Each of these can be set to any 24-bit RGB value. As can be seen from looking at the code, the kernel actually defines a color palette with 256 entries, but only the first 16 are used as regular colors, and the last 16 are an alternative set of colors used to display the cursor. By default, they are simply the inverse of the regular colors, but again, we could change this.
Programming exercise
Suggested programming exercise:
Change the color palette in rasops.c to 16 shades of green, to simulate an old green-screen monochrome monitor.
If you've looked through the rest of rasops.c, you might be wondering where the code that does the actual writing of pixel data to the framebuffer is. Since this code is optimised for each possible bit-depth, it's found in the files rasops1.c, rasops4.c, rasops8.c, rasops15.c, rasops24.c, and rasops32.c
As you might expect, there is a degree of similarity between all of these files. We'll start by looking at rasops32.c, as it's the most commonly used bit depth on modern systems. Apart from a very short rapops32_init() function, this file consists almost entirely of rasops32_putchar(), which as it's name implies, writes a single character's worth of font pixel data to a location in the framebuffer.
This function is called with the parameters you might expect, the row and column for the character to be drawn, (int row, and int col), and the character itself, (u_int uc). An attribute value is also passed, which contains the foreground and background color indices, as well as a flag to control underlining.
We can deduce the format of the attribute value from the code that follows, but first note an interesting optimisation within the RASOPS_CLIPPING ifdef. This code checks the bounds of the supplied co-ordinates, that they actually fall within the size of the terminal. However, the row and column values are supplied to this function as signed integers and could therefore be negative, which would also cause problems. Instead of explicitly testing for <0 as well, we cast them to unsigned before doing the greater-than test. This works, because a negative signed value would become a large unsigned value, and therefore fail the comparison.
In this specific case, a helpful comment on the line above makes it clear why we cast these values to unsigned, and that the lack of a test for <0 isn't an oversight. However, such comments are not always found around code like this in the OpenBSD kernel, so don't expect them. I suggest that you do add such comments in your own code, even if you don't intend to share it. Being sparse with comments does not make you an ‘elite hacker’ by any means, nor does being verbose with them make you any less of a programmer. One of the big differences we see at Exotic Silicon between hobbyist code and code written by serious programmers is in the quality of commenting. Enough said.
The variables b and f are assigned the background and foreground colors, by right-shifting attr by 16 bits, and 24 bits respectively. The values are then logically and'ed with 15, to obtain the low nibble, and the resulting 4-bit value looked up in devcmap, (device color map), to produce a 32-bit color value. This ultimately comes from the RGB color values we saw defined in rasops.c.
So the attribute value attr actually has the following bitwise layout:
|||| |||| |
Foreground Background Underline flag
The underline flag is checked for right at the end of the function.
Important: This guide was originally written with reference to OpenBSD 7.0. Patches submitted to the OpenBSD project by Exotic Silicon changed the bit used for the underline flag and these changes were incorporated in OpenBSD 7.3.
Immediately after the code to assign values to b, and f, we see another small optimisation. Since we are dealing with 32-bit quantities, each pair of these can be combined into a single 64-bit value, and on a 64-bit CPU, a single 64-bit store opcode will almost certainly be faster than two 32-bit stores. Note that every individual character is only ever drawn with two colors, one foreground color and one background color. Although these colors can change for each character printed, each time that rasops32_putchar() is called, it's only ever dealing with two colors. Since every set of two pixels has four possible combinations of 00, 01, 10, and 11, we can store the four resulting pairs of 32-bit values as four 64-bit values, ready to write out two bytes at a time.
This optimisation works quite well, since almost all of the fonts are an even number of pixels wide, 8, 12, 16, or 32. However we currently have one version of the spleen font that is 5x8, and of course the possibility of more fonts with odd widths being added in the future. For this reason, code to draw the characters one pixel at a time is also included at the end of the case statement, after various special cases for the aforementioned common font widths.
Space characters and blue gradients
Interestingly, the space character is hard-coded to write the background color to the framebuffer one byte at a time, regardless of the font size, (and regardless of any actual glyph data in the font). Since this is about the simplest piece of code to understand, just two nested loops writing the background color pixel by pixel, let's try making a cosmetic change to it. Remember that the value being written here is a real 32-bit RGB value, not an index to the color palette. The lower eight bits contain the value for the blue component of the pixel, and since we are already in a loop that counts the height of the character, if we add the value of height to the value of the background, ‘b’, which by default is zero, we should get a nice blue gradient effect within each space character.
Assuming that you've also switched the font from spleen to gallant, the range of values for the loop counting variable height will only be 0-21. This won't give a very bright blue hue unless we multiply it by something, so rather than just using b + height, we'll double the height value and add an offset of one:
((int *)rp)[cnt] = b + 1 + (2 * height);
Formula for a blue color gradient
Rebooting, we now see a visually pleasing gradient fill in place of plain black space characters. However, it doesn't fill the whole display. Only space characters within the actual textual output are rendered by the code that we modified. If we switch to another virtual terminal, however, we can see that the whole screen is re-painted by that code, and the gradient fills the entire display.
If we issue a clear command at the console, however, the screen changes back to a solid black background:
# clear
Issuing a clear command makes the gradient effect disappear
Obviously, the code to handle clearing the terminal, (or more correctly, a region of the terminal display), is elsewhere, and probably optimised for the task, in other words it doesn't just repeatedly print space characters to the screen. In fact, we don't need to look far for this code, just searching for the word ‘clear’ in rasops.c will take us straight to it.
Important note:
At this point, analysing the framebuffer code becomes somewhat more complex. Some of the functions that we are about to discuss exist in two versions, for example one for use with framebuffers that can be read from, and one that is for use with write-only framebuffers.
In this particular example, a flag RI_WRONLY exists, which is set by certain framebuffer drivers, to indicate that framebuffer memory can only be written to and cannot be read from. This is parsed by the rasops_init() function, which sets various function pointers to point to the correct code. The specific functions that will actually be called depend on your framebuffer hardware and which drivers you are using.
In order to keep this article relatively uncomplicated and comprehensible for people who are new to the kernel internals, we've had to make some assumptions and ignore some of the finer details. As a result, the specific modifications to the code in the discussion below may not work on your particular hardware, as the kernel may not even be calling the functions that we've modified.
The function rasops_eraserows() is called with the starting row number, (int row), the number of rows to clear, (int num), and an attribute value from which only the background color is used, (uint32_t attr). The variable clr is used to store the 32-bit RGB value for the background that has been extracted from the supplied attribute. We can modify rasops_eraserows() to create the same gradient effect on spaces by changing clr based on the current value of num, (which is the equivalent to height in rasops32_putchar()).
However, it's more interesting, if perhaps slightly less aesthetic, to make the gradient painted by rasops_eraserows() red, rather than blue. This will let us see exactly which functions the kernel is using to paint the framebuffer at any point.
clr + ((num % 22)<<16)
Formula for a red color gradient
Recompiling and rebooting into the new kernel, everything seems as it was before until we log in and issue a clear command. Now the screen is filled with a red gradient, apart from the first line which contains the shell prompt, which is blue. Now press enter a few times to bring the prompt down a few lines. Notice that the rest of each line remains red, indicating that it hasn't been re-painted. Type ls, without any trailing space or newline. Still, the rest of the line remains red. However, if we now press tab and invoke the command completion function of the korn shell, you can see that the kernel repaints the rest of the terminal line in blue, which lets us know that this was done by rasops32_putchar().
Unfortunately, if we invoke vi editing a large file, such as rasops.c itself, and scroll up and down, we can see that there are still blank spaces with no gradient being displayed. These are obviously being painted by another piece of code. Even if we don't know exactly where to look, thinking logically, since there is a rasops_eraserows() function, it's logical that there might be a corresponding function to erase columns, and indeed there is, rasops_erasecols(). Alternatively, we could just search within the source files in rasops/ for functions that use the variable clr, which would be a good indication that they are doing some writing to the framebuffer.
We can use the following expression as a replacement for clr in rasops_erasecols() to create a gradient in the green channel, and highlight the characters for which this function is being called:
clr + ((num % 22)<<8)
Formula for a green color gradient
Programming exercises
Suggested programming exercises:
  • Find all of the places in the rasops code where spaces can be painted to the framebuffer, and modify them to use the same colored gradient.
  • Modify the character painting code in rasops32.c to use the gradient background for non-space characters as well.
To finish this week's installment, we'll take a quick look at the code to paint the cursor. I originally suspected a bug in the code that initializes ri->ri_do_cursor to point to rasops_wronly_do_cursor() in line 309 of rasops.c, as all of the other functions seem to use their versions for readable framebuffers on my workstation. However, looking through the commit logs, specifically the log entry for revision 1.55 of rasops.c, it seems that the use of rasops_wronly_do_cursor() is preferred for performance reasons. A comment in the code explaining that would have made it more obvious, but at least it's documented somewhere.
Changing the assignment on line 309 to set ri->ri_do_cursor to rasops_do_cursor() seemed to work fine on this hardware without a significant performance hit.
Having enabled the use of rasops_do_cursor(), we can now make some modifications to it. There isn't really much to do with a cursor, but we can certainly change it from a solid block to an underscore type cursor. Looking at the code, we can see that the variable ‘height’ is set to the height of the current font, and then used as a loop control variable in a while loop which decrements it on each iteration.
We can't just reduce the value of ‘height’ to say, four, because that would restrict the painting of the cursor to the first four display lines, since the offset into the display buffer memory is set to the first line and then increased from it's previous value on every iteration. In other words we would create an overscore cursor. Instead, we let the while loop iterate over the whole height of the cursor, but add an if statement to the line which actually negates the pixel data, so that it does so only if height is less than four.
if (height < 4) { *(int32_t *)dp ^= ~0; }
Changing the default block cursor into an underscore style cursor
And with that, we wrap up this week's installment of the series.
This week, we've looked at some of the kernel code involved with implementing the framebuffer. We started by seeing how fonts are encoded, then looked at the logic for font selection. Next, we saw how the color palette is defined, then we moved on to have a look at the raster operations code. We made some whimsical cosmetic changes to the character painting and erasing functions, then lastly looked at how the cursor is implemented.
There is still plenty more to discover in the framebuffer code, in fact we've barely scratched the surface, but we'll leave that for self-study as next week we will be looking at the other side of the terminal, specifically some of the keyboard input handling code.