文章

PainterEngine UEFI 适配笔记

PainterEngine UEFI 适配笔记

前段时间分享了一个跑在 UEFI 环境中的 PainterEngine Demo。最近又在这个基础上添加了输入(键鼠)、文件读取的相关支持。现在有些时间,便记录一下将其移植到 UEFI 环境下的一些心得。主要包含一些关键的移植步骤/技巧,以及遇到的一些问题。

0x00 基础的文件

首先需要新建一个 .inf 文件,然后将 PainterEnginecorekernelruntime 目录下的源码文件列在 [Sources] 下。

然后可以参考 PainterEngine\platform\standalone\px_main.c 添加一个适用于 UEFI 的 px_main.c

最开始,先只关注输出(即显示)。即使用 UEFI 的 Gop->Blt 将 PainterEngine 的 surfaceBuffer/px_color 绘制出来。

这部分主要的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void uefi_gop_render(px_color* gram, px_int width, px_int height)
{
  EFI_STATUS  Status;
  UINTN       Delta;

  Delta = mEfiGop->Mode->Info->HorizontalResolution * 4;

  Status = mEfiGop->Blt (
                      mEfiGop,
                      (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)(gram),
                      EfiBltBufferToVideo,
                      0,
                      0,
                      0,
                      0,
                      width,
                      height,
                      Delta
                      );
}

EFI_STATUS
EFIAPI
PainterEngineUefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  px_int screen_width=240;
  px_int screen_height=320;
  EFI_STATUS  Status;

  Status = gBS->LocateProtocol (&gEfiGraphicsOutputProtocolGuid, NULL, (VOID **) &mEfiGop);
  if (!EFI_ERROR(Status)) {
    screen_width = mEfiGop->Mode->Info->HorizontalResolution;
    screen_height = mEfiGop->Mode->Info->VerticalResolution;
  }

  if (!PX_ApplicationInitialize(&App,screen_width,screen_height))
  {
    return 0;
  }

  timelasttime=PX_TimeGetTime();
  while (1)
  {
    px_dword now=PX_TimeGetTime();
    px_dword elapsed=now-timelasttime;
    timelasttime= now;
    PX_ApplicationUpdate(&App,elapsed);
    PX_ApplicationRender(&App,elapsed);
    uefi_gop_render(App.runtime.RenderSurface.surfaceBuffer, App.runtime.RenderSurface.width, App.runtime.RenderSurface.height);
  }
  
  return 0;
}

需要注意的是,PainterEngineUefiMain 是 inf 中的 ENTRY_POINT,也即最终的 UEFI App 的入口。

而实际我们的 GUI 程序是另一个 main 函数,他在 PainterEngine.h 中会被替换成 px_main,并在 PX_ApplicationInitialize() 中调用。

有一些 PainterEngine 中要使用,但是对于输出无关紧要的接口,暂时不作不支持,可以先不做具体实现。

然后,可以像 PainterEngine 文档中所述,先写一个最简单的 main 函数(注意,函数名必须要是 main)。这些(GUI 程序代码)建议放在一个独立的 c 文件中,当然它也要列在 [Sources] 下。

至此,我们可以将 inf 添加到合适的 dsc 中,然后编译这个模块。当然也可以新建一个专用的 dsc 文件。编译时可能会有些警告等,我们尽量保证不去对原本的 PainterEngine 做改动,所以对于警告可以直接在 inf 的 [BuildOptions] 将其忽略。

0x01 有关 memset memcpy 等

由于代码写法及不同版本编译器行为,在使用 VS2022 编译时,会被编译器插入一些 memxxx 相关代码进行优化,在 link 时会出错。edk2 中有一个 CompilerIntrinsicsLib,虽然它已经从 ArmPkg 中移到了 MdePkg,但仍旧只适用于 ARM 平台,为其添加一些基本的 X64 支持会有一些阻力(当前 edk2 建议避免在 X64 上出现可能让编译器插入 memxxx 的写法)。所以为了移植的简便,以及不对原有代码做过多改动,当前可以在 [Sources] 下加一个源文件,来实现这几个基本的函数。

类似的还有 _fltused__chkstk,前者为了简便直接添加一行 int _fltused = 1; 即可。__chkstk 应该是 App->cache[] 这个超大数组让编译器引入了它,这个在 edk2 的 BaseLib 中有一份,但当前被限制只用于 GCC(提交了一个 PR,不过目前还在等待 review 中),所以当前还是需要为 MSFT 在 inf 中额外加一个 __chkstk 的空实现。

0x02 时间接口

需要特别注意的两个时间接口是 PX_TimeGetTimePX_TimeGetTimeUs,他们的精度是毫秒和微秒。一般 gRT->GetTime 获取到的 EFI_TIME 中的 Nanosecond 都是 0,精度只到秒一级。所以这两个接口无法使用 EFI_TIME 来实现。

TimerLib 中的 GetTimeInNanoSecond 提供了纳秒级别的精度,可以选择使用它来实现。但是 TimerLib 是平台相关的,即便是相同的架构可能也有不同的实现,无法做到完全通用。UEFI Spec 提供了一个基于 TimerLib 的独立于平台的接口 EFI_TIMESTAMP_PROTOCOL,但是考虑到很多平台没有包含 MdeModulePkg\Universal\TimestampDxe\TimestampDxe.inf,所以也没法方便的使用它。

PainterEngineUefiMain 中的 while (1) 通过两次 PX_TimeGetTime 获取的时间相减,得到过去的时间来更新界面(可以理解为 LVGL 中的 tick)。为了能够稍微“通用”一点,当前只能先硬编码一个 elapsed 值,如果针对特定平台,则可以使用正确的 TimerLib 来获取正确的 elapsed 时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifdef REAL_TIMER_LIB
  timelasttime=PX_TimeGetTime();
#endif
  while (1)
  {
#ifdef REAL_TIMER_LIB
    px_dword now=PX_TimeGetTime();
    px_dword elapsed=now-timelasttime;
    timelasttime= now;
#else
    px_dword elapsed = 100;
#endif
    PX_ApplicationUpdate(&App,elapsed);
    PX_ApplicationRender(&App,elapsed);
    uefi_gop_render(App.runtime.RenderSurface.surfaceBuffer, App.runtime.RenderSurface.width, App.runtime.RenderSurface.height);
  }

至此,不出意外的话,编译完成之后便能够得到一个像文章开头提到的 Demo 一样的 .efi 文件,可以拿到 QEMU 中测试一下了。

0x03 输入接口(键盘、鼠标)

因为有了前面移植 LVGL 的经验,所以这部分就简单了些,我们可以把大部分代码直接拿来用,只需要注意一些与 PainterEngine 相关的内容,做一些相应的适配即可。

键盘

键值相关的宏为 PX_VK_,与 edk2 的 SCAN_ 等对应即可。

然后根据输入字符或功能按键调用不同接口,更新状态即可。

1
2
3
4
5
6
    if (e.Event == PX_OBJECT_EVENT_KEYDOWN) {
      PX_Object_Event_SetKeyDown(&e,str[0]);
    } else if (e.Event == PX_OBJECT_EVENT_STRING) {
      PX_Object_Event_SetStringPtr(&e,&str[0]);
    }
    PX_ApplicationPostEvent(&App,e);

鼠标

需要根据鼠标状态的不同数据,组合出不同的 CURSOR 事件类型 PX_OBJECT_EVENT_CURSORxxx,同时使用 PX_Object_Event_SetCursorXPX_Object_Event_SetCursorY 来更新当前的鼠标位置。

需要注意,UEFI 环境下不像 Windows 等环境直接有鼠标的光标,所以还需要一些额外的代码来将鼠标光标显示出来。这部分可以参考 PainterEngine 的“打狐狸”小游戏,只是我们要绘制的“锤子”变成了一个鼠标光标,然后注册对应的 PX_OBJECT_EVENT_CURSORMOVEPX_OBJECT_EVENT_CURSORDRAG 事件,这样在鼠标移动时,光标也便能跟随移动到正确位置了。

1
2
3
4
5
6
7
8
9
void PX_Object_MouseCursorCreate()
{
	PX_Object_MouseCursor* pmousecursor;
	PX_Object* pObject = PX_ObjectCreateEx(mp, root, 0, 0, 0, 0, 0, 0, PX_OBJECT_TYPE_CURSORBUTTON, 0, PX_Object_MouseCursorRender, PX_Object_MouseCursorFree, 0, sizeof(PX_Object_MouseCursor));
	pmousecursor = PX_ObjectGetDescByType(pObject, PX_OBJECT_TYPE_CURSORBUTTON);
	if (!PX_TextureCreateFromMemory(mp, PX_Object_PainterBox_ImageMouseCursor, sizeof(PX_Object_PainterBox_ImageMouseCursor), &pmousecursor->MouseCursor)) return;
	PX_ObjectRegisterEvent(pObject, PX_OBJECT_EVENT_CURSORMOVE, PX_Object_MouseCursorOnMove, PX_NULL);
	PX_ObjectRegisterEvent(pObject, PX_OBJECT_EVENT_CURSORDRAG, PX_Object_MouseCursorOnMove, PX_NULL);
}

针对输入的一些优化

键盘鼠标数据的获取及处理可以放在 while (1) 中进行,但是一定程度上这些操作会阻塞界面的更新,导致“卡卡的”感觉。

为了不阻塞 while (1),可以选择使用 Event 来处理输入数据,这样“观感”就会好上很多。

1
2
3
4
5
6
7
8
9
  Status = gBS->CreateEvent (
                  EVT_TIMER | EVT_NOTIFY_SIGNAL,
                  TPL_NOTIFY,
                  (EFI_EVENT_NOTIFY) CheckEfiInputEvent,
                  (VOID*)NULL,
                  &EfiInputEvent
                  );

  Status = gBS->SetTimer (EfiInputEvent, TimerPeriodic, 10 * 10000);

CheckEfiInputEvent 中获取键鼠数据,并更新相应的状态。

0x04 文件接口

实现文件相关接口之后,便可以从文件系统中读取一些资源以实现更丰富的效果。

主要的接口是 PX_LoadFileToIOData,其他加载各种特定资源的 PX_Loadxxx 接口都依赖它,我们需要实现的也就是它。

代码的主体思路是获取到要读取文件的 DP,然后读取文件,将文件 Buffer 和 Size 传递给 PX_IO_Data 即可。主要代码如下,通过 ImageHandle 获取当前 efi 所在目录的 DP 然后拼接上具体资源相对 efi 文件的相对路径,然后读取文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
PX_IO_Data PX_LoadFileToIOData(const char path[])
{
  PX_IO_Data io;
  EFI_DEVICE_PATH_PROTOCOL  *FileDp;
  EFI_STATUS                Status;
  EFI_FILE_HANDLE           FileHandle;
  CHAR16                    *Char16Path;

  io.buffer=0;
  io.size=0;
  
  if (mCurrentImageDirDp == NULL) {
    mCurrentImageDirDp = GetCurrentImageDirDp (mImageHandle);
  }

  if (mCurrentImageDirDp) {
    Char16Path = AllocateZeroPool (AsciiStrSize(path) * sizeof(CHAR16));
    AsciiStrToUnicodeStrS(path, Char16Path, (AsciiStrSize(path) * sizeof(CHAR16)));
    FileDp = AppendDevicePath(mCurrentImageDirDp, ConvertTextToDevicePath(Char16Path));
    Status = EfiOpenFileByDevicePath (&FileDp, &FileHandle, EFI_FILE_MODE_READ, 0);
    if (!EFI_ERROR(Status)) {
      EFI_FILE_INFO * FileInfo = LibFileInfo (FileHandle, &gEfiFileInfoGuid);
      UINTN FileBufferSize = FileInfo->FileSize;
      UINT8 *FileBuffer = AllocateZeroPool(FileBufferSize);
      Status = FileHandle->Read (FileHandle, &FileBufferSize, FileBuffer);
      if (!EFI_ERROR(Status)) {
        io.buffer = FileBuffer;
        io.size = FileBufferSize;
      }
    }
    if (Char16Path != NULL) {
      FreePool (Char16Path);
    }
  }

  return io;
}

实现这个接口之后,便可以将其他平台已经实现的各种资源加载接口 PX_Loadxxx 直接拿来用了。

0xFF 最终测试

以上都完成后,便可以写一个简单的 App 来做验证。包含对输出(显示,这肯定要有)、输入(键鼠)、文件读取等接口的使用/测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int main (
  VOID
  )
{
  EFI_STATUS                   Status;
  EFI_GRAPHICS_OUTPUT_PROTOCOL *EfiGop = NULL;

  Status = gBS->LocateProtocol (&gEfiGraphicsOutputProtocolGuid, NULL, (VOID **) &EfiGop);
  if (!EFI_ERROR(Status)) {
    screen_width = EfiGop->Mode->Info->HorizontalResolution;
    screen_height = EfiGop->Mode->Info->VerticalResolution;
  }

  PainterEngine_Initialize (screen_width, screen_height);

  PainterEngine_LoadFontModule("font/unifont.ttf", PX_FONTMODULE_CODEPAGE_UTF8, 20);

  PainterEngine_DrawText (screen_width/2, screen_height/2, "你好 PainterEngine", PX_ALIGN_CENTER, PX_COLOR(255, 255, 0, 0));

  PX_Object* myButtonObject;
  myButtonObject=PX_Object_PushButtonCreate(mp,root,screen_width/2-200,screen_height/2+50,200,80,"按钮", PainterEngine_GetFontModule());
  PX_ObjectRegisterEvent(myButtonObject,PX_OBJECT_EVENT_EXECUTE,OnButtonClick,0);

  PX_Object* pObject;
  pObject=PX_Object_EditCreate(mp,root,screen_width/2,screen_height/2+50,200,80,0);
  PX_ObjectRegisterEvent(pObject,PX_OBJECT_EVENT_VALUECHANGED, PX_Object_EditOnTextChanged,PX_NULL);

  return EFI_SUCCESS;
}

以上的代码会加载一个 ttf 字体,显示一行文字,一个按钮(可以用鼠标点击)和一个输入框(可以使用键盘输入信息)。下面是最终的效果:

test

上述相关代码已经贡献到 https://github.com/matrixcascade/PainterEngine,感兴趣的朋友可以去看一看,捉捉虫~

本文由作者按照 CC BY 4.0 进行授权