显示驱动

OVMF BIOS使用了这个作为显卡驱动,具体图形显示的底层实现不是重点,所以这里只是简单介绍。

QemuVideoDxe是一个UEFI Driver Model,对应的EFI_DRIVER_BINDING_PROTOCOL

EFI_DRIVER_BINDING_PROTOCOL  gQemuVideoDriverBinding = {
  QemuVideoControllerDriverSupported,
  QemuVideoControllerDriverStart,
  QemuVideoControllerDriverStop,
  0x10,
  NULL,
  NULL
};
  • Supported函数的流程:
判断是否存在gEfiPciIoProtocolGuid
读取PCI Header为后续使用
QemuVideoDetect
找到支持的显卡之后返回成功

重点是QemuVideoDetect()的实现,找到的显卡:

QemuVideo: QEMU Standard VGA detected

这是QEMU模拟的显卡,具体实现可以不用关注。

  • Start函数中的初始化部分也不需要特别关注,重点在于这里安装的Protocol:
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;
  ///
  /// Pointer to EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE data.
  ///
  EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE          *Mode;
};

对应的GUID是gEfiGraphicsOutputProtocolGuid,这个Protocol会被后续的UEFI显示模块使用,简称是GOP

这个Protocol的成员可以分为两类,跟Mode及相关的操作为一类,Blt为一类。前者是模式的设置,而后者的全称是Block Transfer,真正的作用就是显示输出,之所以叫这个名字,是因为实际上它做的事情就是一个数据的搬运,这个在后面会进一步说明。

Mode的结构体如下:

typedef struct {
  ///
  /// The version of this data structure. A value of zero represents the
  /// EFI_GRAPHICS_OUTPUT_MODE_INFORMATION structure as defined in this specification.
  ///
  UINT32                       Version;
  ///
  /// The size of video screen in pixels in the X dimension.
  ///
  UINT32                       HorizontalResolution;
  ///
  /// The size of video screen in pixels in the Y dimension.
  ///
  UINT32                       VerticalResolution;
  ///
  /// Enumeration that defines the physical format of the pixel. A value of PixelBltOnly
  /// implies that a linear frame buffer is not available for this mode.
  ///
  EFI_GRAPHICS_PIXEL_FORMAT    PixelFormat;
  ///
  /// This bit-mask is only valid if PixelFormat is set to PixelPixelBitMask.
  /// A bit being set defines what bits are used for what purpose such as Red, Green, Blue, or Reserved.
  ///
  EFI_PIXEL_BITMASK            PixelInformation;
  ///
  /// Defines the number of pixel elements per video memory line.
  ///
  UINT32                       PixelsPerScanLine;
} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION;

typedef struct {
  ///
  /// The number of modes supported by QueryMode() and SetMode().
  ///
  UINT32                                  MaxMode;
  ///
  /// Current Mode of the graphics device. Valid mode numbers are 0 to MaxMode -1.
  ///
  UINT32                                  Mode;
  ///
  /// Pointer to read-only EFI_GRAPHICS_OUTPUT_MODE_INFORMATION data.
  ///
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION    *Info;
  ///
  /// Size of Info structure in bytes.
  ///
  UINTN                                   SizeOfInfo;
  ///
  /// Base address of graphics linear frame buffer.
  /// Offset zero in FrameBufferBase represents the upper left pixel of the display.
  ///
  EFI_PHYSICAL_ADDRESS                    FrameBufferBase;
  ///
  /// Amount of frame buffer needed to support the active mode as defined by
  /// PixelsPerScanLine xVerticalResolution x PixelElementSize.
  ///
  UINTN                                   FrameBufferSize;
} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE;

以上信息主要是显卡的物理参数,在显卡本身的初始化过程中就会被确定下来,后面的UEFI驱动会使用到这些参数。

下面是一个获取上述信息的代码示例(beni\BeniPkg\DynamicCommand\DisplayDynamicCommand\Display.c):

  for (Index = 0; Index < GopHandleCount; Index++) {
    Status = gBS->HandleProtocol (
                    GopHandleBuffer[Index],
                    &gEfiGraphicsOutputProtocolGuid,
                    (VOID *)&Gop
                    );
    if (EFI_ERROR (Status)) {
      continue;
    }
    Print (L"MaxMode                        : %d\r\n", Gop->Mode->MaxMode);
    Print (L"Mode                           : %d\r\n", Gop->Mode->Mode);
    Print (L"Info.Version                   : 0x%04x\r\n", Gop->Mode->Info->Version);
    Print (L"Info.HorizontalResolution      : %d\r\n", Gop->Mode->Info->HorizontalResolution);
    Print (L"Info.VerticalResolution        : %d\r\n", Gop->Mode->Info->VerticalResolution);
    Print (L"Info.PixelFormat               : %s\r\n", gPixelFormat[Gop->Mode->Info->PixelFormat]);
    Print (L"PixelInformation.RedMask       : 0x%04x\r\n",
            Gop->Mode->Info->PixelInformation.RedMask);
    Print (L"PixelInformation.GreenMask     : 0x%04x\r\n",
            Gop->Mode->Info->PixelInformation.GreenMask);
    Print (L"PixelInformation.BlueMask      : 0x%04x\r\n",
            Gop->Mode->Info->PixelInformation.BlueMask);
    Print (L"PixelInformation.ReservedMask  : 0x%04x\r\n",
            Gop->Mode->Info->PixelInformation.ReservedMask);
    Print (L"PixelsPerScanLine              : 0x%04x\r\n", Gop->Mode->Info->PixelsPerScanLine);
    Print (L"SizeOfInfo                     : %d\r\n", Gop->Mode->SizeOfInfo);
    Print (L"FrameBufferBase                : 0x%lx\r\n", Gop->Mode->FrameBufferBase);
    Print (L"FrameBufferSize                : 0x%lx\r\n", Gop->Mode->FrameBufferSize);
  }

得到的结果:

在这里插入图片描述

这里得到了两个GOP,但是实际上OVFM中只有一张虚拟的显卡,这个看上去会比较奇怪,但是实际上上述两个Protocol指向的是同一张显卡,之所以会出现两个,直接的原因就是安装了两次EFI_GRAPHICS_OUTPUT_PROTOCOL,而之所以要安装两次的原因在于ConSplitterDxe.inf模块:

  //
  // If both ConOut and StdErr incorporate the same Text Out device,
  // their MaxMode and QueryData should be the intersection of both.
  //
  Status = ConSplitterTextOutAddDevice (&mConOut, TextOut, GraphicsOutput, UgaDraw);

由于本节只讨论图形显示,所以关于UEFI的Console部分不做介绍。如果想要只获取显卡本身的信息,可以通过判断该Protocol对应的Handle是否有安装Device Path来确定,对应的示例函数:

EFI_GRAPHICS_OUTPUT_PROTOCOL *
GetSpecificGop (
  VOID
  )
{
  EFI_STATUS                    Status = EFI_NOT_FOUND;
  EFI_GRAPHICS_OUTPUT_PROTOCOL  *Gop = NULL;
  UINTN                         Index = 0;
  UINTN                         GopHandleCount = 0;
  EFI_HANDLE                    *GopHandleBuffer = NULL;
  EFI_DEVICE_PATH_PROTOCOL      *GopDevicePath = NULL;

  //
  // Get all GOP responding independent video card.
  //
  Status = gBS->LocateHandleBuffer (
                  ByProtocol,
                  &gEfiGraphicsOutputProtocolGuid,
                  NULL,
                  &GopHandleCount,
                  &GopHandleBuffer
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "[%a][%d] Failed. - %r\n", __FUNCTION__, __LINE__, Status));
    goto DONE;
  }

  for (Index = 0; Index < GopHandleCount; Index++) {
    //
    // The video card should have device path.
    //
    Status = gBS->HandleProtocol (
                    GopHandleBuffer[Index],
                    &gEfiDevicePathProtocolGuid,
                    (VOID *)&GopDevicePath
                    );
    if (EFI_ERROR (Status)) {
      continue;
    }
    Status = gBS->HandleProtocol (
                    GopHandleBuffer[Index],
                    &gEfiGraphicsOutputProtocolGuid,
                    (VOID *)&Gop
                    );
    if (EFI_ERROR (Status)) {
      continue;
    } else {
      Print (L"Video card evice path: %s\r\n",
              ConvertDevicePathToText (GopDevicePath, TRUE, TRUE));
      break;
    }
  }

DONE:

  if (NULL != GopHandleBuffer) {
    FreePool (GopHandleBuffer);
    GopHandleBuffer = NULL;
  }

  return Gop;
}

下面对模式的值进行说明:

MaxMode                        : 30	# 显卡支持的所有模式
Mode                           : 0	# 当前使用的模式
Info.Version                   : 0x0000	# 模式版本信息
Info.HorizontalResolution      : 1280	# 分辨率
Info.VerticalResolution        : 800	# 分辨率
Info.PixelFormat               : PixelBlueGreenRedReserved8BitPerColor	# 像素相关的变量,跟具体的显卡有关,这里不用特别关注
PixelInformation.RedMask       : 0x0000
PixelInformation.GreenMask     : 0x0000
PixelInformation.BlueMask      : 0x0000
PixelInformation.ReservedMask  : 0x0000
PixelsPerScanLine              : 0x0500
SizeOfInfo                     : 36	# EFI_GRAPHICS_OUTPUT_MODE_INFORMATION的大小,共计36个字节
FrameBufferBase                : 0xC0000000	# 这个值实际上是显卡的MMIO Bar地址,跟下面的值一起,是底层操作需要关注的
FrameBufferSize                : 0x3E8000

GOP输出的最小单元是像素,所以这里才会涉及到各类像素相关的参数,合起来组成了模式(Mode)的概念。当前的显卡支持30种模式,通过代码可以一一读取:

  for (Index = 0; Index < Gop->Mode->MaxMode; Index++) {
    Status = Gop->QueryMode (Gop, Index, &SizeOfInfo, &ModeInfo);
    if (!EFI_ERROR (Status)) {
      Print (L"Mode                           : %d\r\n", Index);
      Print (L"Info.Version                   : 0x%04x\r\n", ModeInfo->Version);
      Print (L"Info.HorizontalResolution      : %d\r\n", ModeInfo->HorizontalResolution);
      Print (L"Info.VerticalResolution        : %d\r\n", ModeInfo->VerticalResolution);
      Print (L"Info.PixelFormat               : %s\r\n", gPixelFormat[ModeInfo->PixelFormat]);
      Print (L"PixelInformation.RedMask       : 0x%04x\r\n",
              ModeInfo->PixelInformation.RedMask);
      Print (L"PixelInformation.GreenMask     : 0x%04x\r\n",
              ModeInfo->PixelInformation.GreenMask);
      Print (L"PixelInformation.BlueMask      : 0x%04x\r\n",
              ModeInfo->PixelInformation.BlueMask);
      Print (L"PixelInformation.ReservedMask  : 0x%04x\r\n",
              ModeInfo->PixelInformation.ReservedMask);
      Print (L"PixelsPerScanLine              : 0x%04x\r\n", ModeInfo->PixelsPerScanLine);
      Print (L"SizeOfInfo                     : %d\r\n", SizeOfInfo);
      Print (L"----------------------------------------------\r\n");
      FreePool (ModeInfo);
    }

通过上述代码的打印信息可以看到的是各类模式的差别主要来自分辨率,这里不再列出。

最后简单说明如何操作像素,对应的Blt函数声明如下:

typedef
EFI_STATUS
(EFIAPI *EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT)(
  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL            *This,
  IN  EFI_GRAPHICS_OUTPUT_BLT_PIXEL           *BltBuffer    OPTIONAL,
  IN  EFI_GRAPHICS_OUTPUT_BLT_OPERATION       BltOperation,
  IN  UINTN                                   SourceX,
  IN  UINTN                                   SourceY,
  IN  UINTN                                   DestinationX,
  IN  UINTN                                   DestinationY,
  IN  UINTN                                   Width,
  IN  UINTN                                   Height,
  IN  UINTN                                   Delta         OPTIONAL
  );

函数的入参说明如下:

  • This:Protocol指针本身。
  • BltBufferEFI_GRAPHICS_OUTPUT_BLT_PIXEL指针,它的结构体如下:
typedef struct {
  UINT8    Blue;
  UINT8    Green;
  UINT8    Red;
  UINT8    Reserved;
} EFI_GRAPHICS_OUTPUT_BLT_PIXEL;

表示的是一个像素的三原色。比如“Blue=0,Green=0,Red=255“表示的就是一个红色的像素。

而对于BltBuffer,它表示的可以是一个像素,也可以指向像素数组来表示一个区域的所有像素的颜色。

  • BltOperation:输出的操作,其值如下:
///
/// actions for BltOperations
///
typedef enum {
  ///
  /// Write data from the BltBuffer pixel (0, 0)
  /// directly to every pixel of the video display rectangle
  /// (DestinationX, DestinationY) (DestinationX + Width, DestinationY + Height).
  /// Only one pixel will be used from the BltBuffer. Delta is NOT used.
  ///
  EfiBltVideoFill,

  ///
  /// Read data from the video display rectangle
  /// (SourceX, SourceY) (SourceX + Width, SourceY + Height) and place it in
  /// the BltBuffer rectangle (DestinationX, DestinationY )
  /// (DestinationX + Width, DestinationY + Height). If DestinationX or
  /// DestinationY is not zero then Delta must be set to the length in bytes
  /// of a row in the BltBuffer.
  ///
  EfiBltVideoToBltBuffer,

  ///
  /// Write data from the BltBuffer rectangle
  /// (SourceX, SourceY) (SourceX + Width, SourceY + Height) directly to the
  /// video display rectangle (DestinationX, DestinationY)
  /// (DestinationX + Width, DestinationY + Height). If SourceX or SourceY is
  /// not zero then Delta must be set to the length in bytes of a row in the
  /// BltBuffer.
  ///
  EfiBltBufferToVideo,

  ///
  /// Copy from the video display rectangle (SourceX, SourceY)
  /// (SourceX + Width, SourceY + Height) to the video display rectangle
  /// (DestinationX, DestinationY) (DestinationX + Width, DestinationY + Height).
  /// The BltBuffer and Delta are not used in this mode.
  ///
  EfiBltVideoToVideo,

  EfiGraphicsOutputBltOperationMax
} EFI_GRAPHICS_OUTPUT_BLT_OPERATION;
  • SourceX/SourceY:表示BltBuffer或者显示器Buffer中的一个区域的左上角的坐标。
  • DestinationX/DestinationY:表示BltBuffer或者显示器Buffer中的一个区域的左上角的坐标。关于Source和Destination具体是BltBuffer还是显示器Buffer,需要靠BltOperation来确定。
  • Width/Height:表示显示的矩形区域的长和宽。
  • Delta:它在EfiBltVideoFillEfiBltVideoToVideo操作时无效,其它时候表示BltBuffer中的一行像素的字节数。

下面是一个示例:

  SetMem (&FillColour, sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL), 0x0);
  FillColour.Red = 255;
  Status = Gop->Blt (
                  Gop,
                  &FillColour,
                  EfiBltVideoFill,
                  0,
                  0,
                  0,
                  0,
                  Gop->Mode->Info->HorizontalResolution,
                  Gop->Mode->Info->VerticalResolution,
                  0
                  );

此时是会显示一个红色的全屏:

在这里插入图片描述

这里使用了EfiBltVideoFill,而它通过一个像素(这里是一个红色的像素)铺满了一个矩形区域(这里根据后续的参数指定了整个屏幕)。

关于Delta这里增加一个示例,它是一个进度条的模拟:

    Width = Gop->Mode->Info->HorizontalResolution;  // The width of bar.
    Height = BAR_HEIGHT;  // The height of bar.
    Blt = AllocateZeroPool (sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) * Width * Height);
    if (NULL == Blt) {
      DEBUG ((EFI_D_ERROR, "[%a][%d] Out of memory\n", __FUNCTION__, __LINE__));
      Status = EFI_OUT_OF_RESOURCES;
      goto DONE;
    }
    //
    // Buffer for a red process bar.
    //
    for (IndexW = 0; IndexW < Width; IndexW++) {
      for (IndexH = 0; IndexH < Height; IndexH++) {
        Blt[IndexH * Width + IndexW].Red = 255;
      }
    }
    for (IndexW = 0; IndexW < Width; IndexW++) {
      Status = Gop->Blt (
                      Gop,
                      Blt,
                      EfiBltBufferToVideo,
                      0,
                      0,
                      IndexW,
                      0,
                      1,
                      Height,
                      sizeof (EFI_GRAPHICS_OUTPUT_BLT_PIXEL) * Width
                      );
      gBS->Stall (1000 * 10);
    }
  }

得到的结果:

在这里插入图片描述

上图上部的红色部分就是进度条,它会一直走,直到铺满整个宽度。

具体的代码实现可以在https://gitee.com/jiangwei0512/edk2-beni.git找到。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐