Friday, December 25, 2015

OpenSL ESでActivityが非アクティブになった時に音を停止する簡単な方法

前回OpenSL ESでサウンドを再生しましたが、Activityが非アクティブになっても音が鳴り続ける問題がありました。それに対しての簡単な対処法として、バッファを細かく分けてEnqueueしてみました。Enqueueは32768バイト単位でしています。(サイズは適当です)

最終アップデートからの経過時間が0.5秒を超えたらコールバック無視します。この方法の長所は、コールバック一つでループ再生と非アクティブになった時の音停止を同時に実装でき、コードが簡潔になります。Activityから非アクティブの通知を受け取る必要もありません。短所としては、アップデートループで長い処理をすると音が切れてしまうことです。

また、停止した音を再開する処理を盛り込んでいません。ここは、ゲーム毎の実装依存が多いと思いあえてしませんでした。例えば多くの場合は再開すべき音はBGMだけであったりするでしょう。

AndroidのOpenSL ESはコールバックは別スレッドから来るので特に気をつける必要があります。Android特有の実装についてはNDKのdocs/Additional_library_docs/opensles/index.html に説明があるので、一度目を通しておくと良さそうです。

Saturday, December 12, 2015

実践OpenSL ES、AndroidでWAVファイル再生

OpenSL ESによるサウンドエンジンを使い物になるように書いてみます。

まず、気づくのはDirectXを彷彿させるインターフェース型のAPIであることです。多くのメソッドはSLresultで結果を返し、Destroyでリソースを解放して終了します。素直に書くと冗長なコードになりそうですが、まず最初にDirectX開発者にはおなじみの手法を導入します。

SafeDestroyはDestroy呼び出しと同時にnullptrを代入してダングリングポインタにならないようにします。

SLHandleErrorというのは、各種メソッドがエラーコードを返したらソースコード上の行数と関数呼び出しの概要をログに出力するためのマクロです。Direct3D9のサンプルでは"V"という一文字のマクロが多用されていましたが、それです。

SLCallは、インターフェース呼び出しをすっきり書きたいがために書いたマクロです。これを使ってエンジンの初期化と終了を書いてみます。

エンジンの初期化と終了が書けました。

DirectXから来ると戸惑うのが、SLObjectItfとその他インターフェースに分かれていることです。OpenSLでは全てのインスタンスの実体はSLObjectItfであって、何らかのメソッド呼び出しが必要になったらSLObjectItfから追加のインターフェースを取得します。また、SLObjectItfをDestroyするとSLObjectItfから取得したインターフェースも一緒に無効になります。

engineEngineがSafeDestroyではなくnullptrを代入しているのは、engineObjectをSafeDestroyした時に一緒に消滅しているからです。

次は、WAVファイルをロードして"audio player"を作ってみます。

CreateAudioPlayerで、WAVファイルの特性(ステレオ/モノラル、サンプリングレート、ビット数)に合わせた"audio player"を生成します。WaveFormatExはWindows APIにあるWAVEFORMATEX構造体で、この情報からSLDataFormat_PCM構造体を構築します。リファレンス通りに書くとまたコードが長くなりそうですが、例えばSL_SAMPLINGRATE_44_1は44100000と定義されていたりします。samplesPerSecondを1000倍して代入すれば良いわけです。事前定義値は https://www.khronos.org/registry/sles/api/1.1/OpenSLES.h にあります。

次はWAVファイルの再生部です。

Enqueueで再生するバッファを指定します。ループ再生する場合はコールバックを登録して、コールバックからEnqueueを繰り返します。コールバック設定前にSL_PLAYSTATE_STOPPEDを明示するのはステートがSL_PLAYSTATE_STOPPEDではない場合にSL_RESULT_PRECONDITIONS_VIOLATEDエラーが発生するためです。これはPlayメンバ関数が二回目に呼ばれた場合に起こります。

これで、OpenSLの初期化からWAVファイルの再生までができました。ところで、この方法だとActivityが非アクティブになっても音が鳴り続けるという問題があります。この解決は次回書きます。

Saturday, December 5, 2015

Luaのrequireで独自ファイルシステム上のスクリプトをロード

requireでAndroidのAPKのassetsフォルダからスクリプトを読み込みたくて、方法を探ってみました。requireはCのincludeに似た使い方をしますが、requireの実体はモジュール名を引数に取るただの関数です。

dofileとrequireはよく似た関数ですが、いくつか違いがあります。まずrequireは通常拡張子".lua"無しでモジュールを指定します。これを実装してみます。


前回、dofileのファイルシステムを置き換えているので置き換え版のdofileを流用することでLuaだけで書けました。厳密にはpackage.pathを考慮していないという問題がありますが、これだけで用足りることも多いと思います。

ところで、requireで比較的重要なdofileとの違いが、重複requireガードです。これはpackage.loadedテーブルにロード結果が記録される事で行われます。実際にテーブルの中身をprintで出力してみると、以下のようになりました。(左がキー、右が値)
debug   table: 00C9D770
vec4    true
coroutine       table: 00C98AA8
_G      table: 00C92760
package table: 00C98828
math    table: 00C9A2D0
my_class        userdata: 00C9C3B8
io      table: 00C98C38
os      table: 00C99880
bind_win        true
string  table: 00C99FB0
utf8    table: 00C9D6D0
table   table: 00C98BE8
キーを見ると、luaL_openlibsでロードされるLuaの標準ライブラリに混じってrequireでロードしたモジュール(ここではvec4, bind_win, my_classがそれです)の名前が見えます。対して値はそのモジュールをロードした時のreturn値が格納されています。(return値が無い場合はtrue)

また、この値は同じモジュールの二回目以降のrequireで返される値としても使われます。以下の例は、dofileが毎回math.random()を実行するのに対して、requireは初回呼び出し時の値をpackage.loadedにキャッシュする事を確かめるものです。


以上を踏まえたファイルシステム置き換え版のrequireは以下のようになります。

Sunday, November 22, 2015

Luaのdofileで使うファイルシステムを置き換える

Luaはdofile等でスクリプトを読み込む時にfopenを使います。しかし、AndroidのAPKのassetsフォルダはfopenでアクセスできません。独自のファイルシステムを構築したい場合もあります。

そういう時は、以下のようにして、標準のdofileを上書きしてファイルシステムを置き換えます。



LoadFile関数はファイル名を受け取ってmallocで確保したメモリブロックにファイルの内容を格納して返すようにしました。例えばAndroidの場合はassetsフォルダからファイルを持ってくるようにします。このエントリを参考にしてください。

luaL_loadbuffer関数にスクリプトの内容とファイル名を渡します。ファイル名は万一スクリプト内でエラーが発生した場合にLuaがどのファイルの何行目なのかを知らせてくれる為に重要です。

lua_pcallは、読み込んだスクリプトを一度「実行」します。Cのような言語と違い、Luaは関数や変数の定義も「実行」することで行われますので、これをしないとスクリプトを読み込まなかったのと同じことになります。

luaL_loadbuffer又はlua_pcallでエラーが発生した場合、Luaスタックの最上位にエラーメッセージが入ります。ここでは、適当にメッセージを加工してluaL_errorに渡します。これによって、dofileの呼び出し元のスクリプト側のエラーとして処理されます。

最後のreturn lua_gettop(L) - 1;lua_pcallがスタック何かを積んだ場合、その個数を呼び出し元に返します。-1するのはdofileが受け取ったファイル名がスタックを一つ使っているのでそれを除外するためです。説明より実例のほうがわかりやすいと思うので、以下に書いてみました。例えば、以下のような事ができるようになります。



呼びだされたcallee.luaの最後の行にreturn文があり、その戻り値をcaller.luaで受け取っています。こういった使い方が必要ない場合は、return lua_gettop(L) - 1;はreturn 0;に置き換えても不都合はありません。ただし、caller.luaはdofileから戻り値を受け取れなくなります。

Thursday, October 29, 2015

Windows 10でDirectX12のWARPデバイスの生成が失敗する時の処方箋

DirectX12対応ビデオカードを保有していなくてもWindows10だけでDirectX12のプログラムをテストできるWARPデバイスというものがあります。



GitHubで配布されているMicrosoftのサンプルプログラムであるDirectX-Graphics-Samplesに含まれるサンプルは、-warpというコマンドライン引数を渡すとWARPデバイスで実行できます。この時にOutputDebugStringでこんなエラーが出て失敗します。

D3D12GetDebugInterface: This method requires the D3D12 SDK Layers for Windows 10, but they are not present on the system.

また、DirectX11で、D3D11_CREATE_DEVICE_DEBUGを指定した場合も以下のエラーが出ていました。

D3D11CreateDevice: Flags (0x2) were specified which require the D3D11 SDK Layers for Windows 10, but they are not present on the system.

これは、Graphics Tools for Windows 10を入れることで解決します。以下のようにたどってインストールします。








あるいは、管理者権限で以下のコマンドを入力するといいようです。

Dism /online /add-capability /capabilityname:Tools.Graphics.DirectX~~~~0.0.1.0

参考:



  1. http://stackoverflow.com/questions/32809169/use-d3d11-debug-layer-with-vs2013-on-windows-10
  2. https://msdn.microsoft.com/en-us/library/mt125501.aspx#InstallGraphicsTools
  3. http://blogs.msdn.com/b/vcblog/archive/2015/03/31/visual-studio-2015-and-graphics-tools-for-windows-10.aspx

Tuesday, September 22, 2015

Upgrade可能なReaders–Writer Lock を自前で実装する

Readers–writer lockとは、複数のスレッドの同時リードは許可するが、ライトする場合は他のスレッドのリードとライトをすべて排除するというものです。

C++14でshared_timed_mutexがstdに含まれましたが、リードからライトへ昇格(Upgrade)はサポートされませんでした。

実は昇格も比較的簡単に自前で実装できます。C++11以降はstd::threadやstd::atomicなど、スレッドに関する機能が大幅に強化されているので、これらを使って昇格可能なロックを実装してみます。

基本のロック



std::atomicを使います。0ならロックされていない状態、1ならロック状態です。Lockを呼んだ時点ですでにロックされていたらその場でループして待ちます。

compare_exchange_weakというのは、いわゆる"Compare and Swap"、略してCASと呼ばれます。比較と交換を1命令で行う、マルチスレッドの実装に必須の命令です。

whileでループしているので、ロック取得の待機中はCPU時間を消費します。この方式をスピンロックといいます。比較的短時間のロックは効率的ですが、ロック時間が長くなると無駄が生じる特性ががあります。今回の記事はすべてスピンロックで実装しました。

最も簡単なreaders-writer lock


リードロックとライトロックを分けた実装です。リードロックは複数スレッドから同時に可能で、ライトは1つのスレッドから可能です。


val変数の0x40000000ビットをライトロックの為に使い、それより下のビットはリードロックの個数を記録するカウンタとして動作します。

LockInternal関数がポイントで、valの中にtestBitと被るビットがある間は待機し、testBitと被らなくなったらdeltaをvalに加算します。この関数に渡す数値の組み合わせだけでリードロックとライトロックが実現できます。

これを表にするとこうなります。
ステート移動立てるビット待機するビット
アイドル→リードリードライト
アイドル→ライトライトリード、ライト


昇格可能なreaders-writer lock


リードロックからライトロックへの昇格を実装してみます。実は「昇格」をどう定義するかで実装が変わってしまうのですが、以下は実装の一例です。
実はUpgrade関数の内部で「昇格中」という別のステートが存在しています。
ステート移動立てるビット待機するビット
アイドル→リードリードライト、昇格中
アイドル→ライトライトリード、ライト
アイドル→昇格中昇格中無し


なぜ「昇格中」ステートが必要かを説明します。

ライトロック取得の為には他の全てのスレッドのリードロックの解除を待ちます。しかし、リードからライトに移行しようとする複数のスレッドがある時デッドロックが発生することになります。なぜならお互いがリードロックを手放さずに待ち続けているためです。

ここで、「昇格中」というステートを設けます。このステートに入ることで、自身のリードロックを解除し、他のスレッドのリードロックの解除も待ちます。すると残るのは「昇格中」ステートを持つスレッドのみになるので、順次ライトロックを取得できるようになります。

この方法は、デッドロックを避ける事ができますが、「Upgrade関数実行中」に他のスレッドがライトロックを取得できてしまうという欠点があります。Upgrade関数呼び出しの前後でロック対象のデータが変更されている可能性があるわけです。

リードロックを昇格可能なものと不可能なものに分けたreaders-writer lock


昇格の前後でデータが変更されたくない場合、「昇格可能な特殊なリードロックは一つだけ取得可能」とする方法があります。boostのupgrade_lockの実装のようなものです。

リードロックはReadLock/ReadUnlockの組、昇格可能なリードロックはReadLockUpgradable/ReadUnlockUpgradableの組で取得します。

ReadLockUpgradableでロックを取得した場合、他のスレッドからのReadLockを妨げずに共存しますが、他のスレッドからReadLockUpgradableを同時に取得することはできません。1つのスレッドだけがReadLockUpgradableを取れるので、Upgradeの段階で競合が発生しないことと、Upgrade中に他のスレッドがライトロックを取得したりしないことが保障されます。
ステート移動立てるビット待機するビット
アイドル→リードリードライト
アイドル→ライトライト全ビット
アイドル→昇格可能リード昇格可能リードライト、昇格可能リード


リードロックを途切れなくライトロックに昇格可能、ただし失敗付き


全てのリードロックを昇格可能にしたいけれど、Upgrade中に別のWriteLockに割り込まれたくない場合の実装です。代償として、昇格の関数名がTryUpgradeとなっており、昇格が失敗する場合があります。

なぜ失敗するかというと、途切れない昇格を実現するために昇格中にリードロックを手放さないためです。二つ以上のスレッドが同時に昇格しようとするとお互いのリードロック解放を待つためにデッドロックします。そこで、最初に昇格しようとしたスレッドのみ昇格に成功させることにします。

万が一昇格に失敗した場合、次に出来ることはリードロックを解除することだけです。もし、どうしてもライトロックが欲しければ一度リードロックも解除して他のスレッドにライトロックを譲った後、改めてライトロックの取得を試みるしかありません。

この手法の短所は利用者が例外処理を書かなければならず、ただでさえバグを入れやすいマルチスレッド処理の実装がより複雑化しやすい事です。

TryUpgrade関数のwhileループで昇格中ステートへの移行を試みます。他のスレッドに先に昇格中ビットを立てられた場合は失敗します。

自身によって昇格中ビットを立てることに成功すればLockInternalでライトステートへ移行します。readerBitsの最下位ビットを待機対象から除外するのは、自身がリードロックを持っているためリードロックのカウンタが1の時は通過させなければいけないためです。

ステート移動立てるビット待機するビット備考
アイドル→リードリードライト、昇格中
アイドル→ライトライト全ビット
リード→昇格中昇格中無し他のスレッドが昇格中ビットを取得したら失敗
昇格中→ライトライトライト、リードリードロックの1つは自分が持っているので、最下位ビットを待機対象から除外


まとめ


リードロックからライトロックへの昇格が比較的簡単に実装できることがわかりましたが、問題は昇格の方式が色々あって、しかも一長一短があるために標準化しにくいということでした。

素直に昇格はあきらめてshared_timed_mutexで満足するのも手かもしれませんし、boostやIntel TBB等の有名なライブラリを使う場合はそのライブラリで提供された昇格機能に限って使うことにしてもいいかもしれません。

テストに使ったGitHubレポジトリ
https://github.com/yorung/ThreadTest

Saturday, August 22, 2015

DDS自力ロード(Cube Map, Mipmap, DXT1, DXT3, DXT5対応)

Windows10が提供されて誰でもDirectX12による開発ができるようになりました。しかし、DirectXTKやDirectXTexのようなテクスチャのロードが出来るライブラリがまだマイクロソフトから提供されていないようです。DDSは特に複雑なフォーマットではないはずなので、DX12の準備を兼ねて自力でロードを試みてみます。ただし、DirectX11です。(DX12も対応しました。

以下でほぼ全部です。キューブマップ、ミップマップ、圧縮/非圧縮すべて対応しています。エラーチェックはちゃんとしていないので壊れたファイルや未対応の形式でバッファーオーバーランなどの危険があるかもしれません。



ID3D11Texture2DとDDSファイルは共にキューブマップやミップマップの複数のイメージを内包します。よって、DDSからそれらを含むID3D11Texture2Dを生成するときにD3D11_SUBRESOURCE_DATAの配列でその複数イメージを指定します。

ミップマップ入りキューブマップ等になるとD3D11_SUBRESOURCE_DATAの順で悩みそうですが、これはIntroduction To Textures in Direct3D 11が詳しいです。更に都合のよいことに、D3D11_SUBRESOURCE_DATAとDDSの画像の配列順は同じになっているようです。

また、各画像の開始位置を正確にD3D11_SUBRESOURCE_DATAに設定しなければいけませんが、テクスチャ用の DDS ファイルのレイアウトと題されたページが参考になります。

やってみると意外と短いコードで出来たので紹介してみました。