🖌️

Rustでフレームバッファ上にレイヤ-を実装する【MikanOS/Day09】

2023/05/05に公開


カーソルを移動しても背景をつぶさないように!

前回

前回はマウスの割り込みハンドラを実装しました。

https://zenn.dev/elm/articles/435173ba345f5d

初めに

RustでMikanOSに挑戦しています。

今回はDay09の範囲を実装しました。
https://github.com/elm-register/mikanos-rs

Day08(メモリ管理)については、実装自体はしましたが納得がいっていない点と、理解が足りてない点があるため、記事にするのは保留にしています。

ちなみに、MikanOSとは"ゼロからの OS 自作入門"という書籍上で実装するOSの名前です。
http://zero.osdev.jp/

既存のフレームバッファの実装ではカーソルを動かしたときに背景を上書きしてしまっていたため、Day09でその問題を解決させました。

文字だけだとイメージしにくいと思いますので、図を用意しました。

prev

この問題に対応するために、背景やカーソルの部分を別レイヤーとして分けることが今回の主な実装内容になります。

layer

詳細は本を参考にしてください。
ここでは本家とは実装や考え方が異なる部分を説明していきます。

本家と異なる箇所について

レイヤーの抽象化

本家ではレイヤー上にウィンドウが存在し、ウィンドウが保持するピクセルライターを使ってユーザーが好きなように描画をしていくような実装になっています。
(実際にはシャドウバッファに書き込まれますが、それについては後述)

original

クラス図にすると次のようになります。

original

私の実装ではレイヤ上にWindowが存在しておらず、レイヤ自体を抽象化させています。

impl

各レイヤはレイヤの変形操作を提供するトレイトと、描画内容をバックバッファに反映するためのトレイトを実装しています。
そしてLayers(レイヤーを管理する構造体)にはLayerという列挙型にラップされて登録されます。

クラス図にすると次のようになります。

layer

レイヤーという名前ではありますが、基本的に画面上のグラフィックは全てコンポーネントだと考えるような設計にしています。
この方法の利点として、次の章(Day10)で本格的なWindowを作成できるようにするのですが、実装が(恐らく)容易になります。

具体的には次のようなウィンドウを作成予定になります。

window

ウィンドウ自体を複数の子レイヤーから構成される1つのレイヤーと考えることによって、
実装に必要となる個所はWindowLayerと、PlainLayerだけになります(たぶん)。

window_layer

レイヤのアップデート方法

これは本家でも同じですが、レイヤ内の描画時に直接フレームバッファに書き込まれるわけではなく、フレームバッファと同じ形式のバッファ(シャドウバッファ)を用意し、一旦そのバッファに対して書きこまれます。
実際の描画内容を反映する際に各レイヤーが持つシャドウバッファをバックバッファにコピーし、全ての内容が反映されたらバックバッファのデータをフレームバッファにコピーします。

レイヤへの描画

layer_shadow_buffer
レイヤ -> シャドーバッファ

フレームバッファへの描画

shadow_buffer_process
シャドウバッファ -> バックバッファ -> フレームバッファ

本家のほうでは、ピクセルライタで指定のレイヤに描画してから、ユーザーが明示的にDrawというメソッド(図の①に該当します)を呼び出してフレームバッファを更新します。
MikanOS-rsではLayersから指定のレイヤの可変参照を引数にそのレイヤを更新するクロージャーを引数にした、Updateメソッドを使用することで自動的にレイヤー内の内容がフレームバッファに更新されるようにしています。
...文字にするとややこしくなってしまったため、かなり簡略化していますがコードを示します。


pub struct Layers{
    
}


impl Layers{
    pub fn update_layer(
        &mut self,
        key: &str,
        update: impl FnOnce(&mut Layer) 
    ) -> KernelResult{
        // キーに対応したレイヤーの可変参照を取得
        let layer: &mut Layer = self.layer_mut(key)?;
        
        let prev_transform: Transform2D = layer.transform_ref().clone();
        
        // ここでユーザーにレイヤーを更新を依頼します。
        update(layer);
        
        if !updatable(layer){
            // フレームバッファ外に移動したときなどはアップデートできないため、
            // 直前の状態に戻します。
            layer.feed_transform(prev_transform);
            return Ok(());
        }
        // 直前のレイヤーの領域を更新
        self.update_back_buffer(&prev_transform)?;
        // 現在のレイヤーの領域の更新
        self.update_back_buffer(&layer.rect())?;
        self.draw(key, &prev_transform);
        Ok(())
    }
    
    
    
    fn draw(
        &mut self, 
        key: &str,
        prev_transform: &Transform2D
    ) -> KernelResult{
        // 直前の領域と現在の領域を合成して更新するべき領域を取得
        let union = prev_transform.union(&self.layer_ref(key).rec());
        update_frame_buffer(&union, &mut self.back_buffer)
    }
    
}

領域の合成に関しては下の図を参照してください。
ちなみに、わざわざ矩形にしているのは1行ごとにデータをコピーするためです。

union

最後に

次章はウィンドウを使えるようにします。
また、このプロジェクトとは別にRustのライブラリも作ろうとしているので、そちらも出来上がり次第記事にしようと思います。

Discussion