📺

Rustで自作OS上に簡易的なウィンドウを描画する【MikanOS/day10/day11】

2023/06/06に公開

timer
自作OS上にウィンドウを描画

前回

https://zenn.dev/elm/articles/ee7b365ad87545

初めに

現在RustでMikanOSに挑戦しています。
今回はこのOS上にそれっぽいウィンドウを描画し、その上にカウントを表示出来るようにしました。

https://github.com/elm-register/mikanos-rs

ウィンドウの実装

ウィンドウの実装方法自体に前回から大きな変更はありませんが、ツールバーの部分もレイヤーにしています。
下の図のWindowレイヤをルートに各子レイヤーの親子関係を示しています。
WindowとToolbarは複数の子を保持できるようになっているため、この複数の子を取り扱うための共通処理をMultipleLayerとして切り出しました。

window

// multiple_layer.rs
pub struct MultipleLayer {
    layers: Vec<Layer>,

    transform: Transform2D,
}
// layer.rs
#[derive(Delegate)]
#[to(Transformable2D, LayerUpdatable)]
pub enum Layer {
    Cursor(CursorLayer),
    Console(ConsoleLayer),
    Shape(ShapeLayer),
    Window(WindowLayer),
    CloseButton(CloseButtonLayer),
    Multiple(MultipleLayer), // Layerにエントリーさせておきます
    Count(CountLayer),
}

このレイヤーもほかのレイヤーと同様に移動ができます。
移動した場合は子のレイヤも移動させます。

impl Transformable2D for MultipleLayer {
    //---前略---
    
    fn move_to_relative(&mut self, pos: Vector2D<isize>) -> Result<(), TryFromIntError> {
        if self
            .transform
            .move_to_relative(pos)
            .is_ok(){
                for layer in self.layers.iter_mut() {
                    layer.move_to_relative(pos)?;
                }
            }
    
            Ok(())
    }
}

ツールバーはMultipleLayerを使って次のように生成できます。

fn toolbar_layer(config: FrameBufferConfig, transform: &Transform2D) -> Layer {
    let toolbar_transform = Transform2D::new(
        Vector2D::new(3, 3),
        Size::new(transform.size().width() - 6, 24),
    );
    let mut layer = MultipleLayer::new(toolbar_transform);

    layer.new_layer(toolbar_background_layer(config, layer.rect().size()));
    layer.new_layer(toolbar_title_layer(config));
    layer.new_layer(toolbar_close_button(config, layer.transform_ref()));

    layer.into_enum()
}


fn toolbar_background_layer(config: FrameBufferConfig, toolbar_size: Size) -> Layer {
    ShapeLayer::new(
        ShapeDrawer::new(
            config,
            ShapeColors::new(PixelColor::new(0x00, 0x00, 0x84), None),
        ),
        Transform2D::new(Vector2D::zeros(), toolbar_size),
    )
        .into_enum()
}

fn toolbar_title_layer(config: FrameBufferConfig) -> Layer {
    let mut text = ConsoleLayer::new(config, Vector2D::new(24, 4), Size::new(12, 1), ConsoleColors::default().change_background(PixelColor::new(0x00, 0x00, 0x84)));

    text.update_string("Hello Window")
        .unwrap();

    text.into_enum()
}


fn toolbar_close_button(config: FrameBufferConfig, transform: &Transform2D) -> Layer {
    CloseButtonLayer::new(
        config,
        Vector2D::new(
            transform.size().width() - CLOSE_BUTTON_WIDTH - 5,
            (transform.size().height() - CLOSE_BUTTON_HEIGHT) / 2,
        ),
    )
        .into_enum()
}

ウィンドウレイヤも同様にMultipleLayerを使って定義できます。
ちなみにここで使っている"Delegate"はトレイトの実装をこのメンバに自動で委譲させるための自作マクロです。
"auto-delegate"というライブラリ名で公開されているため、使っていただけたら全力で喜びます。(Issue等もお待ちしております。)

https://crates.io/crates/auto-delegate

#[derive(Delegate)]
pub struct WindowLayer {
    #[to(Transformable2D, LayerUpdatable)]
    multiple_layer: MultipleLayer,
}

impl WindowLayer {
    pub fn new(config: FrameBufferConfig, transform: Transform2D) -> Self {
        let mut multiple_layer = MultipleLayer::new(transform.clone());

        multiple_layer.new_layer(shadow_layer(config, &transform));
        multiple_layer.new_layer(window_background_layer(config, &transform));
        multiple_layer.new_layer(toolbar_layer(config, &transform));
        multiple_layer.new_layer(count_layer(config, &transform).unwrap());

        Self { multiple_layer }
    }


    pub fn write_count(&mut self, count: usize) {
        self.multiple_layer
            .layers_mut()
            .get_mut(3)
            .unwrap()
            .require_count()
            .unwrap()
            .write_count(count);
    }


    pub const fn into_enum(self) -> Layer {
        Layer::Window(self)
    }
}

手動で委譲する場合は以下のようになります。

auto-delegateを使わずに委譲を書く場合のコード
impl Transformable2D for WindowLayer {
    fn move_to(&mut self, pos: Vector2D<usize>) {
       self.multiple_layer.move_to(pos)
    }

    fn resize(&mut self, size: Size) {
        self.multiple_layer.resize(size)
    }

    fn rect(&self) -> Rectangle<usize> {
        self.multiple_layer.rect()
    }

    fn pos(&self) -> Vector2D<usize> {
        self.multiple_layer.pos()
    }

    fn transform_ref(&self) -> &Transform2D {
        self.multiple_layer.transform_ref()
    }
}

impl LayerUpdatable for WindowLayer {
    fn update_back_buffer(&mut self, back_buff: &mut ShadowFrameBuffer, draw_area: &Rectangle<usize>) -> KernelResult {
        self.multiple_layer.update_back_buffer(back_buff, draw_area)
    }
}

テキスト描画のバグ修正

以下は前回のものなりますが、マウスカーソルを動かすとあらぬ個所にテキストが描画されてしまっています。
mouse_bug

原因

原因はテキストレイヤの描画領域の計算が上手くいっていないことでした。

レイヤの更新から描画までの反映は次のような手順になっています。

  1. レイヤの更新処理を行う(移動やテキスト書き込みなど)
  2. 各レイヤごとに、移動前と移動後の更新対象レイヤの領域に重なっている領域を取得し、その領域分だけバックバッファを更新
  3. バックバッファをフレームバッファに反映(これにより実際に描画が更新されます。)

2について、例えばマウスカーソルが図のように移動するとします。

update

この時最下層のレイヤから順に描画領域の取得と、その領域分だけバックバッファを更新させます。
上の図の場合、カーソルの下にテキストレイヤがあるため、テキストレイヤとカーソルの重なっている領域分(黄色の箇所)だけ更新します。
text_layer

テキストレイヤの場合テキストをバッファとして管理しているため、更に重なっている領域分だけこのバッファを切り抜く必要があります。

text_layer

最終的に以下のコードで上手く動かせるようになりました。

impl LayerUpdatable for ConsoleLayer {
    fn update_back_buffer(
        &mut self,
        back_buff: &mut ShadowFrameBuffer,
        draw_area: &Rectangle<usize>,
    ) -> KernelResult {
        let relative = draw_area.safe_sub_pos(&self.transform.pos());
        for (y, line) in self
            .console_frame
            .frame_buff_lines()
            .into_iter()
            .flatten()
            .enumerate()
            .skip_while(|(y, _)| *y < relative.origin().y())
        {
            if relative.size().height() <= y {
                return Ok(());
            }

            let x = relative.origin().x();

            if line.len() <= x {
                continue;
            }

            let pos = self.pos() + Vector2D::new(x, y);

            let origin = calc_pixel_pos(&self.config, pos.x(), pos.y())?;
            let len = min(line.len() - x * 4, draw_area.size().width() * 4);

            let end = origin + len;

            back_buff.raw_mut()[origin..end].copy_from_slice(&line[x * 4..(x * 4 + len)]);
        }


        Ok(())
    }
}

Apicタイマ割り込みの実装

割り込みハンドラの用意

まずタイマの割り込みハンドラを定義します。

pub extern "x86-interrupt" fn interrupt_timer_handler(_stack_frame: InterruptStackFrame) {
    INTERRUPT_QUEUE
        .lock()
        .borrow_mut()
        .enqueue(InterruptMessage::ApicTimer);

    LocalApicRegisters::default()
        .end_of_interrupt()
        .notify();
}

次にこのハンドラをIDTに登録します。
OSで予約されている割り込みベクタ以外ならどこでも大丈夫そうです。本家と同じく0x41にしています。

 IDT[InterruptVector::ApicTimer].set_handler(interrupt_timer_handler, type_attribute)?;

Apicタイマの設定

次にタイマの設定をします。

Apicタイマを使うには0xFEE000320番地に存在するAPIC_LVT_TMRを使います。

lvt

InitialCountに初期値を設定すると、CurrentCountにその値が設定され、一定間隔でゼロに向かって下がっていきます。
CurrentCountがゼロに到達すると、InterruptIDに書き込んでいる割り込みベクタに対応するハンドラが呼び出されます。
Timer ModeにPeriodicを指定している場合は再度CurrentCountに初期値が設定されタイマ処理を繰り返します。

この個所のコードは以下になります。

// lvt_timer.rs
#[volatile_bit_field(addr_ty = LvtTimerAddr)]
pub struct LvtTimer {
    interrupt_id_num: InterruptIdNumber,
    mask: Mask,
    timer_mode: TimerModeField,
}
// apic.rs
pub struct LocalApicRegisters {
    local_apic_id: LocalApicId,
    end_of_interrupt: EndOfInterrupt,
    lvt_timer: LvtTimer,
    initial_count: InitialCount,
    current_count: CurrentCount,
    divide_config: DivideConfig,
}
//  local_apic_timer.rs

#[derive(Default)]
pub struct LocalApicTimer {
    local_apic_registers: LocalApicRegisters,
}


impl LocalApicTimer {
    pub fn new() -> Self {
        let local_apic_registers = LocalApicRegisters::default();

        Self {
            local_apic_registers,
        }
    }
}


impl ApicTimer for LocalApicTimer {
    fn start(&mut self, divide: LocalApicTimerDivide) {
        self.local_apic_registers
            .divide_config()
            .update_divide(divide);


        let lvt_timer = self
            .local_apic_registers
            .lvt_timer();

        lvt_timer
            .interrupt_id_num()
            .write_volatile(InterruptVector::ApicTimer as u32)
            .unwrap();

        lvt_timer
            .mask()
            .write_volatile(0)
            .unwrap();

        lvt_timer
            .timer_mode()
            .update_timer_mode(Periodic);

        self.local_apic_registers
            .initial_count()
            .write_volatile(u32::MAX / 5)
            .unwrap();
    }


    fn elapsed(&self) -> u32 {
        let current = self
            .local_apic_registers
            .current_count()
            .read_volatile();

        u32::MAX / 5 - current
    }


    fn stop(&mut self) {
        self.local_apic_registers
            .initial_count()
            .write_volatile(0)
            .unwrap();
    }
}

次回

次回はキーボード入力を実装します。

Discussion