Introduction

The UEFI Graphics Output Protocol (GOP) represents a fundamental shift in how operating systems and bootloaders interact with display hardware. Unlike the legacy VGA BIOS interrupts that dominated PC graphics for decades, GOP provides a modern, standardized interface for framebuffer access during the pre-boot environment. This guide provides a comprehensive, practical approach to implementing GOP-based graphics in your UEFI applications.

Whether you're developing a bootloader, creating a UEFI shell application, or building a custom firmware interface, understanding GOP is essential for displaying graphics before the operating system takes control. We'll cover everything from basic initialization to advanced rendering techniques, complete with working code examples.

Understanding UEFI Graphics Architecture

The Evolution from VGA to GOP

The traditional VGA BIOS provided interrupt-based graphics access through INT 10h. This approach had significant limitations:

  • Hardware dependency: Required specific VGA-compatible hardware
  • Limited resolution: Typically capped at 640×480 with 16 colors
  • Slow performance: BIOS interrupts added significant overhead
  • Real mode only: Required switching to real mode, incompatible with modern protected mode

UEFI GOP addresses these limitations by providing:

  • Hardware abstraction: Works with modern graphics hardware
  • High resolutions: Supports native display resolutions
  • Direct framebuffer access: Linear memory mapping for fast rendering
  • Native mode: Works in UEFI environment without mode switching

GOP Architecture Overview

+------------------+
|  UEFI Application |
+------------------+
        |
        v
+------------------+
|   GOP Protocol   |
+------------------+
        |
        v
+------------------+
|  Graphics Driver |
+------------------+
        |
        v
+------------------+
|  Display Hardware|
+------------------+

The GOP protocol sits between your application and the graphics driver, providing a standardized interface regardless of underlying hardware.

GOP Protocol Structure

Protocol Interface Definition

The GOP protocol is defined in the UEFI specification as follows:

typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL {
    EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt;
    EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;

Mode Information Structure

typedef struct {
    UINT32 MaxHorizontalResolution;
    UINT32 MaxVerticalResolution;
} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE_INFO;

typedef struct {
    UINT32 Version;
    UINT32 HorizontalResolution;
    UINT32 VerticalResolution;
    EFI_GRAPHICS_OUTPUT_PIXEL_FORMAT PixelFormat;
    EFI_GRAPHICS_OUTPUT_PIXEL_BIT_MASK PixelInformation;
    UINT32 PixelsPerScanLine;
} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION;

Pixel Format Types

typedef enum {
    PixelRedGreenBlueReserved8BitPerColor,
    PixelBlueGreenRedReserved8BitPerColor,
    PixelBitMask,
    PixelBltOnly,
    PixelFormatMax
} EFI_GRAPHICS_OUTPUT_PIXEL_FORMAT;

Understanding the pixel format is critical for correct color rendering.

Locating and Initializing GOP

Finding the GOP Protocol

Before using GOP, you must locate it in the UEFI system table:

#include <Uefi.h>
#include <Protocol/GraphicsOutput.h>

EFI_GRAPHICS_OUTPUT_PROTOCOL *gGraphicsOutput = NULL;

EFI_STATUS InitializeGraphics() {
    EFI_STATUS Status;
    
    // Locate the GOP protocol
    Status = gBS->LocateProtocol(
        &gEfiGraphicsOutputProtocolGuid,
        NULL,
        (VOID**)&gGraphicsOutput
    );
    
    if (EFI_ERROR(Status)) {
        // GOP not available, fall back to text mode or other graphics
        Print(L"Failed to locate GOP: %r\n", Status);
        return Status;
    }
    
    Print(L"GOP located successfully!\n");
    return EFI_SUCCESS;
}

Querying Available Modes

Once you have the GOP protocol, query available display modes:

EFI_STATUS QueryAvailableModes() {
    EFI_STATUS Status;
    UINT32 MaxMode = gGraphicsOutput->Mode->MaxMode;
    UINT32 SizeOfInfo;
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
    
    Print(L"Available display modes:\n");
    
    for (UINT32 Mode = 0; Mode < MaxMode; Mode++) {
        Status = gGraphicsOutput->QueryMode(
            gGraphicsOutput,
            Mode,
            &SizeOfInfo,
            &Info
        );
        
        if (!EFI_ERROR(Status)) {
            Print(L"  Mode %d: %dx%d ", 
                  Mode, 
                  Info->HorizontalResolution,
                  Info->VerticalResolution);
            
            switch (Info->PixelFormat) {
                case PixelRedGreenBlueReserved8BitPerColor:
                    Print(L"(RGB)\n");
                    break;
                case PixelBlueGreenRedReserved8BitPerColor:
                    Print(L"(BGR)\n");
                    break;
                case PixelBitMask:
                    Print(L"(BitMask)\n");
                    break;
                default:
                    Print(L"(Unknown)\n");
                    break;
            }
        }
    }
    
    return EFI_SUCCESS;
}

Setting the Display Mode

Select and activate your desired display mode:

EFI_STATUS SetDisplayMode(UINT32 ModeNumber) {
    EFI_STATUS Status;
    
    Status = gGraphicsOutput->SetMode(gGraphicsOutput, ModeNumber);
    
    if (EFI_ERROR(Status)) {
        Print(L"Failed to set mode %d: %r\n", ModeNumber, Status);
        return Status;
    }
    
    Print(L"Display mode set to %d\n", ModeNumber);
    return EFI_SUCCESS;
}

Best Practice: Always check if the mode change succeeded before attempting to render.

Framebuffer Access and Rendering

Understanding the Framebuffer

After setting a mode, the framebuffer is accessible through the GOP mode structure:

typedef struct {
    UINT32 MaxMode;
    UINT32 Mode;
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
    VOID *FrameBufferBase;
    UINTN FrameBufferSize;
} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE;

Basic Pixel Drawing

Here's a simple function to draw a single pixel:

VOID DrawPixel(UINTN X, UINTN Y, EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color) {
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info = gGraphicsOutput->Mode->Info;
    UINT8 *Framebuffer = (UINT8*)gGraphicsOutput->Mode->FrameBufferBase;
    UINTN PixelPosition;
    
    // Validate coordinates
    if (X >= Info->HorizontalResolution || Y >= Info->VerticalResolution) {
        return;
    }
    
    // Calculate pixel position based on pixel format
    switch (Info->PixelFormat) {
        case PixelRedGreenBlueReserved8BitPerColor:
            PixelPosition = (Y * Info->PixelsPerScanLine + X) * 4;
            Framebuffer[PixelPosition + 0] = Color.Red;
            Framebuffer[PixelPosition + 1] = Color.Green;
            Framebuffer[PixelPosition + 2] = Color.Blue;
            // Reserved byte (PixelPosition + 3) typically unused
            break;
            
        case PixelBlueGreenRedReserved8BitPerColor:
            PixelPosition = (Y * Info->PixelsPerScanLine + X) * 4;
            Framebuffer[PixelPosition + 0] = Color.Blue;
            Framebuffer[PixelPosition + 1] = Color.Green;
            Framebuffer[PixelPosition + 2] = Color.Red;
            break;
            
        case PixelBitMask:
            // More complex, requires applying bit masks
            // See advanced section below
            break;
    }
}

Efficient Rectangle Drawing with Blt

The Blt (Block Transfer) function is much more efficient than drawing pixels individually:

EFI_STATUS DrawRectangle(
    UINTN X,
    UINTN Y,
    UINTN Width,
    UINTN Height,
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color
) {
    EFI_STATUS Status;
    
    // Create a buffer filled with the desired color
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer = 
        (EFI_GRAPHICS_OUTPUT_BLT_PIXEL*)AllocatePool(Width * Height * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
    
    if (BltBuffer == NULL) {
        return EFI_OUT_OF_RESOURCES;
    }
    
    // Fill the buffer with the color
    for (UINTN i = 0; i < Width * Height; i++) {
        BltBuffer[i] = Color;
    }
    
    // Use Blt to draw the rectangle
    Status = gGraphicsOutput->Blt(
        gGraphicsOutput,
        BltBuffer,
        EfiBltBufferToVideo,  // Copy from buffer to video
        0, 0,                 // Source X, Y in buffer
        X, Y,                 // Destination X, Y on screen
        Width, Height,        // Width and Height
        0                     // Delta (0 = use Width)
    );
    
    FreePool(BltBuffer);
    return Status;
}

Blt Operations Explained

The Blt function supports several operations:

typedef enum {
    EfiBltVideoFill,      // Fill video rect with single pixel
    EfiBltVideoToBltBuffer, // Copy from video to buffer
    EfiBltBufferToVideo,  // Copy from buffer to video
    EfiBltVideoToVideo,   // Copy within video (scroll, etc.)
    EfiGraphicsOutputBltOperationMax
} EFI_GRAPHICS_OUTPUT_BLT_OPERATION;

EfiBltVideoFill: Most efficient for filling rectangles with solid color
EfiBltBufferToVideo: Best for rendering sprites or images
EfiBltVideoToVideo: Useful for scrolling or copying screen regions

Advanced Rendering Techniques

Line Drawing Algorithm

Implement Bresenham's line algorithm for efficient line drawing:

VOID DrawLine(
    UINTN X0, UINTN Y0,
    UINTN X1, UINTN Y1,
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color
) {
    INTN Dx = (INTN)X1 - (INTN)X0;
    INTN Dy = (INTN)Y1 - (INTN)Y0;
    INTN StepX, StepY;
    INTN Error;
    
    if (Dx < 0) {
        Dx = -Dx;
        StepX = -1;
    } else {
        StepX = 1;
    }
    
    if (Dy < 0) {
        Dy = -Dy;
        StepY = -1;
    } else {
        StepY = 1;
    }
    
    Error = Dx - Dy;
    
    while (TRUE) {
        DrawPixel(X0, Y0, Color);
        
        if (X0 == X1 && Y0 == Y1) {
            break;
        }
        
        INTN Error2 = 2 * Error;
        
        if (Error2 > -Dy) {
            Error -= Dy;
            X0 += StepX;
        }
        
        if (Error2 < Dx) {
            Error += Dx;
            Y0 += StepY;
        }
    }
}

Circle Drawing

Midpoint circle algorithm for efficient circle rendering:

VOID DrawCircle(
    UINTN CenterX,
    UINTN CenterY,
    UINTN Radius,
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color
) {
    INTN X = Radius;
    INTN Y = 0;
    INTN Error = 0;
    
    while (X >= Y) {
        // Draw 8 octants
        DrawPixel(CenterX + X, CenterY + Y, Color);
        DrawPixel(CenterX + Y, CenterY + X, Color);
        DrawPixel(CenterX - Y, CenterY + X, Color);
        DrawPixel(CenterX - X, CenterY + Y, Color);
        DrawPixel(CenterX - X, CenterY - Y, Color);
        DrawPixel(CenterX - Y, CenterY - X, Color);
        DrawPixel(CenterX + Y, CenterY - X, Color);
        DrawPixel(CenterX + X, CenterY - Y, Color);
        
        Y++;
        Error += 1 + 2 * Y;
        
        if (2 * (Error - X) + 1 > 0) {
            X--;
            Error += 1 - 2 * X;
        }
    }
}

Text Rendering

Basic text rendering without relying on UEFI text output:

// Simple 8x8 font bitmap (example for character 'A')
UINT8 Font8x8[128][8] = {
    // ... font data ...
};

VOID DrawChar(UINTN X, UINTN Y, CHAR8 Character, EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color) {
    if (Character < 0 || Character >= 128) {
        return;
    }
    
    for (UINT8 Row = 0; Row < 8; Row++) {
        UINT8 FontRow = Font8x8[Character][Row];
        
        for (UINT8 Col = 0; Col < 8; Col++) {
            if (FontRow & (0x80 >> Col)) {
                DrawPixel(X + Col, Y + Row, Color);
            }
        }
    }
}

VOID DrawString(UINTN X, UINTN Y, CHAR16 *String, EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color) {
    UINTN CharX = X;
    
    while (*String != L'\0') {
        DrawChar(CharX, Y, (CHAR8)*String, Color);
        CharX += 8;  // 8 pixels per character
        String++;
    }
}

Double Buffering

For smooth animations, implement double buffering:

EFI_GRAPHICS_OUTPUT_BLT_PIXEL *gBackBuffer = NULL;
UINTN gScreenWidth = 0;
UINTN gScreenHeight = 0;

EFI_STATUS InitializeDoubleBuffer() {
    EFI_STATUS Status;
    
    gScreenWidth = gGraphicsOutput->Mode->Info->HorizontalResolution;
    gScreenHeight = gGraphicsOutput->Mode->Info->VerticalResolution;
    
    gBackBuffer = (EFI_GRAPHICS_OUTPUT_BLT_PIXEL*)AllocatePool(
        gScreenWidth * gScreenHeight * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL)
    );
    
    if (gBackBuffer == NULL) {
        return EFI_OUT_OF_RESOURCES;
    }
    
    // Clear back buffer
    ZeroMem(gBackBuffer, gScreenWidth * gScreenHeight * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL));
    
    return EFI_SUCCESS;
}

VOID DrawPixelBackBuffer(UINTN X, UINTN Y, EFI_GRAPHICS_OUTPUT_BLT_PIXEL Color) {
    if (X >= gScreenWidth || Y >= gScreenHeight) {
        return;
    }
    gBackBuffer[Y * gScreenWidth + X] = Color;
}

VOID SwapBuffers() {
    gGraphicsOutput->Blt(
        gGraphicsOutput,
        gBackBuffer,
        EfiBltBufferToVideo,
        0, 0,
        0, 0,
        gScreenWidth,
        gScreenHeight,
        0
    );
}

Handling Pixel BitMasks

When the pixel format is PixelBitMask, you need to apply bit masks:

VOID DrawPixelBitMask(
    UINTN X,
    UINTN Y,
    UINT8 Red,
    UINT8 Green,
    UINT8 Blue
) {
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info = gGraphicsOutput->Mode->Info;
    UINT32 *Framebuffer = (UINT32*)gGraphicsOutput->Mode->FrameBufferBase;
    UINT32 PixelColor;
    
    if (X >= Info->HorizontalResolution || Y >= Info->VerticalResolution) {
        return;
    }
    
    // Apply bit masks
    PixelColor = ((Red & 0xFF) >> (8 - Info->PixelInformation.RedMask)) 
                 << Info->PixelInformation.RedShift;
    PixelColor |= ((Green & 0xFF) >> (8 - Info->PixelInformation.GreenMask)) 
                  << Info->PixelInformation.GreenShift;
    PixelColor |= ((Blue & 0xFF) >> (8 - Info->PixelInformation.BlueMask)) 
                  << Info->PixelInformation.BlueShift;
    
    Framebuffer[Y * Info->PixelsPerScanLine + X] = PixelColor;
}

Performance Optimization

Minimizing Memory Allocations

Avoid allocating memory in render loops:

// BAD: Allocating in every frame
VOID RenderFrame() {
    for (INTN i = 0; i < 100; i++) {
        EFI_GRAPHICS_OUTPUT_BLT_PIXEL *Buffer = AllocatePool(...);
        // ... use buffer ...
        FreePool(Buffer);
    }
}

// GOOD: Reuse allocated buffer
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *gRenderBuffer = NULL;

VOID InitializeRenderer() {
    gRenderBuffer = AllocatePool(MAX_BUFFER_SIZE);
}

VOID RenderFrame() {
    // Reuse gRenderBuffer
}

Batch Rendering Operations

Group drawing operations to minimize Blt calls:

// BAD: Multiple Blt calls
DrawRectangle(10, 10, 100, 50, Red);
DrawRectangle(20, 20, 100, 50, Green);
DrawRectangle(30, 30, 100, 50, Blue);

// GOOD: Single Blt with composed buffer
ComposeAllRectanglesInBuffer();
SingleBltCall();

Using Video Fill for Solid Colors

// More efficient than creating a buffer
EFI_GRAPHICS_OUTPUT_BLT_PIXEL FillColor = {255, 0, 0, 0};

gGraphicsOutput->Blt(
    gGraphicsOutput,
    &FillColor,
    EfiBltVideoFill,
    0, 0,
    X, Y,
    Width, Height,
    0
);

Common Issues and Solutions

Issue 1: Black Screen After Mode Set

Problem: Display goes black after calling SetMode.

Causes:

  • Invalid mode number
  • Hardware doesn't support requested mode
  • Framebuffer address changed

Solution:

EFI_STATUS SafeSetMode(UINT32 ModeNumber) {
    // First verify the mode exists
    UINT32 MaxMode = gGraphicsOutput->Mode->MaxMode;
    if (ModeNumber >= MaxMode) {
        Print(L"Invalid mode number\n");
        return EFI_INVALID_PARAMETER;
    }
    
    // Query the mode first to ensure it's valid
    UINT32 SizeOfInfo;
    EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
    EFI_STATUS Status = gGraphicsOutput->QueryMode(
        gGraphicsOutput, ModeNumber, &SizeOfInfo, &Info
    );
    
    if (EFI_ERROR(Status)) {
        Print(L"Mode not supported\n");
        return Status;
    }
    
    // Now set the mode
    return gGraphicsOutput->SetMode(gGraphicsOutput, ModeNumber);
}

Issue 2: Colors Appear Incorrect

Problem: Colors are swapped or wrong intensity.

Cause: Incorrect pixel format handling.

Solution: Always check PixelFormat and handle each case appropriately:

VOID SetPixelCorrectly(UINTN X, UINTN Y, UINT8 R, UINT8 G, UINT8 B) {
    EFI_GRAPHICS_OUTPUT_PIXEL_FORMAT Format = gGraphicsOutput->Mode->Info->PixelFormat;
    
    switch (Format) {
        case PixelRedGreenBlueReserved8BitPerColor:
            // RGB order
            break;
        case PixelBlueGreenRedReserved8BitPerColor:
            // BGR order - swap R and B
            UINT8 Temp = R;
            R = B;
            B = Temp;
            break;
        case PixelBitMask:
            // Apply masks
            break;
    }
    
    // Draw with corrected colors
}

Issue 3: Performance Too Slow

Problem: Rendering is sluggish or causes visible flicker.

Solutions:

  1. Use Blt instead of direct framebuffer access
  2. Implement double buffering
  3. Minimize the number of Blt calls
  4. Use EfiBltVideoFill for solid rectangles
  5. Only redraw changed regions (dirty rectangles)

Complete Example: Simple Boot Screen

Here's a complete example showing a simple graphical boot screen:

#include <Uefi.h>
#include <Protocol/GraphicsOutput.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>

EFI_GRAPHICS_OUTPUT_PROTOCOL *gGraphicsOutput = NULL;

EFI_STATUS
EFIAPI
UefiMain(
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
) {
    EFI_STATUS Status;
    
    // Initialize console
    gST->ConOut->ClearScreen(gST->ConOut);
    Print(L"Initializing Graphics...\n");
    
    // Locate GOP
    Status = gBS->LocateProtocol(
        &gEfiGraphicsOutputProtocolGuid,
        NULL,
        (VOID**)&gGraphicsOutput
    );
    
    if (EFI_ERROR(Status)) {
        Print(L"GOP not available, using text mode\n");
        gBS->Stall(2000000);
        return EFI_SUCCESS;
    }
    
    // Find and set best mode (1024x768 or similar)
    UINT32 BestMode = 0;
    UINT32 MaxResolution = 0;
    
    for (UINT32 Mode = 0; Mode < gGraphicsOutput->Mode->MaxMode; Mode++) {
        UINT32 SizeOfInfo;
        EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
        
        Status = gGraphicsOutput->QueryMode(
            gGraphicsOutput, Mode, &SizeOfInfo, &Info
        );
        
        if (!EFI_ERROR(Status)) {
            UINT32 Resolution = Info->HorizontalResolution * Info->VerticalResolution;
            if (Resolution > MaxResolution && 
                Info->HorizontalResolution >= 1024 &&
                Info->VerticalResolution >= 768) {
                MaxResolution = Resolution;
                BestMode = Mode;
            }
        }
    }
    
    // Set the best mode
    Status = gGraphicsOutput->SetMode(gGraphicsOutput, BestMode);
    if (EFI_ERROR(Status)) {
        Print(L"Failed to set graphics mode\n");
        gBS->Stall(2000000);
        return Status;
    }
    
    // Clear screen to blue
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Blue = {0, 0, 255, 0};
    gGraphicsOutput->Blt(
        gGraphicsOutput,
        &Blue,
        EfiBltVideoFill,
        0, 0, 0, 0,
        gGraphicsOutput->Mode->Info->HorizontalResolution,
        gGraphicsOutput->Mode->Info->VerticalResolution,
        0
    );
    
    // Draw welcome rectangle
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL White = {255, 255, 255, 0};
    DrawRectangle(100, 100, 800, 400, White);
    
    // Draw border
    EFI_GRAPHICS_OUTPUT_BLT_PIXEL Black = {0, 0, 0, 0};
    DrawRectangle(95, 95, 810, 410, Black);
    
    Print(L"\nGraphics initialized successfully!\n");
    Print(L"Resolution: %dx%d\n", 
          gGraphicsOutput->Mode->Info->HorizontalResolution,
          gGraphicsOutput->Mode->Info->VerticalResolution);
    
    gBS->Stall(5000000);  // Wait 5 seconds
    
    return EFI_SUCCESS;
}

Conclusion

The UEFI Graphics Output Protocol provides a powerful, standardized interface for pre-boot graphics. By understanding GOP's architecture, properly handling pixel formats, and using efficient rendering techniques like Blt operations, you can create sophisticated graphical interfaces for your UEFI applications.

Key takeaways from this guide:

  1. Always query available modes before setting a display mode
  2. Handle pixel formats correctly - RGB, BGR, and BitMask require different approaches
  3. Use Blt operations for efficient rendering instead of direct pixel manipulation
  4. Implement double buffering for smooth animations
  5. Validate all operations to handle hardware variations gracefully

With these fundamentals mastered, you're ready to build everything from simple boot screens to complex graphical firmware interfaces. The skills you develop working with GOP will serve you well in low-level graphics programming across many contexts.

Additional Resources

Specifications

  • UEFI Specification (Chapter on Graphics Output Protocol)
  • EDK II Programming Manual

Code References

  • EDK II MdeModulePkg - Reference GOP implementation
  • Various open-source bootloader projects (GRUB, systemd-boot)

Tools

  • UEFI Shell for testing
  • QEMU with OVMF for development and debugging
  • UEFI Development Kit (UDK)

Happy graphics programming!