「ゼロからのOS自作入門」を Rust でやる (第9章)

改造量の多い章だと時間がかかってしまいますが、ゆるゆると続けております。

第9章

マウスカーソルを動かすと背景やコンソールの文字が消えてしまう問題を解決するために、重ね合わせ処理 (レイヤー) を導入する章です。

重ね合わせの実装 (day09a)

以下を実装しました。

  • マウスカーソル、デスクトップ背景など個別の描画要素を意味する Window 構造体と、 Window への描画機能を有する WindowDrawer 構造体の追加
  • 重ね合わせの個々の階層を意味する Layer 構造体
  • Layer の重ね合わせ順序や Frame Buffer への描画を制御する LayerManger 構造体
  • Console 構造体の動作変更 (LayerManager の初期化までは直接フレームバッファに描画、初期化後は Window に描画しフレームバッファへの描画は LayerManager が行う)

github.com

実施していることは 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 を改造しても良かったのですが、パニックハンドラーや例外ハンドラーから複雑な処理を行うと正しく動作しない可能性があったため、シンプルな別構造体として追加しました。

github.com

EmergencyConsole ではフレームバッファを利用して画面描画するのですが、ハンドラー呼出し時の状況によってはフレームバッファのロックが取られており、普通にロックをとるとデッドロックしてしまう可能性があります。 これに対処するため、 EmergencyConsole からロック取得する場合は、既存のロックを強制的にアンロックした上でロックを取得するようにしています。

タイマーの実装 (day09b)

性能測定のために Local APIC タイマーを使えるようにする節です。

github.com

実装については特にコメントはないです。

シャドウバッファの追加 (day09c)

従来処理ではフレームバッファへの描画時に毎回 Color 構造体からフレームバッファの byte 配列フォーマットへの pixel 毎に変換していました。画面サイズが大きいと、この処理の負荷は大きくなります。

これを改善するため、フレームバッファと同じフォーマットでデータを保持するシャドウバッファを各 Window に持たせ、 フレームバッファへの描画時はこのシャドウバッファの内容をコピー (memcpy) するような方式へと変更しました。 これにより Color 構造体から byte 配列フォーマットへの変換が描画の度に毎回行われることがなくなり、性能が改善されます。

github.com

C++ 版とは異なり、 ShadowBuffer という専用構造体を用意しています (この後のコミットで変更されますが)。

コンソールのスクロール速度を測定する (day09d)

コンソールのスクロール速度を測定するためタイマーを設定しています。

github.com

C++ 版とは異なり、実際の描画処理は LayerManager の CoTask で行われるので、時間測定処理も当該 CoTask に追加しています。 また、どの CoTask からの描画依頼かを区別するため、 CoTask 間でやりとりするメッセージに描画時間測定対象か否かを意味するフラグを追加しています。

ShadowBuffer と FrameBuffer の実装共通化

先の節で ShawdorBuffer は FrameBuffer は別の構造体として実装していました。 両者共画面の描画を行うという点は共通で、描画対象が Vec<u8> か FrameBuffer かが違うだけです。 実装共通化のため Vec<u8> と FrameBuffer を抽象化する Buffer トレイトを設け、 Buffer トレイトを実装した型に対して描画する BufferDrawer という構造体を導入しました。

github.com

コンソールの性能改善 (day09e)

コンソールのスクロール時、コンソールの描画範囲全体に対して文字の再描画を行っていました。 文字の描画のためには各文字の字形に応じてドット単位で描画する必要があり、非常に時間がかかります。 これを、描画範囲全体を上方向に移動させることで単純な memcpy で済むようにし処理を高速化しました。

github.com

まとめ

重ね合わせ処理を実装し、マウスカーソルを動かしても背景が消えることがなくなりました。 また、重ね合わせ処理の性能改善により、マウスカーソルの描画自体も高速になった気がします。

また、 C++ 実装もなかなかに Rust らしい実装へと変更できているのではないでしょうか。 この調子で次章も進んでいきたいです。