備忘録やめた

備忘録として使用していたけどやめた.このブログに載せてあるコードのライセンスは別途記載がない限りWTFPL OR NYSLです.

はりぼてOSの解像度と色数を大きくする

30日でできる! OS自作入門

30日でできる! OS自作入門

概要

『30日でできる!OS自作入門』で取り扱っているはりぼてOSでは,最大256色使用できる8ビットカラーを使用しています*1.これに対し,最大16,777,216色使用できる24ビットカラーや32ビットカラーが存在します.この記事では,はりぼてOSで24ビットカラーや32ビットカラーを使用するようにコードを書き換えます.また,解像度も大きくします.

本の中で述べられている解像度の拡大の方法との違い

使用するVBEモードに対応するモードナンバの選択が,本中のやり方とこの記事でのやり方で異なります.本中では,予めVESAが定義したモードナンバを使用していますが,この記事では,BIOSに使用可能なモードナンバを問い合わせ,その中で最も解像度の高い,かつ24ビットカラーか32ビットカラーでダイレクトカラーのモードナンバを使用します.

VBEのバージョンが2.0未満の時代は,予めVESAが定義したモードナンバに対応するVBEモードを使用していましたが,2.0からは,VESAはモードナンバを新たに定義することを止めました*2.バージョン2.0以上でもVESAが既に定義しているモードナンバを使用することも可能ですが,1280x1024以上の解像度が定義されていません*3.これより大きい解像度を使用したい場合,BIOSに利用可能なVBEモードを問い合わせる必要があります.この記事ではBIOSに使用可能なモードナンバを問い合わせ,その中で最も解像度の高い,かつ24ビットカラーか32ビットカラーそしてダイレクトカラーのモードナンバを使用します.

ダイレクトカラー

ダイレクトカラーとは,色をその構成要素に分解し,構成要素毎に値を指定して最終的な色を指定する方式です.例えばRGB,つまり赤緑青の値を指定する方式などが該当します.これに対し,予めパレットに色を登録し,そのパレットの番号を指定する,インデックスカラーというものも存在します.はりぼてOSではこのインデックスカラーを使用しています.今回使用する24ビットカラーや32ビットカラーでは,ダイレクトカラー方式で色を指定します.

24ビットカラーと32ビットカラーの違い

まず,24ビットカラーも32ビットカラーも,赤,緑,青の3色の強さをそれぞれ0から255の計256種類の値で指定します.よって256の3乗で16,777,216色を表すことができます.また,赤緑青でそれぞれ8ビット使用することから,計24ビット使用することになります.

32ビットカラーはこの24ビットに加え,追加の8ビットが与えられます.これはアルファチャンネルと言われ,例えば透明度の設定などに使用されますが,全く使用されない場合もあります.何れにせよ,表現できる総色数は変わりません.

24ビットカラーや32ビットカラー以外を選択肢から外す理由

単純に24ビットカラーや32ビットカラーが一番多くの種類の色を扱えるから,というのもありますが,他の色深度*4,例えば16ビットカラーや15ビットカラーだと色を指定するのが少し面倒なんですよね.VBE3.0の規格書のPage19にある表によれば,15ビットカラーは赤緑青でそれぞれ5ビットを使用します.これならまだいいのですが,16ビットカラーでは1つの色だけ*56ビットを使用しています.このようにビット数が異なると当然その分コードも複雑になるので,ここでは24ビットカラーか32ビットカラーのみを選択肢として残すことにします.

注意

この記事では本の著者が提供しているツールを使用せず,アセンブリ言語コンパイルにはnasmを使用しています.そのため一部文法が異なる場合があります.そして,記事を書いている本人は本の最後まで実装しきれていません*6.故に今回使用しているメモリ領域が後に別の項目で使用される場合もあります.そのような場合,適宜使用するメモリの番地を変更してください.

やり方

asmhead.nasの書き換え

書き換えたものはこちらになります.

以下は説明となります.

定数定義の追加と名称変更

使用するVBEのモードナンバがどこに格納されているかと,VBEの情報の大きさに関する定義を追加します.

VBEMODE EQU 0x0FFC
VBE_INFO_SIZE EQU 512

後にVBEの情報を取得する関数を紹介しますが,VBEの情報は,VBEのバージョンが2.0未満では256バイト,2.0以上では512バイトの大きさです.

また,VMODEに関しては,BPP*7の方が分かりやすいので,名前を変更しています.これは任意です.

BPP EQU 0x0FF2

利用可能な画面モードの取得

VBE3.0の規格書のPage25より引用.

Function 00h - Return VBE Controller Information

Input:

AX = 4F00h Return VBE Controller Information

ES:DI = Pointer to buffer in which to place VbeInfoBlock structure (VbeSignature should be set to 'VBE2' when function is called to indicate VBE 3.0 information is desired and the information block is 512 bytes in size.)

Output: AX = VBE Return Status

Note: All other registers are preserved.

この関数を使うことで,利用可能なVBEモードなどが格納されている情報を取得することができます.

OutputのVBE Return Statusは,0x004Fなら関数の実行の成功,それ以外なら失敗を表します.

情報の構成は,次の構造体のような構成になっています.VBE3.0の規格書Page25に記載されているものを表にしました.

名前 大きさ 格納されているデータなど 説明
VbeSignature db*8 'VESA' VBE Signature
VbeVersioin dw*9 0300h VBE Version
OemStringPtr dd*10 VbeFarPtr to OEM String
Capabilities db 4dup?*11 Capabilities of graphics controller
VideoModePtr dd VbeFarPtr to VideoModeList
TotalMemory dw Number of 64kb memory blocks. Added for VBE 2.0+
OemSoftwareRev dw VBE implementation Software revision
OemVendorNamePtr dd VbeFarPtr to Vendor Name String
OemProductNamePtr dd VbeFarPtr to Product Name String
OemProductRevPtr dd VbeFarPtr to Product Revision String
Reserved db 222dup? Reserved for VBE implementation scratch area
OemData db 256dup? Data Area for OEM Strings

ところで,この関数は本中のP278でも使用されてます.そこではVBEの存在確認としてこの関数を呼び出してまずが,以下のコードはそれに若干改変を加えたものです.具体的には,VBE0x9000を対応付け,それをESレジスタに代入して利用しています.また,scrn_320screen_320に変更しています.この先,同書から引用する際,scrn_320となっているものはすべてscreen_320に変更しています.

VBE EQU 0x9000

MOV AX,VBE
MOV ES,AX
MOV DI,0
MOV AX,0x4F00
INT 0x10
CMP AX,0x004F
JNE screen_320

関数の実行に成功すれば,AXレジスタ0x004Fが代入され,アドレスVESABIOSから512バイト*12,VBEに関する情報が格納されます.異なっていればscreen_320ラベルに飛ばします.本ではもしVBEが存在しなかった場合,解像度を320x200にするという意味でJNE screen_320を書いています.

この関数で得られた情報の中にはVBEのバージョンも含まれています*13.VBEのバージョンが2.0未満である場合,linear frame bufferを使用することができない*14ので,解像度320x200の8ビットカラーを使用します.以下のコードは,本のP279からの引用です.

MOV AX,[ES:DI+4]
CMP AX,0x0200
JB screen_320

linear frame bufferを有効にすると,ビデオRAMのすべてのメモリが一列に並びます.つまり,ディスプレイのどのピクセルもこのビデオRAMのどこかしらに対応しています.linear frame bufferに対応していない場合,ビデオRAMが複数に分けられ,これらを切り替えながら操作する必要があります.

[ES:DI+4]は,VbeVersion変数が構造体の先頭から4バイト離れたところに格納されているためです.

使用するモードナンバの選択

今回は,24ビットカラーあるいは32ビットカラーで,かつ解像度が最大のモードナンバを使用することにします.

使用可能なモードナンバはメモリ上に,配列のように連続して格納されています.モードナンバの終端は0xFFFFという値で終了しています*15.今回はこのモードナンバの配列を走査して,順に解像度と色深度を確かめていきます.

ループの初期化

ループの初期化として,色深度,解像度の横の長さ,縦の長さの初期値を指定します.

MOV BYTE[BPP],8
MOV WORD[SCRNX],320
MOV WORD[SCRNY],200

また,DIレジスタに,VBEの情報の大きさ分の値を代入しておきます.

MOV DI,VBE_INFO_SIZE

VBEの情報を取得したときと同様,モードナンバに対応するVBEモードの情報を取得すると,情報はES:DIを始点として情報が格納されます.従って,VBEの情報を上書きしないために,VBEの情報の大きさの値をDIレジスタに格納しておきます.

ループ本体

ループ開始位置のラベル付与

ループの開始を示すためにラベルを配置します.

select_mode:
定数定義

モードナンバの配列の先頭のアドレスが,先程載せた,VBEの情報が格納されている構造体のvideo_modes変数に格納されています.この変数は構造体の先頭から14バイト離れた位置にあるので,それを定数とします.

VMODE_PTR EQU 14
モードナンバの取得

モードナンバの配列からモードナンバを取得します.

MOV SI,WORD[ES:VMODE_PTR]
MOV FS,WORD[ES:VMODE_PTR+2]
MOV CX,WORD[FS:SI]

そして,もしモードナンバの値が0xFFFFだった場合,モードナンバの選択を終了します.

CMP CX,0xFFFF
JE finish_select_mode
モードナンバに対応するVBEモードの情報取得

VBE3.0の規格書のPage30より引用.

Function 01h - Return VBE Mode Information

Input: AX = 0x4F01 Return VBE Mode Information

CX = Mode number

ES:DI = Pointer to ModeInfoBlock structure

Output: AX = VBE Return Status

Note: All other registers are preserved.

この関数を利用して,VBEモードの情報を取得します.

MOV AX,0x4F01
INT 0x10

もし関数の実行に失敗した場合,次のモードナンバを調べます.

CMP AX,0x004F
JNE next_mode

ところで,モードナンバが配列の要素として格納されていながら,実際にその番号を使用してVBEモードの情報を得ようとして失敗する場合は存在するようです.VBE3.0の規格書のPage27のNoteより引用.

It is responsibility of the application to verify the actual availability of any mode returnedby this function through the Return VBE Mode Information (VBE Function 01h) call.Some of the returned modes may not be available due to the actual amount of memoryphysically installed on the display board or due to the capabilities of the attached monitor.

つまり,例えば使用できるメモリの大きさが,ビデオRAMの大きさよりも小さい場合などが該当するようです.

VBEモードの情報は,以下の構造体のような構成となっています.VBE3.0の規格書のPage30の説明を表にしました.

すべてのバージョンのVBEで格納されている情報
名前 大きさ 格納されているデータなど 説明
ModeAttributes dw mode attributes
WinAAttributes db window A attributes
WinBAttributes db window B attributes
WinGranularity dw window granularity
WinSize dw window size
WinASegment dw window A start segment
WinBSegment dw window B start segment
WinFuncPtr dd real mode pointer to window function
BytesPerScanLine dw bytes per scan line
バージョン1.2以降のVBEで格納されている情報
名前 大きさ 格納されているデータなど 説明
XResolution dw horizontal resolution in pixels of characters
YResolution dw vertical resolution in pixels of characters
XCharSize db character cell width in pixels
YCharSize db character cell height in pixels
NumberOfPlanes db number of memory planes
BitsPerPixel db bits per pixel
NumberOfBanks db number of banks
MemoryModel db memory model type
BankSize db bank size in KB
NumberOfImagePages db number of images
Reserved db 1 reserved for page function
ダイレクトカラー情報
名前 大きさ 格納されているデータなど 説明
RedMaskSize db size of direct color red mask in bits
RedFieldPosition db bit position of lsb of red mask
GreenMaskSize db size of direct color green mask in bits
GreenFieldPosition db bit position of lsb of green mask
BlueMaskSize db size of direct color blue mask in bits
BlueFieldPosition db bit position of lsb of blue mask
RsvdMaskSize db size of direct color reserved mask in bits
RsvdFieldPosition db bit position of lsb of reserved mask
DirectColorModeInfo db direct color mode attributes
バージョン2.0以降のVBEで格納されている情報
名前 大きさ 格納されているデータなど 説明
PhysBasePtr dd physical address for flat memory frame buffer
Reserved dd 0 Reserved - always set to 0
Reserved dw 0 Reserved - always set to 0
バージョン3.0以降のVBEで格納されている情報
名前 大きさ 格納されているデータなど 説明
LinBytesPerScanLine dw bytes per scan line for linear modes
BnkNumberOfImagePages db number of images for banked modes
LinNumberOfImagePages db number of images for linear modes
LinRedMaskSize db size of direct color red mask (linear modes)
LinRedFieldPosition db bit position of lsb of red mask (linear modes)
LinGreenMaskSize db size of direct color green mask (linear modes)
LinGreenFieldPosition db bit position of lsb of green mask (linear modes)
LinBlueMaskSize db size of direct color blue mask (linear modes)
LinBlueFieldPosition db bit position of lsb of blue mask (linear modes)
LinRsvdMaskSize db size of direct color reserved mask (linear modes)
LinRsvdFieldPosition db bit position of lsb of reserved mask (linear modes)
MaxPixelClook dd maximum pixel clock (in Hz) for graphics mode
その他
名前 大きさ 格納されているデータなど 説明
Reserved db 189dub? remainder of ModeInfoBlock
linear frame bufferに対応しているかの確認

VBEモードがlinear frame bufferに対応しているかは,VBEモードの情報の中のattributesの第7ビットが1になっているかで確認します.これが1ならlinear frame bufferに対応しています.

MOV AX,WORD[ES:DI]
AND AX,0x80
CMP AX,0x80
JNE next_mode
packed pixelかどうかの確認

packed pixelというのは,各ピクセルとメモリをバイト単位で結びつけるのではなく,ビット単位で結びつけます.例えば4ビットカラーならば,1バイトを2ピクセルと対応付け,前半後半の4ビットをぞれぞれ1ピクセルと対応付けます.1ピクセルを1バイトと結びつけ,最初の4ビットを使用し残りの4ビットを使用しないのではありません,つまり隙間を作らないという意味でpackedということです.

現在調べているVBEモードがpacked pixelかどうかは構造体のmemory_modelの値を確認することで判別します.

MOV AX,WORD[ES:DI+27]
CMP AX,4
JE valid_mode

[ES:DI+27]の27というのは,memory_modelが構造体の先頭から27バイト離れているためです.もしこの値,すなわちAXの値が4ならば,使用するVBEモードの候補として有効なため,valid_modeラベルに飛ばします.

ダイレクトカラーかどうかの確認

ダイレクトカラーかどうかは,memory_modelの値が6かどうかを確認することで判別します.

CMP AX,6
JE valid_mode

既にmemory_modelの値はAXレジスタに格納しているので,ここではMOVの必要はありません.

最後に,もしpacked pixelでもなくダイレクトカラーでもなければ,次のモードナンバを調べます.

JMP next_mode

ところで,packed pixelかどうか,そしてダイレクトカラーかどうかの確認については,OSDev WikifindMode関数を参考にしました.

解像度の比較

現在調べているVBEモードがpacked pixelかダイレクトカラーを使用すると確認できれば,次は解像度の比較を行います.まずラベルを貼ります.

valid_mode:

解像度の横,縦はそれぞれ構造体のwidthheightに格納されています.構造体の先頭からの距離はそれぞれ18バイト,20バイトです.

MOV AX,WORD[ES:DI+18]
CMP AX,WORD[SCRNX]
JB next_mode

MOV AX,WORD[ES:DI+20]
CMP AX,WORD[SCRNY]
JB next_mode

もし横,縦のどちらかでも,それまでの候補のVBEモードより小さければ次の候補を調べます.

色数の比較

先程述べたように,24ビットカラーか32ビットカラー以外は選択肢から外します.色数は構造体のbppに格納され,先頭から25バイト離れています.

CMP BYTE[ES:DI+25],24
JB next_mode
候補の更新

見事,今までの選考基準を通過したVBEモードが登場した場合,解像度や色数に関する情報を更新します.色数の更新の時,AXレジスタではなくALレジスタを使用していることに注意してください.これは,色数がバイト単位で保存されているためです.

MOV AX,WORD[ES:DI+18]
MOV WORD[SCRNX],AX

MOV AX,WORD[ES:DI+20]
MOV WORD[SCRNY],AX

MOV AL,BYTE[ES:DI+25]
MOV BYTE[BPP],AL

MOV AX,WORD[ES:DI+40]
MOV WORD[VRAM],AX
MOV AX,WORD[ES:DI+40+2]
MOV WORD[VRAM+2],AX

MOV WORD[VBEMODE],CX

次のループへの処理

モードナンバの配列のポインタを更新し,ループの先頭へ飛びます.

MOV AX,WORD[ES:VMODE_PTR]
ADD AX,2
MOV WORD[ES:VMODE_PTR],AX

JMP select_mode

ループ終了後の処理

まずラベルを追加します.

finish_select_mode:
解像度と色数の確認

解像度や色数が初期値のままの場合,使用可能なVBEモードが存在しないので320x200を使用します.

CMP WORD[SCRNX],320
JNE set_vbe_mode

CMP WORD[SCRNY],200
JNE set_vbe_mode

CMP BYTE[BPP],8
JNE set_vbe_mode

JMP screen_320
VBEモードの設定

まずラベルを貼ります.

set_vbe_mode:

VBEモードを登録します.以下の情報はVBE3.0の規格書のPage40からです.

Function 02h - Set VBE Mode

Input:

AX = 4F02h Set VBE Mode

BX = Desired Mode to set

ES:DI = Pointer to CRTCInfoBlock structure

Output:

AX = VBE Return Status

Note: All other registers are preserved.

BXレジスタの,各ビットの意味は以下の表のとおりです.

第nビット 説明
0-8 Mode number
9-10 Reserved (must be 0)
11 0 Use current default refresh rate
11 1 Use user specified CRTC values for refresh rate
12-13 Reserved for VBE/AF (must be 0)
14 0 Use windowed frame buffer model
14 1 Use linear/flat frame buffer model
15 0 Clear display memory
15 1 Don't clear display memory

この関数が失敗する場合もあります.その場合,320x200を使用します.

MOV AX,0x4F02
MOV BX,WORD[VBEMODE]
OR BX,0x4000
INT 0x10

CMP AX,0x004F
JE keystatus

表の通り,BXレジスタの第14ビット目は,linear frame bufferを使用するか否かについてのビットであり,このビットが1だと使用することを表明します.OR BX,0x4000というコードはそのためです.

320x200を使用する時の処理

本中のP280のコードを使用していますが,ラベルはscrn320からscreen_320に,VMODEBPPに変更しています.

screen_320:
    MOV AL,0x13
    MOV AH,0x00
    INT 0x10
    MOV BYTE [BPP],8
    MOV WORD [SCRNX],320
    MOV WORD [SCRNY],200

本ではこの先,ビデオRAMの番地を0x000A0000に指定していますが,この値にするとQEMU上で何も表示されなくなる場合があります*16「30日でできる! OS自作入門」をMac向けに環境構築するによれば,番地を0xFD000000に変更することで表示されるようです.

MOV DWORD [VRAM],0xFD000000

Cファイルの書き換え

基本的なこと

今まではビデオRAMの特定の番地に,色の番号を格納していましたが,ダイレクトカラーを使用する時は番号ではなく構成要素のそれぞれの値を格納します.今回使用する24ビットカラーや32ビットカラーでは赤,緑,青の色の強さを格納します.

例として,本中のP94にある,boxfill8関数を改造します.元の関数は以下の通りです.

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
    int x, y;
    for (y = y0; y <= y1; y++){
        for (x = x0; x <= x1; x++)
            vram[y * xsize + x] = c;
    }
    return;
}

書き換え後は以下の通りです.

void boxfill8(struct BootInfo* boot_info, unsigned c, int x0, int y0, int x1, int y1)
{
    int x, y;
    int xsize = boot_info->scrnx;
    char* vram = boot_info->vram;
    char vmode = boot_info->vmode;

    for (y = y0; y <= y1; y++) {
        for (x = x0; x <= x1; x++) {
            int idx = (y * xsize + x) * vmode / 8;
            vram[idx] = (c & 0x0000FF);
            vram[idx + 1] = (c & 0x00FF00) >> 8;
            vram[idx + 2] = (c & 0xFF0000) >> 16;
        }
    }
}

まず,改造前では引数cの型がunsigned charとなっていたのが,改造後はunsignedとなっているところに気をつけてください.改造前ではこの引数には色のインデックスを代入していましたが,改造後は色のRGBの値を代入します.

次にfor文中で,ビデオRAMに値を格納しているところに注目してください.まず,格納する番地です.vmode / 8を掛けているところに注意してください.これは1つのピクセルを表すのに必要なバイト数です.RGB値はB,G,Rの順番で,分解して格納します.R,G,Bの順番ではないことに注意してください.

このように,24ビットカラーや32ビットカラーを使用する時は,ビデオRAMに値を格納する処理すべてにおいて変更が必要となります.

320x200への対応

関数の失敗や,使用可能なVBEモードが存在しないと言った理由で320x200を使用する場合があります.このような場合,ダイレクトカラーではなくインデックスカラーを使用することになります.従ってRGB値をインデックスに変換するなど,何かしらの処理が必要となります.こちらの説明については長くなってしまうので今は省略させていただきます.

トラブルシューティング

上手く行かない時は,QEMUでメモリの特定の番地の値を確認するといいかもしれません.QEMUでのメモリの値の確認はこちらの「QEMUでメモリの内容を見る」を参考にしました.

ここでは,色数などの情報を確認します.QEMUを使用してOSを起動したあと,Ctrl+Alt+2QEMUのターミナルを開いてください.

起動したあと,以下のコマンドを入力してください.

xp /10xb 0x0FF2

xp /10xb 0x0FF2を打ったあとのQEMUのスクリーンショット
xp /10xb 0x0FF2を打ったあとの画面(拡大しています)

はじめの1バイトは色深度で,この場合は0x18すなわち24ビットカラーです.1つ飛ばして次の2バイトは解像度の横の長さで,この場合は0x0A00すなわち2560ピクセルです.リトルエンディアンなのではじめのバイトが下位,次のバイトが上位であることに注意してください.その次は解像度の縦の長さで,0x0640すなわち1600ピクセルです.表示される色がおかしい場合,はじめの1バイトの値が0x180x20ではないかもしれません.

終わりの言葉

はりぼてOSを24ビットカラーや32ビットカラーに対応させることができました.また解像度も大きくしたので,そのうち背景画像を設定できるようになるのではと思っています.

github.com

僕が製作中のOSです.Rustで書かれていますが,もしよければ見ていってください.

参考文献

(AT)memorymap - os-wiki

Memory Map (x86) - OSDev Wiki

monitor - Is 32-bit color depth enough? - Graphic Design Stack Exchange

QEMUでメモリの内容を見る - コケッココケッコ

VBE3.0の規格書

VESA - os-wiki

VESA BIOS Extensions - Wikipedia

VESA Video Modes - OSDev Wiki

色深度 - Wikipedia

「30日でできる! OS自作入門」をMac向けに環境構築する - Qiita

*1:パレットに16色しか登録していないので4ビットカラーと勘違いするかもしれない(僕も勘違いした)ですが,P83に記載があるように8ビットカラーを使用しています.

*2:VBE3.0の規格書のPage19にあるNoteを参照.

*3:VBE3.0の規格書のPage19にある表を参照

*4:1ピクセル毎に使用するビット数

*5:恐らく緑

*6:具体的にはまだ9日目に到達していない.

*7:bits per pixel

*8:バイト

*9:ワード

*10:ダブルワード

*11:4つの要素が存在する

*12:VBEのバージョンが2.0未満ならば216バイト

*13:表中のVbeVersion

*14:VBE3.0の規格書のAppendix 3 - Differences Between VBE Revisionsを見ると,VBE2.0でFlat Frame Buffer,すなわちlinear frame bufferが追加されたと書かれている.

*15:C言語の文字列が'\0'で終わるような感じ

*16:僕も遭遇した.