自作OSに挑戦する日記 30日目 & 31日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 30「高度なアプリケーション」

今回やった内容

  • 計算機
  • テキストビューア
  • 音楽プレイヤー
  • 画像ビューア
  • IPL改良

環境

作業記録

 前回まででOSが完成したので、最終日はアプリケーションを作っていきます。

計算機

 入力した計算式の結果を出力するアプリです。

f:id:guguru0014:20190526142855p:plain

テキストビューア

 テキストファイルの閲覧が出来るアプリです。h, j, k ,l キーで操作することができます。

f:id:guguru0014:20190526143426p:plain

音楽プレイヤ

 BEEP音で音楽を再生するアプリです。QEMUではBEEP音のエミュレートができないため、実際に音楽を聴くことは出来ませんでした…。

f:id:guguru0014:20190526143748p:plain

画像ビューア

 JPEG or BMP形式の画像ファイルを閲覧できるアプリです。

f:id:guguru0014:20190526143844p:plain
👍

IPL改良

 HariboteOS起動の高速化をします。ディスク読み込みを1セクタずつ行うのではなく、複数のセクタをまとめて読み込むようにします。  ALレジスタに読み込むセクタ数を指定した後、BIOSの0x13番を呼ぶことで複数セクタをまとめて読み込むことができます。

; ディスク読み込み(高速版), 複数セクタを同時に読み込む
; ES : 読み込み番地, CH : シリンダ, DH : ヘッダ, CL : セクタ, RX : 読み込みセクタ数
readfast:
        MOV      AX, ES            ; <ESからセクタ数(AL)を計算>
        SHL      AX, 3           ; AXを32で割ってAHに結果を代入したのと同じ動きをする AX -> AH | AL
        AND      AH, 0x7f        ; 128で割った余りを求める
        MOV      AL, 128         ; AL = 128 - AH => 一番近い64KBの境界まで何セクタあるか
        SUB      AL, AH            ; 1セクタ(512KB) * 128 - 1セクタ(512KB) - AH(端数)

        MOV      AH, BL            ; ALの値を調節する
        CMP  BH, 0           ; if(BH != 0){ AH = 18; }
        JE       .skip1
        MOV      AH, 18
.skip1:
        CMP      AL, AH            ; if(AL > AH){ AL = AH; }
        JBE      .skip2
        MOV      AL, AH
.skip2:
        MOV      AH, 19
        SUB      AH, CL
        CMP  AL, AH            ; if(AL > AH) { AL = AH; }
        JBE  .skip3
        MOV      AL, AH
.skip3:
        PUSH BX               ; 失敗回数を数えるレジスタ
        MOV  SI, 0

retry:
        MOV      AH, 0x02        ; ディスク読み込み命令のために設定
        MOV      BX, 0
        MOV      DL, 0x00
        PUSH ES
        PUSH DX
        PUSH CX
        PUSH     AX
        INT  0x13           ; ディスク読み出し(ALに設定したセクタ分読む)
        JNC      next         ; エラーが起きなければnextへ
        ADD      SI, 1
        CMP      SI, 5           ; 5回連続で読み込みに失敗したら
        JAE      error
        MOV      AH, 0x00
        MOV      DL, 0x00
        INT      0x13           ; ドライブリセット
        POP  AX
        POP  CX
        POP  DX
        POP  ES
        JMP  retry
next:
        POP  AX
        POP  CX
        POP  DX
        POP  BX
        SHR      BX, 5           ; BXを512バイト単位にする(5ビット右シフト)
        MOV      AH, 0
        ADD      BX, AX
        SHL      BX, 5           ; BXを512バイト単位から32バイト単位へ(5ビット右シフト)
        MOV      ES, BX            ; ES += AL * 0x20
        POP  BX
        SUB  BX, AX
        JZ       .ret
        ADD      CL, AL
        CMP  CL, 18          ; if(CL <= 18){ goto readfast; }
        JBE      readfast
        MOV      CL, 1
        ADD  DH, 1
        CMP  DH, 2           ; if(DH < 2){ goto readfast; }
        JB       readfast
        MOV  DH, 0
        ADD  CH, 1
        JMP  readfast
.ret:
        RET

 AXレジスタの値を32で割ってその値をAHレジスタから取り出すという操作を、左シフト操作で行なっています。

f:id:guguru0014:20190526145527j:plain
AXレジスタの上位8ビットがAH、下位8ビットがAL


 これで30日本全ての内容が終わりました!!

(全体の)まとめ

 自作OS本終わりです!!完成までの期間は4ヶ月でした〜!ブログ完走も出来たので良かったです。

 30日という短い期間でOSが完成するのかと不思議でしたが、キーボード操作やマウス操作、例外などの割り込み処理や、メモリ管理、マルチタスクシステム、ウィンドウシステムなど…OSの基本的な機能は全て実装することができました。 途中、筆者の方のオリジナルプログラムを使う場面で苦戦しましたが、その分得られるものが大きかったです。

 これからは低い方に潜っていきたいと思います。4月にHelloWorld本を手に入れたのでそれを読みつつ、OS再自作のための計画を建てていきます。

 最後に…自作OS本オススメなのでぜひ読んでみてください!

自作OSに挑戦する日記 29日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 29「圧縮と簡単なアプリケーション」

今回やった内容

  • tek圧縮対応
  • 標準関数作成
  • 非矩形ウィンドウ
  • Beautiful Ball
  • インベーダー

環境

作業記録

tek圧縮対応

 筆者の方が作成された圧縮形式 tek にHariboteOSを対応させます。

 早速実装&圧縮だ!…といきたいのですが、本の付録にある圧縮プログラムはWindows用の実行ファイルのみとなっていて 、ファイル圧縮を実際に試してみることはできませんでした。

 圧縮は無理でしたが、OSに組み込む解凍プログラムは動かすことができました。若干のエラーが出ましたが、他の方のブログを参考に修正する事が出来ました。ありがとうございます(ページ下にURLを貼らさせていただいてます)。

 任意のファイルを圧縮することは出来ませんでしたが、既に圧縮済みのファイルが本の付録にあるのでこれを使って動作をテストします(前回使用した日本語フォントファイル)。


 以下2つの図は 、28日目と29日目それぞれHariboteOS上で ls コマンドを実行した状態です。28日目時点でのHariboteOSには未圧縮のものを、29日目途中までのHariboteOSには圧縮済みのフォントファイルを転送しました。

f:id:guguru0014:20190523003724p:plain
28日目

f:id:guguru0014:20190523004210p:plain
29日目

 japanese.fnt が圧縮されていないもの、jpn_tek.fnt がtek圧縮済みのものです。tek圧縮によって 145472B (142KB) から 58009B (56KB) へとファイルサイズが半分以下になっています。ちなみに、 圧縮されたフォントファイルを使用しても日本語表示が出来ます。解凍プログラムが正常に動作している証拠です。


 tek圧縮に対応したところで、遂にHariboteOSが完成しました!初めて本を開いてから4ヶ月、ようやく完成する事が出来ました!
f:id:guguru0014:20190523004809j:plain

 29日目の残りと30日目は全てアプリ作成です!OS作成に比べてプログラムも単純なので、特にソースなどは載せずテンポ良く進めていきたいと思います〜

非矩形ウィンドウ

 ウィンドウ上に透明の図形を描画する事で非矩形ウィンドウを実現しています。

f:id:guguru0014:20190523010158p:plain
notrec

Beautiful Ball

 色の違う線を複数描画する事で、アプリ名の通り美しい球を表現しています。

f:id:guguru0014:20190523010153p:plain
bball

インベーダーゲーム

 インベーダーゲームです。実はこのゲームにはあるストーリーがあります。気になった方はぜひ自作OS本を買ってみてください。

 インベーダーゲームまで実装し、29日目終了です!

まとめ

 遂にHariboteOSが完成しました!そして、残り1日で自作OS本も終わりです。
 残り1日終わらせたら何をしようかと学校などで色々考えていましたが、本当にどうしよう…。HariboteOS新機能、自作言語、HelloWorld本、ネットワーク、Linuxカーネル…やりたい事が多すぎて1つに決められる気がしません。とりあえずもう少し悩みたいと思います。

参考にさせてもらった方のブログ

 ありがとうございました!

bttb.s1.valueserver.jp

自作OSに挑戦する日記 28日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 28「ファイルと日本語表示」

今回やった内容

  • alloca
  • ファイルAPI
  • 日本語表示

環境

作業記録

alloca

 allocaの実装をします。

alloca はスタック上にメモリ領域を確保するための関数のことで、(初めて知った)スタック上に4KB以上のメモリ領域を確保しようとするとコンパイラalloca を使おうとするらしいです。

 …との記述が本にあったのですが、alloca を実装せずに4KB以上の領域を使う変数を宣言するアプリを実行してみたところ、特にエラーは起きませんでした。使っているコンパイラが呼ばないようになっているのか、それとも別に原因があるのか…自作OS本が終わってから詳しく調べなおしたいと思います(コードを変えたりしながら1時間程度格闘したのですが分かりませんでした…)。

 エラーが起きなかったとはいえ、今後バグが出てくるのも怖いのでとりあえず実装することにしました。実装はとても簡単で、ESPを要求されたバイトだけ操作するだけです。

[BITS 32]

        GLOBAL      __alloca

[SECTION .text]

__alloca:
    SUB     ESP, EAX
    ADD     EAX, 4
    JMP     DWORD [ESP + EAX - 4]       ; RETの代わり

 ESPの値が変更されていてRETが使えないため、いつもとは違う方法でアドレスを指定しJMPするようにしています。

ファイルAPI

 ファイルを操作するAPIを実装します。

## ファイルオープン

### 引数

- EDX : 21
- EBX : ファイル名

### 返り値

- EAX : ファイルハンドル(読み込み失敗時には0)

## ファイルクローズ

### 引数

- EDX : 22
- EAX : ファイルハンドル

## ファイルシーク

### 引数

- EDX : 23
- EAX : ファイルハンドル
- ECX : シークモード
    - 0 => ファイルの先頭を原点にとする
    - 1 => 現在読み込み中の場所を原点とする
    - 2 => ファイルの終端を原点とする
- EBX : シーク量

## ファイルサイズ取得

### 引数

- EDX : 24
- EAX : ファイルハンドル
- ECX : ファイルサイズ取得モード
    - 0 : ファイルサイズ
    - 1 : 現在読み込み位置は先頭から何バイトの位置か
    - 2 : 現在読み込み位置からファイルの終端は何バイトあるか

### 返り値

- EAX : ファイルサイズ

## ファイル読み込み

### 引数

- EDX : 25
- EAX : ファイルハンドル
- EBX : バッファの番地
- ECX : 最大読み込みバイト数

### 返り値

- EAX : 今回読み込めたバイト数

## コマンドライン取得

### 引数

- EDX : 26
- EBX : コマンドラインを格納するバッファのアドレス
- ECX : 何バイトまで格納するか

### 返り値

- EAX : 何バイト格納されたか

## 言語モード取得

### 引数

- EDX : 27

### 返り値

- EAX : 言語モード
    - 0 : ASCII English
    - 1 : Shift-JIS
    - 2 : EUC-JP

 それぞれの操作のイメージはこんな感じです。図内の黒三角はファイルハンドルを、中央の並んだ箱のようなものはディスクを表しています。

f:id:guguru0014:20190520204957p:plain
ファイルを操作するイメージ


 これでアプリからファイルの読み込みが出来るようになったので、OSの標準コマンドとして実装していた cat コマンドをアプリとして置き換えます。アプリとして動作させると強制終了が出来るので利便性がUPします。
 また、cat のためにコマンドライン取得APIも作りました。このAPIの動作は名前の通りで、実行直前に入力されたコマンドを取得することができます。

/* cat.c */

#include "api_link_head.h"

void HariMain(){
    char str[40];

    // ファイル名をコマンドラインから取得
    char command[40], *command_p;
    api_get_command(command, 40);
    for(command_p = command; *command_p > ' '; ++ command_p);    // スペースまで読み飛ばす(コマンド名無視)
    for(; *command_p == ' '; ++ command_p);                      // スペースを読み飛ばす(区切り無視)

    // ファイルを開く
    int fhandle = api_fopen(command_p);
    char read_c;
    if(fhandle != 0){
        while(1){
            if(api_fread(fhandle, &read_c, 1) == 0){
                break;
            }
            api_putchar(read_c);
        }
    }else{
        msprintf(str, "File Not Found...\n");
        api_putstr(str);
    }

    api_putchar('\n');
    api_end();
}

日本語表示

 ついにHariboteOSも日本語対応です。全角文字の文字コードの表現方法が少し特殊でした(「面」「区」「点」…)。
 日本語フォントファイルは「OSASK」で使われているデータをありがたく使わさせて貰うことにします…!OS本についてくるCD内からフォントデータを取り出し、少し手を加えればフォントファイル完成です。

 フォントファイルは容量節約のため、非漢字・第一標準漢字・第三標準漢字の3種類のフォントのみが含まれています(本が全部終わった後に全漢字に対応させます)。

 本来より小さいフォントファイルですが、それでも145KBもあるのでOS自体には含めず、ファイルとしてOSから読み込むようにします。

/* bootpack.c */

// 日本語フォントファイル(japanese.fnt)を読み込む
extern char hankaku[4096];
unsigned char *japanese = (unsigned char *) memman_alloc_4k(memman, 16 * 256 + 32 * 94 * 47);
struct FILEINFO *finfo = file_search("japanese.fnt", (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
int *fat = (int *) memman_alloc_4k(memman, 4 * 2880);
read_fat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200));

// フォント設定
if(finfo != 0){
    load_file(finfo->clustno, finfo->size, japanese, fat, (char *) (ADR_DISKIMG + 0x003e00));
}else{
    for(int idx = 0; idx < 16 * 256; ++ idx){   // 半角部分のみを既存のフォントで置き換える
        japanese[idx] = hankaku[idx];
    }
    for(int idx = 16 * 256; idx < 16 * 256 + 32 * 94 * 47; ++ idx){     // 日本語部分は0xffで埋める
        japanese[idx] = 0xff;
    }
}
*((int *) 0x0fe8) = (int) japanese;
memman_free_4k(memman, (int) fat, 4 * 2880);

 フォントデータはメモリの 0x0fe8 番地に置くようにします。


 フォントが用意できたので実際に表示できるようにします。

/* graphic.c */

// EUC
if(task->langmode == 2){
    for(; *str != 0x00; ++ str){
        if(task->langbyte1 == 0){       // 1バイト目
            if(0x81 <= *str && *str <= 0xfe){
                task->langbyte1 = *str;
            }else{
                putfont8(vram, width, x, y, color, japanese + *str * 16);
            }
        }else{                          // 2バイト目
            int k = task->langbyte1 - 0xa1;
            int t = *str - 0xa1;
            task->langbyte1 = 0;
            char *font = japanese + 256 * 16 + (k * 94 + t) * 32;
            putfont8(vram, width, x - 8, y, color, font);
            putfont8(vram, width, x, y, color, font + 16);
        }
    }
}

 これはEUCエンコードされた全角文字1文字を日本語フォントで表示するコードです。全角文字は連続する2バイトで表されるため半角文字のように1バイト読み込んですぐ表示…はできません。なので、1バイト目を読んだときはそのデータを task->langbyte1 に一旦格納し、2バイト目を読み込んだ時点で表示するようにしています。

 また、HariboteOSではEUCだけでなくShift-JISでエンコードされた文字も表示出来るようにしました(UTF-8にも対応させたい)。


 ということで28日目完成です!(バグの修正のために29日目の内容が少し入ってますが…)

 ついにHariboteOSでも日本語表示が出来るようになりました!

まとめ

   表示部分だけですが、HariboteOSが遂に日本語に対応しました! 将来的には入力にも対応させたいです。
 進化が大きかった分、今回は詰まったところが少し多かったです。特に alloca についてはもう少し調べてみたいと思います。

自作OSに挑戦する日記 27日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 27 「LDTとライブラリ」

今回やった内容

  • バグ修正
  • アプリ実行中でもコンソールウィンドウを閉じれるように
  • アプリを守ろう
  • ファイル・Makefile整理

環境

作業記録

バグ修正

 前回追加した open コマンドで立ち上げたアプリは「x」をクリックしても終了できないというバグがありました…。 アプリはキー入力やタイマなどの受信処理以外は常にスリープしているため、終了メッセージがFIFOに追加されても気づくことができません。なので、メッセージをFIFOに追加した後に task_run を呼び、終了メッセージを強制的に確認させるようにしました。

/* bootpack.c */

// コンソール上のアプリケーションを強制終了する(backspace + shift)
if(data == 256 + 0x0e && key_shift){
    struct TASK *task = key_to_window->task;
    if(task != 0 && task->tss.ss0 != 0){
        // 強制終了割り込み表示
        struct CONSOLE *console = task->console;
        console_putstr(console, "\nKeyboard Interrupt\n");

        // end_appに飛ぶようにする
        io_cli();
        task->tss.eax = (int) &(task->tss.esp0);
        task->tss.eip = (int) end_app;
        io_sti();
        task_run(task, -1, 0);
        continue;
    }
}

アプリ実行中でもコンソールを閉じれるように

 現在のHariboteOSは、アプリ実行中のコンソールウィンドウの「x」を押してもコンソールを閉じることが出来ないようになっています(コンソールへの入力は全てアプリ側で受信しているため)。これを直すために、アプリ側でも終了メッセージを待機しておいて、受信時には対応するコンソールを終了させる処理を実行するようにします。

f:id:guguru0014:20190519010840p:plain
イメージ

/* api.c */

// FIFOから取り出したデータが終了メッセージだった
if(data == 4){
    timer_cancel(console->cursor_timer);
    io_cli();
    fifo32_put(osfifo, console->sheet - sheet_ctl->sheet_data + 2024);
    console->sheet = 0;
    io_sti();
}
/* bootpack.c */

// コンソールウィンドウを消す、タスクは動かしたまま
if(2024 <= data && data <= 2279){
    struct SHEET *sheet = sheet_ctl->sheet_data + (data - 2024);
    memman_free_4k(memman, (int) sheet->buf, 400 * 300);
    sheet_free(sheet);
}

アプリケーションを守ろう

 HariboteOSでのアプリ実行に1つ重大な欠陥があったので直します。欠陥とは、あるアプリが他のアプリのセグメントにアクセスする事が出来るというものです。

f:id:guguru0014:20190519012115p:plain
悪意のあるアプリを動かすと…

 この欠陥を直すために、LDT(Local Descriptor Table)を使います。GDT(Grobal Descriptro Table)とは少し異なり、GDTは動作している全てのタスクから共通で使えるセグメントを設定するのに対し、LDTはあるタスク内のみで有効なセグメントを設定する事ができます。このLDTを使って、アプリ内のセグメントへ外部からのアクセスが出来ないようにします。

 タスクを管理する構造体でLDT情報を扱えるようにし…

/* bootpack.h */

struct TASK{
    int sel, flags;
    int level, priority;
    struct FIFO32 fifo;
    struct TSS32 tss;
    struct SEGMENT_DESCRIPTOR ldt[2];
    struct CONSOLE *console;
    int ds_base, stack_addr;
};

 タスク初期化時にLDTのアドレスを記録して…

/* multitask.c */

// TASK_CTL初期化
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
task_ctl = (struct TASK_CTL *) memman_alloc_4k(memman, sizeof(struct TASK_CTL));
for(int i = 0; i < MAX_TASKS; ++ i){
    task_ctl->tasks[i].flags = 0;
    task_ctl->tasks[i].sel = (TASK_GDT0 + i) * 8;
    task_ctl->tasks[i].tss.ldtr = (TASK_GDT0 + i + MAX_TASKS) * 8;
    task_ctl->tasks[i].priority = 1;
    set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &task_ctl->tasks[i].tss, AR_TSS32);
    set_segmdesc(gdt + TASK_GDT0 + i + MAX_TASKS, 15, (int) task_ctl->tasks[i].ldt, AR_LDT);
}

 アプリ起動時にセグメントの設定を行うようにします。

/* console.c */

// セグメント設定
// アクセス権に0x60を足すとアプリケーション用のセグメントになる
task->ds_base = (int) app_mem_addr;
set_segmdesc(task->ldt + 0, file_info->size - 1, (int) exec_addr, AR_CODE32_ER + 0x60);
set_segmdesc(task->ldt + 1, seg_size - 1, (int) app_mem_addr, AR_DATA32_RW + 0x60);

~~~

// 実行
start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));

 アプリ内のセグメントへの外部からのアクセスが例外として扱われるようになりました〜!

f:id:guguru0014:20190519013254p:plain

ファイル・Makefile整理

 ファイルを整理し、OSとアプリのソースを分離していきます。ファイルを整理して…Makefileを変更して…を繰り返し、最終的には以下のような構成になりました。

├──    Makefile
├──    app_make.txt
└──    appbase/
 │  ├────    Makefile
 │  ├────    api_link_head.h
 │  ├────    api_link_obj.asm
 │  ├────    har.ld
 │  └────    msprintf.c
 └──    build_result/
 │  ├────    colorapp.hrb
 │  ├────    haribote.img
 │  ├────    haribote.sys
 │  ├────    timerapp.hrb
 │  └────    walkapp.hrb
 └──    colorapp/
 │  ├────    Makefile
 │  ├────    api_link_head.h
 │  ├────    api_link_obj.asm
 │  ├────    colorapp.c
 │  ├────    har.ld
 │  └────    msprintf.c
 └──    haribote/
 │  ├────    Makefile
 │  ├────    api.c
 │  ├────    asmhead.asm
 │  ├────    bootpack.c
 │  ├────    bootpack.h
 │  ├────    bootpack.map
 │  ├────    console.c
 │  ├────    desctbl.c
 │  ├────    fifo32.c
 │  ├────    fifo8.c
 │  ├────    file.c
 │  ├────    functions.c
 │  ├────    graphic.c
 │  ├────    hankaku.c
 │  ├────    har.ld
 │  ├────    int.c
 │  ├────    inthandler.c
 │  ├────    ipl10.asm
 │  ├────    keyboard.c
 │  ├────    memory.c
 │  ├────    mouse.c
 │  ├────    msprintf.c
 │  ├────    multitask.c
 │  ├────    nasmfunc.asm
 │  ├────    other_disk.asm
 │  ├────    sheet.c
 │  ├────    timer.c
 │  └────    window.c
 └──    other/
 │  ├────    hankaku.txt
 │  ├────    har.ld
 │  └────    makefont.py
 ├──    reference.md
 └──    timerapp/
 │  ├────    Makefile
 │  ├────    api_link_head.h
 │  ├────    api_link_obj.asm
 │  ├────    har.ld
 │  ├────    msprintf.c
 │  └────    timerapp.c
 └──    walkapp/
 │  ├────    Makefile
 │  ├────    api_link_head.h
 │  ├────    api_link_obj.asm
 │  ├────    har.ld
 │  ├────    msprintf.c
 │  └────    walkapp.c

 OS本体のソースコードharibote 内に、アプリはそれぞれにディレクトリを与えてビルドやリンクをアプリ単位で行うようにしました。
 整理に少し時間はかかりましたが…これまでに比べ格段にファイルの管理がし易くなりました!

まとめ

 実装!実装!だった24~26日目と比べ、今回の内容は優しめでした(時間的にも実装量的にも)。見た目や動作には影響しませんが、ファイル管理がし易くなったのが今回1番嬉しいところです。
 残り3日、自作OS本も遂に終わりが見えてきました…!

自作OSに挑戦する日記 26日目

 「30日でできる!OS自作入門」を読んで分かったことや、とりあえず書いておきたいことなどを書いていきます。
 この本はChapterが1から30まであるので、各チャプター毎に1記事書いていきます。

Chapter 26 「ウィンドウ移動高速化」

今回やった内容

  • ウィンドウ移動高速化
  • 複数コンソール
  • コンソールを閉じれるように
  • start コマンド
  • open(ncst) コマンド

環境

作業記録

ウィンドウ移動高速化

 HariboteOSではウィンドウをシートという単位で管理し、描画はシートの重なり情報を記録した配列を元に行なっています。このシートの重なりを記録する処理なのですが、シートの枚数 x シート幅 x シート高さ = 0(n^3)で3重ループを回しています。この重なり情報はウィンドウを少しでも移動する度に記録し直しているため、ウィンドウのサイズが大きくなるほど、ウィンドウの数が増えるほどとてももっさりとした動きになってしまっていました。
 これではいけない…ということで処理を修正していきます。

無駄な分岐をなくす

 重なり判定は1要素ずつ行われ、毎回透明色であるかどうか(=描画する必要があるか)確認しています。もし、シート内に透明色が含まれない場合はこの確認処理は無駄であるため、処理前に1度だけ分岐を入れて無駄な処理を行わないようにします。コードがちょっと長くなりますが…許容範囲です。

/* 載せれる量のコードではないため… */

if(シート内に透明色が含まれる){
    透明色判定なし処理
}else{
    透明色判定あり処理
}

 …この状態で実行してもそこまで高速になったとは感じませんが、内部処理の無駄をなくすことが出来ました。

書き込み速度を上げる

 シートの重なり状態は以下のような配列によって保存されています。

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
 0, 1, 1, 1, 2, 2, 2, 2, 2, 0,
 0, 0, 0, 0, 2, 2, 2, 2, 2, 0,
 0, 3, 3, 3, 2, 2, 2, 2, 2, 0,
 0, 3, 3, 3, 2, 2, 2, 2, 2, 0,
 0, 3, 3, 3, 2, 2, 2, 2, 2, 0]

 この配列の状態を実際のシート配置で表すとこうなります。

f:id:guguru0014:20190517002331p:plain

 重なり状態は1要素ずつ丁寧に更新をしているのですが、これでは無駄が多すぎるのでいくつかまとめて更新出来るようにします。
 重なり状態を保存している配列はchar型なので1つの要素のサイズは1バイトです。この配列に対してint型(4バイト)を使って更新すると、一度にchar型の要素を4つ分更新することが出来ます。普通の代入では不可能ですが、ポインタを使うことで可能になります。速度も1バイト分書き込むのと同じなので、4倍の速度で更新を行うことが出来ます。

f:id:guguru0014:20190517003847p:plain
一度に4バイト書き込むイメージ

/* sheet.c */

int sid4 = sheet_id | sheet_id << 8 | sheet_id << 16 | sheet_id << 24;

~~
// sheet_ctl->g_mapはchar型の配列
int *wp = (int *) &sheet_ctl->g_map[vram_y * sheet_ctl->width + vram_x];
for(int x = 0; x < buf_x1; ++ x){
    wp[x] = sid4;
}

 重なり状態を保持しているchar型の配列をint型のポインタにキャストしてから更新を行なっています。


 シートの重なり状態更新処理だけでなく、実際に画面描画を行なう処理でも4バイトまとめて書き込む方法を使うようにしました。
 これで実行すると、以前とは比べ物にならないくらいウィンドウ移動が早くなりました…!400 x 300 とかなり大きいサイズのコンソールでも移動が楽になりました。

マウスの移動遅延をなくす

 ここまででもかなり早くなったのですが、まだマウスを止めた時にもっさりするような感じがあります。これは、マウスの移動速度に対してウィンドウの更新処理が追いついていないから…らしいです(本より)。マウスやウィンドウの移動は、FIFOにマウスのデータが到着次第すぐ行なっています。これを変えて、FIFOに溜まっているデータが無くなって移動が確定した段階で描画を行うようにします。

/* bootpack.c */

// FIFOバッファが空だった
if(fifo32_status(&osfifo) == 0){
    if(mouse_draw_x >= 0){
        io_sti();
        sheet_slide(sheet_mouse, mouse_draw_x, mouse_draw_y);
        mouse_draw_x = -1;
    }else if(win_draw_x != 0x7fffffff){
        io_sti();
        sheet_slide(key_to_window, win_draw_x, win_draw_y);
        win_draw_x = 0x7fffffff;
    }else{
        task_sleep(task_main);
        io_sti();
    }
    continue;
}

draw_mouse_*draw_win_* にそれぞれ描画する座標を入れるようにして、FIFOバッファがからだった時にまとめて更新するようにしています。

複数コンソール

 前回の改造で表示できるコンソールの数を増やせるようになりましたが、実行中に増やすことは出来ませんでした(コードを直接変更する必要があった)。また、起動直後に2枚コンソールが表示されているのも少し邪魔です。…ということで、キーボードやマウスを使って自由に枚数を操作できるようにします。

コンソールを増やす

 shift + Tab を押すことで新規コンソールが立ち上がるようにします。これに合わせ、起動処理に書かれていたコンソール立ち上げ処理を関数にして、いつでも呼べるようにします。

/* bootpack.c */

struct SHEET *open_console(struct SHEET_CTL *sheet_ctl, unsigned int mem_size){
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct TASK *task = task_alloc();
    int *fifo_console = (int *) memman_alloc_4k(memman, 128 * 4);
    struct SHEET *sheet = sheet_alloc(sheet_ctl);
    unsigned char *buf = (unsigned char *) memman_alloc_4k(memman, 400 * 300);

    ~~ タスクやシートの設定 ~~

    return sheet;
}

 shift + Tab が押された時の処理も書きます。

/* bootpack.c */

// コンソールを生成する
if(data == 256 + 0x0f && key_shift){
    key_window_off(key_to_window);
    key_to_window = open_console(sheet_ctl, mem_size);
    key_window_on(key_to_window);
    sheet_slide(key_to_window, 300, 240);
    sheet_updown(key_to_window, sheet_ctl->top);
}

 これでコンソールを自由に増やせるようになりました。それぞれのコンソールは独立して動いているので、好きなだけアプリを実行できるようになりました。

コンソールを閉じれるように

 アプリと同じく、コンソールも閉じれるようにします。コンソールを閉じるためにはメモリ解放やタスク構造体の解放など必ずやらなければいけない処理があります。これを close_console にまとめます。

/* bootpack.c */

// コンソールタスクを終了
void kill_console_task(struct TASK *task){
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    task_sleep(task);
    memman_free_4k(memman, task->stack_addr, 64 * 1024);
    memman_free_4k(memman, (int) task->fifo.buf, 128 * 4);
    task->flags = 0;    // task_freeの代わり
    return;
}

// コンソールを閉じる
void close_console(struct SHEET *sheet){
    struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    struct TASK *task = task_now();
    memman_free_4k(memman, (int) sheet->buf, 400 * 300);
    sheet_free(sheet);
    kill_console_task(sheet->task);
    return;
}

 コンソールウィンドウを閉じる処理と、タスク終了とメモリ解放を行う処理は別々の関数にします(タスクだけを閉じる処理を後で書くため)。スタックやFIFOを安全に解放できるように、処理中はタスクをスリープさせます。

 kill_console_task 内でタスクをスリープしている都合上、以下のような流れでコンソールを閉じる処理を行います。

f:id:guguru0014:20190517201511p:plain

 コンソールの「x」が押されるか exit コマンドがコンソールで実行されると、コンソールタスク自体で出来る処理を行った後、終了処理要請をFIFOにputするようにします(コンソールタスク自体で出来る処理:タイマーキャンセルなど)。


 これでウィンドウ関連の実装が終わりました〜!ウィンドウの移動も早くなり、コンソールを増やしたり減らしたり…も自由に行えるようになりました。

start コマンド

 引数に指定したコマンドを新規コンソールで実行するコマンド start を実装します。処理自体はとても単純で、新規コンソールを立ち上げた後、そのFIFOに対してコマンドを送り込むだけです。

/* console.c */

void command_start(struct CONSOLE *console, char command[]){
    // 新規コンソール立ち上げ
    struct SHEET_CTL *sheet_ctl = (struct SHEET_CTL *) *((int *) 0xfe4);
    struct SHEET *sheet = open_console(sheet_ctl, mem_size);
    struct FIFO32 *fifo = &sheet->task->fifo;
    sheet_slide(sheet, 300, 300);
    sheet_updown(sheet, sheet_ctl->top);

    // 引数に指定されたコマンドを実行
    for(int idx = 6; command[idx] != 0; ++ idx){
        fifo32_put(fifo, command[idx] + 256);
    }
    fifo32_put(fifo, 256 + 0x0a);
    console_newline(console);
    return;
}

open(ncst) コマンド

 start とは少し違う動きをする open コマンドを実装します(本だと ncst ですが open の方が馴染みがあるためこうしました)。open コマンドは引数に指定したコマンドを、新規コンソールを立ち上げずに実行するものです。

/* console.c */

void command_open(struct CONSOLE *console, char command[]){
    // 新規コンソールタスク立ち上げ
    struct TASK *task = open_console_task(0);
    struct FIFO32 *fifo = &task->fifo;

    // 引数に指定されたコマンドを実行
    for(int idx = 5; command[idx] != 0; ++ idx){
        fifo32_put(fifo, command[idx] + 256);
    }
    fifo32_put(fifo, 256 + 0x0a);
    console_newline(console);
    return;
}

 基本的には start と変わりません。コンソールを立ち上げずに実行するという特別な動作をするので、他のコードも一部変更しました。


 これで26日目完成です!実行すると以下のようになります。

 OSっぽい感じがさらに増した…

まとめ

 26日目は変更した箇所が多く実装も少し重かったですが、無事動いて良かったです。
 ウィンドウ操作も楽になりコマンドも増えて、段々と快適なOSに近づいている感じがします。