Wayland - ウィンドウの表示 (wl_shell_surface)

ウィンドウの表示
ここでは、wl_shell_surface を使って、実際にウィンドウを表示してみます。

ただし、Wayland のウィンドウは基本的に、タイトルバーやウィンドウ枠が付属しないため、現段階ではユーザー側でウィンドウを閉じる方法がないので、アニメーションでウィンドウ画像のアルファ値を徐々に上げていき、完全に不透明になったら、自動的に終了させています。
プログラム
※ここからは、Wayland クライアントの処理を client.c に、
共有メモリイメージの処理を imagebuf.c にまとめているので、そちらも併せてコンパイルしてください。

$ cc -o test 05_window.c client.c imagebuf.c -lwayland-client -lrt

05_window.c
/******************************
 * ウィンドウの表示
 ******************************/

#include <stdio.h>
#include <wayland-client.h>

#include "client.h"
#include "imagebuf.h"


//---------------

typedef struct
{
    Client *client;
    Window *win;
    int count;
}callback_data;

//---------------


/* イメージ描画
 *
 * 黒→赤のグラデーション */

static void draw_image(ImageBuf *p,int alpha)
{
    uint8_t *pd = (uint8_t *)p->data,val;
    int ix,iy;

    for(iy = p->height; iy > 0; iy--)
    {
        for(ix = p->width, val = 0; ix > 0; ix--, pd += 4)
        {
            //B-G-R-A (Little Endian)
        
            pd[0] = pd[1] = 0;
            pd[2] = val++;
            pd[3] = alpha;
        }
    }
}


//--------------------------
// wl_callback
//--------------------------


static void _callback_done(void *data,struct wl_callback *callback,uint32_t time_ms);

static const struct wl_callback_listener g_callback_listener = {
    _callback_done
};

/* done イベント */

void _callback_done(void *data,struct wl_callback *callback,uint32_t time_ms)
{
    callback_data *p = (callback_data *)data;

    wl_callback_destroy(callback);

    p->count++;

    printf("count: %d\r", p->count);
    fflush(stdout);

    if(p->count == 256)
        //イベントループを終了させる
        p->client->finish_loop = 1;
    else
    {
        //---- 更新

        draw_image(p->win->img, p->count);

        //新しいコールバック

        callback = wl_surface_frame(p->win->surface);

        wl_callback_add_listener(callback, &g_callback_listener, data);

        //適用

        Window_update(p->win);

        /* Window_update() の代わりに以下を使うと、
         * 画像のアルファ値が適用されない (常に不透明) */

        //Window_updateOpaque(p->win);
    }
}


//---------------


int main(void)
{
    Client *client;
    Window *window;
    struct wl_callback *callback;
    callback_data dat;

    //初期化

    client = Client_new(0);

    Client_init(client);

    //ウィンドウ作成
    /* wl_surface
     * wl_shell_surface */

    window = Window_create(client, 256, 256, NULL);

    //アニメ用コールバック

    dat.client = client;
    dat.win = window;
    dat.count = 0;

    callback = wl_surface_frame(window->surface);

    wl_callback_add_listener(callback, &g_callback_listener, &dat);

    //最初の画面を描画&更新

    draw_image(window->img, 0);

    Window_update(window);

    //イベントループ

    Client_loop_simple(client);

    //

    Window_destroy(window);

    Client_destroy(client);

    return 0;
}
wl_compositor
ウィンドウをデスクトップ画面に表示するために、wl_compositor が必要なので、バインドします。
wl_compositor は、画面の合成を行います。

if(strcmp(name, "wl_compositor") == 0)
    p->compositor = wl_registry_bind(reg, id, &wl_compositor_interface, 1);

wl_compositor の子オブジェクトとして、wl_surfacewl_region があり、バインドすると、これらを作成することができます。

ver 2 で wl_surface_set_buffer_transform()
ver 3 で wl_surface_set_buffer_scale()
ver 4 で wl_surface_damage_buffer() が追加されています。

今回は ver 1 の機能しか使わないので、バージョンは 1 にしています。
wl_surface
ウィンドウを表示するためには、まず最初に wl_surface が必要です。

これは、画面に表示する内容を管理するためのオブジェクトです。
主にウィンドウの表示用に使いますが、マウスカーソル画像などでも使います。

一つのウィンドウごとに、一つ作成してください。

struct wl_surface *wl_compositor_create_surface(
  struct wl_compositor *wl_compositor);
wl_shell
ウィンドウの操作を行うために、wl_shell が必要なので、バインドします。

wl_shell の子オブジェクトとして wl_shell_surface があり、これを作成することができます。
(wl_shell_surface を作成する機能しかない)。

各ウィンドウごとに wl_shell_surface を作成します。
wl_shell_surface の作成
struct wl_shell_surface *wl_shell_get_shell_surface(
    struct wl_shell *wl_shell, struct wl_surface *surface);

既存の wl_surface を元に、wl_shell_surface を作成します。
よって、先に wl_surface を作成しておく必要があります。

wl_shell_surface の作成と同時に、wl_surface には「シェルサーフェス」としての役割が与えられ、ウィンドウ表示用のイメージとして扱われることになります。
ウィンドウ装飾について
Wayland における基本的なウィンドウは、タイトルバーやウィンドウ枠などの装飾を含みません。

よって、クライアント側が自分でそれらのイメージを描画し、ボタンや枠などを操作した時の処理などを実装する必要があります。
トップレベルウィンドウとして表示
void wl_shell_surface_set_toplevel(struct wl_shell_surface *wl_shell_surface);

サーフェスを、通常のトップレベルウィンドウとして表示させます。

他にも、フルスクリーンや、ポップアップウィンドウとして表示させることもできます。
wl_shell_surface イベント
wl_shell_surface が作成できたら、イベントハンドラを設定します。

ウィンドウに対して起こるイベントを処理できます。

int wl_shell_surface_add_listener(
  struct wl_shell_surface *wl_shell_surface,
  const struct wl_shell_surface_listener *listener, void *data);

--------------

struct wl_shell_surface_listener {

void (*ping)(void *data,struct wl_shell_surface *wl_shell_surface,uint32_t serial);

void (*configure)(void *data,struct wl_shell_surface *wl_shell_surface,
    uint32_t edges,int32_t width,int32_t height);

void (*popup_done)(void *data,struct wl_shell_surface *wl_shell_surface);
};

ping
クライアントが応答可能な状態かどうかを判断するために、サーバーから不定期に呼ばれます。

これに返答がない場合、プログラムがフリーズしたと見なし、「応答がありません」などのダイアログが出て、強制終了させられます。

正常に動作していることを示すために、ハンドラ内で wl_shell_surface_pong() を実行して、サーバーに応答する必要があります。

static void _shell_surface_ping(
    void *data,struct wl_shell_surface *shell_surface,uint32_t serial)
{
    wl_shell_surface_pong(shell_surface, serial);
}

configure
ウィンドウサイズが変更される時に呼ばれます。

wl_shell_surface_resize() を実行すると、ユーザーによるウィンドウのリサイズ動作が行われますが、そこでサイズが変更される時に呼ばれます。

クライアントは、ここでウィンドウ用のイメージバッファを再作成して、ウィンドウサイズを変更する必要があります。

popup_done
ポップアップウィンドウとして表示した時に、ユーザーが他のウィンドウをクリックした時など、ポップアップを終了するタイミングで呼ばれます。
ウィンドウの中身
ここまでの時点で、ウィンドウとして表示をすることは出来ますが、中身のイメージはまだ空の状態です。

wl_buffer を作成して、ウィンドウイメージ用のバッファを用意できたら、マッピングした共有メモリバッファ内に直接アクセスして、中身を描画していきます。
イメージフォーマット
フォーマットは、XRGB8888 または ARGB8888 を使います。
いずれも 1 px = 4 byte で、リトルエンディアンで R・G・B・A(X) が並んでいます。

ARGB を使うと、アルファ値を使ってウィンドウを半透明にすることができます。

XRGB では、X の値は使われることなく、完全不透明 (A = 255) となります。

リトルエンディアンなので、1 byte 単位で見ると「B - G - R - A(X)」の順で、バッファの先頭から「X は右方向、Y は下方向」となります。
wl_surface でイメージを適用させる
イメージが書き込めたら、wl_surface に対して、そのイメージをウィンドウの内容として適用するように要求します。

# 指定イメージをセット

void wl_surface_attach(struct wl_surface *wl_surface,
  struct wl_buffer *buffer, int32_t x, int32_t y);

# 指定範囲を更新させる

void wl_surface_damage(struct wl_surface *wl_surface,
  int32_t x, int32_t y, int32_t width, int32_t height);

# すべての変更を実際に適用させる

void wl_surface_commit(struct wl_surface *wl_surface);

wl_surface_attach() で、指定した wl_buffer のイメージを、ウィンドウの内容としてセットします。

ただし、セットした瞬間に、ウィンドウの内容がすぐに更新されるわけではありません。
これらの変更を実際に適用させるには、最後に wl_surface_commit() を実行する必要があります。

保留中のデータについて
サーフェスには、現在画面に表示されているサーフェスの状態とは別に、適用保留中のデータが存在します。

wl_surface_attach() を含む、wl_surface の内容変更の関数は、その保留中のデータを変更するだけで、画面上の内容とは別の所にセットされます。

wl_surface_commit() を実行すると、保留中のすべてのデータが、実際の画面上に適用されて、内容が更新されます。

ダブルバッファリング
wl_surface のイメージや、その他の状態などのデータは、「ダブルバッファリング」されています。

attach でセットされた wl_buffer は、直接ウィンドウのイメージとして使われているわけではありません。
サーフェス内部に現在の状態を保持するデータがあって、commit 時には、そこにデータをコピーする形となります。

そのため、commit 後は、attach でセットした wl_buffer のデータはサーフェスからは使われなくなるので、その後は wl_buffer を削除したり、イメージの内容を変更したりしても問題ありません。

より正確には、wl_buffer の release イベントが呼ばれた時点で、コンポーザーがそのバッファに対するアクセスを終えたということなので、その後は wl_buffer を自由に使えます。

※ commit 直後の段階では、まだコンポーザーはバッファにアクセスしていません。

wl_surface_damage()
wl_surface_damage() は、サーフェスの指定範囲の内容を更新させます。

最初のウィンドウ表示時は全体が更新されるため、この関数は使わなくても問題ありませんが、それ以降は、更新させる範囲を毎回指定する必要があります。
frame コールバック
wl_surface_frame() を使うと、再描画を行うべきタイミングで、設定したコールバック関数を呼ぶことができます。
アニメーションさせたい時や、画面の再描画のタイミングを得たい時に使います。

# アニメーション用の wl_callback を作成

struct wl_callback *wl_surface_frame(struct wl_surface *wl_surface);

# wl_callback のイベントハンドラを設定

int wl_callback_add_listener(struct wl_callback *wl_callback,
    const struct wl_callback_listener *listener, void *data);

struct wl_callback_listener
{
    void (*done)(void *data, struct wl_callback *wl_callback, uint32_t callback_data);
};

wl_surface_frame() で作成した wl_callback に対して、wl_callback_add_listener() 関数でイベントハンドラを設定すると、次の描画を行うタイミングで、設定したハンドラ関数が呼ばれます。

なお、「指定時間後に実行させる」などの、タイマーとしての役割は持っていません。

あくまで、次に描画を行うべきタイミングの時に来るため、ウィンドウの描画ができない状況では、コールバックが通知されません。
うちの環境では、大体 16〜17 ms くらいの間隔で送信されました。

ポイント
  • 上記の関数実行後に wl_surface_commit() を実行しないと、有効になりません。
  • done イベントが呼ばれるのは、一回の関数実行につき一回だけです。
    通知を継続させたい場合は、done イベント内で再度同じことを実行する必要があります。
  • done イベント内では、wl_callback_destroy() を使って、wl_callback を破棄する必要があります。
  • callback_data は、wl_surface の場合は、ミリ秒単位の、現在の時間です。
    (どの時間を基準とするかは定義されていないので、前回の値との差を経過時間として使ってください)
  • サーフェスが他のウィンドウによって完全に隠れている場合など、描画しても画面上に変化がないような状態では、サーバーはコールバックを通知しません。
完全不透明の範囲の設定
wl_surface_set_opaque_region() で、wl_region によって範囲をセットすると、その範囲内は完全不透明として扱われ、イメージ内のアルファ値を使った合成が行われなくなります。

これにより、サーバーによるウィンドウの合成処理を簡略化することが出来ます。

struct wl_region *region;

region = wl_compositor_create_region(compositor);
wl_region_add(region, 0, 0, width, height);
wl_surface_set_opaque_region(surface, region);
wl_region_destroy(region);

wl_surface_commit(surface);

wl_region は、矩形範囲を保持するオブジェクトです。
wl_surface_set_opaque_region() で範囲を設定した後は、すぐに削除して構いません。

完全不透明として指定された範囲は、イメージ内でアルファ値が 255 以外に設定されていても、常に A = 255 として合成されます。

ウィンドウイメージに半透明な部分が含まれる場合、そのウィンドウの後ろにある画面にイメージを合成する必要があるため、先に背景を再描画し、その上にイメージをアルファ合成する必要があります。

しかし、ウィンドウのイメージが完全不透明 (A = 255) の場合は、後ろの画面が完全にそのウィンドウで隠れるため、背景を描画する必要がなくなります。

そのため、完全不透明の範囲を指定することにより、その範囲の背景描画とアルファ合成の処理を省いて、高速化することができます。

使わなくても普通に動作はしますが、少しでも処理を軽くしたい場合は、使ったほうが良いでしょう。