2016年8月10日水曜日

gamepad-osxの実装詳解

前回記事で書いた gamepad-osx の実装内容を軽く解説。

まず、mac OS (OSX) や iOS で HID(Human Interface Device; キーボード、マウス、ゲームパッドといった入力デバイス)へのアクセスは HID Manager 経由で行うので生成。

IOHIDManagerRef hid_manager = IOHIDManagerCreate(kCFAllocatorDefaultkIOHIDOptionsTypeNone);

次に、取得対象のデバイス情報を指定した CFMutableArray を作成する。
Objective-cなら割と簡単にできるが、Core Foundation を用いる必要があるので物凄く面倒くさい。

static void append_matching_dictionary(CFMutableArrayRef matcher, uint32_t page, uint32_t use)
{
    CFMutableDictionaryRef result = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    if (!result) return;
    CFNumberRef pageNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
    CFDictionarySetValue(result, CFSTR(kIOHIDDeviceUsagePageKey), pageNumber);
    CFRelease(pageNumber);
    CFNumberRef useNumber = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &use);
    CFDictionarySetValue(result, CFSTR(kIOHIDDeviceUsageKey), useNumber);
    CFRelease(useNumber);
    CFArrayAppendValue(matcher, result);
    CFRelease(result);
}
余談ですが、Core Foundation を C/C++ で書いたコードを見ていつも思うのが「無駄なオーバーヘッド要因が異様に多い」ってことですね。その代わり拡張性は高い。これが所謂 Mach の設計思想から引き継がれたオドロオドロしい部分なんだろうと思います。要するに効率性ではなく柔軟性・拡張性みたいなところに特化した仕組みで、だからNeXTSTEPは実用に耐えられなかったのだろうと。iOS世代もそのCFを内部で引きずり続けているから、結局のところ処理効率はそんなに良くない。何故スマホ初期の頃にピュアなLinux Phoneが出なかったのかと今でも思っています(今となっては商業的な意味で手遅れですが)。

上記関数の page は kHIDPage_GenericDesktop で use は以下のような値を指定する。
なお、ジョイスティックとゲームパッドは別useになっているので注意。私の手持ちのゲームパッドの場合, kHIDUsage_GD_GamePad を指定しないと認識しませんでした。
・kHIDUsage_GD_Joystick : ジョイスティック
・kHIDUsage_GD_GamePad : ゲームパッド
・kHIDUsage_GD_Keyboard : キーボード
・kHIDUsage_GD_Mouse : マウス(トラックパッド)

そして、作成したCFMutableArrayで指定したものが取得したいデバイスだということを HID Manager へ通知して、そのデバイスが接続されたことと切断したことを検出するコールバックを登録。


IOHIDManagerSetDeviceMatchingMultiple(hid_manager, matcher);
IOHIDManagerRegisterDeviceMatchingCallback(hid_manager, device_attached, c);
IOHIDManagerRegisterDeviceRemovalCallback(hid_manager, device_detached, c);

なお、接続(切断)のコールバックの形式は以下のような感じ。(detachedも同じ)

void device_attached(void* ctx, IOReturn result, void* sender, IOHIDDeviceRef device);

このコールバックでは、接続(切断)されたデHIDの情報を教えてくれるだけなので、そのHIDからの入力を検出したり解除するには、更に別のコールバックを設定する必要があります。(詳細は後述)

あとは、HID Managerを動かすrun-loopをスケジュールしてOpenすれば、デバイスの接続と切断が検知できるようになります。

IOHIDManagerScheduleWithRunLoop(hid_manager, CFRunLoopGetMain(), kCFRunLoopCommonModes);
IOHIDManagerOpen(hid_manager, kIOHIDOptionsTypeNone);

ここまでが、HIDの接続・切断を検出するまでの手続きです。
あとは、接続を検出したHIDに対して入力を受け付けるコールバックを設定してあげれば、入力された内容を検出することが可能になります。
これは接続時のコールバックで次のように実行すればOK。

IOHIDDeviceOpen(device, kIOHIDOptionsTypeNone);
IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
IOHIDDeviceRegisterInputValueCallback(device, device_input, c);

デバイス入力コールバックの形式は以下のような感じです。

void device_input(void* ctx, IOReturn result, void* sender, IOHIDValueRef value);

最後に、この入力コールバックで以下を発行すれば入力された情報が検出できることになります。

IOHIDElementRef element = IOHIDValueGetElement(value);
type = IOHIDElementGetType(element);
page = IOHIDElementGetUsagePage(element);
usage = IOHIDElementGetUsage(element);
val = IOHIDValueGetIntegerValue(value));

ね、簡単でしょ?

Steamで購入したとある日本製のゲームが、ゲームパッドに対応できていない(海外製のゲームはだいたい対応できている)ので、そのサポートに対して「簡単だからゲームパッドの入力に対応してくれ」とお願いしてみたのですが、日本語でIOKitの仕様を正しく解説できているサイトがほぼ皆無だったんですよね...

そういう事情もあって、もしかすると多少ニーズがあるかもしれないと思い、かなり簡単な手続きでmac OSのアプリでゲームパッド入力に対応できるライブラリ(gamepad-osx)を作ってみた感じです。

まともな資料は英語しか無かったけど、冷静に読めば結構簡単なことしか書いていないので、特に困る人が居ない→だから日本語資料が無いのかもしれませんが。

ちなみにこのIOKitというフレームワークですが、mac OS(OSX)の中ではかなりベーシックなもので、Wikipediaなんかを見ても(かなりあっさりとだが)MachやBSDと同列に解説されています。要するに、mac OS(OSX)の仕組みを理解する上でかなり重要な部分なので、技術書ぐらい幾つかあるんじゃないかなと思ってAmazonで検索してみたのですが、見事に皆無。
https://www.amazon.co.jp/s/ref=nb_sb_noss?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&url=search-alias%3Daps&field-keywords=IOKit&rh=i%3Aaps%2Ck%3AIOKit
Appleによる情報統制でも掛かっているのかな?
まぁ、このブログは妄言ということで許される(のだろうか^^;)