15 KiB
Introduction to the Kernel Graphics Subsystem
What is the Kernel Graphics Subsystem?
The Kernel Graphics Subsystem is the kernel subsystem that is responsible to manage all graphics devices, framebuffers, hardware 3D acceleration, memory mappings, etc.
Responsibilities
- Provide a convenient interface to all supported video hardware in the Kernel.
- Manage 3D rendering on supported hardware.
Current Limitations and Future features?
- No locking on who can do
mmap
on DisplayConnector devices currently, which can lead to malicious applications "fighting" with WindowServer on what is shown to the user from the framebuffer.
DisplayConnector Devices
The Display Connector devices are an abstraction layer to what is essentially the
management layer of hardware display (commonly known as scanouts) output connectors.
The idea of using such type of device was inspired by Linux, which has a struct called
drm_connector
as a base structure for other derived structures in the various Linux DRM drivers.
A Display connector device is typically connected to a group of other connectors as well,
as it's generally common to have video hardware that utilizes multiple hardware connectors
to VGA, DisplayPort, HDMI, DVI, etc. However, it can be a stand-alone device too, which
is the case for the GenericDisplayConnector
class, that can be initialized without being
attached to a parent PCI device object at all.
Each display connector is programmatically accessible via a device file, in the
/dev/gpu/
directory with a name connectorX
(X is replaced with the minor number).
Each display connector could be mmap
-ed to gain control to video RAM directly.
This works nicely with the kernel TTY subsystem thanks to the role of virtual memory
in the subsystem.
Hardware framebuffers
History lesson on ISA, PCI, VGA (and SVGA)
Since the beginning of video hardware with old-school ISA VGA display adapters, there was a window being mapped in the physical address space, being translated by the motherboard chipset as read/write to video RAM. When SuperVGA came along in the 90s, it expanded the usage of that small VGA window (where it was in very low memory) to high resolution framebuffers in very high memory regions. This tradition continues today to some extent (excluding hardware which requires DMA from main memory to video memory), because it's relatively cheap and easy way to let operating systems to access video RAM directly without too much trouble.
Since the main PC x86 computer bus was the IBM ISA bus, there was no easy way to tell where the resources of each card were actually located at the IO space nor in the physical memory space. There were a couple of attempts to fix this - the most notable was the Plug-and-Play standard.
The real change came from a new computer bus in the mid 90s - the PCI bus. This new bus was PnP friendly - no more hardcoded resource allocations which means also that OS drivers can find where the firmware (BIOS) mapped the BAR (Base address registers) for the actual resources. This was also the era where SuperVGA video adapters started to appear, taking advantage of this new bus.
Since VGA was introduced, countless amount of vendors brought their own implementations and video adapters for usage in the PC market. By now, most of them are gone, leaving the major vendors (Intel, AMD and Nvidia) to still be able to manufacture video adapters which are today commonly known as Graphics Processing Unit (abbreviated as GPU) - due to the fact that today video adapters are not only outputting pixels to the computer screen, but have a whole set of processors to take care of heavy computational tasks of graphics assets, and even general processing tasks nowadays.
SuperVGA was only the first step into this direction, yet SuperVGA is not a standard, but a marketing name for is essentially each video adapters' vendor tried to do in the 90s - building an extension upon VGA. All of these vendors did that without creating a unified standard, like with VGA, which ensured everyone are conforming to well-known and expected video hardware behavior. To try to cope with the dire situation, the VBE (Video BIOS extensions) standard was created to help BIOS and operating system vendors to be able to get high resolution framebuffer from any hardware that complied to the standard. When UEFI came along, the vendors agreed to create the Graphics output protocol (known as UEFI GOP), to provide the same set of features that VBE had, but now is usable from 64-bit kernel code as long as the kernel didn't shutdown the UEFI services (which it really should do) after completing the boot process.
Then how does it all apply to the subsystem?
Glad you asked! Since hardware framebuffers are still relevant today, we use them to put pixels so the video encoder of a GPU can convert these bits into light, so you could actually see a picture from a computer screen. Each GPU implements its own internal functionality so it might vary from very simple devices (like the QEMU bochs-display device, which is nothing more than framebuffer region and a couple of registers to manage it) to very complex devices, such as bare metal devices (like Intel integrated GPUs, etc).
The Kernel graphics subsystem strives to manage all of these devices in a unified fashion as much as possible. Of course, actual implementations should vary internally in the amount of code to handle the actual device, but all basic API being exposed to userspace is the same.
The role of MMUs and virtual memory
One of the primary goals of the subsystem to is to allow userspace applications, like the WindowServer, to utilize the hardware framebuffers, so we can see the SerenityOS desktop, and to ensure the internal TTY subsystem in the Kernel can use the same framebuffers to put output from kernel virtual consoles when desired to (i.e. the user switched to the Virtual console from another console that is in graphics mode).
The SerenityOS kernel utilizes the MMU and virtual memory in a very neat trick to
give the "feel of control" to whoever did the mmap
syscall on a DisplayConnector
device, while keeping the control to the Kernel to decide who accesses the actual VRAM
at a given time. This works by working with the following assumptions:
- Current usage of
mmap
is only for direct framebuffer manipulation. This means that if we add support for batch buffers or other objects that should reside in VRAM, this trick can lead to catastrophic incidents with the underlying hardware. This happens to be this way, due to the fact that we essentially can take VRAM access from the WindowServer at anytime we want, without the WindowServer being aware of this so it can still function in the background. - We need to know the maximum dimensions of the framebuffers when initializing the device and creating the DisplayConnector device at runtime. This happens because we map all the possible pages of VRAM framebuffer at that time, and also reserve the same amount of pages in usable physical memory space, so we could reserve the contents of VRAM between the switch from graphics mode to console mode and vice-versa.
The actual implementation is quite simple, yet powerful enough to let everyone
live comfortably - each DisplayConnector device is backed by a special VMObject (VMObject is
the base class for managing virtual memory scenarios easily) that is created when the
DisplayConnector device is initialized - we need to find the physical address of the
start of the framebuffer and the maximum resource size (this is where PCI BARs play their role,
as we can determine with them the physical address by reading their values and also
the maximum resource size, by doing a very simple write 1s-and-read trick that was introduced
with the PCI bus when it was created). Then when the object is created, the code ensures
we reserve for later usage the same amount of pages somewhere else to ensure we preserve
the contents of VRAM between the switch from console and graphics mode and vice-versa.
The special VMObject is tied to each Memory::Region
object, so it can instruct each
virtual-to-physical memory mapping to be actually re-mapped to wherever we want in physical
address space, therefore, we do not interrupt any userspace application from drawing its pixels
to the framebuffer in the background.
Do you plan supporting old VGA adapters?
Given the nature of the user experience SerenityOS strives to deliver to the users, a core requirement from the first day of this project was to only support 32 bit-per-pixel (also known as True-color framebuffer) hardware framebuffers. We do support hardware framebuffers that neglect the alpha-channel (essentially it's a 24 bit-per-pixel), as long as each pixel is aligned to 4 bytes. The QEMU std-vga (bochs-display with VGA capabilities) device was chosen as the first device to be supported in the project, and that was an excellent choice for that time to put up with the said requirement.
This hard requirement is due to the fact that supporting anything besides True-color framebuffers is a waste of time for a new modern kernel. Not only that, but relying on VGA with modern monitors is essentially settling for blurry, badly-shaped graphics on a computer monitor, due to unoptimized resolution scaling with modern screen ratios.
Old VGA adapters are certainly not capable of using high resolution framebuffers when operating in pure native VGA mode (i.e. not operating in an extension mode of the video adapter), therefore, if the Kernel cannot find a suitable framebuffer to work with or a video adapter it has a driver for, then the last resort is to use the old VGA text mode console. Therefore, the SerenityOS kernel will probably never support pure VGA functionality. That technology was good for operating systems in the 90s, but is not usable anymore.
By doing so, we ensure that legacy cruft is not introduced in the Kernel space. This indeed helps keeping the Graphics subsystem lean and flexible to future changes.
What about the Video BIOS Extensions? It can gives high resolution framebuffers without writing native drivers!
As for using Video BIOS extensions - this requires us to be able to call to BIOS 16-bit real mode code. The solutions for these are:
- Drop to real mode, invoke the BIOS interrupt and return to our kernel.
- Writing a Real-Mode 16-bit emulator, either in Kernel space or userspace.
- Use Intel VT-x extensions to simulate a processor running in Real mode.
- Use the old v8086 mode in x86 processors to get an hardware monitor of 16-bit tasks.
Neither of these options is suitable for us. Dropping to real mode is quite dangerous task, and breaks the concept of memory protection entirely. Writing a real mode emulator is the safest solution, yet can take a not negligible amount of effort to get something usable and correct. Using the hardware options such as Intel VT-x or the v8086 mode are almost equally equivalent to writing an emulator.
We will probably never support using the Video BIOS extensions because of these reasons:
- Major part of this project is to maximize usability and fun on what we do, and turning into legacy-cruft to temporarily solve a solution is not the right thing to do.
- VBE is not usable on machines that lack support of BIOS. As of 2022, this increasingly becomes a problem because many PC vendors dropped support for BIOS (known as CSM [Compatibility Support Module] in UEFI terms).
- VBE is limited to whatever the vendor decided to hardcode in the OptionROM of the video adapter, which means it can limit us to a small set of resolutions and bits-per-pixel settings, some of these settings are not convenient for us, nor suitable for our needs.
- VBE lacks the support of detecting if the screen actually supports the resolution settings, which means that the operating system has to use other methods to determine if screen output is working properly (e.g. waiting for a couple of seconds for user confirmation on the selected settings). This is because VBE lacks support of getting the screen EDID because most of the time, the EDID resides in a ROM in the computer screen, which is inaccessible without using specific methods to extract it (via the Display Data Channel), which are not encoded or implemented in the PCI OptionROM of the device. This is in contrast to native drivers which are able to do this, and VGA, that never relied on such methods and instead relied on all video adapters and computer screen to use an well-known specification-defined display modes.
What are the native drivers that are included in the kernel? what type of configurations are supported?
The kernel can be configured to operate in the following conditions:
- Fully-enable the graphics subsystem, initialize every device being supported.
- Only use the pre-initialized framebuffer from the bootloader, don't initialize anything else.
- Don't use any framebuffer, don't initialize any device.
By default, we try to fully-initialize the graphics subsystem, which means we iterate over all PCI devices, searching for VGA compatible devices or Display Controller devices.
We currently natively support QEMU std-vga (and bochs-display) device, VirtIO GPU, VMWare SVGA II adapter, and Intel Graphics (Gen 4 only). We try our best to avoid using a pre-initialized framebuffer, so if we detect any of the said devices, we simply ignore the pre-initialized framebuffer from the bootloader.
The user can choose to use a different condition of the Graphics subsystem, but hardware limitations such as lack of supported hardware can either lead the Kernel to use a pre-initialized framebuffer or completely "abandon" graphics usage (as was mentioned in third condition), making the system usable only through a VGA 80x25 text mode console.
Userspace APIs
Unified Graphics IOCTLs
All graphics ioctls are currently unified and being implemented in one final method
of the DisplayDevice
class, to keep implementation consistent as much as possible.
Syscalls
The read
and write
syscalls are not supported and probably will never be. In the transition
period from the old framebuffer code in the Kernel to the current design, the mmap
syscall was
quite dangerous and did not handle multiple userspace programs trying to use it on one device.
Since that was resolved and mmap
can be used safely, read
and write
syscalls are no longer
needed and are considered obsolete for this device because no userspace program in Serenity will
ever need to use them, or test them at the very least.
The ioctl
syscall is used to control the DisplayConnector device - to invoke
changing of the current mode-set of a framebuffer, flush the framebuffer, etc.
Major and minor numbering
The major number is fixed at 226. Minor number is allocated incrementally as instances are initialized.