SDL Source Tour Vol.4

気が触れたような勢いで自分では書いているつもりです。 今回はジョイスティックの話の続きです。同時にイベントの話の続きでもあります。

はじめに

第2回で、ジョイスティックの状態を管理する SDL_Joystick構造体が、 ユーザからは内部が見えづらいように隠蔽され、ジョイスティックの 区別を行うハンドルとして扱われていることを説明しました。

そのSDL_Joystick構造体に実際にジョイスティックの現在の状態を格納する 処理が見当たらないという理由から、第3回で イベントモデルについて調査しました。イベント管理は別スレッドで 実行され、そしてそのそしてイベントスレッドから SDL_JoystickUpdate()という関数が呼び出されていることを発見しました。

これらの理解を元に、再度ジョイスティックのコードを読み進めことにします。

SDL_JoystickUpdate

前回見てきた通り、SDL_JoystickUpdateは引数を とらず、また戻り値もない関数でした。 実際のコードは src/joystick/SDL_joystick.cにあります。

void SDL_JoystickUpdate(void)
{
	int i;

	for ( i=0; SDL_joysticks[i]; ++i ) {
		SDL_SYS_JoystickUpdate(SDL_joysticks[i]);
	}
}
各ジョイスティックごとにSDL_SYS_JoystickUpdateを呼び出しています。 これがドライバごとのUpdate用関数なのでしょう。

では早速実装を...なのですが、さてどのドライバのコードを読みましょうか... 身近なbsdを...と思ったんですがSDL-1.2.0(いつもこのドキュメントで コードをリンクしているのはSDL-1.2.0ベースのコードです)には bsdのディレクトリはまだできていません。単純なdc(Dreamcast)も同じ理由で 使えません。とここまで書いてきて第2回で bsdのコード使ってたことを思い出してしまいました... あれはSDL-1.2.5の コードです。いきなり約束やぶってしまいました。執筆当時はSDL-1.2.5のコード 使おう→やっぱりやめよう、という流れだったのですがこの部分の 確認を忘れてました。すいません

話の続きです。なるべく細かい話はしたくないので、したくてもできない src/joystick/beosを選択します。 src/joystick/beos/SDL_bejoystick.ccを 眺めます。ccってあんたC++のソースでは?と思った方は鋭いです。 私もびっくりしました(殴) でも全体がextern "C" {}で囲まれていますので 安心してもよいような気がします。

では実装を...と思ったんですが(またかい) BeOS版の SDL_SYS_JoystickInitと、 SDL_SYS_JoystickOpen (SDL_JoystickOpenから呼び出されています)の内容を軽く眺めておきましょう。 まずはSDL_SYS_JoystickInitから

int SDL_SYS_JoystickInit(void)
{
        BJoystick joystick;
        int numjoysticks;
        int i;
        int32 nports;
        char name[B_OS_NAME_LENGTH];

        /* Search for attached joysticks */
        nports = joystick.CountDevices();
        numjoysticks = 0;
        memset(SDL_joyport, 0, (sizeof SDL_joyport));
        memset(SDL_joyname, 0, (sizeof SDL_joyname));
        for ( i=0; (SDL_numjoysticks < MAX_JOYSTICKS) && (i < nports); ++i ) {
                if ( joystick.GetDeviceName(i, name) == B_OK ) {
                        if ( joystick.Open(name) != B_ERROR ) {
                                BString stick_name;
                                joystick.GetControllerName(&stick_name);
                                SDL_joyport[numjoysticks] = strdup(name);
                                SDL_joyname[numjoysticks] =
                                                   strdup(stick_name.String());
                                numjoysticks++;
                                joystick.Close();
                        }
                }
        }
        return(numjoysticks);
}
BeOSにおけるジョイスティックいじりではBJoystickなるクラスを 使うようです。CountDevices()メソッドによりポートの数を得ることができ、 その中で実際に開くことができるジョイスティックを調べ、その名前を 格納しています。

続いてSDL_SYS_JoystickOpenです。ちょっと長めなのでコードの中に コメント入れてみます。

int SDL_SYS_JoystickOpen(SDL_Joystick *joystick)
{
	BJoystick *stick;

	/* Create the joystick data structure */
*  joystick->hwdataは、ドライバ依存のデータを入れるための構造体
*  (struct joystick_hwdata)へのポインタです。第2回参照のこと。
*  これにメモリを確保します。BeOSのドライバにおけるstruct joystick_hwdataは
*  同じソースの上の方で
*
* struct joystick_hwdata {
*	BJoystick *stick;
*	uint8 *new_hats;
*	int16 *new_axes;
*};
*
*  と定義されています。

	joystick->hwdata = (struct joystick_hwdata *)
	                   malloc(sizeof(*joystick->hwdata));

	if ( joystick->hwdata == NULL ) {
		SDL_OutOfMemory();
		return(-1);
	}
	memset(joystick->hwdata, 0, sizeof(*joystick->hwdata));

*  新しいBJoystickのインスタンスを作成し、それをjoystick->hwdata に
*  入れます。
	stick = new BJoystick;
	joystick->hwdata->stick = stick;

	/* Open the requested joystick for use */
*  Openメソッドを呼ぶことで実際にジョイスティックのオープンを行います

	if ( stick->Open(SDL_joyport[joystick->index]) == B_ERROR ) {
		SDL_SetError("Unable to open joystick");
		SDL_SYS_JoystickClose(joystick);
		return(-1);
	}

	/* Set the joystick to calibrated mode */
	stick->EnableCalibration();

*  SDL_Joystickの準備をおこないます。ここに入ったnbuttons/naxes/nhatsの
*  値に従って、buttons、axes、hatsのメモリが確保されることは第2回で
*  説明した通りです。

	/* Get the number of buttons, hats, and axes on the joystick */
	joystick->nbuttons = stick->CountButtons();
	joystick->naxes = stick->CountAxes();
	joystick->nhats = stick->CountHats();

	joystick->hwdata->new_axes = (int16 *)
	                  malloc(joystick->naxes*sizeof(int16));
	joystick->hwdata->new_hats = (uint8 *)
	                  malloc(joystick->nhats*sizeof(uint8));
	if ( ! joystick->hwdata->new_hats || ! joystick->hwdata->new_axes ) {
		SDL_OutOfMemory();
		SDL_SYS_JoystickClose(joystick);
		return(-1);
	}

	/* We're done! */
	return(0);
}

というわけで、BJoystickというクラスがジョイスティックの扱いを一手に 引き受けてくれているようですので、これを念頭において、いよいよ SDL_SYS_JoystickUpdateの 内容を確認したいと思います。といってもソースの詳細を見るのが 目的ではありませんので、箇条書風に並べてみます。

イベントスレッドから呼ばれたSDL_JoystickUpdateは、ドライバ依存の SDL_SYS_JoystickUpdateを呼び出し、そのSDL_SYS_JoystickUpdateは、 ふたたびサブシステム本体側にある SDL_PrivateJoystick??? という関数を呼び出すということが分かりました。 SDL_Joystick内の状態を変化させる操作そのものはドライバには 依存しませんので、ドライバ側としては何がどう変わったのかを把握して、 サブシステム本体に通知するという流れになるようですね。

SDL_PrivateJoystick???

では実際にSDL_PrivateJoystick???を眺めてみましょう。 代表としてSDL_PrivateJoystickAxisを:


int SDL_PrivateJoystickAxis(SDL_Joystick *joystick, Uint8 axis, Sint16 value)
{
	int posted;

	/* Update internal joystick state */
	joystick->axes[axis] = value;

	/* Post the event, if desired */
	posted = 0;
#ifndef DISABLE_EVENTS
	if ( SDL_ProcessEvents[SDL_JOYAXISMOTION] == SDL_ENABLE ) {
		SDL_Event event;
		event.type = SDL_JOYAXISMOTION;
		event.jaxis.which = joystick->index;
		event.jaxis.axis = axis;
		event.jaxis.value = value;
		if ( (SDL_EventOK == NULL) || (*SDL_EventOK)(&event) ) {
			posted = 1;
			SDL_PushEvent(&event);
		}
	}
#endif /* !DISABLE_EVENTS */
	return(posted);
}
まず、SDL_Joystickの内容を変化させます。 その後、DISABLE_EVENTSが定義されておらず、かつ SDL_ProcessEvents[SDL_JOYAXISMOTION]がSDL_ENABLEである場合、 SDL_Eventに内容をセットし、 SDL_PushEventを呼び出しています。 ただし、SDL_EventOKがNULLでない場合は、先にそのSDL_EventOKを 呼び出し、0以外が帰ってきたときのみSDL_PushEventを呼び出すようです。

戻り値はイベントをプッシュしたときは1、そうでないときは0のようです。

イベント操作

長らく謎だったSDL_ProcessEvents、SDL_EventOkがここで出現しました。 軽くおさらいしておきましょう

ざっと眺めてみますと...

zinnia@freesia:~/build/SDL/SDL-1.2.5/src[3]% grep SDL_ProcessEvent **/*.[ch]
events/SDL_active.c:    if ( SDL_ProcessEvents[SDL_ACTIVEEVENT] == SDL_ENABLE ) {
events/SDL_events.c:Uint8 SDL_ProcessEvents[SDL_NUMEVENTS];
events/SDL_events.c:    memset(SDL_ProcessEvents,SDL_ENABLE,sizeof(SDL_ProcessEvents));
events/SDL_events.c:    SDL_ProcessEvents[SDL_SYSWMEVENT] = SDL_IGNORE;
events/SDL_events.c:                    if ( SDL_ProcessEvents[type] != SDL_IGNORE ) {
events/SDL_events.c:                    SDL_ProcessEvents[type] = state;
events/SDL_events.c:    current_state = SDL_ProcessEvents[type];
events/SDL_events.c:                    SDL_ProcessEvents[type] = state;
events/SDL_events.c:    if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
events/SDL_events_c.h:extern Uint8 SDL_ProcessEvents[SDL_NUMEVENTS];
events/SDL_expose.c:    if ( SDL_ProcessEvents[SDL_VIDEOEXPOSE] == SDL_ENABLE ) {
events/SDL_keyboard.c:  if ( SDL_ProcessEvents[event.type] == SDL_ENABLE ) {
events/SDL_mouse.c:     if ( SDL_ProcessEvents[SDL_MOUSEMOTION] == SDL_ENABLE ) {
events/SDL_mouse.c:     if ( SDL_ProcessEvents[event.type] == SDL_ENABLE ) {
events/SDL_quit.c:      if ( SDL_ProcessEvents[SDL_QUIT] == SDL_ENABLE ) {
events/SDL_resize.c:    if ( SDL_ProcessEvents[SDL_VIDEORESIZE] == SDL_ENABLE ) {
joystick/SDL_joystick.c:        if ( SDL_ProcessEvents[SDL_JOYAXISMOTION] == SDL_ENABLE ) {
joystick/SDL_joystick.c:        if ( SDL_ProcessEvents[SDL_JOYHATMOTION] == SDL_ENABLE ) {
joystick/SDL_joystick.c:        if ( SDL_ProcessEvents[SDL_JOYBALLMOTION] == SDL_ENABLE ) {
joystick/SDL_joystick.c:        if ( SDL_ProcessEvents[event.type] == SDL_ENABLE ) {
video/cybergfx/SDL_amigaevents.c:               if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
video/windib/SDL_dibevents.c:                   if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
video/windx5/SDL_dx5events.c:                   if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
video/x11/SDL_x11events.c:              if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
video/x11/SDL_x11events.c:              if ( SDL_ProcessEvents[SDL_SYSWMEVENT] == SDL_ENABLE ) {
各サブシステムではSDL_ProcessEventsは参照のみで、値が変更されるのは src/events/SDL_events.cのみのようですね。では実際に眺めてみましょう。 SDL_StartEventLoopは、 前回眺めましたので(そして↑でおさらいしましたので 省略)。残るのは SDL_EventStateだけのようですね。 ちょっと長いのでまたコード中にコメント書いてみます。
*  ドキュメントにある通り、SDL_EventStateは、
*  stateに指定した値によってイベントの状態を有効にしたり(SDL_ENABLE)、
*  無効にしたり(SDL_IGNORE)、現在の状態を知る(SDL_QUERY)ための関数です。
*  type については こちらを参照
Uint8 SDL_EventState (Uint8 type, int state)
{
	SDL_Event bitbucket;
	Uint8 current_state;

	/* If SDL_ALLEVENTS was specified... */
	if ( type == 0xFF ) {
		current_state = SDL_IGNORE;
*  すべてのイベントについて走査
		for ( type=0; type<SDL_NUMEVENTS; ++type ) {
			if ( SDL_ProcessEvents[type] != SDL_IGNORE ) {
*  一つでも有効なものがあればSDL_ENABLEがcurrent_stateに
				current_state = SDL_ENABLE;
			}
*  SDL_ProcessEvents[type] に引数で指定された値をセット
			SDL_ProcessEvents[type] = state;
*  SDL_eventstateの該当するビットもそれにあわせて変更
			if ( state == SDL_ENABLE ) {
				SDL_eventstate |= (0x00000001 << (type));
			} else {
				SDL_eventstate &= ~(0x00000001 << (type));
			}
		}
*  SDL_PollEventを呼び出します。溜まってるイベントを読み捨て?
		while ( SDL_PollEvent(&bitbucket) > 0 )
			;
		return(current_state);
	}

*  以下はSDL_ALLEVENTS(type == 0xff)でないときです。
*  やはり、該当するSDL_eventstateを動機させ、SDL_PollEventを呼び出しています。
	/* Just set the state for one event type */
	current_state = SDL_ProcessEvents[type];
	switch (state) {
		case SDL_IGNORE:
		case SDL_ENABLE:
			/* Set state and discard pending events */
			SDL_ProcessEvents[type] = state;
			if ( state == SDL_ENABLE ) {
				SDL_eventstate |= (0x00000001 << (type));
			} else {
				SDL_eventstate &= ~(0x00000001 << (type));
			}
			while ( SDL_PollEvent(&bitbucket) > 0 )
				;
			break;
		default:
			/* Querying state? */
			break;
	}
	return(current_state);
}
SDL_ENABLE、SDL_IGNOREいずれの場合も、変更前の状態(current_state)が 戻り値になります。SDL_EventState(SDL_ALLEVENTS, SDL_QUERY) のときの 挙動がちょっと気になりますが...

と、いうわけで、SDL_ProcessEventsと、SDL_eventstateはまったく 同じ内容を2通りの方法で表現しただけ、のように見えます。そして 外部(つまりSDLの他のサブシステムなど)ではSDL_ProcessEventsを参照し、 内部(SDL_events.c)では(staticである)SDL_eventstateを参照するようです。 ...なぜでしょうね...

一方、SDL_EventOKに値をセットすることができるのは、 SDL_SetEventFilterのみのようです。 こちらも、セット後はイベントを読み捨てる処理を行っているようです。 SDL_EventFilterについての解説は こちらを御覧下さい。

ここまでのまとめです。

イベントの実体(SDL_Event)

前節で、デバイスからイベントが投げられる様子を眺めました。 該当部分のコードはこんな感じでした:

	if ( SDL_ProcessEvents[SDL_JOYAXISMOTION] == SDL_ENABLE ) {
		SDL_Event event;
		event.type = SDL_JOYAXISMOTION;
		event.jaxis.which = joystick->index;
		event.jaxis.axis = axis;
		event.jaxis.value = value;
		if ( (SDL_EventOK == NULL) || (*SDL_EventOK)(&event) ) {
			posted = 1;
			SDL_PushEvent(&event);
		}
	}
まず、SDL_Eventをつくり、中身をセットして、そのアドレスを SDL_PushEventに渡しています。

イベント処理に親しくない方のために補足しますと、 SDL_Eventは、SDLが発生することのできるすべてのイベントに関する構造体を 1つにまとめた巨大な共用体です。 定義はinclude/SDL_events.hに あります。

typedef union {
	Uint8 type;
	SDL_ActiveEvent active;
	SDL_KeyboardEvent key;
	SDL_MouseMotionEvent motion;
	SDL_MouseButtonEvent button;
	SDL_JoyAxisEvent jaxis;
	SDL_JoyBallEvent jball;
	SDL_JoyHatEvent jhat;
	SDL_JoyButtonEvent jbutton;
	SDL_ResizeEvent resize;
	SDL_ExposeEvent expose;
	SDL_QuitEvent quit;
	SDL_UserEvent user;
	SDL_SysWMEvent syswm;
} SDL_Event;
中に入ってる構造体の定義は同じソースの上のほうにありますので 御覧下さい。ちなみに私の環境ではsizeof(SDL_Event)は20でした。

SDL_Eventは共用体ですから、それらの中身は排他的なものです。 そのため、使う側と受ける側で共用体の中のどれを使っているのかを 示す変数が必要になります。 SDL_Event内のtypeメンバによってswitchしてから、中身にアクセスするのが 典型的なイベント処理ルーチンですが、それもこういった理由によるものです。

ところが、SDL_Eventでは他のイベント構造体とtypeそのものも共用されています。 にもかかわらず、まずtypeメンバを参照し、その後に 各構造体にアクセスするというのはいったいどういうことでしょうか。 ...などと勿体つけてもしょうがないのですが、 要するに、各イベント構造体の先頭のフィールドは必ずUint8 typeである、 ということがミソになっています。つまりこんな感じです

SDL_Event.typeにアクセスするとき
+-------------+
| type        |
+-------------+
|             |
| 残りはゴミ  |
|             |
|             |
|             |
+-------------+

SDL_Event.それ以外 にアクセスするとき(例: key)
+-------------+
| type        | ← これはSDL_KeyboardEventの第一メンバ
+-------------+
| which       |
+-------------+
| state       |
+-------------+
|             |
| ゴミ        |
+-------------+

さて、SDL_PushEventに、そのようにしてセットしたSDL_Eventのアドレスを 渡しています。ここで作られたSDL_Eventそのものはifを抜けてしまえば どこかに消えてしまいますので、SDL_PushEvent側で何らかのコピーが 行われているというあたりは想像がつきます。

イベントキュー操作

ではSDL_PushEventの 実装を見てみましょう。src/joystick から再び src/events に戻ってきました。

int SDL_PushEvent(SDL_Event *event)
{
        return(SDL_PeepEvents(event, 1, SDL_ADDEVENT, 0));
}
現在(1.2.5)の実装は多少変わっています(戻り値の処理)が、SDL_PeepEventsを 呼び出していることは変わりません。 では次にSDL_PeepEventsです。 Peepには「覗き見する」といった意味があるようです。 説明によると、 actionに与えた内容によって、 イベントを追加(SDL_ADDEVENT)、イベントの覗き見(SDL_PEEKEVENT)、 イベントの取得(SDL_GETEVENT)を行うことができるようです。 PEEKとGETの違いは見たイベントをキューから取り除く(GET)か取り除かない(PEEK)か、 のようです。なお、peekもpeepに似たような意味を持っているようです。 BASICだとpoke/peekでお馴染ですね(?)

ちょっと長目ですので引用しつつコメントします。

/* Lock the event queue, take a peep at it, and unlock it */
int SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action,
								Uint32 mask)
*  numevents はイベントの個数です。今回は1です
*  actionは上で説明した通りです。今回はSDL_ADDEVENTなので追加です
*  maskは、対象にするイベントの種類を指定します。フィルタですので、
*  制限しないときは0にします(今回の例)。
{
	int i, used;

*  SDL_EventQ.activeが0になるのは、SDL_StopEventThreadが
*  呼び出されたときです。このときは何もせずに帰ります。
	/* Don't look after we've quit */
	if ( ! SDL_EventQ.active ) {
		return(-1);
	}

	/* Lock the event queue */
	used = 0;
	if ( SDL_mutexP(SDL_EventQ.lock) == 0 ) {
*  まず、ロックを取得しました。
*  第3回で調べたときのロックはSDL_EventLock.lockのものですので
*  混同しないでください。

		if ( action == SDL_ADDEVENT ) {
			for ( i=0; i<numevents; ++i ) {
				used += SDL_AddEvent(&events[i]);
*  イベントを追加する実体はSDL_AddEventのようですね。
*  また、複数のイベントを同時に渡せる作りになっていることも分かりました。
*  つまり、SDL_Eventのアドレスを渡していたのは、実際にはSDL_Eventの
*  配列の先頭アドレスだったということです。
			}
		} else {

                       *** イベント覗き見処理本体 ***

		}
*  ロックを解放します
		SDL_mutexV(SDL_EventQ.lock);

	} else {
		SDL_SetError("Couldn't lock event queue");
		used = -1;
	}
*  実際に処理されたイベントの数を返します
	return(used);
}
else内「*** イベント覗き見処理本体 ***」は、SDL_PEEKEVENTまたは SDL_GETEVENT処理の本体です。今はSDL_PushEventから来る SDL_ADDEVENTの処理を眺めているときですので、ここの中身をこの段階で 考察するのは本意ではないのですが...ついでなので見てみます。
			SDL_Event tmpevent;
			int spot;
			/* If 'events' is NULL, just see if they exist */
			if ( events == NULL ) {
				action = SDL_PEEKEVENT;
				numevents = 1;
				events = &tmpevent;
			}
もしeventsがNULLだったら、tmpeventに対して1コだけSDL_PEEKEVENTする という処理になります。この関数が終わったときにはtmpeventは なくなっていますが、戻り値が「処理したイベントの数」ですので、 eventsにNULLを入れて呼び出すと、 0ならイベントなし、1ならイベントあり、という結果を得ることができます。
			spot = SDL_EventQ.head;
spotはintの変数です。SDL_EventQ.headも同様です。つまり配列の インデックスを保持しています。実際のイベントの配列は SDL_EventQ.eventになります。これは要素数がMAXEVENTSの配列です。

以下のループは 取り出した(または覗き見した)イベントの個数(used)が指定された numeventsに達するか、イベントキューの末尾に達したら抜けます

			while ((used < numevents)&&(spot != SDL_EventQ.tail)) {
				if ( mask & SDL_EVENTMASK(SDL_EventQ.event[spot].type) ) {
					events[used++] = SDL_EventQ.event[spot];
					if ( action == SDL_GETEVENT ) {
						spot = SDL_CutEvent(spot);
					} else {
						spot = (spot+1)%MAXEVENTS;
					}
				} else {
					spot = (spot+1)%MAXEVENTS;
				}
			}

対象のイベントがmaskによってフィルタリングされ、それに通過したら 実際にeventsに対してSDL_EventQ.eventの中身を送ります。 SDL_GETEVENTなら、SDL_CutEventを呼び出します。引数はそのときの インデックスになります。 そうでない場合(SDL_PEEKEVENT)は、インデックスを1進めます。ただし MAXEVENTSに達した場合は0に戻ります。ということは、SDL_CutEvent内にも これと同じようにインデックスを進める処理が入っているのだろうと 想像できますね。

では、SDL_AddEventの内容を見てみましょう。

static int SDL_AddEvent(SDL_Event *event)
{
	int tail, added;

	tail = (SDL_EventQ.tail+1)%MAXEVENTS;
	if ( tail == SDL_EventQ.head ) {
		/* Overflow, drop event */
		added = 0;
	} else {
※1		SDL_EventQ.event[SDL_EventQ.tail] = *event;
		if (event->type == SDL_SYSWMEVENT) {
			/* Note that it's possible to lose an event */
			int next = SDL_EventQ.wmmsg_next;
			SDL_EventQ.wmmsg[next] = *event->syswm.msg;
		        SDL_EventQ.event[SDL_EventQ.tail].syswm.msg =
						&SDL_EventQ.wmmsg[next];
			SDL_EventQ.wmmsg_next = (next+1)%MAXEVENTS;
		}
		SDL_EventQ.tail = tail;
		added = 1;
	}
	return(added);
}
概要は以下の通りです ※1で行われるコピーはshallow copyですが、SDL_SysWMEvent と、 SDL_UserEvent を除けばポインタは存在しませんので問題ないでしょう。 それ以外にも、SDL_SysWMEventは特別扱いになっているようですね。

戻り値は、成功したら1、失敗したら0、というよりは追加した イベントの数と考えたほうがよいでしょう。実際、先程の呼び出し元を 見てみますと

				used += SDL_AddEvent(&events[i]);
というふうになっていました。

ここまで見てきたように、

というルールでアクセスします。 いずれの場合も、その処理が「覗き見」でない 場合は、headまたはtailをインクリメントします。ただし MAXEVENTSを越えたら0に戻るという条件つきのインクリメントです。 head/tailについて補足すると となります。つまりSDL_EventQ.event[tail] は、正確に言うとイベントの 末尾ではありません。すでにイベントが溜まっている場合は、次に イベントが来たときに書くべき場所になります。

headとtailが一致した場合、

ということになります。中身はただの配列ですので、それをFIFOのルールで リング状態に管理するだけですね。

同じようなルールで、SDL_CutEventも 実装されています。概要だけ示そうと思ったんですが、 イベントキュー操作の本質のあるコードですのでちょっと真面目に 眺めてみます。

static int SDL_CutEvent(int spot)
{
	if ( spot == SDL_EventQ.head ) {
		SDL_EventQ.head = (SDL_EventQ.head+1)%MAXEVENTS;
		return(SDL_EventQ.head);
もしカットする場所が先頭だったら、先頭の位置を1つずらして終わります。
	} else
	if ( (spot+1)%MAXEVENTS == SDL_EventQ.tail ) {
		SDL_EventQ.tail = spot;
		return(SDL_EventQ.tail);
カットする場所がイベントの末尾だったら、その場所が 次回イベントが来たときに書きこむべき場所、つまり SDL_EventQ.tail に なります。 SDL_EventQ.tail は、次にイベントが来たときに書く場所、ですので、 今現在「イベントの末尾」は、その一つまえ 、つまりspot、 ということになります。これはhead == spot == tailのときには 成立しませんが、head == spotのときは1つ上のifにひっかかっていますので ここには来ないわけです。
	} else
	/* We cut the middle -- shift everything over */
	{
		int here, next;

		/* This can probably be optimized with memcpy() -- careful! */
		if ( --SDL_EventQ.tail < 0 ) {
			SDL_EventQ.tail = MAXEVENTS-1;
		}
		for ( here=spot; here != SDL_EventQ.tail; here = next ) {
			next = (here+1)%MAXEVENTS;
			SDL_EventQ.event[here] = SDL_EventQ.event[next];
		}
		return(spot);
	}
	/* NOTREACHED */
}
カットする場所が先頭でも末尾でもない場合は、カットする部分よりも後ろ (tail側)にあるイベントを1つずつ詰めてやる必要があります。 まず、カット後のtailはかならず1つ前のインデックスを持ちます。 0を越えてしまったらMAXEVENTS - 1ということになります。 その後のforループでカットしたい場所から、tailの1つ前までのイベントに 対して、隣のイベントをコピーしてゆくという処理を行います。

イベントとスレッドの関係

第3回で見てきたときは、イベントはスレッドで 呼び出されていて、定期的にSDL_GobbleEventsが見てまわってイベントを 収集している、という説明をしました。しかし実際には(BeOSを除いて) 特別な指定をしなければイベントスレッドは走りません。特別な指定とは、 SDL_Init(SubSystem)にSDL_INIT_EVENTTHREADを渡すことです。(ただし WindowsとmacintoshはSDL_INIT_EVENTTHREADを渡すとエラーになります。 つまりイベントスレッドは使えません)

あまり深い考察をしたわけではありませんが(というかgrep SDL_JoystickUpdate **/*.[ch]ですが)、 実は、SDL_PumpEventsという 関数の中に、 SDL_GobbleEvents内にあった 処理とそっくりな処理があります。そして、 ドキュメントにある通り、 SDL_PumpEventsは、 SDL_PollEventSDL_WaitEventの呼び出しのたびに 呼び出されています。これらの関数はSDL_PumpEventsを呼び出した後、 前の節で眺めたSDL_PeepEventsを呼び出し、イベントがなかった場合に すぐに戻る(SDL_PollEvent)、またはイベントが来るまで待つ(SDL_WaitEvent) という処理をしています。

つまり、イベントスレッドが走っていなくても、イベントを調べにゆく その段階で各サブシステムに対してイベント収集の指示が 出ているということなのでした。

なおpumpはポンプという表現でお馴染なように「吸いだす」「汲み上げる」 といった意味があります。

そのほか

今回はだいぶ長くなってしまいましたので、残りは駆け足で見てみましょう。

SDL_JOYAXISイベント、またはSDL_GetJoystickAxisで得られるのは Sint16の値、つまり-32768〜32767の値です。これらの値を 実際に更新しているのは、上で見てきた通り、SDL_PrivateJoystickAxis()ですが、 では実際に各ドライバがどのようにその値をセットしているのか、 SDL 1.2.5時点のソースを見て簡単にまとめてみました(これが書きたいために 随分回り道をしたもんだ) なお、SDL_PrivateJoystickAxisの引数は、 順番に、SDL_Joystickのポインタ、軸の種類(番号)、軸の値(Sint16)となっています。 また、ハットなどの軸はとりあえず無視します。

最後はほとんど手抜きですね...すいません。まあ原則として デバイスから渡ってきた値を範囲補正などを噛ませた後に直接渡しているといった あたりで問題ないでしょう。

それにしてもmacosとかdarwinのkで始まる定数は一体何者なんだろう...

なお、今更言うまでもありませんが、 上で紹介してきたイベントキューいじり系の関数はいずれも イベントキューをロックした状態で呼び出す必要があります。 今回見てきた流れでいうと、SDL_PeepEventsがロックをかけていますので、 その内部で呼ばれているstaticな関数は、それがロックされた状態で 呼び出されていることを仮定したコードということになっています (コメントにも書かれていますが)

まとめ

第2回からの流れをおさらいしてみましょう。

※1 については下の補遺を御覧下さい。

coming soon

これを書いている時点でもう第5回は書き終えているのですが、次回は SDL_RWopsについてのお話です。

補遺

※1 嘘を書いてしまいました。SDL_JoystickEventStateは、 ジョイスティック関連のイベントを一括でENABLE/DISABLE/QUERYするための 関数です。ですから、SDL_JoystickEventState(SDL_IGNORE)すると

        if ( SDL_numjoysticks && (SDL_eventstate & SDL_JOYEVENTMASK) ) {
の条件を通らなくなり、SDL_JoystickUpdateは呼ばれなくなります。

第3回の最初に見てきた通り、WM関係以外の イベントはデフォルトで有効になっています。状況をまとめますと:

となります。SDL_JoystickEventState(SDL_IGNORE)をしてしまった場合、 イベントサブシステムからSDL_JoystickUpdateが呼ばれなくなりますが、 SDL_JoystickUpdate自体はpublicなmethodですので、SDL_JoystickGet???を 呼び出すのに先立ってSDL_JoystickUpdateを自力で呼べば済むということになります。

つまり、SDL_JoystickEventState(SDL_ENABLE)を実行して初めてイベントが 有効になる、というのは誤解で、実際には敢えて SDL_JoystickEventState(SDL_IGNORE)を実行しなければ常にどれかのイベントが キューに溜まっているということなのでした。

ていうかドキュメントに書いてあるじゃん... これで自分が翻訳担当者だったらどうしようと思ったが 違ったのでよかった(^^;


戻る

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