GOPフレームバッファへのピクセル単位描画API整備

フレームバッファへのテキスト描画とpanic表示の刷新の記事では、GOPフレームバッファ上に固定サイズのグリフを並べるテキストコンソールを実装しました。今回はこれとは別に、ピクセル単位で任意の位置に描画できるAPIを整備します。文字の格子に縛られない座標系で図形を描けるようにすることが目的です。

ピクセル・矩形・線の描画API

新設したkernel/graphics.zigは、フレームバッファ上の任意のピクセルを直接操作するための3つの基本操作を提供します。

// kernel/graphics.zig
pub fn setPixel(x: usize, y: usize, color: u32) void {
    if (x >= fb_width or y >= fb_height) return;
    pixels[y * stride + x] = color;
}

pub fn fillRect(x: usize, y: usize, w: usize, h: usize, color: u32) void {
    const x_end = @min(x + w, fb_width);
    const y_end = @min(y + h, fb_height);
    if (x >= x_end or y >= y_end) return;
    var py: usize = y;
    while (py < y_end) : (py += 1) {
        var px: usize = x;
        while (px < x_end) : (px += 1) pixels[py * stride + px] = color;
    }
}

どちらの関数も、指定された座標がフレームバッファの範囲外であれば描画をスキップします。fillRectは矩形の右端・下端が画面外にはみ出す場合、実際に描画する範囲を画面内に収まるよう@minでクランプしてから塗りつぶします。

直線の描画には、Bresenhamのアルゴリズムを使用します。浮動小数点演算を使わず、整数の加減算だけで「次にどのピクセルを塗るか」を決定できる古典的な手法です。

// kernel/graphics.zig
pub fn drawLine(x0: i64, y0: i64, x1: i64, y1: i64, color: u32) void {
    var x = x0;
    var y = y0;
    const dx: i64 = @intCast(@abs(x1 - x0));
    const dy: i64 = -@as(i64, @intCast(@abs(y1 - y0)));
    const sx: i64 = if (x0 < x1) 1 else -1;
    const sy: i64 = if (y0 < y1) 1 else -1;
    var err = dx + dy;

    while (true) {
        if (x >= 0 and y >= 0) setPixel(@intCast(x), @intCast(y), color);
        if (x == x1 and y == y1) break;
        const e2 = 2 * err;
        if (e2 >= dy) {
            err += dy;
            x += sx;
        }
        if (e2 <= dx) {
            err += dx;
            y += sy;
        }
    }
}

始点・終点を符号付き整数で受け取ることで、右下がり・左上がりなど8方向すべての傾きに同じロジックで対応しています。dxは水平方向の距離、dyは符号を反転させた垂直方向の距離で、この2つの誤差項errを毎ステップ更新しながら、水平方向・垂直方向のどちらに進むか(あるいは両方に進むか)を判定します。

色の扱いはkernel/console.zigと同じ考え方を踏襲しています。GOPのRGB/BGRという2種類のピクセルフォーマットの違いを吸収するため、Color構造体(RGB各8bit)を実際のフレームバッファフォーマットに応じたu32へ変換するpackColorを用意し、呼び出し側はsetPixel/fillRect/drawLineに対して常にパック済みのu32を渡します。

テキストコンソールとは独立したレイヤーとして

kernel/console.zigはすでにフレームバッファへ直接グリフを描画するコードを持っていますが、今回はこれを新しい描画API経由に置き換える形にはしていません。コンソール・シェル・panic表示という、現時点で実際に動いている描画経路に手を入れるリスクに対して、得られる利益(重複コードの削減)が見合わないと判断したためです。kernel/graphics.zigはピクセル座標系で完結する独立したモジュールとして追加し、テキストコンソールの文字グリッドとは別の描画対象として共存させています。

そのため、この2つのレイヤーは互いの描画内容を認識しません。テキストコンソールがスクロールしたり新しい行を書き込んだりする際、その領域にピクセルAPIで描いた内容があれば単純に上書きされます。ウィンドウや描画レイヤーの管理は行っておらず、ROADMAP上でも任意項目として位置づけているため、現時点では意図的にこの制約を残しています。

動作確認用のシェルコマンド

新しい描画APIが実際に機能していることを確認できるよう、簡易TUIシェルの記事で実装したkernel/shell.ziggfxtestコマンドを追加しました。

// kernel/shell.zig
fn cmdGfxtest() void {
    const w = graphics.width();
    const x0: usize = if (w > 170) w - 170 else 0;
    const y0: usize = 8;

    graphics.fillRect(x0, y0, 120, 60, graphics.packColor(graphics.blue));
    graphics.drawLine(@intCast(x0), @intCast(y0), @intCast(x0 + 119), @intCast(y0 + 59), graphics.packColor(graphics.yellow));
    graphics.drawLine(@intCast(x0 + 119), @intCast(y0), @intCast(x0), @intCast(y0 + 59), graphics.packColor(graphics.yellow));

    var i: usize = 0;
    while (i < 10) : (i += 1) {
        graphics.setPixel(x0 + 130 + i, y0 + 10, graphics.packColor(graphics.green));
    }

    console.print("gfxtest: drew a filled rect with a yellow X and a row of green pixels near the top-right corner\n", .{});
}

画面右上に青い塗りつぶし矩形を描き、その対角線を黄色の線2本で描いて矩形の内部にXを作り、さらに矩形の右側に緑のピクセルを10個横に並べます。描画位置を画面左上のテキスト出力から離れた右上に固定しているのは、シェルのプロンプトやコマンド出力とすぐに重ならないようにするためです。

スコープ外にしたもの

ROADMAP上ではいくつかの発展要素を任意項目として挙げていましたが、今回はいずれも見送りました。

  • フォントレンダリングの高度化(可変幅フォント、アンチエイリアス等):Phase 10で導入した8x8ビットマップフォントで現状は十分と判断し、変更していません
  • 簡易ウィンドウ/描画レイヤーの検討:前述の通り、テキストコンソールとピクセル描画APIを独立したレイヤーとして共存させるに留め、両者を統合管理する仕組みは実装していません
  • マウス入力対応Phase 11のUSB HID対応はBoot Protocolキーボードのみを対象にしており、Hub越しの接続やマウスのレポート形式には対応していないため、今回は着手していません

QEMUでの動作確認

これまでのPhaseと同様、QEMUのqemu-xhciデバイスとusb-kbdデバイスを組み合わせた環境でQEMU+OVMF上の先行動作確認を行いました。

gfxtestを入力・実行し、画面右上に青い矩形と黄色の対角線、その右側に緑のピクセル列が描かれることを確認しました。矩形・線・個別ピクセルのいずれも、意図した位置・色で描画されています。続けてhelpecho <文字列>meminfo、およびhelppと入力してからBackspaceでhelpに修正する行編集についても再確認し、描画APIの追加によるコンソール・シェル側の回帰がないことを確認しました。