在 UEFI 环境下开发图形应用时,我们经常需要在屏幕上绘制图像、显示 Logo 或实现简单 UI。EFI_GRAPHICS_OUTPUT_PROTOCOL(简称 GOP)提供了两种最基本的绘图方式:直接操作 FrameBuffer 内存,或者调用 Blt(Block Transfer)接口。
一、背景:UEFI 的图形输出协议
UEFI(统一可扩展固件接口)中的 Graphics Output Protocol (GOP) 负责管理显示输出。通过该协议,我们可以获取当前显示模式(分辨率、像素格式)、设置新模式,并将像素数据写入屏幕。
GOP 提供了两种绘图途径:
-
直接映射 FrameBuffer
通过
Gop->Mode->FrameBufferBase获得显存基址,然后像操作普通内存一样向其中写入像素值。这种方式非常直接,但需要开发者自行处理像素格式、行对齐等底层细节。 -
Blt 接口
Gop->Blt()是一个标准的块传输函数,支持填充、拷贝、内存 ↔ 显存传输等多种操作。它接受独立于硬件像素格式的EFI_GRAPHICS_OUTPUT_BLT_PIXEL结构体,由驱动完成格式转换。
为了直观对比,我们来看两个完整的 UEFI 应用程序:一个采用直接写 FrameBuffer,另一个使用 Blt 接口。两者功能相同------枚举显示模式、设置合适分辨率、绘制渐变背景和白色十字,最后清屏退出。
二、方式一:直接操作 FrameBuffer
这是最直接的方法,代码中直接向显存地址写入像素数据。
c
UINT32 *FrameBuffer = (UINT32*)Gop->Mode->FrameBufferBase;
UINT32 PixelsPerScanLine = Gop->Mode->Info->PixelsPerScanLine;
for (y = 0; y < Vertical; y++) {
for (x = 0; x < Horizontal; x++) {
// 计算 RGB 分量
UINT8 r = (UINT8)(x * 255 / Horizontal);
UINT8 g = (UINT8)(y * 255 / Vertical);
UINT8 b = 0x80;
UINT32 color;
// 手动处理像素字节序
if (SwapRedBlue) {
color = (b << 16) | (g << 8) | r;
} else {
color = (r << 16) | (g << 8) | b;
}
FrameBuffer[y * PixelsPerScanLine + x] = color;
}
}
关键点
- 像素格式依赖 :代码必须根据
Gop->Mode->Info->PixelFormat自行决定 R、G、B 的排列顺序。示例中通过SwapRedBlue布尔值来区分 RGB 和 BGR,并手动组装 32 位颜色值。 - 行步长 :
PixelsPerScanLine通常是显卡硬件对齐后的每行像素数,可能大于Horizontal。直接使用y * PixelsPerScanLine + x才能正确定位到每个像素的地址。 - 绘制十字和清屏 :同样使用
for循环逐像素修改显存。
优点
- 零额外内存开销:无需分配 CPU 侧的缓冲区,直接操作显存。
- 灵活:可以实现任意逐像素算法(如抗锯齿、色彩变换等)。
缺点
- 兼容性差 :若显卡采用自定义位掩码格式(
PixelBitMask)或禁止直接访问(PixelBltOnly),该方式将彻底失效。 - CPU 负担重:每次写入都跨越 PCIe 总线,对于全屏绘制,大量循环会导致明显的性能下降。
- 代码复杂:必须谨慎处理字节序和对齐,不同显卡的行为可能存在差异。
三、方式二:使用 Blt 接口
方式二遵循 UEFI 规范推荐的标准化做法,将所有绘图操作封装为对 Gop->Blt() 的调用。
c
// 1. 在系统内存中构建 BltBuffer
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer;
gBS->AllocatePool(EfiBootServicesData,
Horizontal * Vertical * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL),
(VOID**)&BltBuffer);
for (y = 0; y < Vertical; y++) {
for (x = 0; x < Horizontal; x++) {
UINTN idx = (UINTN)y * Horizontal + x;
BltBuffer[idx].Red = (UINT8)(x * 255 / Horizontal);
BltBuffer[idx].Green = (UINT8)(y * 255 / Vertical);
BltBuffer[idx].Blue = 0x80;
BltBuffer[idx].Reserved = 0;
}
}
// 2. 一次性 Blt 到屏幕
Gop->Blt(Gop, BltBuffer, EfiBltBufferToVideo,
0, 0, // SourceX, SourceY
0, 0, // DestX, DestY
Horizontal, Vertical,
0); // Delta = 0,表示行宽 = Width * sizeof(PIXEL)
// 3. 用 EfiBltVideoFill 画白色十字
EFI_GRAPHICS_OUTPUT_BLT_PIXEL White = {0xFF, 0xFF, 0xFF, 0x00};
Gop->Blt(Gop, &White, EfiBltVideoFill, 0, 0, 0, cy, Horizontal, 1, 0);
Gop->Blt(Gop, &White, EfiBltVideoFill, 0, 0, cx, 0, 1, Vertical, 0);
关键点
- 无需关心字节序 :直接填充
EFI_GRAPHICS_OUTPUT_BLT_PIXEL的Red、Green、Blue字段,驱动会自动转换。 - Delta 参数 :这里传入
0,表示源缓冲区没有额外填充;如果有偏移(SourceX/Y 非零),则需设置正确的行字节数。 - 硬件加速指令 :
EfiBltVideoFill使用单个像素作为填充色,驱动会以最高效的方式(可能借助 GPU)填满指定矩形区域,比软件循环快得多。
优点
- 高兼容性 :支持所有像素格式,包括
PixelBltOnly。 - 代码简洁:不需要手动处理字节序和对齐,意图清晰。
- 性能优异:批量传输和硬件填充减少了 CPU 与显存的交互次数。
- 标准接口:符合 UEFI 规范,移植性更好。
缺点
- 额外内存开销 :需要分配与全屏大小相当的
BltBuffer(例如 1080p 下约 8 MB),对于内存紧张的早期启动环境可能不友好。
四、全方位对比
| 对比维度 | 直接写 FrameBuffer | Blt 接口 |
|---|---|---|
| 像素字节序处理 | 手动,易出错 | 自动转换,无需关心 |
| 行填充(Padding) | 需使用 PixelsPerScanLine 手动计算 |
通过 Delta 参数由驱动处理 |
| 额外内存占用 | 无 | 需分配全屏大小的 BltBuffer |
| 绘制效率(全屏) | 多次 CPU 写操作,较慢 | 单次 DMA 传输,较快 |
| 硬件加速支持 | 无法利用 | 支持 EfiBltVideoFill 等填充操作 |
兼容 PixelBltOnly |
❌ 不支持 | ✅ 支持 |
| 代码维护性 | 逻辑混杂底层细节 | 抽象层次高,易于阅读 |
五、代码
方式1:
bash
/**
* @brief 显示系统启动时的 LOGO 图 方式1:直接写 FrameBuffer
*
*/
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/GraphicsOutput.h>
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
UINT32 MaxMode;
UINT32 ModeNumber;
UINT32 SelectedMode = 0;
UINTN SizeOfInfo;
UINT32 Horizontal = 0;
UINT32 Vertical = 0;
UINTN Index;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *ModeInfo = NULL;
BOOLEAN SwapRedBlue = FALSE;
// 1. 获取 GOP Protocol
Status = gBS->LocateProtocol(
&gEfiGraphicsOutputProtocolGuid,
NULL,
(VOID**)&Gop
);
if (EFI_ERROR(Status)) {
Print(L"Failed to locate GOP\n");
return Status;
}
Print(L"GOP protocol found successfully!\n");
// 2. 枚举所有显示模式
Print(L"=== Enumerating All Display Modes ===\n");
MaxMode = Gop->Mode->MaxMode;
Print(L"MaxMode = %d\n\n", MaxMode);
for (ModeNumber = 0; ModeNumber < MaxMode; ModeNumber++) {
Status = Gop->QueryMode(Gop, ModeNumber, &SizeOfInfo, &ModeInfo);
if (EFI_ERROR(Status)) {
Print(L"QueryMode failed for mode %d: %r\n", ModeNumber, Status);
continue;
}
Print(L"Mode %2d: %4d x %4d, PixelFormat = %d\n",
ModeNumber,
ModeInfo->HorizontalResolution,
ModeInfo->VerticalResolution,
ModeInfo->PixelFormat
);
if (ModeInfo != NULL) {
gBS->FreePool(ModeInfo);
ModeInfo = NULL;
}
}
// 暂停让用户查看枚举结果
Print(L"\nPress Enter to continue...");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
while (TRUE) {
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
EFI_INPUT_KEY Key;
if (!EFI_ERROR(SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key))) {
if (Key.UnicodeChar == L'\r') {
break;
}
}
}
// 3. 设置显示模式
Status = Gop->SetMode(Gop, SelectedMode);
if (EFI_ERROR(Status)) {
Print(L"Failed to set mode %d: %r\n", SelectedMode, Status);
return Status;
}
Horizontal = Gop->Mode->Info->HorizontalResolution;
Vertical = Gop->Mode->Info->VerticalResolution;
Print(L"Selected mode %d with resolution %d x %d\n", SelectedMode, Horizontal, Vertical);
Print(L"Mode %d set successfully!\n", SelectedMode);
// 4. 当前显示模式信息
Print(L"\n=== Current Mode Information ===\n");
Print(L"Current Mode Number: %d\n", Gop->Mode->Mode);
Print(L"FrameBuffer Base: 0x%016lx\n", Gop->Mode->FrameBufferBase);
{
UINT32 SizeMB = (UINT32)(Gop->Mode->FrameBufferSize / (1024 * 1024));
UINT32 SizeKB = (UINT32)((Gop->Mode->FrameBufferSize % (1024 * 1024)) / 1024);
Print(L"FrameBuffer Size: %d bytes (%d.%02d MB)\n",
Gop->Mode->FrameBufferSize,
SizeMB,
(SizeKB * 100) / 1024);
}
Print(L"Horizontal Resolution: %d\n", Gop->Mode->Info->HorizontalResolution);
Print(L"Vertical Resolution: %d\n", Gop->Mode->Info->VerticalResolution);
Print(L"Pixel Format: %d\n", Gop->Mode->Info->PixelFormat);
// 解释像素格式
switch (Gop->Mode->Info->PixelFormat) {
case PixelRedGreenBlueReserved8BitPerColor:
Print(L" -> Pixel Format: RGB (8:8:8, with reserved byte)\n");
break;
case PixelBlueGreenRedReserved8BitPerColor:
Print(L" -> Pixel Format: BGR (most common on PC)\n");
break;
case PixelBitMask:
Print(L" -> Pixel Format: Custom bitmask\n");
break;
case PixelBltOnly:
Print(L" -> Pixel Format: Blt only (no direct framebuffer access)\n");
break;
default:
Print(L" -> Pixel Format: Unknown\n");
}
// 5. 根据像素格式确定颜色通道排列
SwapRedBlue = (Gop->Mode->Info->PixelFormat == PixelRedGreenBlueReserved8BitPerColor);
// 4. 等待 Enter 键
Print(L"Press Enter to draw...\n");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
while (TRUE) {
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
EFI_INPUT_KEY Key;
if (!EFI_ERROR(SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key))) {
if (Key.UnicodeChar == L'\r') {
break;
}
}
}
// 5. 绘制渐变背景与中心十字
if (Horizontal > 0 && Vertical > 0) {
UINT32 *FrameBuffer = (UINT32*)Gop->Mode->FrameBufferBase;
UINT32 PixelsPerScanLine = Gop->Mode->Info->PixelsPerScanLine;
UINT32 x, y;
UINT32 cx = Horizontal / 2;
UINT32 cy = Vertical / 2;
for (y = 0; y < Vertical; y++) {
for (x = 0; x < Horizontal; x++) {
UINT8 r = (UINT8)(x * 255 / Horizontal);
UINT8 g = (UINT8)(y * 255 / Vertical);
UINT8 b = 0x80;
UINT32 color;
if (SwapRedBlue) {
color = (b << 16) | (g << 8) | r;
} else {
color = (r << 16) | (g << 8) | b;
}
FrameBuffer[y * PixelsPerScanLine + x] = color;
}
}
for (x = 0; x < Horizontal; x++) {
FrameBuffer[cy * PixelsPerScanLine + x] = 0x00FFFFFF;
}
for (y = 0; y < Vertical; y++) {
FrameBuffer[y * PixelsPerScanLine + cx] = 0x00FFFFFF;
}
}
// 6. 等待按键退出
Print(L"Press any key to exit...\n");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
// 7. 清屏为黑色
if (Horizontal > 0 && Vertical > 0) {
UINT32 *FrameBuffer = (UINT32*)Gop->Mode->FrameBufferBase;
UINT32 PixelsPerScanLine = Gop->Mode->Info->PixelsPerScanLine;
UINT32 x, y;
for (y = 0; y < Vertical; y++) {
for (x = 0; x < Horizontal; x++) {
FrameBuffer[y * PixelsPerScanLine + x] = 0x00000000;
}
}
}
return EFI_SUCCESS;
}
方式2
bash
/**
* @brief 显示系统启动时的 LOGO 图 方式2:使用 Blt接口绘制
*
*/
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/GraphicsOutput.h>
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
UINT32 MaxMode;
UINT32 ModeNumber;
UINT32 SelectedMode = 0;
UINTN SizeOfInfo;
UINT32 Horizontal = 0;
UINT32 Vertical = 0;
UINTN Index;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *ModeInfo = NULL;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer = NULL; // Blt 像素缓冲(B/G/R/A 结构体,无需管字节序)
// 1. 获取 GOP Protocol
Status = gBS->LocateProtocol(
&gEfiGraphicsOutputProtocolGuid,
NULL,
(VOID**)&Gop
);
if (EFI_ERROR(Status)) {
Print(L"Failed to locate GOP\n");
return Status;
}
Print(L"GOP protocol found successfully!\n");
// 2. 枚举所有显示模式
Print(L"=== Enumerating All Display Modes ===\n");
MaxMode = Gop->Mode->MaxMode;
Print(L"MaxMode = %d\n\n", MaxMode);
for (ModeNumber = 0; ModeNumber < MaxMode; ModeNumber++) {
Status = Gop->QueryMode(Gop, ModeNumber, &SizeOfInfo, &ModeInfo);
if (EFI_ERROR(Status)) {
Print(L"QueryMode failed for mode %d: %r\n", ModeNumber, Status);
continue;
}
Print(L"Mode %2d: %4d x %4d, PixelFormat = %d\n",
ModeNumber,
ModeInfo->HorizontalResolution,
ModeInfo->VerticalResolution,
ModeInfo->PixelFormat
);
if (ModeInfo != NULL) {
gBS->FreePool(ModeInfo);
ModeInfo = NULL;
}
}
// 暂停让用户查看枚举结果
Print(L"\nPress Enter to continue...");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
while (TRUE) {
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
EFI_INPUT_KEY Key;
if (!EFI_ERROR(SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key))) {
if (Key.UnicodeChar == L'\r') {
break;
}
}
}
// 3. 设置显示模式
Status = Gop->SetMode(Gop, SelectedMode);
if (EFI_ERROR(Status)) {
Print(L"Failed to set mode %d: %r\n", SelectedMode, Status);
return Status;
}
Horizontal = Gop->Mode->Info->HorizontalResolution;
Vertical = Gop->Mode->Info->VerticalResolution;
Print(L"Selected mode %d with resolution %d x %d\n", SelectedMode, Horizontal, Vertical);
Print(L"Mode %d set successfully!\n", SelectedMode);
// 4. 当前显示模式信息
Print(L"\n=== Current Mode Information ===\n");
Print(L"Current Mode Number: %d\n", Gop->Mode->Mode);
Print(L"FrameBuffer Base: 0x%016lx\n", Gop->Mode->FrameBufferBase);
{
UINT32 SizeMB = (UINT32)(Gop->Mode->FrameBufferSize / (1024 * 1024));
UINT32 SizeKB = (UINT32)((Gop->Mode->FrameBufferSize % (1024 * 1024)) / 1024);
Print(L"FrameBuffer Size: %d bytes (%d.%02d MB)\n",
Gop->Mode->FrameBufferSize,
SizeMB,
(SizeKB * 100) / 1024);
}
Print(L"Horizontal Resolution: %d\n", Gop->Mode->Info->HorizontalResolution);
Print(L"Vertical Resolution: %d\n", Gop->Mode->Info->VerticalResolution);
Print(L"Pixel Format: %d\n", Gop->Mode->Info->PixelFormat);
// 解释像素格式
switch (Gop->Mode->Info->PixelFormat) {
case PixelRedGreenBlueReserved8BitPerColor:
Print(L" -> Pixel Format: RGB (8:8:8, with reserved byte)\n");
break;
case PixelBlueGreenRedReserved8BitPerColor:
Print(L" -> Pixel Format: BGR (most common on PC)\n");
break;
case PixelBitMask:
Print(L" -> Pixel Format: Custom bitmask\n");
break;
case PixelBltOnly:
Print(L" -> Pixel Format: Blt only (no direct framebuffer access)\n");
break;
default:
Print(L" -> Pixel Format: Unknown\n");
}
// 等待 Enter 键后开始绘制
Print(L"Press Enter to draw...\n");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
while (TRUE) {
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
EFI_INPUT_KEY Key;
if (!EFI_ERROR(SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key))) {
if (Key.UnicodeChar == L'\r') {
break;
}
}
}
// 5. 使用 Blt 接口绘制渐变背景与中心十字
if (Horizontal > 0 && Vertical > 0) {
UINTN BltBufSize = (UINTN)Horizontal * Vertical * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL);
UINT32 x, y;
UINT32 cx = Horizontal / 2;
UINT32 cy = Vertical / 2;
EFI_GRAPHICS_OUTPUT_BLT_PIXEL White = {0xFF, 0xFF, 0xFF, 0x00};
// 5.1 在 CPU 内存中构建渐变 BltBuffer(结构体直接写 B/G/R,不关心硬件字节序)
Status = gBS->AllocatePool(EfiBootServicesData, BltBufSize, (VOID**)&BltBuffer);
if (EFI_ERROR(Status)) {
Print(L"AllocatePool for BltBuffer failed: %r\n", Status);
} else {
for (y = 0; y < Vertical; y++) {
for (x = 0; x < Horizontal; x++) {
UINTN idx = (UINTN)y * Horizontal + x;
BltBuffer[idx].Red = (UINT8)(x * 255 / Horizontal);
BltBuffer[idx].Green = (UINT8)(y * 255 / Vertical);
BltBuffer[idx].Blue = 0x80;
BltBuffer[idx].Reserved = 0x00;
}
}
// 5.2 把 BltBuffer 一次性 Blt 到屏幕 (0,0)
Status = Gop->Blt(Gop, BltBuffer, EfiBltBufferToVideo,
0, 0, // SourceX, SourceY
0, 0, // DestX, DestY
Horizontal, Vertical,
0); // Delta=0 -> 使用 Width*sizeof(PIXEL)
if (EFI_ERROR(Status)) {
Print(L"Blt EfiBltBufferToVideo failed: %r\n", Status);
}
gBS->FreePool(BltBuffer);
BltBuffer = NULL;
}
// 5.3 用 EfiBltVideoFill 画白色中心十字(硬件加速,不需要构建缓冲)
// 横线:从 (0, cy) 开始,宽=Horizontal,高=1
Gop->Blt(Gop, &White, EfiBltVideoFill,
0, 0, 0, cy, Horizontal, 1, 0);
// 竖线:从 (cx, 0) 开始,宽=1,高=Vertical
Gop->Blt(Gop, &White, EfiBltVideoFill,
0, 0, cx, 0, 1, Vertical, 0);
}
// 6. 等待按键退出
Print(L"Press any key to exit...\n");
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
SystemTable->BootServices->WaitForEvent(1, &SystemTable->ConIn->WaitForKey, &Index);
// 7. 用 EfiBltVideoFill 清屏为黑色(1 次调用 = 整屏填充)
if (Horizontal > 0 && Vertical > 0) {
EFI_GRAPHICS_OUTPUT_BLT_PIXEL Black = {0x00, 0x00, 0x00, 0x00};
Gop->Blt(Gop, &Black, EfiBltVideoFill,
0, 0, 0, 0, Horizontal, Vertical, 0);
}
return EFI_SUCCESS;
}
.inf:
bash
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = ShowLogo
FILE_GUID = 5B1A8D6E-7C3F-4E2A-9D01-3F1B2A4C5D6E
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
ShowLogo.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
UefiBootServicesTableLib
[Protocols]
gEfiGraphicsOutputProtocolGuid