「ゼロからのOS自作入門」を Rust でやる (第9章)
改造量の多い章だと時間がかかってしまいますが、ゆるゆると続けております。
- シリーズ最初の記事: 「ゼロからのOS自作入門」を Rust でやる (第1章~第4章) - gifnksmの雑多なメモ
- 前回: 「ゼロからのOS自作入門」を Rust でやる (第7章) - gifnksmの雑多なメモ
- 次回: 「ゼロからのOS自作入門」を Rust でやる (第10章~第12章) - gifnksmの雑多なメモ
- 関連記事一覧: ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
第9章
マウスカーソルを動かすと背景やコンソールの文字が消えてしまう問題を解決するために、重ね合わせ処理 (レイヤー) を導入する章です。
重ね合わせの実装 (day09a)
以下を実装しました。
- マウスカーソル、デスクトップ背景など個別の描画要素を意味する
Window
構造体と、Window
への描画機能を有するWindowDrawer
構造体の追加 - 重ね合わせの個々の階層を意味する
Layer
構造体 Layer
の重ね合わせ順序や Frame Buffer への描画を制御するLayerManger
構造体Console
構造体の動作変更 (LayerManager
の初期化までは直接フレームバッファに描画、初期化後はWindow
に描画しフレームバッファへの描画はLayerManager
が行う)
実施していることは C++ 版と同様なのですが、実装の詳細が異なります。 具体的には以下の差異があります。
- C++ 版:
LayerManager
をグローバル変数として定義し各 Window の更新処理から直接メソッドを実行 - Rust 版: 各
Window
の更新処理およびLayerManager
の処理はそれぞれ専用のCoTask
で実施。Window
の更新処理により画面の再描画が必要になった場合、LayerManager
のCoTask
へとイベントを通知し、それを受けたLayerManager
のタスクが描画を行う
上記を実現するために、 sync::mpsc::{Sender, Receiver}
のような async/await 対応したキューを作成しました。
(マウスカーソル移動の時に作成したものを共通的に使える構造体として定義しなおしました)
tokio の mpsc キュー の実装は複雑なのでこのような構造体を定義するのは難しいと思っていたのですが、機能を絞れば簡単に実装することができました。
例えば、今回作成したキューでは tokio の Receiver
or Sender
の drop 後に send
/recv
を Err
で復帰させる機能などは実装していません。
EmergencyConsole
の追加
重ね合わせ処理の導入により LayerManager
初期化後は、 Console
に書き込んだ文字列の描画は LayerManager
の CoTask
が行うようになりました。
Console
へ書き込んだ文字列が画面に表示されるためには async/await のランタイム (Executor
) や各 CoTask
が正常に動作している必要があります。
通常の処理で Console
を使う限りは問題ないのですが、パニックハンドラーや例外ハンドラーの中で文字列を出力したい場合、これでは問題があります。
ハンドラー実行以降はプログラムの実行が停止してしまいランタイムも動作しないため画面に文字列が描画されないためです。
この問題に対処するため、パニックハンドラーや例外ハンドラーからの文字列表示のため EmergencyConsole
を導入しました。
Console
を改造しても良かったのですが、パニックハンドラーや例外ハンドラーから複雑な処理を行うと正しく動作しない可能性があったため、シンプルな別構造体として追加しました。
EmergencyConsole
ではフレームバッファを利用して画面描画するのですが、ハンドラー呼出し時の状況によってはフレームバッファのロックが取られており、普通にロックをとるとデッドロックしてしまう可能性があります。
これに対処するため、 EmergencyConsole
からロック取得する場合は、既存のロックを強制的にアンロックした上でロックを取得するようにしています。
タイマーの実装 (day09b)
性能測定のために Local APIC タイマーを使えるようにする節です。
実装については特にコメントはないです。
シャドウバッファの追加 (day09c)
従来処理ではフレームバッファへの描画時に毎回 Color
構造体からフレームバッファの byte 配列フォーマットへの pixel 毎に変換していました。画面サイズが大きいと、この処理の負荷は大きくなります。
これを改善するため、フレームバッファと同じフォーマットでデータを保持するシャドウバッファを各 Window
に持たせ、 フレームバッファへの描画時はこのシャドウバッファの内容をコピー (memcpy
) するような方式へと変更しました。
これにより Color
構造体から byte 配列フォーマットへの変換が描画の度に毎回行われることがなくなり、性能が改善されます。
C++ 版とは異なり、 ShadowBuffer
という専用構造体を用意しています (この後のコミットで変更されますが)。
コンソールのスクロール速度を測定する (day09d)
コンソールのスクロール速度を測定するためタイマーを設定しています。
C++ 版とは異なり、実際の描画処理は LayerManager
の CoTask
で行われるので、時間測定処理も当該 CoTask
に追加しています。
また、どの CoTask
からの描画依頼かを区別するため、 CoTask
間でやりとりするメッセージに描画時間測定対象か否かを意味するフラグを追加しています。
ShadowBuffer
と FrameBuffer
の実装共通化
先の節で ShawdorBuffer
は FrameBuffer
は別の構造体として実装していました。
両者共画面の描画を行うという点は共通で、描画対象が Vec<u8>
か FrameBuffer
かが違うだけです。
実装共通化のため Vec<u8>
と FrameBuffer
を抽象化する Buffer
トレイトを設け、 Buffer
トレイトを実装した型に対して描画する BufferDrawer
という構造体を導入しました。
コンソールの性能改善 (day09e)
コンソールのスクロール時、コンソールの描画範囲全体に対して文字の再描画を行っていました。
文字の描画のためには各文字の字形に応じてドット単位で描画する必要があり、非常に時間がかかります。
これを、描画範囲全体を上方向に移動させることで単純な memcpy
で済むようにし処理を高速化しました。
まとめ
重ね合わせ処理を実装し、マウスカーソルを動かしても背景が消えることがなくなりました。 また、重ね合わせ処理の性能改善により、マウスカーソルの描画自体も高速になった気がします。
また、 C++ 実装もなかなかに Rust らしい実装へと変更できているのではないでしょうか。 この調子で次章も進んでいきたいです。