借り初めのひみつきち

仮ブログです。

今週の MYOS 🧚‍♀️

MYOS でも簡単なゲームを動かせそうになってきたので準備をします。

多くのゲームは画面内をキャラクターが動き回りますが、キャラクターを描画する時は背景との重ね合わせ処理が必要になりますし、キャラクターが移動するときはそれに加えて元いた場所の背景を復元しないと画面がぐちゃぐちゃになります。それらを自力で管理するのは結構大変です。

レトロゲームフレームワーク 🧚‍♀️

古き良きレトロゲーム機は CPU もメモリも貧弱でしたが、スプライト 🧚‍♀️ という機能を持っていました。 これは背景とは別に小さい画像オブジェクトを重ねることができる機能で、先述のめんどくさい重ね合わせ処理をハードウェアで処理してくれます。 この機能のため、レトロゲーム機は貧弱なCPU能力と少ないメモリでもキャラクターが画面中を動き回るゲームを実現することができました。

というわけで、 MYOS にも似たような機能を実装します。

f:id:neriring16:20210821155043p:plain

ベンチマーク用のサンプルアプリを起動するとウィンドウの中をボールが動き回ります。

ちなみに現在のゲームAPIハローワールドがこんな感じのコードになります。

#![no_main]
#![no_std]

use megoslib::game::v1::prelude::*;

#[no_mangle]
fn _start() {
    let presenter = GameWindow::new("hello", Size::new(128, 64));
    presenter
        .screen()
        .draw_string(Point::new(0, 3), 0, b"Hello, world!");
    loop {
        presenter.sync();
    }
}

f:id:neriring16:20210818025101p:plain

これに少し加えて以下のようにすると、文字がスクロールします。簡単ですね。 *1

#![no_main]
#![no_std]

use megoslib::game::v1::prelude::*;

#[no_mangle]
fn _start() {
    let presenter = GameWindow::new("hello", Size::new(128, 64));
    presenter
        .screen()
        .draw_string(Point::new(0, 3), b"Hello, world!");
    loop {
        presenter.sync();

        presenter.screen().control_mut().scroll_x -= 1;
        presenter.set_needs_display();
    }
}

ゲーム API を有効化すると、 wasm のアプリと OS の間で複数回のデータやり取りで描画が遅くなるのを防ぐため、ゲーム画面描画専用の固定メモリマップが割り当てられます。

まだ仕様は完全に確定していませんが、よくあるレトロゲーム機の画面仕様と似たような仕様となっており、解像度や色数に同じような制限があるものの、プログラムサイズ数KB〜数10KBの規模でレトロゲームが簡単に実装できることを目標にしています。 将来はもう少し制限を緩くした version 2 以降も実装できたらいいなという願いを込めて、現在実装しているゲーム API には v1 というプレフィックスをつけています。

ゲームウィンドウ高速化

ゲーム API を実装する過程で気付きがありました。

以前検証した単純な計算ループのベンチでは qemu より実機の方が1桁くらい速かったですが、スプライトのデモに FPS 表示を付けてみると、なんと qemu より実機の方が遅いです。 そして、ウィンドウを画面端に移動して描画部分を極限まで小さくすると FPS 表示が跳ね上がることを確認できます。

スプライトはウィンドウの状態に関係なく内部バッファに対して描画されますが、内部バッファからウィンドウへの描画は実際の画面の表示領域に依存します。 画面端にウィンドウを表示すると、クリッピングによってウィンドウの描画範囲が狭くなり、描画するビットマップの面積も小さくなって転送量が減ります。 画面端にウィンドウを移動してFPSが跳ね上がるということは、スプライト自体の描画よりもウィンドウの描画に時間がかかっていることになります。

MYOS もその先祖の MOE も、半透明のウィンドウどうしの重ね合わせ描画をサポートするために、すべてのウィンドウ描画はいったんバックバッファで合成した後で実際の画面に転送します。 MOE の時代のウィンドウは透過ウィンドウと不透明ウィンドウの2種類のウィンドウが存在し、不透明ウィンドウは高速化のために重ね合わせ処理の一部を省略して描画していました。 MYOS では基本的に全てのウィンドウに影がつくため、影の合成のために不透明ウィンドウという概念が消滅しました。*2 つまり、全てのウィンドウはその内容が不透明であったとしても毎回後ろのウィンドウを描画した後アルファブレンド合成していたのです。

それは遅くなるわけです。

ウィンドウの形が角丸が基本になって矩形ではなくなってしまったり影を描画しなくてはいけない関係上、今後も完全な不透明ウィンドウは MYOS には基本的に存在しないでしょう。 一方、ウィンドウの一部分を再描画する場合、内容が不透明であれば自分より背面のウィンドウは描画しなくて良いはずです。

というわけで、一部の条件を満たしたウィンドウの部分再描画でアルファブレンドしないようにしたところ、 qemuFPS が倍以上に跳ね上がりました。 今まで半分以上ウィンドウ再描画の時間だったということになります。

一方、実機の速度向上は限定的でした。

実機でもウィンドウを端に寄せて描画範囲を狭くすると FPS が跳ね上がるので描画が遅いことがわかりますが、メインメモリ内で完了するアルファブレンド処理はさほど負荷ではなく、実際に画面に描画する VRAM 転送する処理が一番遅いということなのでしょう。

エミュレーターは一般に実機より遅いと思われがちですが、 VRAM アクセスは特別で、エミュレーターの中からは遅い VRAM に直接アクセスできなかったり表示時の転送がいい感じに効率化されているので実機より速くなることも多いです。

*1:前半ではスプライトの話をしたいたのに、実はこのハローワールドはスプライト機能を一切使っていないことに気づいてしまいました。要望があればそのうちもう少し詳しいサンプルプログラムを作って解説するかもしれません。

*2:実際には一番後ろにあるデスクトップに相当するウィンドウはそれ以上後ろに何もないので不透明ウィンドウが完全に消滅したわけではないですが