Skip to content

VGA & Framebuffers

Getting output onto a screen is one of the earliest milestones in kernel development, and x86 offers two quite different mechanisms for it: legacy VGA text mode, a fixed, character-oriented display requiring almost no setup, and a linear framebuffer, a block of memory representing individual pixels directly, on which every modern graphics mode (and every operating system’s actual graphical output) ultimately relies.

Text mode presents the screen as an 80-column by 25-row grid of characters, backed by memory starting at the fixed physical address 0xB8000, which BIOS-based firmware conventionally leaves already active on boot; a kernel can write to this memory immediately, with no mode-setting step required, making it the simplest possible display output for early development. Each character cell occupies two consecutive bytes: the ASCII character code, followed by an attribute byte encoding foreground and background color.

volatile uint16_t *vga_buffer = (uint16_t *)0xB8000;
void vga_put_char(int col, int row, char c, uint8_t color) {
vga_buffer[row * 80 + col] = (uint16_t)c | ((uint16_t)color << 8);
}

The attribute byte splits into a 4-bit foreground color (bits 0–3) and a 3-bit background color (bits 4–6), with bit 7 historically either a further background color bit or a blink-enable flag depending on the specific VGA hardware mode configured (a detail rarely relevant to a kernel doing nothing more than printing ordinary text). The hardware cursor’s position is separately controlled through a pair of VGA CRTC index/data ports (0x3D4/0x3D5), written as a 16-bit row-major offset into the same 80×25 grid the buffer itself uses.

Text mode has no equivalent for drawing arbitrary graphics: anything beyond fixed character glyphs requires a genuine pixel-addressable framebuffer, a block of memory, generally far larger than text mode’s tiny buffer, where each pixel corresponds to a fixed-size group of bytes (commonly 32 bits per pixel, holding red, green, blue, and sometimes an unused alpha channel) at a location computable directly from its x/y coordinates and the framebuffer’s configured pitch (the number of bytes per row, which is not always simply width times bytes-per-pixel, since hardware sometimes pads each row to a convenient alignment).

void put_pixel(struct framebuffer *fb, int x, int y, uint32_t color) {
uint8_t *pixel = fb->address + y * fb->pitch + x * (fb->bpp / 8);
*(uint32_t *)pixel = color;
}

Unlike text mode’s fixed address, a framebuffer’s location, resolution, and pixel format are not architecturally fixed values a kernel can simply assume; they must be discovered from firmware, or explicitly requested and configured, before they can be used at all.

On BIOS-based systems, the VESA BIOS Extensions (VBE) provide a real-mode interrupt interface for querying available graphics modes and their framebuffer address, resolution, and pixel layout, and for switching into a chosen mode, necessarily performed while still in real mode, since VBE is a BIOS service, before any protected or long mode transition takes the CPU past where BIOS calls remain usable. UEFI systems instead use the Graphics Output Protocol (GOP), a Boot Services protocol returning equivalent information (framebuffer address, resolution, pixel format) through ordinary UEFI protocol calls rather than a real-mode interrupt, consistent with UEFI’s generally more structured interface described under BIOS vs. UEFI. A Multiboot-compliant bootloader can additionally set up a framebuffer and pass its details directly in the Multiboot information structure, sparing the kernel from implementing either VBE or GOP querying itself.

Framebuffer memory should be mapped write-combining rather than either fully cacheable or fully uncacheable, when the underlying hardware and page-table format support it: write-combining lets the CPU batch individual pixel writes into larger bus transactions, which noticeably improves performance for anything doing substantial per-pixel drawing, while still avoiding the correctness hazards a fully cached mapping would introduce for memory the display hardware reads directly. A common mistake when switching from VGA text mode to a graphical framebuffer is forgetting that they are entirely separate display paths with no shared state: code written to print debug text via 0xB8000 stops having any visible effect at all once a different video mode has been set, silently, rather than producing any indication of why output has stopped appearing.

  1. ^ OSDev Wiki, “VGA Hardware”: a detailed hobbyist reference covering VGA register-level programming beyond text mode.
  2. ^ VESA, VBE Core Function Specification: defines the BIOS-era mode query and framebuffer discovery interface.
  • Serial Port: a simpler output path commonly used before a display driver exists at all.
  • PCI: how a discrete graphics card is discovered before its framebuffer can be configured.
  • Console and Terminal Emulation: the escape-sequence interpreter most kernels build directly on top of this drawing layer.
  • Writing a UEFI Bootloader: the Boot Services flow GOP framebuffer discovery is one call within.