| name | osdev-graphics |
| description | Graphics output for OS kernels. VGA text mode, VESA/VBE framebuffer, UEFI GOP, linear framebuffer pixel plotting and font drawing. Use when implementing video output, text consoles, framebuffer drivers, or 2D graphics. |
| origin | MCC |
Graphics Output for OS Kernels
Complete reference for video output from VGA text mode through UEFI GOP, covering framebuffer setup and basic rendering in bare-metal x86 and x86-64 kernels.
When to Use
- Writing a VGA text-mode console driver (putchar, scrolling, cursor)
- Setting up a VESA/VBE framebuffer before entering protected mode
- Obtaining a framebuffer via UEFI GOP in a UEFI bootloader
- Drawing pixels, rectangles, and text in a linear framebuffer
- Implementing double buffering for flicker-free rendering
- Choosing between VGA text, VBE, and GOP for your kernel's display
VGA Text Mode
Start here because it is the simplest path to visible output. The BIOS boots into VGA mode 3 (80x25 text) by default -- no mode switching required. You write characters directly to a memory-mapped buffer and the hardware renders them.
Memory Layout
The text buffer lives at physical address 0xB8000. Each character cell is 2 bytes:
Byte 0: ASCII character code
Byte 1: Attribute byte
The 80x25 grid means 2000 cells, 4000 bytes total. To address cell (x, y):
volatile uint16_t *vga = (volatile uint16_t *)0xB8000;
vga[y * 80 + x] = (uint16_t)character | ((uint16_t)attribute << 8);
Attribute Byte
Bit 76543210
||||||||
|||||^^^-- Foreground color (0-7)
||||^----- Foreground bright bit
|^^^------ Background color (0-7)
^--------- Blink enable (or background bright bit)
Why the blink bit matters: By default, bit 7 enables blinking rather than bright background. This means you only get 8 background colors. To get 16 background colors instead, disable blink by reading port 0x3DA (to reset the attribute controller flip-flop), writing 0x30 to port 0x3C0, then writing the current value with bit 3 cleared to 0x3C0.
Color Table
| Value | Color | Value | Bright Color |
|---|
| 0 | Black | 8 | Dark Gray |
| 1 | Blue | 9 | Light Blue |
| 2 | Green | 10 | Light Green |
| 3 | Cyan | 11 | Light Cyan |
| 4 | Red | 12 | Light Red |
| 5 | Magenta | 13 | Light Magenta |
| 6 | Brown | 14 | Yellow |
| 7 | Light Gray | 15 | White |
Cursor Control
The hardware cursor position is set via the CRT Controller (CRTC) registers at ports 0x3D4 (index) and 0x3D5 (data). The cursor position is a 16-bit linear offset into the text buffer:
void set_cursor(int x, int y) {
uint16_t pos = y * 80 + x;
outb(0x3D4, 14);
outb(0x3D5, pos >> 8);
outb(0x3D4, 15);
outb(0x3D5, pos & 0xFF);
}
Scrolling
Scroll by copying rows upward in the buffer, then clearing the last row:
void scroll(void) {
volatile uint16_t *vga = (volatile uint16_t *)0xB8000;
for (int i = 0; i < 24 * 80; i++)
vga[i] = vga[i + 80];
for (int i = 24 * 80; i < 25 * 80; i++)
vga[i] = ' ' | (0x07 << 8);
}
See references/vga-text.md for the complete text mode API, all CRTC registers, and cursor shape configuration.
VESA/VBE
Why Upgrade from VGA Text
VGA text mode gives you 80x25 characters with a fixed font. For higher resolution, graphical output, or custom fonts, you need a pixel-based framebuffer. VBE (VESA BIOS Extensions) provides this via BIOS interrupts that configure the video card for linear framebuffer access at resolutions up to the monitor's native resolution.
Critical Constraint: Real Mode Only
VBE uses BIOS INT 0x10, which only works in real mode. You must set up VBE before entering protected or long mode. Practical approach: configure VBE in your bootloader, or use a Multiboot-compliant bootloader (GRUB) that does it for you.
Setup Sequence
Step 1 -- Get VBE Info (optional):
regs.ax = 0x4F00;
regs.es = segment(info);
regs.di = offset(info);
int10(®s);
Step 2 -- Get Mode Info:
regs.ax = 0x4F01;
regs.cx = mode_number;
regs.es = segment(mode_info);
regs.di = offset(mode_info);
int10(®s);
Step 3 -- Set Mode with LFB:
regs.ax = 0x4F02;
regs.bx = mode_number | 0x4000;
int10(®s);
Why | 0x4000: Without bit 14, VBE uses a banked framebuffer (64KB window at 0xA0000 that you page through). The linear framebuffer maps the entire screen into a contiguous physical address range -- vastly simpler to program.
Multiboot Shortcut
GRUB can set the mode for you. In your Multiboot header, request a framebuffer:
; Multiboot2 framebuffer tag
framebuffer_tag:
dd 5 ; type = framebuffer
dd 20 ; size
dd 1024 ; preferred width
dd 768 ; preferred height
dd 32 ; preferred depth
The bootloader passes framebuffer info in the Multiboot info structure -- base address, pitch, width, height, and bits per pixel.
See references/vesa-vbe.md for complete VBE info block and mode info block structures, mode enumeration, and protected-mode access details.
UEFI GOP
Why GOP Exists
On UEFI systems, there is no BIOS INT 0x10. The Graphics Output Protocol (GOP) replaces VBE entirely. It provides pixel-mode framebuffers with a clean protocol interface. GOP does not support text modes -- only pixel-oriented graphics.
Locating and Using GOP
EFI_GRAPHICS_OUTPUT_PROTOCOL *gop;
EFI_GUID gopGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
status = uefi_call_wrapper(BS->LocateProtocol, 3, &gopGuid, NULL, (void**)&gop);
Mode Enumeration
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info;
UINTN info_size;
for (UINTN i = 0; i < gop->Mode->MaxMode; i++) {
gop->QueryMode(gop, i, &info_size, &info);
}
Setting a Mode
gop->SetMode(gop, desired_mode_number);
Pixel Format
GOP typically uses one of:
| PixelFormat enum | Layout | Byte order in memory |
|---|
| PixelRedGreenBlueReserved8BitPerColor | RGBX | R, G, B, _ |
| PixelBlueGreenRedReserved8BitPerColor | BGRX | B, G, R, _ |
| PixelBitMask | Custom | Check PixelInformation masks |
Why BGRA is common: Most hardware and UEFI firmware defaults to BGRX (blue in lowest byte). Always check info->PixelFormat rather than assuming.
Blt Operations
GOP provides hardware-accelerated block transfer:
EFI_GRAPHICS_OUTPUT_BLT_PIXEL fill = {0, 0, 255, 0};
gop->Blt(gop, &fill, EfiBltVideoFill, 0, 0, x, y, width, height, 0);
gop->Blt(gop, NULL, EfiBltVideoToVideo, sx, sy, dx, dy, w, h, 0);
Passing Framebuffer to Kernel
Your UEFI bootloader must pass framebuffer info to the kernel before ExitBootServices:
typedef struct {
uint64_t base;
uint64_t size;
uint32_t width;
uint32_t height;
uint32_t pitch;
uint32_t pixel_format;
} framebuffer_info_t;
After ExitBootServices, GOP protocol calls no longer work. You operate on the raw framebuffer.
See references/uefi-gop.md for complete EFI_GRAPHICS_OUTPUT_PROTOCOL structure, Blt operation types, and pixel format detection.
Linear Framebuffer
Once you have a framebuffer (from VBE, GOP, or Multiboot), rendering is the same regardless of source.
Pixel Address Calculation
uint8_t *pixel = framebuffer_base + y * pitch + x * bytes_per_pixel;
Why pitch differs from width * bpp: The hardware may pad each scanline for alignment. A 1024-pixel-wide display at 32bpp might have a pitch of 4096 bytes, or it might be 4224 bytes. Always use the pitch value from VBE mode info or GOP, never calculate it from width.
Common Pixel Formats
| Format | BPP | Byte layout | Notes |
|---|
| 32-bit BGRX | 4 | B, G, R, X | Most common (VBE and GOP) |
| 32-bit RGBX | 4 | R, G, B, X | Some GOP firmware |
| 24-bit BGR | 3 | B, G, R | Awkward alignment, avoid if possible |
| 16-bit RGB565 | 2 | RRRRRGGG GGGBBBBB | Legacy, low memory |
Putpixel
void putpixel(uint8_t *fb, uint32_t pitch, int x, int y,
uint8_t r, uint8_t g, uint8_t b) {
uint8_t *pixel = fb + y * pitch + x * 4;
pixel[0] = b;
pixel[1] = g;
pixel[2] = r;
pixel[3] = 0;
}
Drawing Primitives
Do not implement higher-level primitives by calling putpixel in a loop -- recomputing the address per pixel wastes cycles. Use pointer arithmetic:
void fill_rect(uint8_t *fb, uint32_t pitch, int x, int y,
int w, int h, uint8_t r, uint8_t g, uint8_t b) {
for (int row = 0; row < h; row++) {
uint8_t *line = fb + (y + row) * pitch + x * 4;
for (int col = 0; col < w; col++) {
line[col * 4] = b;
line[col * 4 + 1] = g;
line[col * 4 + 2] = r;
line[col * 4 + 3] = 0;
}
}
}
For horizontal lines, memset or memcpy with a repeated pixel value is even faster.
Double Buffering
Writing directly to the framebuffer causes visible tearing as pixels update mid-scan. The fix:
- Allocate a back buffer the same size as the framebuffer (pitch * height bytes)
- Draw everything to the back buffer
- Copy the entire back buffer to the framebuffer in one
memcpy
uint8_t *backbuffer = kmalloc(pitch * height);
memcpy(framebuffer, backbuffer, pitch * height);
This eliminates tearing at the cost of one extra memory copy per frame. For a kernel console, you only need to copy when the screen content actually changes.
Font Rendering
In a linear framebuffer, there is no hardware text rendering. You must draw characters yourself using bitmap font data.
PSF Format (PC Screen Font)
PSF is the simplest usable bitmap font format. PSF2 header:
struct psf2_header {
uint32_t magic;
uint32_t version;
uint32_t header_size;
uint32_t flags;
uint32_t num_glyphs;
uint32_t bytes_per_glyph;
uint32_t height;
uint32_t width;
};
An 8x16 font means each glyph is 16 bytes (one byte per row, 8 pixels per byte, MSB = leftmost pixel).
Rendering a Character
void draw_char(uint8_t *fb, uint32_t pitch, int cx, int cy,
char ch, uint8_t *font, uint32_t font_h, uint32_t font_w,
uint32_t fg, uint32_t bg) {
uint8_t *glyph = font + (unsigned char)ch * font_h;
for (uint32_t row = 0; row < font_h; row++) {
uint8_t bits = glyph[row];
uint32_t *pixel = (uint32_t *)(fb + (cy + row) * pitch + cx * 4);
for (uint32_t col = 0; col < font_w; col++) {
pixel[col] = (bits & (1 << (font_w - 1 - col))) ? fg : bg;
}
}
}
Why 8x16 is standard: At 1024x768, an 8x16 font gives you 128x48 characters -- a usable text console. At higher resolutions, consider 8x16 with 2x scaling (each font pixel drawn as a 2x2 block) or a larger font like 16x32.
Building a Framebuffer Console
Combine font rendering with a text buffer to build a console:
- Track cursor position (column, row in character coordinates)
- On newline, advance row; if past bottom, scroll the back buffer up by
font_height * pitch bytes
- On printable character, draw_char and advance column
- Scrolling:
memmove the back buffer up by one character row, clear the bottom row, then copy to framebuffer
Choosing Your Approach
Boot method?
├── BIOS boot
│ ├── Need text only, simplest path → VGA Text Mode (0xB8000)
│ └── Need graphics/higher resolution → VBE framebuffer (set in bootloader)
├── UEFI boot
│ └── GOP framebuffer (only option, no text mode)
└── Post-boot rendering
├── Have framebuffer from any source → Linear framebuffer drawing
└── Need text console → Bitmap font on framebuffer
Recommendation: Start with VGA text mode to get visible output immediately. Upgrade to a framebuffer (VBE or GOP depending on boot path) when you need graphics. Implement a framebuffer text console early -- once you have one, it works regardless of how you obtained the framebuffer.
Common Pitfalls
| Pitfall | Consequence | Fix |
|---|
| Using width * bpp instead of pitch | Pixels draw at wrong positions, garbled display | Always use pitch from VBE mode info or GOP |
| Calling VBE INT 0x10 after entering protected mode | Triple fault or garbage | Set VBE mode in bootloader before mode switch |
| Assuming pixel format without checking | Colors wrong (red/blue swapped) | Read PixelFormat from GOP or color masks from VBE |
| Writing to framebuffer without mapping it | Page fault | Map framebuffer physical address in page tables (uncacheable or write-combining) |
| Not using volatile for VGA text buffer | Compiler optimizes away writes | Declare pointer as volatile uint16_t * |
| Forgetting to pass framebuffer info to kernel | Kernel has no display after ExitBootServices | Save base, pitch, width, height, bpp in boot info struct |
| Scrolling by redrawing every character | Extremely slow | Use memmove on the buffer, only redraw changed content |
| No double buffering | Visible tearing and flicker | Allocate back buffer, copy on update |
| Mapping framebuffer as cacheable | Stale pixels, visual artifacts | Map as write-combining (PAT) or uncacheable |
Related Skills
osdev-boot-sequence -- bootloader setup, entering protected/long mode
osdev-paging -- mapping the framebuffer in virtual memory
osdev-interrupts -- keyboard input for your text console
osdev-toolchain -- cross-compiler setup, QEMU testing