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

ウィンドウの表示
実際にウィンドウを表示してみます。

ただし、タイトルバーやウィンドウ枠が付いていないので、ユーザー側でウィンドウを閉じる方法がないため、アニメーションでアルファ値を徐々に上げた後、自動的に終了させています。

なお、ソースコードが長くなるので、今までにやった内容は 05sub.h, 05sub.c にまとめました。
05sub.c も一緒にコンパイルしてください。

> 05sub.h
> 05sub.c

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

<05_window.c>
#include <stdio.h>
#include "05sub.h"

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

wayland *g_wl;

typedef struct
{
    struct wl_surface *surface;
    imagebuf *img;
    int count,
        loop;
}callback_data;

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

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


/* イメージ描画 */

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)
        {
            pd[0] = pd[1] = 0;
            pd[2] = val++;
            pd[3] = alpha;
        }
    }
}

//----------- wl_shell_surface

static void _shell_surface_ping(
    void *data,struct wl_shell_surface *shell_surface,uint32_t serial)
{
    wl_shell_surface_pong(shell_surface, serial);
    printf("ping %u\n", serial);
}

static void _shell_surface_configure(
    void *data,struct wl_shell_surface *shell_surface,
    uint32_t edges,int32_t width,int32_t height)
{
    //サイズ変更時
}

static void _shell_surface_popup_done(void *data,struct wl_shell_surface *shell_surface)
{
    //ポップアップ終了時
}

static const struct wl_shell_surface_listener g_shell_surface_listener = {
    _shell_surface_ping,
    _shell_surface_configure,
    _shell_surface_popup_done
};

//----------- wl_buffer

static void _buffer_release(void *data,struct wl_buffer *buffer)
{
    //サーバーが buffer へのアクセスを終えた時
}

static const struct wl_buffer_listener g_buffer_listener = {
    _buffer_release
};

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

static const struct wl_callback_listener g_callback_listener = {
    _callback_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++;

    if(p->count == 256)
        p->loop = 0;
    else
    {
        //更新

        _draw_image(p->img, p->count);

        wl_surface_attach(p->surface, p->img->buffer, 0, 0);
        wl_surface_damage(p->surface, 0, 0, p->img->width, p->img->height);

    /*
        //不透明範囲設定
        struct wl_region *region;
        region = wl_compositor_create_region(g_wl->compositor);
        wl_region_add(region, 0, 0, p->img->width, p->img->height);
        wl_surface_set_opaque_region(p->surface, region);
        wl_region_destroy(region);
    */

        //新しいコールバック

        callback = wl_surface_frame(p->surface);
        wl_callback_add_listener(callback, &g_callback_listener, data);

        //適用

        wl_surface_commit(p->surface);
    }
}

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

int main(void)
{
    struct wl_surface *surface;
    struct wl_shell_surface *shell_surface;
    struct wl_callback *callback;
    imagebuf *img;
    callback_data dat;

    g_wl = wayland_init();

    //surface 作成

    surface = wl_compositor_create_surface(g_wl->compositor);
    shell_surface = wl_shell_get_shell_surface(g_wl->shell, surface);

    wl_shell_surface_set_toplevel(shell_surface);

    wl_shell_surface_add_listener(shell_surface,
        &g_shell_surface_listener, NULL);

    //イメージ作成

    img = imagebuf_create(g_wl, 256, 256, 1);

    wl_buffer_add_listener(img->buffer, &g_buffer_listener, NULL);

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

    dat.surface = surface;
    dat.img = img;
    dat.count = 0;
    dat.loop = 1;

    callback = wl_surface_frame(surface);

    wl_callback_add_listener(callback, &g_callback_listener, &dat);

    //最初の画面

    _draw_image(img, 0);

    wl_surface_attach(surface, img->buffer, 0, 0);
    wl_surface_commit(surface);

    //

    while(wl_display_dispatch(g_wl->display) != -1 && dat.loop);

    //

    wl_shell_surface_destroy(shell_surface);
    wl_surface_destroy(surface);

    imagebuf_destroy(img);

    wayland_finish(g_wl);

    return 0;
}
解説
wl_compositor
wl_compositor は、画面の合成を行うオブジェクトです。

ウィンドウを表示するには必須となるので、wl_registry のハンドラ内でバインドして取得してください。
2017/09 時点での最新バージョンは 4 です。

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

wl_compositor を使うと、
wl_compositor_create_surface() で、画面管理用の wl_surface
wl_compositor_create_region() で、範囲指定用の wl_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_surface
wl_surface だけでは、ウィンドウとしての操作はできません。
ウィンドウとして表示したり操作できるようにするには、wl_shell_surface が必要です。

ただし、Wayland では、タイトルバーやウィンドウ枠などのウィンドウ装飾は、サーバー側で作成してくれません。
X11 の場合は、ウィンドウマネージャがウィンドウ枠などを作って処理を行ってくれていましたが、
Wayland では、クライアントが自分でウィンドウ枠などを作って、処理をしなければなりません。

wl_shell_surface の作成
wl_shell_get_shell_surface() を使うと、既存の wl_surface から wl_shell_surface を作成できます。
これにより、wl_surface にシェルサーフェスとしての役割が与えられるので、取得した wl_shell_surface を使うことで、ウィンドウとしての操作をすることができるようになります。

struct wl_shell_surface *wl_shell_get_shell_surface(
    struct wl_shell *wl_shell, struct wl_surface *surface);

ウィンドウとして表示
wl_shell_surface_set_toplevel() を使うと、サーフェスを通常のトップレベルウィンドウとして表示できます。
他にも、フルスクリーンやポップアップウィンドウとして表示することも出来ます。

void wl_shell_surface_set_toplevel(struct wl_shell_surface *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() を実行して、サーバーに応答します。

これが行われないと、プログラムがフリーズしたと見なされて、"応答がありません" などのダイアログが出ることになります。
マウスカーソルがウィンドウ内に入ったり、ボタンが押されたりすると、呼ばれるようです。
configureウィンドウサイズが変更される時に呼ばれます。

wl_shell_surface_resize() を実行した時、ウィンドウのリサイズ動作が行われますが、そこでサイズが変更される時に呼ばれます。
クライアントは、ここでイメージバッファを再作成して、ウィンドウサイズを変更する必要があります。
popup_doneポップアップとして表示した時に、ユーザーが他のウィンドウなどをクリックした時など、ポップアップを終了するタイミングで呼ばれます。
ウィンドウ内イメージを適用
ここまでで、ウィンドウとして表示することは出来ますが、中身のイメージはまだ空の状態です。
wl_buffer を作成して、イメージバッファを用意できたら、マッピングした共有メモリバッファ内に直接アクセスして、描画していきます。

イメージフォーマット
フォーマットは、XRGB8888 または ARGB8888 を使います。
いずれも 1px = 4byte で、リトルエンディアンで並んでいます。

ARGB を使うと、アルファ値を使ってウィンドウを半透明にすることができます。
XRGB は、X 部分にデータとして 1byte が存在しますが、値は使われることがなく、完全不透明となります。

リトルエンディアンなので、バッファ内を 1byte 単位で見ると、「B-G-R-X/A 順」、
バッファの先頭から 「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 のイメージや、その他の状態などのデータは、ダブルバッファリングされています。

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

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

より正確には、wl_buffer の release イベントが呼ばれると、
commit 後にコンポーザーがそのバッファのアクセスを終えたということなので、その後は 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() 関数でイベントハンドラを設定すると、次の描画を行うタイミングで、設定したハンドラ関数が呼ばれます。

指定 ms 後に実行させるなどの、タイマーとしての役割は持っていません。
あくまで、次に描画を行うべきタイミングの時に来るため、描画できない状況では、コールバックが通知されません。
うちの環境では、大体 16〜17 ms くらいの間隔で送信されました。

クライアントは、設定したハンドラ関数内で、描画更新処理を行います。

ポイント
  • この要求は、上記の要求後に wl_surface_commit() を実行しないと、有効になりません。
  • ハンドラ関数が呼ばれるのは、一回の設定につき一回だけです。
    続けて通知をさせたい場合は、ハンドラ関数内で再度コールバックを設定する必要があります。
  • ハンドラ関数内で、wl_callback_destroy() を使って、wl_callback を破棄する必要があります。
  • 作成した wl_callback は、関数が呼ばれた後にそのポインタを使ってはならない。
  • callback_data は、wl_surface の場合は、ミリ秒単位の、現在の時間です (何を基準とするかは定義されていない)
  • サーフェスが他のウィンドウによって完全に隠れている場合など、描画しても画面上に変化がない場合、サーバーはコールバックを通知しません。

注意点
GNOME では、呼ばれた関数内で毎回描画更新処理を行わないと、次のコールバック関数が実行されません。
コールバックのイベント自体は生成されていますが、マウスカーソルを動かすなど、何か動作が行われないと、コールバックが実際に実行されないようです。
(何も変化がない状態だと、アニメーションは止まったままで、マウスカーソルを動かすと、その間だけ動き出す)

weston では、画面更新せずにコールバックだけ設定し直しても、問題なく動作します。
GNOME の場合は、そういう仕様なのかどうかはわかりませんが、アニメーションとして使う場合、描画更新せずに空白の時間を作ることはできなさそうです。
完全不透明の範囲の設定
コールバックでの画面更新部分で、コメント化しているコードがありますが、そのコードのコメントを解除して実行すると、ウィンドウは半透明ではなくなり、完全に不透明となります。

struct wl_region *region;

region = wl_compositor_create_region(g_wl->compositor);
wl_region_add(region, 0, 0, p->img->width, p->img->height);
wl_surface_set_opaque_region(p->surface, region);
wl_region_destroy(region);

...

wl_surface_commit(p->surface);

wl_surface_set_opaque_region() は、サーフェスの完全不透明な範囲を設定します。
wl_region は、矩形範囲を保持するオブジェクトです。
範囲を設定した後は、wl_region はすぐに削除して構いません。

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

この関数は、サーフェスの一部が半透明/透明のピクセルを含まない場合に、描画を最適化する目的で使います。

半透明な部分が含まれる場合は、その後ろにある画面のイメージが必要になるため、背景を再描画し、その上にサーフェスの内容を合成する必要がありますが、サーフェスが完全不透明の場合は、後ろの画面が完全に隠れるため、背景を描画する必要がなくなります。
そのため、背景の描画処理を省いて、高速化することができます。

使わなくても普通に動作はしますが、少しでも処理を軽くしたいなら、実行しておくべきです。