Step to UEFI (233)屏幕分辨率研究

最近测试 (USH)UEFI Shell Helper 的时候发现一个奇怪的现象:直接启动内置的 Shell 时,会全屏打字显示;而启动 USH 上面的 Shell 后,显示字体非常小。具体照片如下,使用 HDMI 显示器,下图是正常情况:

显示器中分辨率正常,字体足够大

下面是从 USH 启动后的照片,可以看到居中,字体非常小:

分辨率很高,字很小

于是针对这个现象进行一番研究。首先,重新编译 BIOS ,替换它内置的 Shell.efi 为和 USH 相同的版本,测试显示仍然有这样的现象。之后,编译 DEBUG 版本的BIOS, 在串口 Log 中看到如下字样:

GraphicsConsole video resolution 1920 x 1200
Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 80, Row = 50
Graphics - Mode 2, Column = 100, Row = 31
Graphics - Mode 3, Column = 240, Row = 63
……………..
GraphicsConsole video resolution 800 x 600
Graphics - Mode 0, Column = 80, Row = 25
Graphics - Mode 1, Column = 0, Row = 0
Graphics - Mode 2, Column = 100, Row = 31

虽然没有找到直接证据,但是感觉上问题和当前屏幕分辨率有关。从代码上上,上面的Log 来自于 edk2\MdeModulePkg\Universal\Console\GraphicsConsoleDxe 代码中。于是,首先编写一个查看当前系统 GRAPHICS_CONSOLE_DEV 的代码,这个结构体定义如下:

typedef struct
{
        UINTN                            Signature;
        EFI_GRAPHICS_OUTPUT_PROTOCOL     *GraphicsOutput;
        EFI_UGA_DRAW_PROTOCOL            *UgaDraw;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  SimpleTextOutput;
        EFI_SIMPLE_TEXT_OUTPUT_MODE      SimpleTextOutputMode;
        GRAPHICS_CONSOLE_MODE_DATA       *ModeData;
        EFI_GRAPHICS_OUTPUT_BLT_PIXEL    *LineBuffer;
} GRAPHICS_CONSOLE_DEV;

对于我们来说,首先枚举系统中的 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL ,之后使用下面的 CR 定义即可找到GRAPHICS_CONSOLE_DEV 结构体,这些都是定义在 edk2\MdeModulePkg\Universal\Console\GraphicsConsoleDxe\GraphicsConsole.h 中的代码:

#define GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS(a) \
  CR (a, GRAPHICS_CONSOLE_DEV, SimpleTextOutput, GRAPHICS_CONSOLE_DEV_SIGNATURE)

具体代码如下:

Private = GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS (SimpleTextOutput);
                
if (Private->Signature != GRAPHICS_CONSOLE_DEV_SIGNATURE) {
                        continue;
                }

Print(L"Show ModeData:\n");
Print(L" Col      : %d\n",Private->ModeData->Columns);
Print(L" Row      : %d\n",Private->ModeData->Rows);
Print(L" DeltaX   : %d\n",Private->ModeData->DeltaX);
Print(L" DeltaY   : %d\n",Private->ModeData->DeltaY);
Print(L" GopWidth : %d\n",Private->ModeData->GopWidth);
Print(L" GopHeight: %d\n",Private->ModeData->GopHeight);
Print(L" GopMode  : %d\n",Private->ModeData->GopModeNumber);

这里可以看到内置 Shell 和启动USH 上的 Shell 会有一些差别,前者运行结果:

Show ModeData:
 Col      : 80
 Row      : 25
 DeltaX   : 80
 DeltaY   : 62
 GopWidth : 800
 GopHeight: 600
 GopMode  : 2

后者运行结果:

Show ModeData:
 Col      : 80
 Row      : 25
 DeltaX   : 640
 DeltaY   : 362
 GopWidth : 1920
 GopHeight: 1200
 GopMode  : 0

两者运行在不同的分辨率下。接下来的问题就是:当运行在 1920x1200 时,是否有机会再切成 800x600 的分辨率呢?

这里还要从 GraphicsConsole 代码入手。在 CheckModeSupported() 函数中,我们可以看到代码使用 GraphicsOutput->SetMode 进行分辨率的切换,于是我们照搬这个代码到我们的 Application 中,切换为 800x600:

        //
        // if not supporting current mode, try 800x600 which is required by UEFI/EFI spec
        //
        HorizontalResolution = 800;
        VerticalResolution   = 600;
        Status = CheckModeSupported (
                     Private->GraphicsOutput,
                     HorizontalResolution,
                     VerticalResolution,
                     &ModeNumber
                     );

运行结果分辨率看起来是正确的,但是内容偏于一隅:

都在一边

这时候我注意到前面还有两个参数DeltaX  和 DeltaY   ,屏幕内容显示的位置应该是这里决定的。查看代码,在InitializeGraphicsConsoleTextMode() 函数中,有计算这两个参数的代码如下:

  NewModeBuffer[ValidCount].DeltaX        = (HorizontalResolution - (NewModeBuffer[ValidCount].Columns * EFI_GLYPH_WIDTH)) >> 1;
  NewModeBuffer[ValidCount].DeltaY        = (VerticalResolution - (NewModeBuffer[ValidCount].Rows * EFI_GLYPH_HEIGHT)) >> 1;   

其中EFI_GLYPH_WIDTH 定义为 8,EFI_GLYPH_HEIGHT定义为 19。例如,当前如果是 800x600分辨率,那么

DeltaX = (800-(80*8))/2=80 就是前面 Private->ModeData->DeltaX 中给出的值。这里我猜测这样的设计是为了保证屏幕内容居中所以进行了这样的设定。但是DeltaX和DeltaY并不会因为切换分辨率而有所不同(屏幕分辨率是GraphicsOutput负责,字符显示由GRAPHICS_CONSOLE_DEV 负责)。所以,我们应该需要手工设定 DeltaX 和 DeltaY,然后再对  Protocol 进行 Reset。完整代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle
#include  <Library/ShellLib.h>
#include  <Library/MemoryAllocationLib.h>
#include  <Library/BaseMemoryLib.h>
#include  <Protocol/UgaDraw.h>
#include  <Library/DebugLib.h>
#include  <stdio.h>
#include  <stdlib.h>

//
// Device Structure
//
#define GRAPHICS_CONSOLE_DEV_SIGNATURE  SIGNATURE_32 ('g', 's', 't', 'o')

typedef struct
{
        UINTN   Columns;
        UINTN   Rows;
        INTN    DeltaX;
        INTN    DeltaY;
        UINT32  GopWidth;
        UINT32  GopHeight;
        UINT32  GopModeNumber;
} GRAPHICS_CONSOLE_MODE_DATA;

typedef struct
{
        UINTN                            Signature;
        EFI_GRAPHICS_OUTPUT_PROTOCOL     *GraphicsOutput;
        EFI_UGA_DRAW_PROTOCOL            *UgaDraw;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  SimpleTextOutput;
        EFI_SIMPLE_TEXT_OUTPUT_MODE      SimpleTextOutputMode;
        GRAPHICS_CONSOLE_MODE_DATA       *ModeData;
        EFI_GRAPHICS_OUTPUT_BLT_PIXEL    *LineBuffer;
} GRAPHICS_CONSOLE_DEV;

#define GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS(a) \
  CR (a, GRAPHICS_CONSOLE_DEV, SimpleTextOutput, GRAPHICS_CONSOLE_DEV_SIGNATURE)

// Include/Protocol/SimpleTextOut.h
EFI_GUID        gEfiSimpleTextOutProtocolGuid  = { 0x387477C2, 0x69C7, 0x11D2,
        { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }
};

/**
  Check if the current specific mode supported the user defined resolution
  for the Graphics Console device based on Graphics Output Protocol.

  If yes, set the graphic devcice's current mode to this specific mode.

  @param  GraphicsOutput        Graphics Output Protocol instance pointer.
  @param  HorizontalResolution  User defined horizontal resolution
  @param  VerticalResolution    User defined vertical resolution.
  @param  CurrentModeNumber     Current specific mode to be check.

  @retval EFI_SUCCESS       The mode is supported.
  @retval EFI_UNSUPPORTED   The specific mode is out of range of graphics
                            device supported.
  @retval other             The specific mode does not support user defined
                            resolution or failed to set the current mode to the
                            specific mode on graphics device.

**/
EFI_STATUS
CheckModeSupported (
  EFI_GRAPHICS_OUTPUT_PROTOCOL  *GraphicsOutput,
  IN  UINT32                    HorizontalResolution,
  IN  UINT32                    VerticalResolution,
  OUT UINT32                    *CurrentModeNumber
  )
{
  UINT32     ModeNumber;
  EFI_STATUS Status;
  UINTN      SizeOfInfo;
  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
  UINT32     MaxMode;

  Status  = EFI_SUCCESS;
  MaxMode = GraphicsOutput->Mode->MaxMode;

  for (ModeNumber = 0; ModeNumber < MaxMode; ModeNumber++) {
    Status = GraphicsOutput->QueryMode (
                       GraphicsOutput,
                       ModeNumber,
                       &SizeOfInfo,
                       &Info
                       );
    if (!EFI_ERROR (Status)) {
      if ((Info->HorizontalResolution == HorizontalResolution) &&
          (Info->VerticalResolution == VerticalResolution)) {
        if ((GraphicsOutput->Mode->Info->HorizontalResolution == HorizontalResolution) &&
            (GraphicsOutput->Mode->Info->VerticalResolution == VerticalResolution)) {
          //
          // If video device has been set to this mode, we do not need to SetMode again
          //
          FreePool (Info);
          break;
        } else {
          Status = GraphicsOutput->SetMode (GraphicsOutput, ModeNumber);
          if (!EFI_ERROR (Status)) {
            FreePool (Info);
            break;
          }
        }
      }
      FreePool (Info);
    }
  }

  if (ModeNumber == GraphicsOutput->Mode->MaxMode) {
    Status = EFI_UNSUPPORTED;
  }

  *CurrentModeNumber = ModeNumber;
  return Status;
}

INTN
EFIAPI
main (
        IN UINTN Argc,
        IN CHAR16 **Argv
)
{
        UINTN                            NumHandles;
        EFI_STATUS                       Status;
        EFI_HANDLE                      *HandleBuffer;
        GRAPHICS_CONSOLE_DEV             *Private;
        UINTN                            Index;
        EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *SimpleTextOutput;
        UINT32                          HorizontalResolution;
        UINT32                          VerticalResolution;
        UINT32                               ModeNumber;
        EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE    *Mode;        
        //
        // Locate all handles that are using the SFS protocol.
        //
        Status = gBS->LocateHandleBuffer(
                        ByProtocol,
                        &gEfiSimpleTextOutProtocolGuid,
                        NULL,
                        &NumHandles,
                        &HandleBuffer);
        if (EFI_ERROR(Status) != FALSE)
        {
                Print(L"failed to locate any handles using the EfiSimpleTextOutProtocol\n");
                goto CleanUp;
        }

        for (Index = 0; (Index < NumHandles); Index += 1)
        {
                Status = gBS->HandleProtocol(
                                HandleBuffer[Index],
                                &gEfiSimpleTextOutProtocolGuid,
                                (VOID**)&SimpleTextOutput);

                if (EFI_ERROR(Status))
                {
                        Print(L"Failed to locate SimpleTextOutProtocol.\n");
                        continue;
                }
                
                Private = GRAPHICS_CONSOLE_CON_OUT_DEV_FROM_THIS (SimpleTextOutput);
                
                if (Private->Signature != GRAPHICS_CONSOLE_DEV_SIGNATURE) {
                        continue;
                }
                
                Print(L"Show ModeData:\n");
                Print(L" Col      : %d\n",Private->ModeData->Columns);
                Print(L" Row      : %d\n",Private->ModeData->Rows);
                Print(L" DeltaX   : %d\n",Private->ModeData->DeltaX);
                Print(L" DeltaY   : %d\n",Private->ModeData->DeltaY);
                Print(L" GopWidth : %d\n",Private->ModeData->GopWidth);
                Print(L" GopHeight: %d\n",Private->ModeData->GopHeight);
                Print(L" GopMode  : %d\n",Private->ModeData->GopModeNumber);

                // Set new screen offset
                Private->ModeData->DeltaX=80;
                Private->ModeData->DeltaY=62;
                //
                // if not supporting current mode, try 800x600 which is required by UEFI/EFI spec
                //
                HorizontalResolution = 800;
                VerticalResolution   = 600;
                Status = CheckModeSupported (
                             Private->GraphicsOutput,
                             HorizontalResolution,
                             VerticalResolution,
                             &ModeNumber
                             );
                
                Mode = Private->GraphicsOutput->Mode;
                
                if (EFI_ERROR (Status) && Mode->MaxMode != 0) {
                  //
                  // Set default mode failed or device don't support default mode, then get the current mode information
                  //
                  HorizontalResolution = Mode->Info->HorizontalResolution;
                  VerticalResolution = Mode->Info->VerticalResolution;
                  ModeNumber = Mode->Mode;
                  Print(L" CheckModeSupported failed\n");
                } else {
                  Print(L" CheckModeSupported passed\n");
                }
                Private->SimpleTextOutput.Reset(&Private->SimpleTextOutput,TRUE);
        }

CleanUp:
        if (HandleBuffer != NULL)
        {
                FreePool(HandleBuffer);
        }

        return(0);
}

经过上述操作之后,可以满足要求,不过有时候并不太稳定需要多次执行。遇到同样问题的朋友可以试试。

完整代码和编译后的EFI 程序:

虽然没有找到从内置Shell 和U盘上 Shell 启动之后分辨率不同的原因,但是我们有了一个可以在 Shell 下切换分辨率的工具,这个问题也算有一个解决方法。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注