GraphicsConsoleDxe

【UEFI实战】UEFI图形显示(显示驱动)中已经介绍了如何使用显卡驱动安装的GOP来进行像素级别的显示,本文介绍的内容是对像素的包装,最终变成普通字符的输出。

模块简述

本模块将原本的GOP包装成了字符输出的显示模块。GOP输出的最小单元是像素,而经过包装之后,文本模式输出的最小单元变成了一个个的字符。

本模块也是一个UEFI Driver Model,对应的EFI_DRIVER_BINDING_PROTOCOL

EFI_DRIVER_BINDING_PROTOCOL  gGraphicsConsoleDriverBinding = {
  GraphicsConsoleControllerDriverSupported,
  GraphicsConsoleControllerDriverStart,
  GraphicsConsoleControllerDriverStop,
  0xa,
  NULL,
  NULL
};

Supported函数就是一系列Protocol的判断,包括:

  • gEfiGraphicsOutputProtocolGuidgEfiUgaDrawProtocolGuid,不过前面一节已经介绍过,gEfiGraphicsOutputProtocolGuid会被安装,它会被优先使用。
  • gEfiDevicePathProtocolGuid,对于一个PCI的显卡,这个也是会安装的。
  • gEfiHiiDatabaseProtocolGuidgEfiHiiFontProtocolGuid,两者是UEFI用户界面的基本接口,要使用显示输出,它们也是必须的。尤其是gEfiHiiFontProtocolGuid,它是像素和字符之间的桥梁,负责完成两者的转换。

Start函数的一个主要工作是初始化如下的结构体:

//
// Graphics Console Device Private Data template
//
GRAPHICS_CONSOLE_DEV  mGraphicsConsoleDevTemplate = {
  GRAPHICS_CONSOLE_DEV_SIGNATURE,		// 一个标识,SIGNATURE_32 ('g', 's', 't', 'o')
  (EFI_GRAPHICS_OUTPUT_PROTOCOL *)NULL,	// 显卡初始化模块安装的Protocol,实际绘制字体的接口
  (EFI_UGA_DRAW_PROTOCOL *)NULL,		// 如果上一个存在,这个就可以不需要了
  {	// EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL基本接口,也是后续UEFI代码操作的接口,完成字符输出及相关操作
    GraphicsConsoleConOutReset,
    GraphicsConsoleConOutOutputString,
    GraphicsConsoleConOutTestString,
    GraphicsConsoleConOutQueryMode,
    GraphicsConsoleConOutSetMode,
    GraphicsConsoleConOutSetAttribute,
    GraphicsConsoleConOutClearScreen,
    GraphicsConsoleConOutSetCursorPosition,
    GraphicsConsoleConOutEnableCursor,
    (EFI_SIMPLE_TEXT_OUTPUT_MODE *)NULL	// 它指向的就是下面的结构体
  },
  {	// EFI_SIMPLE_TEXT_OUTPUT_MODE
    0,		// QueryMode()和SetMode()支持的模式数,由于没有初始化,所以现在默认是0
    -1,		// 当前的模式,-1表示的是无效的模式
    EFI_TEXT_ATTR (EFI_LIGHTGRAY,          EFI_BLACK),	// 当前字体输出属性,包括前景色和背景色
    0,		// 光标列位置
    0,		// 光标行位置
    FALSE	// 光标是否可见
  },
  (GRAPHICS_CONSOLE_MODE_DATA *)NULL,	// 这个看上去是一个指针,但是实际上是一个数组,表示当前文本显示支持的模式
  (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)NULL	// 像素点的属性,其实是一个蓝绿红表示的值
};

对应的Start函数的流程:

0
非0
获取底层绘画接口GOP
获取PCD表示的以像素为单位的水平和垂直的分辨率
判断分辨率的值
通过GOP获取最大支持的分辨率
使用获取到的分辨率来设置文本模式, 对应函数InitializeGraphicsConsoleTextMode
判断非0分辨率是否支持
使用默认的分辨率800*600
使用该非0分辨率
打印获取到的分辨率
使用该分辨率以及对应的模式号来完成后续初始化
最终安装gEfiSimpleTextOutProtocolGuid

整个流程主要是以下的几个步骤:

  • 获取可用的分辨率;
  • 初始化文本模式信息;
  • 安装最终的Protocol。

文本模式

跟GOP类似,文本显示也有它自己的模式(这里以模式表示GOP的模式,以文本模式表示SimpleTextOutProtocol的模式),不过相比前者这个要简单许多:

/**
  @par Data Structure Description:
  Mode Structure pointed to by Simple Text Out protocol.
**/
typedef struct {
  ///
  /// The number of modes supported by QueryMode () and SetMode ().
  ///
  INT32    MaxMode;

  //
  // current settings
  //

  ///
  /// The text mode of the output device(s).
  ///
  INT32      Mode;
  ///
  /// The current character output attribute.
  ///
  INT32      Attribute;
  ///
  /// The cursor's column.
  ///
  INT32      CursorColumn;
  ///
  /// The cursor's row.
  ///
  INT32      CursorRow;
  ///
  /// The cursor is currently visible or not.
  ///
  BOOLEAN    CursorVisible;
} EFI_SIMPLE_TEXT_OUTPUT_MODE;

具体参数的意义可以看英文说明或者前面的注释。GOP中的显示是以像素为单位的,而文本模式中显示是以一个小的矩形为单位的,每个矩形包含一个字符,这里就通过Column和Row来指定位置。前面的两个结构体成员MaxModeMode跟GOP中的模式类似,也表示有多个,并指定了其中的一个,这也说明还存在另外的一个数组来表示所有支持的文本模式,它们描述了像素到字符矩形之间的关系,其结构体表示如下:

typedef struct {
  UINTN     Columns;		// 表示文本模式对应的列数
  UINTN     Rows;			// 表示文本模式对应的行数
  INTN      DeltaX;			// 文本显示相对于GOP显示的水平偏移
  INTN      DeltaY;			// 文本显示相对于GOP显示的垂直偏移
  UINT32    GopWidth;		// GOP显示的宽度,即水平像素个数
  UINT32    GopHeight;		// GOP显示的高度,即垂直像素个数
  UINT32    GopModeNumber;	// 对应GOP模式的Index
} GRAPHICS_CONSOLE_MODE_DATA;

这对应到前文提到的数组:

//
// Graphics Console Device Private Data template
//
GRAPHICS_CONSOLE_DEV  mGraphicsConsoleDevTemplate = {
  // 略
  (GRAPHICS_CONSOLE_MODE_DATA *)NULL,	// 这个看上去是一个指针,但是实际上是一个数组,表示当前文本显示支持的模式
  // 略
};

通过上述两个结构体就构成了完整的文本模式,并与底层GOP模式绑定。

下面的示例代码显示了当前支持的所有文本模式:

VOID
ShowTextMode (
  IN  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *Stp
  )
{
  EFI_STATUS  Status = EFI_ABORTED;
  UINTN       Index = 0;
  UINTN       Col = 0;
  UINTN       Row = 0;

  Print (L"Current Text Mode:\r\n");
  Print (L" MaxMode       : %d\r\n", Stp->Mode->MaxMode);
  Print (L" Mode          : %d\r\n", Stp->Mode->Mode);
  Print (L" Attribute     : 0x%x\r\n", Stp->Mode->Attribute);
  Print (L" CursorColumn  : %d\r\n", Stp->Mode->CursorColumn);
  Print (L" CursorRow     : %d\r\n", Stp->Mode->CursorRow);
  Print (L" CursorVisible : %d\r\n", Stp->Mode->CursorVisible);

  Print (L"Supported Text Mode:\r\n");
  for (Index = 0; Index < Stp->Mode->MaxMode; Index++) {
    Status = Stp->QueryMode (Stp, Index, &Col, &Row);
    if (EFI_ERROR (Status)) {
      Print (L"%d. Not supported.\r\n", Index);
      continue;
    }
    Print (L"%d. Column: %d, Row: %d\r\n", Index, Col, Row);
  }
}

得到的结果:

在这里插入图片描述

这里有几点需要说明:

  1. 相比于GOP的QueryMode()函数,文本模式下的QueryMode()只能查看行和列两个参数,而底层的GRAPHICS_CONSOLE_MODE_DATA并不能直接查看,如果想要获取这些信息,可以通过在本模块增加DEBUG信息来查看:
ModeData 0
Columns       : 80
Rows          : 25
DeltaX        : 320
DeltaY        : 162
GopWidth      : 1280
GopHeight     : 800
GopModeNumber : 0
ModeData 1
Columns       : 0
Rows          : 0
DeltaX        : 0
DeltaY        : 0
GopWidth      : 1280
GopHeight     : 800
GopModeNumber : 0
ModeData 2
Columns       : 100
Rows          : 31
DeltaX        : 240
DeltaY        : 105
GopWidth      : 1280
GopHeight     : 800
GopModeNumber : 0
ModeData 3
Columns       : 128
Rows          : 40
DeltaX        : 128
DeltaY        : 20
GopWidth      : 1280
GopHeight     : 800
GopModeNumber : 0
ModeData 4
Columns       : 160
Rows          : 42
DeltaX        : 0
DeltaY        : 1
GopWidth      : 1280
GopHeight     : 800
GopModeNumber : 0
  1. 这里可以看到总共支持5个,至于为什么会是这么几个,会在后面的章节中说明。
  2. mode命令是查看当前支持的文本模式,可以看到少了几个,这个的原因之后再分析。

最后再说明一下文本模式中的Attribute这个结构体成员,它的值可以分为两类,一类是颜色,跟GOP的像素颜色对应;另一类表示文本是窄体还是宽体,它对应的一个示例就是英文是窄体而中文是宽体,它们的不同导致了绘制一个字体需要的像素的不同。后面一节会进一步介绍。Attribute目前的取值:

//
// EFI Console Colours
//
#define EFI_BLACK         0x00
#define EFI_BLUE          0x01
#define EFI_GREEN         0x02
#define EFI_CYAN          (EFI_BLUE | EFI_GREEN)
#define EFI_RED           0x04
#define EFI_MAGENTA       (EFI_BLUE | EFI_RED)
#define EFI_BROWN         (EFI_GREEN | EFI_RED)
#define EFI_LIGHTGRAY     (EFI_BLUE | EFI_GREEN | EFI_RED)
#define EFI_BRIGHT        0x08
#define EFI_DARKGRAY      (EFI_BLACK | EFI_BRIGHT)
#define EFI_LIGHTBLUE     (EFI_BLUE | EFI_BRIGHT)
#define EFI_LIGHTGREEN    (EFI_GREEN | EFI_BRIGHT)
#define EFI_LIGHTCYAN     (EFI_CYAN | EFI_BRIGHT)
#define EFI_LIGHTRED      (EFI_RED | EFI_BRIGHT)
#define EFI_LIGHTMAGENTA  (EFI_MAGENTA | EFI_BRIGHT)
#define EFI_YELLOW        (EFI_BROWN | EFI_BRIGHT)
#define EFI_WHITE         (EFI_BLUE | EFI_GREEN | EFI_RED | EFI_BRIGHT)

//
// Macro to accept color values in their raw form to create
// a value that represents both a foreground and background
// color in a single byte.
// For Foreground, and EFI_* value is valid from EFI_BLACK(0x00) to
// EFI_WHITE (0x0F).
// For Background, only EFI_BLACK, EFI_BLUE, EFI_GREEN, EFI_CYAN,
// EFI_RED, EFI_MAGENTA, EFI_BROWN, and EFI_LIGHTGRAY are acceptable
//
// Do not use EFI_BACKGROUND_xxx values with this macro.
//
#define EFI_TEXT_ATTR(Foreground, Background)  ((Foreground) | ((Background) << 4))

#define EFI_BACKGROUND_BLACK      0x00
#define EFI_BACKGROUND_BLUE       0x10
#define EFI_BACKGROUND_GREEN      0x20
#define EFI_BACKGROUND_CYAN       (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_GREEN)
#define EFI_BACKGROUND_RED        0x40
#define EFI_BACKGROUND_MAGENTA    (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_RED)
#define EFI_BACKGROUND_BROWN      (EFI_BACKGROUND_GREEN | EFI_BACKGROUND_RED)
#define EFI_BACKGROUND_LIGHTGRAY  (EFI_BACKGROUND_BLUE | EFI_BACKGROUND_GREEN | EFI_BACKGROUND_RED)

//
// We currently define attributes from 0 - 7F for color manipulations
// To internally handle the local display characteristics for a particular character,
// Bit 7 signifies the local glyph representation for a character.  If turned on, glyphs will be
// pulled from the wide glyph database and will display locally as a wide character (16 X 19 versus 8 X 19)
// If bit 7 is off, the narrow glyph database will be used.  This does NOT affect information that is sent to
// non-local displays, such as serial or LAN consoles.
//
#define EFI_WIDE_ATTRIBUTE  0x80

这里详细说明了当前支持的字体类型和颜色。

从GOP模式到文本模式

从GOP模式到文本模式,首先需要解决的就是前面章节中提到的像素转换成行列的问题,它主要在InitializeGraphicsConsoleTextMode()函数中完成:

EFI_STATUS
InitializeGraphicsConsoleTextMode (
  IN UINT32                       HorizontalResolution,
  IN UINT32                       VerticalResolution,
  IN UINT32                       GopModeNumber,
  OUT UINTN                       *TextModeCount,
  OUT GRAPHICS_CONSOLE_MODE_DATA  **TextModeData
  )

该函数的入参是像素构成的长和宽以及对应的GOP模式,出参是对应的可适配的文本模式。下面简单介绍该函数的实现,以此来了解像素到行列的转换关系。

  1. 首先根据水平和垂直的像素个数来确定最大支持的列和行,这个计算关系如下:
  MaxColumns = HorizontalResolution / EFI_GLYPH_WIDTH;	// 8
  MaxRows    = VerticalResolution / EFI_GLYPH_HEIGHT;	// 19

这里的8和19的来源是UEFI规范中定义的窄体字,后面会进一步说明。按照UEFI规范的要求,最小支持的行列必须要满足80x25的要求,所以紧接着有以下的判断:

  //
  // According to UEFI spec, all output devices support at least 80x25 text mode.
  //
  ASSERT ((MaxColumns >= 80) && (MaxRows >= 25));

以当前OVMF的示例,像素是1280x800,所以MaxColumns = 160,MaxRows = 42,它会被作为支持全屏的行列,因此会放到mGraphicsConsoleModeData中,这样实际代码中支持的所有行列是这样的:

GRAPHICS_CONSOLE_MODE_DATA  mGraphicsConsoleModeData[] = {
  { 100, 31 },  //  800 x 600
  { 128, 40 },  // 1024 x 768
  { 160, 42 },  // 1280 x 800
  { 240, 56 },  // 1920 x 1080
  // 上面的都是硬编码的
  //
  // New modes can be added here.
  // The last entry is specific for full screen mode.
  //
  { 160,   42  }	// 代码根据像素实际生成的
};

但是mGraphicsConsoleModeData并不是最终文本模式能够支持的行列(显然上表有重复),这里还需要进行一些处理。

  1. 第一个处理是增加几个默认的行列,主要是80x25和80x50两种情况:
  //
  // Mode 0 and mode 1 is for 80x25, 80x50 according to UEFI spec.
  //
  ValidCount = 0;

  NewModeBuffer[ValidCount].Columns       = 80;
  NewModeBuffer[ValidCount].Rows          = 25;
  NewModeBuffer[ValidCount].GopWidth      = HorizontalResolution;
  NewModeBuffer[ValidCount].GopHeight     = VerticalResolution;
  NewModeBuffer[ValidCount].GopModeNumber = GopModeNumber;
  NewModeBuffer[ValidCount].DeltaX        = (HorizontalResolution - (NewModeBuffer[ValidCount].Columns * EFI_GLYPH_WIDTH)) >> 1;
  NewModeBuffer[ValidCount].DeltaY        = (VerticalResolution - (NewModeBuffer[ValidCount].Rows * EFI_GLYPH_HEIGHT)) >> 1;
  ValidCount++;

  if ((MaxColumns >= 80) && (MaxRows >= 50)) {
    NewModeBuffer[ValidCount].Columns = 80;
    NewModeBuffer[ValidCount].Rows    = 50;
    NewModeBuffer[ValidCount].DeltaX  = (HorizontalResolution - (80 * EFI_GLYPH_WIDTH)) >> 1;
    NewModeBuffer[ValidCount].DeltaY  = (VerticalResolution - (50 * EFI_GLYPH_HEIGHT)) >> 1;
  }

  NewModeBuffer[ValidCount].GopWidth      = HorizontalResolution;
  NewModeBuffer[ValidCount].GopHeight     = VerticalResolution;
  NewModeBuffer[ValidCount].GopModeNumber = GopModeNumber;
  ValidCount++;

这里需要关注DeltaXDeltaY的值,这样做是为了保证即使文本模式不能全屏,但是也能够占据在屏幕的正中间。

还需要注意那个if判断,显然这个条件在现在的OVMF环境下是满足要求的,这就导致了第二项中的行列都不会被赋值,都是默认的0。这比较奇怪,目前还不确定原因。

  1. 之后的代码开始处理mGraphicsConsoleModeData中的文本模式,判断是否有效的点有:a)行列不能超过最大值,所以{ 240, 56 }这一项就不满足要求,b)不能有重复项,这里主要是防止最后一项{MaxColumns,MaxRows}跟前面的冲突,其它的都是代码写死的,它们之间应该不存在重复的可能。

最终可用的行列在DEBUG信息中显示如下:

Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 0, Row = 0
Graphics - Mode 2, Column = 100, Row = 31
Graphics - Mode 3, Column = 128, Row = 40
Graphics - Mode 4, Column = 160, Row = 42

这也跟前面的分析是一致的。

到这里已经将像素都分割成了行列,并且可以看到最适配的是160x42的情况,但是从前面的例子以及mode命令打印的情况来看,实际使用的却是100x31,这个由于不影响本节内容的说明,所以暂时不关注。

后面需要关注的是,当像素分割成一个个矩形之后(窄体对应矩形是8x19个像素),该如何在这个矩形中表示一个字符,这个可以通过下图很明显地看出来:

在这里插入图片描述

图中的最小的矩形就是一个个的像素,而其中的圆形可以通过不同的颜色来描述(注意不是真的有圆形),这样就可以勾勒出一个字。通过观察上图,就可以通过GOP来“写”出一个字符,这里以A这个字符为例,只需要将上图中有圆形的那些位置对应的像素改成其它颜色,就可以显示出来,它们对应的位置是:

  UINT8                         BltIndex[NARROW_HEIGHT * NARROW_WIDTH] = {
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 0, 0, 0, 0,
    0, 0, 1, 1, 1, 0, 0, 0,
    0, 1, 1, 0, 1, 1, 0, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 1, 1, 1, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    1, 1, 0, 0, 0, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0
  };

这里用数组模拟一个8x19的像素,只要值为1,就用其它颜色表示,这样就构造出了一个A字,下面是剩余的代码:

  for (Index = 0; Index < NARROW_HEIGHT * NARROW_WIDTH; Index++) {
    if (BltIndex[Index]) {
      Blt[Index].Red = 0xFF;
    }
  }

  Gop->Blt (
        Gop,
        Blt,
        EfiBltBufferToVideo,
        0,
        0,
        0,
        0,
        Width,
        Height,
        0
        );

最终得到的结果:

在这里插入图片描述

可以看到左上角就显示了一个A。不过这只是一个简单的例子,如何输出字符可以直接调用EFI_SIMPLE_TEXT_OUTPUT_PROTOCOLOutputString()函数即可,它的实现是:

EFI_STATUS
EFIAPI
GraphicsConsoleConOutOutputString (
  IN  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *This,
  IN  CHAR16                           *WString
  )

除了一些特殊字符和特殊情况(比如回车键意味着换行,而换行时刚好已经在显示的最后一行则要全局地向上平移,这个时候参数EfiBltVideoToVideo就能派上用处)的处理有所不同,其它的处理都在如下的函数:

/**
  Draw Unicode string on the Graphics Console device's screen.

  @param  This                  Protocol instance pointer.
  @param  UnicodeWeight         One Unicode string to be displayed.
  @param  Count                 The count of Unicode string.

  @retval EFI_OUT_OF_RESOURCES  If no memory resource to use.
  @retval EFI_UNSUPPORTED       If no Graphics Output protocol and UGA Draw
                                protocol exist.
  @retval EFI_SUCCESS           Drawing Unicode string implemented successfully.

**/
EFI_STATUS
DrawUnicodeWeightAtCursorN (
  IN  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *This,
  IN  CHAR16                           *UnicodeWeight,
  IN  UINTN                            Count
  )

而将字符转换成像素,则依赖于EFI_HII_FONT_PROTOCOL

    Status = mHiiFont->StringToImage (
                         mHiiFont,
                         EFI_HII_IGNORE_IF_NO_GLYPH | EFI_HII_DIRECT_TO_SCREEN | EFI_HII_IGNORE_LINE_BREAK,
                         String,
                         FontInfo,
                         &Blt,
                         This->Mode->CursorColumn * EFI_GLYPH_WIDTH + Private->ModeData[This->Mode->Mode].DeltaX,
                         This->Mode->CursorRow * EFI_GLYPH_HEIGHT + Private->ModeData[This->Mode->Mode].DeltaY,
                         NULL,
                         NULL,
                         NULL
                         );

不仅仅是转换,该函数也完成了最终的输出。关于HII Font的实现,将在后续进一步介绍。

Logo

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

更多推荐