組み込みの埋まってるとこ

システム寄りの組み込みCプログラミングBLOG

SDL2でCPU描画した画像を高速に表示する

ちょっとした実験用にCPUで描画した結果を素早くウインドウで表示できる環境があると便利です。 そこでSDL2を使い、フレームバッファに見立てた配列を画像としてウインドウに表示する簡単なプログラムを作成した例を紹介します。

SDL2でのプログラム例

まずはSDL2で配列を画像としてウインドウに表示するプログラムのメインループは次のようになります。

#include <stdint.h>
#define SDL_ASSERT_LEVEL 3
#include <SDL.h>


#define FRAME_WIDTH  320
#define FRAME_HEIGHT 240
#define FRAME_PITCH  (FRAME_WIDTH*sizeof(uint32_t))
#define WINDOW_WIDTH  (FRAME_WIDTH*2)
#define WINDOW_HEIGHT (FRAME_HEIGHT*2)


uint8_t frameBuf[FRAME_PITCH*FRAME_HEIGHT];

void Draw(void *pixels, int width, int height, int pitch);

int main(int argc, char *argv[])
{
    int err;

    err = SDL_Init(SDL_INIT_EVERYTHING);
    SDL_assert_release(err == 0);

    SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);

    /* Window and Renderer */
    SDL_Window *win;
    SDL_Renderer *rend;
    err = SDL_CreateWindowAndRenderer(
        WINDOW_WIDTH, WINDOW_HEIGHT, 0,
        &win, &rend);
    SDL_assert_release(err == 0);

    SDL_RenderSetLogicalSize(rend, FRAME_WIDTH, FRAME_HEIGHT);
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");//"nearest");

    /* Texture */
    SDL_Texture *tex = SDL_CreateTexture(rend, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, FRAME_WIDTH, FRAME_HEIGHT);
    SDL_assert_release(tex != NULL);

    /* Main loop */
    while (1) {
        SDL_Event event;
        SDL_PollEvent(&event);
        if(event.type == SDL_QUIT) {
            break;
        }

        Draw(frameBuf, FRAME_WIDTH, FRAME_HEIGHT, FRAME_PITCH);
        SDL_UpdateTexture(tex, NULL, frameBuf, FRAME_PITCH);

        SDL_SetRenderTarget(rend, NULL);
        SDL_RenderCopy(rend, tex, NULL, NULL);
        SDL_RenderPresent(rend);
    }

    /* Finalize */
    SDL_DestroyTexture(tex);
    SDL_DestroyRenderer(rend);
    SDL_DestroyWindow(win);
    SDL_Quit();

    return 0;
}

SDL2でのアクセラレーション

SDLでは画像を表示する方法としてハードウェアで描画するRendererとCPUで描画するSurfaceがあります。 今回は実験用にCPUで細かく描画することを目標としているためSurfaceを使用してもよいのですが、 CPUでメインメモリに描画したデータを表示するだけの目的でRendererを使用したとしても 高速に表示できる等それなりのメリットを得ることができるためRendererを使用しています。

ウインドウとRendererの生成

ここは特に難しいところは無いと思いますが、 表示のとき拡大して表示するためにSDL_RenderSetLogicalSizeを使って Rendererが表示する実際のデータのサイズをウインドウの表示サイズの半分であることを設定しています。 このときどのように拡大または縮小するかのフィルタリングの設定をしているのが SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest")です。 フィルタリングには最近傍のnearesetの他に、線形フィルタのlinearと異方性フィルタのbestがありますが、 linearとbestはRendererが対応していなければnearestになります。

表示の際のフィルタリングの設定はTextureを生成する前に行わないと効果がありませんのでこのタイミングで設定しています。

 SDL_Window *win;
    SDL_Renderer *rend;
    err = SDL_CreateWindowAndRenderer(
        WINDOW_WIDTH, WINDOW_HEIGHT, 0,
        &win, &rend);
    SDL_assert_release(err == 0);

    SDL_RenderSetLogicalSize(rend, FRAME_WIDTH, FRAME_HEIGHT);
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "nearest");

テクスチャのカラーフォーマットとアクセスパターン

メインメモリに描画したデータは直接Rendererで表示することができないためいったんTextureに変換してから表示します。 このときカラーフォーマットは変換されないため、 メインメモリに描画するときのカラーフォーマットとTextureのカラーフォーマットは同じにしておく必要があります。

次のプログラムはカラーフォーマットをRGBA8888としたときのテクスチャの生成とCPU描画するとき画素にRGBの各色成分を描き込む例です。

 SDL_Texture *tex = SDL_CreateTexture(rend, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STREAMING, FRAME_WIDTH, FRAME_HEIGHT);

    Uint32 pixel = R<<24 | G<<16 | B<<8 | A<<0; /* RGBA8888での画素の生成例 */

Texutureの生成にはカラーフォーマットの他にもう一つアクセスパターンという重要なパラメータがあります。 SDL_CreateTextureの3番目の引数でそのアクセスパターンを指定することができます。 今回生成するTextureはCPUで描画したデータにより更新されることがあるためSDL_TEXTURE_ACCESS_STREAMINGを指定しています。

SDL_TEXTUREACCESS_TARGETを指定するとRendererからしか更新できなくなり、悪いことにSDLだけではRendererを使って画素単位でテクスチャを書き換える方法が無いようなので今回の目的では使用することができません。 もう一つSDL_TEXTUREACCESS_STATICというパターンがありますが、これはファイルからロードしたTextureのように、Rendererに渡した後で書き換えが起こらないときに指定します。

CPUでの描画とTextureの更新

CPUによりメインメモリ上の配列を更新し、それをTextureに反映するプログラムは次のようになります。

    ...
    Draw(frameBuf, FRAME_WIDTH, FRAME_HEIGHT, FRAME_PITCH);
    SDL_UpdateTexture(tex, NULL, frameBuf, FRAME_PITCH);
    ...

void Draw(void *pixels, int width, int height, int pitch)
{
    for (int iy = 0; iy < height; iy++) {
        for (int ix = 0; ix < width; ix++) {
            uint32_t *p = (uint32_t *)((uintptr_t)pixels + iy*pitch + ix*sizeof(*p));

            uint8_t r,g,b;
            r = 0x00;
            g = 0x00;
            b = 0xff * iy/height; /* 縦グラデーション */
                
            *p++ = r<<24 | g<<16 | b<<8 | 0xff<<0;
        }
    }
}

CPUでの描画はメインメモリ上の配列に対するものであるため、更新後にその内容をSDL_UpdateTextureでTextureに反映しています。

自作のDraw関数はCPUで描画する関数としては一般的な構成になっていると思いますが、どのようなカラーフォーマットにでも応用できるように画素の型は関数の引数としてはvoid*とし、画素を記憶している配列の横幅はピッチとして論理的な画像の横幅とは別のパラメータにしています。

Textureのウインドウへの表示

ここまでの手順でCPU描画したデータをTextureに変換するところまでが完了しました。次はいよいよよウインドウへの表示です。

その表示部分のプログラムは次のようになっています。

    SDL_SetRenderTarget(rend, NULL);
    SDL_RenderCopy(rend, tex, NULL, NULL);
    SDL_RenderPresent(rend);

SDL_SetRenderTargetの2番目の引数をNULLにすると、Rendererの描画対象がTextureではなくウインドウになります。 描画対象をウインドウにした後SDL_RenderClearでウインドウを規定色でクリアしておくこともありますが、 今回は続くSDL_RenderCopyでウインドウの全領域を書き換えているため省略しています。 最後にSDL_RenderPresentするとついに画面にCPU描画した結果が表示されます。

まとめ

CPUで描画した配列をウインドウに表示できるライブラリは他にもいろいろとありますが、 今回はC言語でプログラムできることを最重要条件として、 オープンソースで使用方法も難しくなく、将来的にマウスやキーボードによる入力や音声の再生も扱うことが可能であるSDL2を選択しました。

目的のためにはSurfaceを使用するだけでも十分ですが、Rendererを使用するとより便利であるためハードウェア的な事情で必要となるTextureの説明も交えてそのプログラム例を紹介しました。