Top

第3章 画面に絵を描く

シェルっぽいものが一応できたので、次はGUIっぽいものを作ってみます。まずは画面へ絵を描く方法を紹介し、アイコン代わりに矩形を描いてみます。サンプルプログラムは"sample3_1_draw_rect"ディレクトリです。

3.1 EFI_GRAPHICS_OUTPUT_PROTOCOL

グラフィック描画には"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のように記載されています。

EFI_GRAPHICS_OUTPUT_PROTOCOLのGUID

図3.1: EFI_GRAPHICS_OUTPUT_PROTOCOLのGUID

LocateProtocolの定義はリスト3.1の通りです。

リスト3.1: LocateProtocolの定義

unsigned long long (*LocateProtocol)(
        struct EFI_GUID *Protocol,
        void *Registration,
        void **Interface);

引数の意味は以下の通りです。

struct EFI_GUID *Protocol
取得したいプロトコルのGUIDを指定。
void *Registration
オプショナル。必要に応じてレジストレーションキーというものを指定するらしい。本書では使用しない(NULL指定)。
void **Interface
プロトコル構造体の先頭アドレスを格納するポインタを指定。

SystemTableと同じく、EFI_GRAPHICS_OUTPUT_PROTOCOLも、efi_init関数でグローバル変数へ格納することにします。LocateProtocol処理を追加したefi_initはリスト3.2の通りです。

リスト3.2: sample3_1_draw_rect/efi.c

 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)")。なお、例によって、定義の内容は本書で使用するメンバのみに限定しています。定義の全体は仕様書を確認してください。

リスト3.3: EFI_GRAPHICS_OUTPUT_PROTOCOLの定義

struct EFI_GRAPHICS_OUTPUT_PROTOCOL {
        unsigned long long _buf[3];
        struct EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE {
                unsigned int MaxMode;
                unsigned int Mode;
                struct EFI_GRAPHICS_OUTPUT_MODE_INFORMATION {
                        unsigned int Version;
                        unsigned int HorizontalResolution;
                        unsigned int VerticalResolution;
                        enum EFI_GRAPHICS_PIXEL_FORMAT {
                                PixelRedGreenBlueReserved8BitPerColor,
                                PixelBlueGreenRedReserved8BitPerColor,
                                PixelBitMask,
                                PixelBltOnly,
                                PixelFormatMax
                        } PixelFormat;
                } *Info;
                unsigned long long SizeOfInfo;
                unsigned long long FrameBufferBase;
        } *Mode;
};

画面描画はフレームバッファへピクセルデータを書き込むことで行います。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ビット」を想定します。もし異なる場合は、適宜読み替えてください。

リスト3.4: EFI_GRAPHICS_PIXEL_FORMATの定義

enum EFI_GRAPHICS_PIXEL_FORMAT {
        PixelRedGreenBlueReserved8BitPerColor,
        PixelBlueGreenRedReserved8BitPerColor,
        PixelBitMask,
        PixelBltOnly,
        PixelFormatMax
};

なお、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)")。

リスト3.5: EFI_GRAPHICS_OUTPUT_BLT_PIXELの定義

struct EFI_GRAPHICS_OUTPUT_BLT_PIXEL {
        unsigned char Blue;
        unsigned char Green;
        unsigned char Red;
        unsigned char Reserved;
};

以上で、画面に矩形を描く為のUEFIの定義の追加は終了です。

3.2 矩形を描くサンプルを実装

フレームバッファの先頭アドレスもピクセルフォーマットも分かったので、後はピクセルフォーマットに従ったバイト列をフレームバッファの領域へ書き込むだけです。まずは1ピクセルを指定の座標に描く関数"draw_pixel"を作成します(リスト3.6)。グラフィックス関係の処理は"graphics.c"というソースファイルを作成し、そこへ追加します。

リスト3.6: sample3_1_draw_rect/graphics.c(draw_pixel関数)

 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では、以下を行っています。

  1. GOP->Mode->Info->HorizontalResolutionで水平解像度を取得
  2. 水平解像度の値と与えられたX、Y座標値から1ピクセルを描くアドレスを計算
  3. 2.へcolor変数の値を書き込み

なお、ここで取得している水平解像度は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のように実装できます。

リスト3.7: sample3_1_draw_rect/graphics.c(draw_rect関数)

 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で定義します。

リスト3.8: RECT構造体の定義

struct RECT {
        unsigned int x, y;
        unsigned int w, h;
};

以上を使用して、画面に矩形を描く"rect"コマンドを追加してみます。追加後のshell.cはリスト3.9の通りです。

リスト3.9: sample3_1_draw_rect/shell.c

 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の通りです。

rectコマンド実行の様子

図3.2: rectコマンド実行の様子

3.3 GUIモードを追加する

GUIのモードへ切り替える"gui"コマンドを追加してみます。サンプルのディレクトリは"sample3_2_add_gui_mode"です。

GUIモードとしては、まずはアイコンに見立てた矩形を1つ配置するだけです。GUIモードの実装は、新たにgui.cを作成し、そこへ記述することにします。そして、シェルからは、guiコマンドが実行されたときに、gui.cに実装されているgui関数を呼び出すこととします。

gui.cの実装をリスト3.10に示します。

リスト3.10: sample3_2_add_gui_mode/gui.c

 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に示します。

リスト3.11: sample3_2_add_gui_mode/shell.c

 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)へ遷移します。

guiコマンド実行の様子

図3.3: guiコマンド実行の様子

アイコン一つ表示するだけのGUIモード

図3.4: アイコン一つ表示するだけのGUIモード


Top