2022/06/03(金) [n年前の日記]
#1 [xlib] Xlibについて勉強中その2
Xlibについて勉強中。
環境は、Ubuntu Linux 20.04 LTS。Windows10 x64 21H2 + VMware Player 上で動かしている。
環境は、Ubuntu Linux 20.04 LTS。Windows10 x64 21H2 + VMware Player 上で動かしている。
◎ ウインドウの表示と画像の描画。 :
とりあえず、ウインドウを表示して、四角、円、画像を描画するところまで動作確認してみた。
_02win.c
使用画像: _ufo2.xpm
Makefile: _Makefile
コンパイルは以下。
実行は以下。
少し解説。
xpm画像は中身がテキストファイル。C言語のソースファイル内で include して画像データのバイナリにアクセスすることができる。ちなみに、GIMP で xpm としてエクスポートすれば xpm画像を作れる。
画像を扱う時は Pixmap か XImage を使うらしいけど、今回は Pixmap を使って処理してみた。
xpm から Pixmap に変換しているのは以下。
透過画像を描画する時は、XSetClipMask()、XSetClipOrigin()、XCopyArea()、XSetClipMask() を使う。
Pixmap や GC は、プログラムを終了する前に、利用していたメモリ領域を解放しておかないといけない。XFreePixmap()、XFreeGC() を使って解放する。
_02win.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/xpm.h>
#include <math.h>
/* include ufo2_xpm */
#include "ufo2.xpm"
#define WIN_W 512
#define WIN_H 512
#define WIN_X 100
#define WIN_Y 100
#define BORDER 4
#ifndef M_PI
#define M_PI 3.14159265358979
#endif
#define deg2rad(x) (x * M_PI / 180.0)
typedef struct _Pixmaps
{
Pixmap pix;
Pixmap mask;
} Pixmaps;
static void update(Display *dpy, Drawable win, GC gc, double ang, Pixmaps pixs);
/* get a color value from a color name */
static unsigned long
get_color(Display *dpy, char *colorname)
{
Colormap cmap;
XColor near_color, true_color;
cmap = DefaultColormap(dpy, DefaultScreen(dpy));
XAllocNamedColor(dpy, cmap, colorname, &near_color, &true_color);
return near_color.pixel;
}
int main(void)
{
Display *dpy;
Window root;
Window win;
int screen;
XEvent evt;
unsigned long black, white;
GC gc;
Pixmaps pixs;
int busy_loop;
double ang;
/* open display */
dpy = XOpenDisplay(NULL);
root = DefaultRootWindow(dpy);
screen = DefaultScreen(dpy);
/* get color black and white */
white = WhitePixel(dpy, screen);
black = BlackPixel(dpy, screen);
/* create window */
win = XCreateSimpleWindow(dpy, root,
WIN_X, WIN_Y, WIN_W, WIN_H, BORDER,
black, white);
XSelectInput(dpy, win, KeyPressMask | ExposureMask);
XMapWindow(dpy, win);
/* wait display window */
do
{
XNextEvent(dpy, &evt);
} while (evt.type != Expose);
XMoveWindow(dpy, win, WIN_X, WIN_Y);
/* create graphics context */
gc = XCreateGC(dpy, win, 0, 0);
XSetGraphicsExposures(dpy, gc, False);
/* load image */
if (XpmCreatePixmapFromData(dpy, win, ufo2_xpm,
&pixs.pix, &pixs.mask, NULL))
{
fprintf(stderr, "Error not load xpm.");
return 1;
}
ang = 0;
busy_loop = 1;
while (busy_loop)
{
/* event */
while (XPending(dpy))
{
XNextEvent(dpy, &evt);
switch (evt.type)
{
/*
case Expose:
if (evt.xexpose.count == 0)
{
}
break;
*/
case KeyPress:
busy_loop = 0;
break;
default:
break;
}
}
update(dpy, win, gc, ang, pixs);
ang += 1.0;
if (ang >= 360)
ang -= 360;
XFlush(dpy);
usleep(16 * 1000); /* wait */
}
XFreePixmap(dpy, pixs.pix);
XFreePixmap(dpy, pixs.mask);
XFreeGC(dpy, gc);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return 0;
}
/* update and draw */
static void
update(Display *dpy, Drawable win, GC gc, double ang, Pixmaps pixs)
{
XWindowAttributes xgwa;
int scrw, scrh;
int r, x, y, w, h;
int r2, x2, y2, w2, h2;
/* get window attributes */
XGetWindowAttributes(dpy, win, &xgwa);
scrw = xgwa.width; /* window width */
scrh = xgwa.height; /* window height */
w = 96;
h = 96;
r = 200;
x = r * cos(deg2rad(ang)) + (scrw / 2) - (w / 2);
y = r * sin(deg2rad(ang)) + (scrh / 2) - (h / 2);
w2 = 128;
h2 = 128;
r2 = 128;
x2 = r2 * cos(deg2rad(-ang)) + (scrw / 2) - (w2 / 2);
y2 = r2 * sin(deg2rad(-ang)) + (scrh / 2) - (h2 / 2);
/* clear window */
XClearWindow(dpy, win);
/* draw shape */
XSetForeground(dpy, gc, get_color(dpy, "blue"));
XFillRectangle(dpy, win, gc, x - 8, y - 8, w + 16, h + 16);
XSetForeground(dpy, gc, get_color(dpy, "cyan"));
XFillArc(dpy, win, gc, x, y, w, h, 0, 360 * 64);
/* draw image (Pixmap) */
XSetClipMask(dpy, gc, pixs.mask); /* set clip mask */
XSetClipOrigin(dpy, gc, x2, y2); /* set clip origin */
XCopyArea(dpy, pixs.pix, win, gc, 0, 0, w2, h2, x2, y2); /* copy Pixmap */
XSetClipMask(dpy, gc, None); /* reset clip */
}
使用画像: _ufo2.xpm
Makefile: _Makefile
コンパイルは以下。
gcc 02win.c -o 02win -I /usr/include/X11 -lX11 -lXext -lm -lXpmUbuntu Linux だから上のような指定になっているけれど、別のディストリでは「-I /usr/include/X11」のあたりを変えることになるのかもしれない。巷の解説ページでは別の場所を指定している場合がほとんどなので…。
実行は以下。
./02win
少し解説。
- Xlib を使う時は、#include <X11/Xlib.h> を最初のあたりで書く。コンパイラ(リンカ)には、-lX11 -lXext を追加。
- xpm画像を扱う時は、#include <X11/xpm.h> を以下略。コンパイラ(リンカ)には、-lXpm を追加。
- sin,cos 等の数学関数を使う時は、#include <math.h> を以下略。コンパイラ(リンカ)には、-lm を追加。
xpm画像は中身がテキストファイル。C言語のソースファイル内で include して画像データのバイナリにアクセスすることができる。ちなみに、GIMP で xpm としてエクスポートすれば xpm画像を作れる。
画像を扱う時は Pixmap か XImage を使うらしいけど、今回は Pixmap を使って処理してみた。
xpm から Pixmap に変換しているのは以下。
/* load image */
if (XpmCreatePixmapFromData(dpy, win, ufo2_xpm,
&pixs.pix, &pixs.mask, NULL))
{
fprintf(stderr, "Error not load xpm.");
return 1;
}
- ufo2_xpm が、xpm のバイナリが入ってる変数名。
- pixs.pix に非透過部分が、pixs.mask に透過部分が入るように指定してる。
- XpmCreatePixmapFromData() は、処理成功で0、失敗でエラーコードが返る。ちなみにC言語は、0は偽、0以外は真になる仕様。
透過画像を描画する時は、XSetClipMask()、XSetClipOrigin()、XCopyArea()、XSetClipMask() を使う。
/* draw image (Pixmap) */
XSetClipMask(dpy, gc, pixs.mask); /* set clip mask */
XSetClipOrigin(dpy, gc, x2, y2); /* set clip origin */
XCopyArea(dpy, pixs.pix, win, gc, 0, 0, w2, h2, x2, y2); /* copy Pixmap */
XSetClipMask(dpy, gc, None); /* reset clip */
- XSetClipMask() で、透過部分が入ってる Pixmap を指定して、クリップマスクを設定。
- XSetClipOrigin() で、クリップマスクの原点を設定。
- XCopyArea() で、Pixmap を別の何か(Drawable。ウインドウかPixmap。今回はウインドウが転送先)にコピー。この時、クリップマスクが反映されて、透過ではない部分だけがコピーされる。
- 最後にクリップマスクをリセットしておかないと、次回、別の何かを描画する際に前回のクリップマスクが働いていしまうので注意。
Pixmap や GC は、プログラムを終了する前に、利用していたメモリ領域を解放しておかないといけない。XFreePixmap()、XFreeGC() を使って解放する。
◎ Pixmapを拡大縮小してみる。 :
画像の描画ができたので、画像を拡大縮小できないか試してみる。
どうしてそんなことをしたいのかというと、例えばスクリーンセーバを作る際に、ウインドウサイズ(ディスプレイサイズ)が異なると画像レイアウトが違ってきてしまうからで…。一旦小さい画面(Pixmap)に描いておいて、その小さい画面をウインドウサイズのPixmapに拡大して、それをウインドウに描画することができれば、ウインドウサイズ(ディスプレイサイズ)が違っていても、画像のレイアウトがめちゃくちゃにならずに済むのではないかと。
処理時間的に厳しいのではないかと予想しながら書き始めてみたけど、やり方次第ではどうにかなりそうな気配を感じた。以下のキャプチャ動画は、640 x 360 の Pixmap を 1440 x 900 の Pixmap に拡大してる例。上下にふわふわしてるけど、これでも毎フレーム拡大処理をしている。
_04scale.c
使用画像は以下。
_bg01.xpm
_bg02.xpm
コンパイルは以下。
実行は以下。
少し解説。
最初に処理を書いた時は、愚直に、1ドットずつ XCopyArea() を呼んでみたけれど、このやり方だと圧倒的に処理時間がかかってしまって話にならなかった。
そこでふと、xscreensaver のソース群の中で目にした処理を思い出した。一旦横方向にのみ引き延ばして、その横方向に引き延ばしたものを縦に引き延ばすというやり方。
後者は、前者と比べて圧倒的に速かった。時間を測定してみたところ、以下のような感じになった。
考えてみたら、例えば 1920x1080のサイズの場合、前者は XCopyArea() を 2073600回呼んでしまうけど、後者なら3000回呼ぶだけで済むわけで、そのあたりが効いてるのかなと。ただ、Pixmap へのアクセス回数は、横に引き延ばす段階が入ってくる分、増えてしまうはずなのだけど…。Xlib関係はCPUだけが必死に頑張って処理をしてるものと思い込んでたけど、もしかしてGPUが頑張ってる時もあるのだろうか。それとも、Pixmap間の転送は処理が速くなるように何か工夫してるのだろうか。
何にせよ、このぐらいの速さで拡大描画できるなら、一旦ファミコンレベルの解像度の画面を作っておいて、ソレをデスクトップ一杯に拡大描画しても、60FPS程度なら出せるかもしれない。大昔の2Dゲーム画面っぽいスクリーンセーバを作れそうな予感。
どうしてそんなことをしたいのかというと、例えばスクリーンセーバを作る際に、ウインドウサイズ(ディスプレイサイズ)が異なると画像レイアウトが違ってきてしまうからで…。一旦小さい画面(Pixmap)に描いておいて、その小さい画面をウインドウサイズのPixmapに拡大して、それをウインドウに描画することができれば、ウインドウサイズ(ディスプレイサイズ)が違っていても、画像のレイアウトがめちゃくちゃにならずに済むのではないかと。
処理時間的に厳しいのではないかと予想しながら書き始めてみたけど、やり方次第ではどうにかなりそうな気配を感じた。以下のキャプチャ動画は、640 x 360 の Pixmap を 1440 x 900 の Pixmap に拡大してる例。上下にふわふわしてるけど、これでも毎フレーム拡大処理をしている。
_04scale.c
/*
Scaling Pixmap.
Ryzen 5 5600x + VMware Player + Ubuntu Linux 20.04 LTS.
1920 x 1080.
slow result : 0.510753 sec
fast result : 0.000045 sec
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <X11/Xlib.h>
#include <X11/xpm.h>
#include <math.h>
#include <time.h>
/* include xpm binary */
#include "bg01.xpm"
#define XPM_W 640
#define XPM_H 360
#define XPM_NAME bg01_xpm
/*
#include "bg02.xpm"
#define XPM_W 320
#define XPM_H 180
#define XPM_NAME bg02_xpm
*/
/* kind. 0: slow, 1: fast */
#define SCALING_KIND 1
#define WIN_X 0
#define WIN_Y 64
#define BORDER 0
#ifndef M_PI
#define M_PI 3.14159265358979
#endif
#define deg2rad(x) (x * M_PI / 180.0)
/* get a color value from a color name */
static unsigned long
get_color(Display *dpy, char *colorname)
{
Colormap cmap;
XColor near_color, true_color;
cmap = DefaultColormap(dpy, DefaultScreen(dpy));
XAllocNamedColor(dpy, cmap, colorname, &near_color, &true_color);
return near_color.pixel;
}
/* scale Pixmap */
static void
scale_pixmap(Display *dpy, Drawable win,
Pixmap *src, Pixmap *dst,
int src_w, int src_h, int dst_w, int dst_h)
{
GC gc;
float xscale, yscale;
int x, y;
struct timespec start, end;
double diff;
xscale = (float)src_w / dst_w;
yscale = (float)src_h / dst_h;
gc = XCreateGC(dpy, win, 0, 0);
XSetGraphicsExposures(dpy, gc, False);
timespec_get(&start, TIME_UTC);
switch (SCALING_KIND)
{
case 0:
/* slow */
for (y = 0; y < dst_h; y++)
{
int sy = (int)(y * yscale);
for (x = 0; x < dst_w; x++)
XCopyArea(dpy, *src, *dst, gc, (int)(x * xscale), sy, 1, 1, x, y);
}
break;
case 1:
/* fast */
for (x = dst_w - 1; x >= 0; x--)
XCopyArea(dpy, *src, *dst, gc, (int)(x * xscale), 0, 1, src_h, x, 0);
for (y = dst_h - 1; y >= 0; y--)
XCopyArea(dpy, *dst, *dst, gc, 0, (int)(y * yscale), dst_w, 1, 0, y);
break;
default:
break;
}
XFreeGC(dpy, gc);
timespec_get(&end, TIME_UTC);
diff = (end.tv_sec + (double)end.tv_nsec / 1000000000) - (start.tv_sec + (double)start.tv_nsec / 1000000000);
printf("%lf sec\n", diff);
}
/* update and draw */
static void
update(Display *dpy, Drawable win, GC gc, double ang, Pixmap pix, Pixmap buf)
{
XWindowAttributes xgwa;
int r, x, y;
int buf_w, buf_h;
/* get window attributes */
XGetWindowAttributes(dpy, win, &xgwa);
buf_w = xgwa.width; /* window width */
buf_h = xgwa.height; /* window height */
scale_pixmap(dpy, win, &pix, &buf, XPM_W, XPM_H, buf_w, buf_h);
r = 64;
x = 0;
/* x = r * cos(deg2rad(ang)); */
y = r * sin(deg2rad(ang));
/* clear window */
XClearWindow(dpy, win);
/* draw image (Pixmap) */
XSetGraphicsExposures(dpy, gc, False);
XCopyArea(dpy, buf, win, gc, 0, 0, buf_w, buf_h, x, y); /* copy Pixmap */
}
int main(void)
{
Display *dpy;
Window root;
Window win;
int screen;
int display_w, display_h;
XEvent evt;
unsigned long black, white;
XWindowAttributes xgwa;
GC gc;
Pixmap pix;
Pixmap buf;
int busy_loop;
double ang;
/* open display */
dpy = XOpenDisplay(NULL);
root = DefaultRootWindow(dpy);
screen = DefaultScreen(dpy);
/* get display size */
display_w = DisplayWidth(dpy, screen);
display_h = DisplayHeight(dpy, screen);
printf("Screen: %d x %d\n", display_w, display_h);
/* get color black and white */
white = WhitePixel(dpy, screen);
black = BlackPixel(dpy, screen);
/* create window */
win = XCreateSimpleWindow(dpy, root,
WIN_X, WIN_Y, display_w, display_h, BORDER,
white, black);
XSelectInput(dpy, win, KeyPressMask | ExposureMask);
XMapWindow(dpy, win);
/* wait display window */
do
{
XNextEvent(dpy, &evt);
} while (evt.type != Expose);
XMoveWindow(dpy, win, WIN_X, WIN_Y);
/* create graphics context */
gc = XCreateGC(dpy, win, 0, 0);
XSetGraphicsExposures(dpy, gc, False);
/* load image */
if (XpmCreatePixmapFromData(dpy, win, XPM_NAME, &pix, NULL, NULL))
{
fprintf(stderr, "Error not load xpm.");
return 1;
}
XGetWindowAttributes(dpy, win, &xgwa);
buf = XCreatePixmap(dpy, win, display_w, display_h, xgwa.depth);
XSetForeground(dpy, gc, get_color(dpy, "green"));
XFillRectangle(dpy, buf, gc, 0, 0, display_w, display_h);
ang = 0;
busy_loop = 1;
while (busy_loop)
{
/* event */
while (XPending(dpy))
{
XNextEvent(dpy, &evt);
switch (evt.type)
{
/*
case Expose:
if (evt.xexpose.count == 0)
{
}
break;
*/
case KeyPress:
busy_loop = 0;
break;
default:
break;
}
}
update(dpy, win, gc, ang, pix, buf);
ang += 4.0;
if (ang >= 360)
ang -= 360;
XFlush(dpy);
usleep(16 * 1000); /* wait */
}
XFreePixmap(dpy, pix);
XFreePixmap(dpy, buf);
XFreeGC(dpy, gc);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return 0;
}
使用画像は以下。
_bg01.xpm
_bg02.xpm
コンパイルは以下。
gcc 04scale.c -o 04scale -I /usr/include/X11 -lX11 -lXext -lm -lXpm
実行は以下。
./04scale
少し解説。
最初に処理を書いた時は、愚直に、1ドットずつ XCopyArea() を呼んでみたけれど、このやり方だと圧倒的に処理時間がかかってしまって話にならなかった。
/* slow */
for (y = 0; y < dst_h; y++)
{
int sy = (int)(y * yscale);
for (x = 0; x < dst_w; x++)
XCopyArea(dpy, *src, *dst, gc, (int)(x * xscale), sy, 1, 1, x, y);
}
そこでふと、xscreensaver のソース群の中で目にした処理を思い出した。一旦横方向にのみ引き延ばして、その横方向に引き延ばしたものを縦に引き延ばすというやり方。
/* fast */
for (x = dst_w - 1; x >= 0; x--)
XCopyArea(dpy, *src, *dst, gc, (int)(x * xscale), 0, 1, src_h, x, 0);
for (y = dst_h - 1; y >= 0; y--)
XCopyArea(dpy, *dst, *dst, gc, 0, (int)(y * yscale), dst_w, 1, 0, y);
後者は、前者と比べて圧倒的に速かった。時間を測定してみたところ、以下のような感じになった。
- slow result : 0.510753 sec
- fast result : 0.000045 sec
- CPU: AMD Ryzen 5 5600x
- OS: Ubuntu Linux 20.04 LTS (Windows10 x64 21H2 + VMware Player上で動作)
- 画面解像度 : 1920 x 1080
考えてみたら、例えば 1920x1080のサイズの場合、前者は XCopyArea() を 2073600回呼んでしまうけど、後者なら3000回呼ぶだけで済むわけで、そのあたりが効いてるのかなと。ただ、Pixmap へのアクセス回数は、横に引き延ばす段階が入ってくる分、増えてしまうはずなのだけど…。Xlib関係はCPUだけが必死に頑張って処理をしてるものと思い込んでたけど、もしかしてGPUが頑張ってる時もあるのだろうか。それとも、Pixmap間の転送は処理が速くなるように何か工夫してるのだろうか。
何にせよ、このぐらいの速さで拡大描画できるなら、一旦ファミコンレベルの解像度の画面を作っておいて、ソレをデスクトップ一杯に拡大描画しても、60FPS程度なら出せるかもしれない。大昔の2Dゲーム画面っぽいスクリーンセーバを作れそうな予感。
◎ 2022/06/05追記。 :
Ubuntu Linux 20.04 LTSの場合、xpm画像を扱う時は、libxpm-dev をインストールしておかないとコンパイルできない模様。
sudo apt install libxpm-dev
[ ツッコむ ]
以上です。