シェルっぽいものが一応できたので、次はGUIっぽいものを作ってみます。まずは画面へ絵を描く方法を紹介し、アイコン代わりに矩形を描いてみます。サンプルプログラムは"sample3_1_draw_rect"ディレクトリです。
グラフィック描画には"EFI_GRAPHICS_OUTPUT_PROTOCOL"を使用します(仕様書"11.9 Graphics Output Protocol(P.464)"参照)。ただし、このプロトコルはSystemTableのメンバにはありません。
実はほとんどのプロトコルはSystemTable->BootServicesの中の関数を使用してプロトコルの先頭アドレスを取得する必要があります(仕様書"4.4 EFI Boot Services Table(P.80)"参照)。BootServicesはEFI_BOOT_SERVICESという構造体で、主にブートローダー向けにUEFIが提供している関数(サービス)を持ちます。*1
[*1] "~_SERVICES"には他に"EFI_RUNTIME_SERVICES"があり、こちらもSystemTable->RuntimeServicesという形でSystemTableから参照できます。
SystemTableのメンバに無いほとんどのプロトコルは、SystemTable->BootServices->LocateProtocol関数(仕様書"6.3 Protocol Handler Services(P.184)"参照)でプロトコル構造体の先頭アドレスを取得できます。LocateProtocol関数は、プロトコル毎に一意に決められている"GUID"からプロトコルの先頭アドレスを取得する関数です。"GUID"も仕様書に記載されています。例えばEFI_GRAPHICS_OUTPUT_PROTOCOLの場合、図3.1のように記載されています。
LocateProtocolの定義はリスト3.1の通りです。
引数の意味は以下の通りです。
SystemTableと同じく、EFI_GRAPHICS_OUTPUT_PROTOCOLも、efi_init関数でグローバル変数へ格納することにします。LocateProtocol処理を追加したefi_initはリスト3.2の通りです。
1: #include "efi.h" 2: #include "common.h" 3: 4: struct EFI_SYSTEM_TABLE *ST; 5: struct EFI_GRAPHICS_OUTPUT_PROTOCOL *GOP; 6: 7: void efi_init(struct EFI_SYSTEM_TABLE *SystemTable) 8: { 9: struct EFI_GUID gop_guid = {0x9042a9de, 0x23dc, 0x4a38, \ 10: {0x96, 0xfb, 0x7a, 0xde, \ 11: 0xd0, 0x80, 0x51, 0x6a}}; 12: 13: ST = SystemTable; 14: ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL); 15: ST->BootServices->LocateProtocol(&gop_guid, NULL, (void **)&GOP); 16: }
これで、EFI_GRAPHICS_OUTPUT_PROTOCOLを取得できました。EFI_GRAPHICS_OUTPUT_PROTOCOLの定義はリスト3.3の通りです(仕様書"11.9.1 Blt Buffer(P.466)")。なお、例によって、定義の内容は本書で使用するメンバのみに限定しています。定義の全体は仕様書を確認してください。
画面描画はフレームバッファへピクセルデータを書き込むことで行います。GOP->Mode->FrameBufferBase変数からフレームバッファの先頭アドレスを取得できます。ピクセルフォーマットはGOP->Mode->Info->PixelFormat変数から確認できます。PixelFormatは"enum EFI_GRAPHICS_PIXEL_FORMAT"というenum定数です。このenum定数により、ピクセルフォーマットが何であるかを確認できます(仕様書"11.9.1 Blt Buffer(P.467)")。
EFI_GRAPHICS_PIXEL_FORMATの定義はリスト3.4の通りです。筆者が動作確認に使用できる環境(ThinkPad E450とQEMUのOVMF)では全て、ピクセルフォーマットはPixelBlueGreenRedReserved8BitPerColorであったため、本書ではピクセルフォーマットは「BGR+Reserved各8ビット」を想定します。もし異なる場合は、適宜読み替えてください。
なお、GOP->Mode->Info->PixelFormatの値を確認するには画面に数値を出力できる必要があります。次の章のサンプル(sample4_1_get_pointer_stateディレクトリ)で、16進数で数値を出力する関数puthをcommon.cに追加しますので、参考にしてみてください。
加えて、ピクセルフォーマットのバイト列を定義するEFI_GRAPHICS_OUTPUT_BLT_PIXEL構造体もリスト3.5に示します(仕様書"11.9.1 Blt Buffer(P.474)")。
以上で、画面に矩形を描く為のUEFIの定義の追加は終了です。
フレームバッファの先頭アドレスもピクセルフォーマットも分かったので、後はピクセルフォーマットに従ったバイト列をフレームバッファの領域へ書き込むだけです。まずは1ピクセルを指定の座標に描く関数"draw_pixel"を作成します(リスト3.6)。グラフィックス関係の処理は"graphics.c"というソースファイルを作成し、そこへ追加します。
1: void draw_pixel(unsigned int x, unsigned int y, 2: struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL color) 3: { 4: unsigned int hr = GOP->Mode->Info->HorizontalResolution; 5: struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL *base = 6: (struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)GOP->Mode->FrameBufferBase; 7: struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL *p = base + (hr * y) + x; 8: 9: p->Blue = color.Blue; 10: p->Green = color.Green; 11: p->Red = color.Red; 12: p->Reserved = color.Reserved; 13: }
リスト3.6では、以下を行っています。
GOP->Mode->Info->HorizontalResolution
で水平解像度を取得なお、ここで取得している水平解像度はUEFIファームウェアがデフォルトで認識している画面モードの解像度で、筆者のThinkPad E450の場合640ピクセルでした。画面モードはEFI_GRAPHICS_OUTPUT_PROTOCOLのSetMode関数で変更できますので興味があれば試してみてください(仕様書"11.9.1 Blt Buffer(P.473)")。SetMode関数ではモード番号を指定しますが、この番号の最大値は同じくEFI_GRAPHICS_OUTPUT_PROTOCOLのMode->MaxModeから確認できます(仕様書"11.9.1 Blt Buffer(P.466)")。
次に、draw_pixelを使用して、draw_rectをリスト3.7のように実装できます。
1: void draw_rect(struct RECT r, struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL c) 2: { 3: unsigned int i; 4: 5: for (i = r.x; i < (r.x + r.w); i++) 6: draw_pixel(i, r.y, c); 7: for (i = r.x; i < (r.x + r.w); i++) 8: draw_pixel(i, r.y + r.h - 1, c); 9: 10: for (i = r.y; i < (r.y + r.h); i++) 11: draw_pixel(r.x, i, c); 12: for (i = r.y; i < (r.y + r.h); i++) 13: draw_pixel(r.x + r.w - 1, i, c); 14: }
なお、RECT構造体はリスト3.8のようにgraphics.hで定義します。
以上を使用して、画面に矩形を描く"rect"コマンドを追加してみます。追加後のshell.cはリスト3.9の通りです。
1: #include "common.h" 2: #include "graphics.h" 3: #include "shell.h" 4: 5: #define MAX_COMMAND_LEN 100 6: 7: void shell(void) 8: { 9: unsigned short com[MAX_COMMAND_LEN]; 10: struct RECT r = {10, 10, 100, 200}; /* 追加 */ 11: 12: while (TRUE) { 13: puts(L"poiOS> "); 14: if (gets(com, MAX_COMMAND_LEN) <= 0) 15: continue; 16: 17: if (!strcmp(L"hello", com)) 18: puts(L"Hello UEFI!\r\n"); 19: else if (!strcmp(L"rect", com)) /* 追加 */ 20: draw_rect(r, white); /* 追加 */ 21: else 22: puts(L"Command not found.\r\n"); 23: } 24: }
実機で実行した様子は図3.2の通りです。
GUIのモードへ切り替える"gui"コマンドを追加してみます。サンプルのディレクトリは"sample3_2_add_gui_mode"です。
GUIモードとしては、まずはアイコンに見立てた矩形を1つ配置するだけです。GUIモードの実装は、新たにgui.cを作成し、そこへ記述することにします。そして、シェルからは、guiコマンドが実行されたときに、gui.cに実装されているgui関数を呼び出すこととします。
gui.cの実装をリスト3.10に示します。
1: #include "efi.h" 2: #include "common.h" 3: #include "graphics.h" 4: #include "gui.h" 5: 6: void gui(void) 7: { 8: struct RECT r = {10, 10, 20, 20}; 9: 10: ST->ConOut->ClearScreen(ST->ConOut); 11: 12: /* ファイルアイコンに見立てた矩形を描画 */ 13: draw_rect(r, white); 14: 15: while (TRUE); 16: }
gui関数を呼び出すguiコマンドを追加したshell.cをリスト3.11に示します。
1: #include "common.h" 2: #include "graphics.h" 3: #include "shell.h" 4: #include "gui.h" /* 追加 */ 5: 6: #define MAX_COMMAND_LEN 100 7: 8: void shell(void) 9: { 10: unsigned short com[MAX_COMMAND_LEN]; 11: struct RECT r = {10, 10, 100, 200}; 12: 13: while (TRUE) { 14: puts(L"poiOS> "); 15: if (gets(com, MAX_COMMAND_LEN) <= 0) 16: continue; 17: 18: if (!strcmp(L"hello", com)) 19: puts(L"Hello UEFI!\r\n"); 20: else if (!strcmp(L"rect", com)) 21: draw_rect(r, white); 22: else if (!strcmp(L"gui", com)) /* 追加 */ 23: gui(); /* 追加 */ 24: else 25: puts(L"Command not found.\r\n"); 26: } 27: }
図3.3のようにguiコマンドを実行すると、アイコンに見立てた矩形をひとつだけ描いた状態の画面(図3.4)へ遷移します。