SDL Source Tour Vol.3

はじめに

第2回を書き終えてすぐに書き始めたのですが、 結局公開までに2週間かかってしまいました。今回はイベントモデルのお話です。

概要

SDLでは、外部デバイスやウインドウシステム内の状態など、外部環境の変化を イベントという形でユーザに通知します。メールボックスに溜まる メールのようなもので、ユーザがそれを読み出すという処理をしなければ 溜まりつづけるわけです。そのためにSDLは内部でイベントキューというものを 持っています。メールのアナロジーで言うならメールボックスがまさにそれに あたります。

ここでは、イベントの実体はどのようなもので、それがどのように ユーザの手に渡るのか、それから、イベント生成のメカニズムについて 眺めてみたいと思います。

初期化(1)

イベントサブシステムの初期化もSDL_Init(SubSystem)によって行われますが、 VideoやAudio、Joystickのように独立したサブシステム名を割当てられているわけでは ありません。実際には、SDL_INIT_VIDEO(Videoサブシステム)を初期化することで、 同時にイベントサブシステムの初期化も行うようになっています。

イベントとして扱われるのは、前回のジョイスティックを除けば、

などがあります。最後のものはともかく、残りのものは同じOSでも 環境によって取り扱い方法が違います。Linuxでもコンソールで動いてるときと Xの上で動かしているときではこれらの取得方法は異なったものになりますね。 これらの状況が、使用するビデオドライバとつねに一意に対応しているかどうかは 分かりませんが、現在のSDLではそのような実装になっています。

そんなわけで、イベント関連の初期化のきっかけは、例外的に SDL_VideoInit内部にあります。

と、その前に、まずはそのSDL_VideoInitの呼び出し元である SDL_InitSubSystemを眺めてみます。

int SDL_InitSubSystem(Uint32 flags)
{
#ifndef DISABLE_VIDEO
	/* Initialize the video/event subsystem */
	if ( (flags & SDL_INIT_VIDEO) && !(SDL_initialized & SDL_INIT_VIDEO) ) {
		if ( SDL_VideoInit(getenv("SDL_VIDEODRIVER"),
		                   (flags&SDL_INIT_EVENTTHREAD)) < 0 ) {
			return(-1);
		}
		SDL_initialized |= SDL_INIT_VIDEO;
	}
#else
	if ( flags & SDL_INIT_VIDEO ) {
		SDL_SetError("SDL not built with video support");
		return(-1);
	}
#endif
flagsにSDL_INIT_VIDEOがセットされていて、SDL_initializedに SDL_INIT_VIDEOがセットされてない場合は、SDL_VideoInit関数を呼び出します。 初期化に成功した場合はSDL_initializedにSDL_INIT_VIDEOをセットしておきます。 これで次回にSDL_INIT_VIDEOつきでSDL_InitSubSystemを呼ばれたときは 素通りすることになります。

今回は関係ありませんが、getenvは、該当する環境変数が存在しないときは NULLを返すことを覚えておきましょう。 第二引数ではflags から SDL_INIT_EVENTTHREADにあたるビットだけを残して 他を0にしています。 これらの定数はSDL.hで定義されて います。

/* These are the flags which may be passed to SDL_Init() -- you should
   specify the subsystems which you will be using in your application.
*/
#define	SDL_INIT_TIMER		0x00000001
#define SDL_INIT_AUDIO		0x00000010
#define SDL_INIT_VIDEO		0x00000020
#define SDL_INIT_CDROM		0x00000100
#define SDL_INIT_JOYSTICK	0x00000200
#define SDL_INIT_NOPARACHUTE	0x00100000	/* Don't catch fatal signals */
#define SDL_INIT_EVENTTHREAD	0x01000000	/* Not supported on all OS's */
#define SDL_INIT_EVERYTHING	0x0000FFFF
SDL_Init(SDL_INIT_VIDEO)といった呼び出しの場合は、SDL_INIT_EVENTTHREADに あたるビットは0になっていることが分かります。

といった前提を元にSDL_VideoInitを眺めてみましょう。 ビデオ関連のコードは無視してイベント関連のコードだけを洗ってみます。 まず、117行目から 早速イベントの処理が行われています。

int SDL_VideoInit (const char *driver_name, Uint32 flags)
{
	SDL_VideoDevice *video;
	int index;
	int i;
	SDL_PixelFormat vformat;
	Uint32 video_flags;

	/* Toggle the event thread flags, based on OS requirements */
#if defined(MUST_THREAD_EVENTS)
	flags |= SDL_INIT_EVENTTHREAD;
#elif defined(CANT_THREAD_EVENTS)
	if ( (flags & SDL_INIT_EVENTTHREAD) == SDL_INIT_EVENTTHREAD ) {
		SDL_SetError("OS doesn't support threaded events");
		return(-1);
	}
#endif
となります。これらの定数MUST_THREAD_EVENTSとCANT_THREAD_EVENTSは どのように定義されているのか、探してみます。 src/events/SDL_sysevents.hに ありました。単純に ということのようです。では先を急ぎましょう(急ぐの?) SDL_VideoInit()に戻ります。 232行目:
	/* Start the event loop */
	if ( SDL_StartEventLoop(flags) < 0 ) {
		SDL_VideoQuit();
		return(-1);
	}
SDL_StartEventLoop(flags) で、ようやくsrc/events/以下に話が 進むわけです。上のフラグ調整からこの行までにflagsの値を変更する 場所が他にないこと、それから SDL_VideoQuitでイベントの後始末を行っていること、 などを確認して、src/video/からとりあえず抜けることにしましょう。

初期化(2)

さてsrc/events/に やってきました。他のサブシステムと異なり、ここではドライバごとの サブディレクトリといったものはないようです。

では早速SDL_StartEventLoopから眺めてみましょう。

/* This function (and associated calls) may be called more than once */
int SDL_StartEventLoop(Uint32 flags)
{
	int retcode;

この関数(とそれが呼び出しているもの)は複数回呼ばれることがあるよ、 とのこと。ちょっと脱線してどのような場面で呼ばれるのか 調べてみましょう。
zinnia@freesia:~/build/SDL/SDL-1.2.5/src[2]% find . -name "*.c" -exec grep -H SDL_StartEventLoop \{\} \;
./video/SDL_video.c:    if ( SDL_StartEventLoop(flags) < 0 ) {
./events/SDL_events.c:int SDL_StartEventLoop(Uint32 flags)
んー本当に呼ばれることがあるんでしょうかねえ。とりあえず保留にして 話を戻します。
	/* Clean out the event queue */
	SDL_EventThread = NULL;
	SDL_EventQ.lock = NULL;
	SDL_StopEventLoop();
初期化に先立って、イベントの状態を保存するイベントキューを クリアし、すでに動いているイベントスレッドを停止します。
	/* No filter to start with, process most event types */
	SDL_EventOK = NULL;
	memset(SDL_ProcessEvents,SDL_ENABLE,sizeof(SDL_ProcessEvents));
	SDL_eventstate = ~0;
SDL_ProcessEvents全体に、SDL_ENABLEを埋めつくした後、 SDL_eventstateの全ビットを1にしています。ここらへんの 処理の意味を考えてみましょう。 これらはソースの上の方(49行目付近で宣言されています。
/* Public data -- the event filter */
SDL_EventFilter SDL_EventOK = NULL;
Uint8 SDL_ProcessEvents[SDL_NUMEVENTS];
static Uint32 SDL_eventstate = 0;
SDL_EventOKはSDL_EventFilter型だそうです。SDL_EventFileterの定義は include/SDL_events.h:288行目に あります。
/*
  This function sets up a filter to process all events before they
  change internal state and are posted to the internal event queue.

  The filter is protypted as:
*/
typedef int (*SDL_EventFilter)(const SDL_Event *event);
const SDL_Event* を引数にとり、intを返す関数へのポインタだそうです。 この関数がイベントの中身にフィルタをかけて、イベントキューに持ってゆく 処理をするようですね。

続いてSDL_ProcessEventsですが、これは要素数SDL_NUMEVENTSのUint8の 配列だそうです。SDL_NUMEVENTSについては、SDL_EventFilterと同様に include/SDL_events.h:47行目付近に 定義されています。SDL_NOEVENT = 0 から始まるenumになっており、

       /* Events SDL_USEREVENT through SDL_MAXEVENTS-1 are for your use */
       SDL_USEREVENT = 24,
       /* This last event is only for bounding internal arrays
	  It is the number of bits in the event mask datatype -- Uint32
        */
       SDL_NUMEVENTS = 32
現在では32になっています。24〜31まではユーザーイベントとして 使えますよ、とのこと。これはドキュメントにも明記されています

そしてSDL_eventstate、これはSDL_NUMEVENTSのコメントにもあるように 各ビットがそれぞれのイベントの状態をあらわしています。 なのでSDL_NUMEVENTSビット(つまり32ビット = Uint32)の変数です。

ここでの処理をまとめます

SDL_ProcessEventの値の意味、それからSDL_eventstateの意味については 追い追い考察します。
	/* It's not save to call SDL_EventState() yet */
	SDL_eventstate &= ~(0x00000001 << SDL_SYSWMEVENT);
	SDL_ProcessEvents[SDL_SYSWMEVENT] = SDL_IGNORE;
SDL_SYSWMEVENTだけは、SDL_eventstateでは該当するビットを0に、 SDL_ProcessEventsは該当する番号をSDL_IGNOREにしています。 コメントの意味がちょっと掴みづらいですねえ...
	/* Initialize event handlers */
	retcode = 0;
	retcode += SDL_AppActiveInit();
	retcode += SDL_KeyboardInit();
	retcode += SDL_MouseInit();
	retcode += SDL_QuitInit();
	if ( retcode < 0 ) {
		/* We don't expect them to fail, but... */
		return(-1);
	}
SDL_AppActiveInit()SDL_KeyboardInit()SDL_MouseInit()SDL_QuitInit()を、 それぞれ呼び出して、1つでも失敗したらエラー、というコードのようですね。 ...とりあえずその実装については後まわしにして、先に進みます。
	/* Create the lock and event thread */
	if ( SDL_StartEventThread(flags) < 0 ) {
		SDL_StopEventLoop();
		return(-1);
	}
	return(0);
}
今までの部分で下準備ができたので、いよいよ SDL_StartEventThreadを呼び出して イベントスレッドを開始します。この関数の詳細については節を改めましょう。

以上のような流れでした。例によって、この関数に渡されてきた flagsが、SDL_StartEventThreadにそのまま渡されていることを 確認しておきます。

イベントスレッド開始

SDL_VideoInitからはるばる 渡ってきたflagsを伴って SDL_StartEventThread入口に やってきました。

static int SDL_StartEventThread(Uint32 flags)
{
	/* Reset everything to zero */
	SDL_EventThread = NULL;
	memset(&SDL_EventLock, 0, sizeof(SDL_EventLock));

SDL_EventThreadは、 ご想像の通り
static SDL_Thread *SDL_EventThread = NULL;	/* Thread handle */
SDL_Threadへのポインタとなっています。 SDL_EventLockは...
/* Private data -- event locking structure */
static struct {
	SDL_mutex *lock;
	int safe;
} SDL_EventLock;
です。ゼロクリアされています。
	/* Create the lock and set ourselves active */
#ifndef DISABLE_THREADS
	SDL_EventQ.lock = SDL_CreateMutex();
	if ( SDL_EventQ.lock == NULL ) {
#ifdef macintosh /* On MacOS 7/8, you can't multithread, so no lock needed */
		;
#else
		return(-1);
#endif
	}
#endif /* !DISABLE_THREADS */
SDL_EventQは以下のような 構造体です。
#define MAXEVENTS	128
static struct {
	SDL_mutex *lock;
	int active;
	int head;
	int tail;
	SDL_Event event[MAXEVENTS];
	int wmmsg_next;
	struct SDL_SysWMmsg wmmsg[MAXEVENTS];
} SDL_EventQ;
このMAXEVENTSはSDL_MAXEVENTS(32)とは別物ですのでご注意下さい。 どうやらキューの長さを決める定数のようです。 まずlockメンバにSDL_CreateMutex()によりmutex(MUTually EXclusive)を 作ります。マルチスレッドプログラミングに明るくない方は、 とりあえずロックのための機構だということをなんとなく覚えておいてください。

mutexの作成に失敗した場合はここで終わりです。ただしmacintoshの場合のみ 失敗しても続行するようですね。

以上はDISABLE_THREADSが定義されていないときの処理です。 DISABLE_THREADSが定義されているときはここでは何もしません

	SDL_EventQ.active = 1;
activeという変数に1が入りました。まあたぶん1でactiveで0でinactiveなんでしょう。

さてここでようやく、flagsの長い旅も終わるようです

	if ( (flags&SDL_INIT_EVENTTHREAD) == SDL_INIT_EVENTTHREAD ) {
		SDL_EventLock.lock = SDL_CreateMutex();
		if ( SDL_EventLock.lock == NULL ) {
			return(-1);
		}
		SDL_EventLock.safe = 0;

		SDL_EventThread = SDL_CreateThread(SDL_GobbleEvents, NULL);
		if ( SDL_EventThread == NULL ) {
			return(-1);
		}
	} else {
		event_thread = 0;
	}
	return(0);
}
flagsにSDL_INIT_EVENTTHREADがセットされていなかったときは、event_threadに 0を入れて終わりです。event_threadが何者なのかについては後ほど。

SDL_INIT_EVENTTHREADが渡ってきた場合は、ifの中の処理が行われます。 先程見たSDL_EventLockに対してmutexを作成し、safeを0にします(unsafeという ことでしょうか)。その後、 SDL_EventThread(SDL_Threadのポインタであることを思い出してください)に SDL_CreateThreadでthreadを作成します。スレッドプログラミングに 明るくない方は、とりあえず別のスレッドが生成されて、 第一引数に与えられた関数ポインタがそのスレッドで実行を始めるという ふうに覚えておいでください。第二引数は、スレッド間でやりとりする データがあるときに必要になってくるものですが(具体的には第一引数の関数を 呼び出すときの引数になります←ややこしい)、今回はNULLなので無視して おきましょう。つまり、ここではSDL_GobbleEventsが別スレッドで動き始める といった感じでしょうか。

初期化、スレッド生成が正しく終わると、SDL_Initの呼び出し元まで 戻り値を伝播させつつ帰ってゆきます。それとは別のスレッドで、 SDL_GobbleEventsが動き始めるわけです。

イベントスレッド本体

SDL_GobbleEventsが、 実際のイベントスレッド本体となります。gobbleとは「がつがつ食べる」 「むさぼり食う」といった意味だそうです。

ここからはメインスレッドとは別に動いていますので、処理を追うのが ちょっとややこしくなります。また、Timerサブシステムとの関連も 出てきて、ここで一度に説明するのは大変なので、概要のみ説明いたします。

static int SDL_GobbleEvents(void *unused)
{
	SDL_SetTimerThreaded(2);
	event_thread = SDL_ThreadID();
	while ( SDL_EventQ.active ) {
            処理本体
        }
	SDL_SetTimerThreaded(0);
	event_thread = 0;
	return(0);
}
ループの中、「処理本体」と書かれているところは後回しにします。 SDL_SetTimerThread()は、とりあえず忘れます(いきなり)。 event_threadは、ここで現在のthread ID(SDL_ThreadID()により取得)が 格納されますが、SDL_GobbleEvents内では実際には使われておりません。 SDL_EventQ.activeは、SDL_StartEventThreadの時点では1になっていた ことを思い出してください。これが0になるまでループは続きます。 ループ内にSDL_EventQ.activeを0にする処理は入っていません。

event_thread、SDL_EventQ.activeは、いずれも外から参照されたり、 また外からの要因で変更されたりするわけです(変更はSDL_EventQ.activeの方のみ)。 ではどのように参照されたり、変更されるのかを見てみます。 とりあえず、どちらもstaticな変数であり、またSDL_EventQの構造体の 定義もこのソース内で行われていますので、このソースの中で 該当する部分を探せば充分でしょう。 先にSDL_EventQ.activeですが、これは SDL_StopEventThreadの中に 0にする処理が入っていますので、これが呼び出されるとSDL_GobbleEventsの ループも終了するといえそうです。

event_threadの方はというと、まず SDL_EventThreadIDで 使われています。まあ当然でしょうが... 他に、SDL_Lock_EventThreadSDL_Unlock_EventThreadで使われて いるようです。 まずはSDL_Lock_EventThreadから:

void SDL_Lock_EventThread(void)
{
	if ( SDL_EventThread && (SDL_ThreadID() != event_thread) ) {
		/* Grab lock and spin until we're sure event thread stopped */
		SDL_mutexP(SDL_EventLock.lock);
		while ( ! SDL_EventLock.safe ) {
			SDL_Delay(1);
		}
	}
}
ifの条件は何を調べているのでしょうか。 前の節は、SDL_EventThreadが確かにあることを確認していますが、うしろの 節は...SDL_ThreadID()は、現在動いているスレッドのIDを返す関数ですが、 それがevent_threadと同じかどうかを調べて、違ったときは ifの中が実行されます。つまりイベントスレッドの中でSDL_Lock_EventThreadを 呼び出したときは何もしないけど、イベントスレッドの外からSDL_Lock_EventThreadを 呼び出したときは、ロックの処理を行うということになります。

ロック処理の本体ですが、まずSDL_EventLock.lock(SDL_mutex*です)の指す mutexに対してSDL_mutexPを仕掛けます。 SDL_EventLock.lockに対してSDL_mutexVを呼び出すまでの間、 他のスレッドからSDL_EventLock.lockに(SDL_mutexPで)ロックをかけようとすると、 その場で待たされることになります。もちろん、自分がロックかけようとしたときに すでにロックがかかっていた場合はそのロックが解除されるまで待たされるわけです。 この機構により、whileループに進めるスレッドは1つだけということに なります。 ループ内では、SDL_EventLock.safeが0の間、ずっと待ち続けるようです。 SDL_StartEventThreadで、SDL_EventLock.safeを0にしていたことを 思い出してください。どこかの場面でこれが0以外になると、このループを抜けて ロックが成功することになります。

ご想像の通り、SDL_Unlock_EventThreadは以下のようになります。

void SDL_Unlock_EventThread(void)
{
	if ( SDL_EventThread && (SDL_ThreadID() != event_thread) ) {
		SDL_mutexV(SDL_EventLock.lock);
	}
}
かけられたロックはここで解除されます。

ではSDL_EventLock.safeはどんなときに値が変化するのかを眺めてみましょう。 ...どうも、先程省略していた「処理本体」が該当しそうです。 中身を見てみます

	while ( SDL_EventQ.active ) {
               ビデオとキーボードについてのコード

#ifndef DISABLE_JOYSTICK
		/* Check for joystick state change */
		if ( SDL_numjoysticks && (SDL_eventstate & SDL_JOYEVENTMASK) ) {
			SDL_JoystickUpdate();
		}
#endif
「ビデオとキーボードについてのコード」についてはとりあえず忘れて ください(……) ジョイスティックのコードが出てきました。 ジョイスティックが1つ以上あって、SDL_eventstateでSDL_JOYEVENTMASKに 対応するビットが立ってたら、SDL_JoystickUpdateを呼び出します。 この1文を書くためにこの回があるわけですのでもう一度書きます。
ジョイスティックが1つ以上あって
SDL_eventstateでSDL_JOYEVENTMASKに対応するビットが立ってたら
SDL_JoystickUpdateを呼び出します
では、SDL_JOYEVENTMASKに対応するビットは普段どうなっているのでしょうか? SDL_StartEventLoopの処理を思い出していただきたいのですが、 あのときやった処理は
  SDL_ProcessEvents、それからSDL_eventstateの初期化を行います。
  SDL_SYSWMEVENTだけが他と反対の状態を持つようにします。
というものでした。SDL_eventstateには最初全てのビットが1にセットされ、 その後にSDL_SYSWMEVENTに対応するビットだけが0になったのでしたね。 つまり、デフォルトでジョイスティック関連のイベントは有効に なっています。

SDL_JOYEVENTMASKは、include/SDL_events.hで定義されている通り、

#define SDL_EVENTMASK(X)	(1<<(X))
enum {
	SDL_ACTIVEEVENTMASK	= SDL_EVENTMASK(SDL_ACTIVEEVENT),
(中略)
	SDL_JOYEVENTMASK	= SDL_EVENTMASK(SDL_JOYAXISMOTION)|
	                          SDL_EVENTMASK(SDL_JOYBALLMOTION)|
	                          SDL_EVENTMASK(SDL_JOYHATMOTION)|
	                          SDL_EVENTMASK(SDL_JOYBUTTONDOWN)|
	                          SDL_EVENTMASK(SDL_JOYBUTTONUP),
実際に発生している5つのイベントの集合です。これらに対応するビットのうち 1つでも1になっていれば、SDL_JoystickUpdateが呼び出されることになります。

「処理本体」の話を続けます:

		/* Give up the CPU for the rest of our timeslice */
		SDL_EventLock.safe = 1;
		if( SDL_timer_running ) {
			SDL_ThreadedTimerCheck();
		}
		SDL_Delay(1);
ここが、先程探しあてた「SDL_EventLock.safeが0以外になる場所」です。 その後の処理についてはコメントにある通り、待ち時間をとって 他のスレッドにチャンスを与える場所だという程度の理解でよいでしょう。
		/* Check for event locking.
		   On the P of the lock mutex, if the lock is held, this thread
		   will wait until the lock is released before continuing.  The
		   safe flag will be set, meaning that the other thread can go
		   about it's business.  The safe flag is reset before the V,
		   so as soon as the mutex is free, other threads can see that
		   it's not safe to interfere with the event thread.
		 */
		SDL_mutexP(SDL_EventLock.lock);
		SDL_EventLock.safe = 0;
		SDL_mutexV(SDL_EventLock.lock);
	}
ここでSDL_EventLock.lockによるロックが行われ、SDL_EventLock.safeが 再び0に戻されます。ここはイベントスレッド内ですから、 SDL_(Un)Lock_EventThreadは無効であることに注意してください。 SDL_EventLock.safeを1にする場所がここにしかないことを確認できれば、 SDL_EventLock.safeを1にするためにロックが必要ないことも分かります。 つまり以下のような流れになります。

まとめ

coming soon

ここまで説明してきたことは、イベントスレッドが各サブシステムに イベント処理を依頼する、というスレッド本体の処理だけでした。実際に イベントがどのように蓄積され、また取り出されてゆくのかについての 説明がすっぽりと抜け落ちています。それから、スレッドに対応していない システムでのイベントの扱いについてもクリアにはなっていません。

例えば、SDL_ENABLEやSDL_IGNOREを入れていたSDL_ProcessEvents(Uint8の配列)は いったいどうなった、とか、SDL_EventOkは何者、とか...また、微妙に 関係ないですが、なんでウインドウマネージャに関するイベントだけが 特別扱いなのかという謎も残ったままです。

せっかくジョイスティック関係の話の流れでこちらに来たわけですので、 次回は実際に各サブシステムがイベントをどのように用意するか、それから、 それらのイベントはどのようにしてユーザプログラムに渡されてゆくのか、 といったあたりを考察したいと思います。


戻る

Zinnia (zinnia@risky-safety.org)
このWebコンテンツ(ここから辿れるもの)に対する コメントのメールは許可なく公開することがあります。 (最近多い無礼なメール対策であって穏当なメールをいきなり公開したり することはありません)