Look at these two consoles:
Same font, same colors, same… everything? Other than for the actual text they display, they look identical, don’t they? But the one on the right can do things that the one on the left cannot. Witness this:
A square? OK, meh, we had those in the DOS days with box-drawing characters. But a circle?! That’s only possible because the console on the right is a hybrid console that supports mixing the usual textual grid of a terminal with overlapping graphics.
Now, if you have been following the development of EndBASIC, this is not surprising. The defining characteristic of the EndBASIC console is that it’s hybrid as the video shows. What’s newsworthy, however, is that the EndBASIC console can now run directly on a framebuffer exposed by the kernel. No X11 nor Wayland in the picture (pun intended).
But how? The answer lies in NetBSD’s flexible wscons framework, and this article dives into what it takes to render graphics on a standard Unix system. I’ve found this exercise exciting because, in the old days, graphics were trivial (mode 13h, anyone?) and, for many years now, computers use framebuffer-backed textual consoles. The kernel is obviously rendering “graphics” by drawing individual letters; so why can’t you, a user of the system, do so too?
wscons(4), or Workstation Console in its full form, is NetBSD’s framework to access the physical console attached to a computer.
wscons abstracts the details of the hardware display and input devices so that the kernel and the user-space configuration tools can treat them all uniformly across the tens of platforms that NetBSD supports. If you use wsconsctl(8) on a modern amd64 laptop to control its display, you use wsconsctl on an ancient vax box to control its display too.
The output architecture of wscons is composed of multiple devices, layered like this:
-
wsdisplay(4) sits at the top of the stack and implements the console in hardware-independent terms. The functionality at this level includes handling of VT100-like sequences, cursor positioning logic, text wrapping, scrolling decisions, etc.
-
Under wsdisplay sit the drivers that know how to access specific hardware devices. These include, among others: vga(4), which does not do graphics at all; genfb(4), which is a generic framebuffer driver that talks to the “native” framebuffer of the system (e.g. the one configured by the EFI); and radeonfb(4), which implements an accelerated console on AMD cards. These drivers know how to initialize and interact with the hardware.
-
Under the graphical drivers sits vcons(4), the driver that implements one or more graphical consoles in terms of a grid of pixels. vcons is parameterized on “raster operations” (rasops), a set of virtual methods to perform low-level operations. An example is the
moverows
method, which is used by wsdisplay to implement scrolling in the most efficient way provided by the hardware. vcons provides default (inefficient) implementations of these methods, but the upper drivers like radeonfb can provide hardware-accelerated specializations when instantiating vcons. vcons also interacts with wsfont(4) to render text to the console.
The input architecture of wscons is similar in terms of layering of devices, albeit somewhat simpler:
-
wsmux(4) is an optional component that multiplexes multiple input devices under a single virtual device for event extraction.
-
wskbd(4) sits at the top of the stack (not accounting for wsmux) and implements generic keyboard handling. The functionality at this level includes translating keycodes to layouts, handling key input repetition, and more. wskbd exposes a stream of wsevents to user-space so that user-space can process state changes (e.g. key presses).
-
Under wskbd sit the device drivers that know how to deal with specific hardware devices. These include, among others: ukbd(4) for USB keyboard input and pckbd(4) for PC/AT keyboard input. These drivers wait for hardware input, generate events, and provide a map of keycodes to key symbols to the upper layer so that wskbd can operate in generic terms.
The input architecture can handle other types of devices like mice and touch panels (both via wsmouse(4)), but I’m not going to cover those here. Just know that they sit under wsmux at the equivalent level of wskbd and produce a set of wsevents in the exact same manner as wskbd.
As you can sense from the overview, the whole architecture under wsdisplay is geared towards video devices… if it wasn’t for the vga driver: in the common case, wsdisplay is backed by a graphical framebuffer managed by vcons for text rendering, yet the user only sees a textual console. But if the kernel has direct access to the framebuffer, so should user-space too.
The details on how to do this click if you read through the operations described in the wsdisplay manual page. In particular, you may notice the WSDISPLAYIO_GET_FBINFO
call which retrieves extended information about, you guessed it, a framebuffer display.
Let’s try it: I wrote a trivial program to open the display device (named /dev/ttyE0
for reasons that escape me), call this function, and store the results in an fbinfo
structure:
// wsdisplay-fbinfo.c
// https://jmmv.dev/src/netbsd-graphics-wo-x11/wsdisplay-fbinfo.c
#include
#include
#include
#include
#include
#include
#include
#include
int main(void) {
// Open the main wsdisplay device.
int fd = open("/dev/ttyE0", O_RDWR | O_NONBLOCK | O_EXCL);
if (fd == -1)
err(1, "open failed");
// Query information about the framebuffer.
struct wsdisplayio_fbinfo fbinfo;
if (ioctl(fd, WSDISPLAYIO_GET_FBINFO, &fbinfo) == -1)
err(1, "ioctl failed");
close(fd);
exit(EXIT_SUCCESS);
}
Hmm, but this program does not have any visible output, right? The code just queries the framebuffer information and does nothing with it. The reason is that the content of the wsdisplayio_fbinfo
structure is large and I didn’t want to pretty-print it myself. I thought it’d be fun to show you how to use GDB to inspect large data structures and how to script the process. Here, look:
gdb -q
-ex 'set print pretty on'
-ex 'break exit'
-ex 'run'
-ex 'frame 1'
-ex 'print fbinfo'
-ex 'cont'
-ex 'quit'
./wsdisplay-fbinfo
This call to GDB starts the sample program shown above and automates various GDB co