DXRuby Advent Calendar 2014 : DXRuby機能リクエストにお答えする

この記事はDXRuby Advent Calendar 2014の5日目です。4日目の記事はしのかろさんのDXRuby機能リクエストでした。

しのかろさんはDXRubyを使い込まれているようで鋭い指摘が多い。確かに使っていると「何でこれが無いのかな?」って感じる物なのだが、実は今までに検討したり実験したりして諦めたり却下したりしたものだったりするのだな。
今回は「マイナーな機能を取り上げて解説するコーナー」のはずだったのだが、いい機会なので急遽予定を変更してこれらの疑問にまとめて答えておこう。題して「DXRuby機能リクエストにお答えする」。
内容としてはRuby/DXRubyの仕様的な話やDirectXの話もありでちょっと難しいかもしれない。

DXRuby::Windowモジュール

to_imageメソッド、またはto_render_targetメソッド

手元のメモに「Window.to_imageを追加してWindow.get_screen_shotをそれとImage#saveの組み合わせに置き換える」というのが書いてある。Window.get_screen_shotはフロントバッファを取得してファイルに出力するので、例えば他のウィンドウに一部隠れている場合などにきちんとした画像が取得できない。ビデオドライバなどによるポストレンダリングの効果が反映された画像を取得する方法はこれしかないので一長一短ではある。いずれにせよ直接ファイルに吐き出すのではなく、間にImageを挟んだほうがよさげなのは確かだろう。

ウィンドウのアイコン化

ウィンドウを最大化したり、マウスでリサイズできるようにしたり、というのはなんとなーく考えてはいる。主にWS関連で。ただ、サイズを変更するとデバイスの初期化が発生するので、マウスでのリサイズで連続的に発生されたりすると大変つらい。そのあたりはうまいこと処理できそうな気もするし、難しそうな気もする。とりあえず現状、最小化することはマウスクリックで可能なのにそのメソッドが無かったり、状態を取得できなかったりというのは地味によろしくないと思う。

ティアリング防止

1.4.1のときにチャレンジしたが、DirectXの動作とDXRubyの動作の相性問題でウィンドウモード時のティアリング防止は挫折した。そもそもディスプレイのリフレッシュレートとゲームの指定FPSが若干ずれている場合に、どのように描画するのが良いのかの明確な答えを持ち合わせていない。Windows7で何かしらの対策が施されたとかなんかそんな話があった気がするのでまたチャレンジしてみるかも。

before_call, after_call

このメソッドは1.4.1ですでに追加してしまったので、現状の使用方法で結果が変わるような変更は無い。ただ、例えばデフォルトでハッシュが入ってるけどbefore_call=でコンテナを置き換えることができる、とかの実装は可能かもしれない。その場合でもeachのブロックが受け取る引数はハッシュとそれ以外で変わるので、そのへんをどう実装するかが悩みどころである。つまり例えば、配列が渡された場合は配列の最後のデータ、それ以外は受け取ったデータに対してcallを呼ぶ、という実装でいいのかどうなのか。そしてそのような動作をメソッドの説明に書くべきか。

DXRuby::Imageクラス

ピクセル全体に対するイテレータ

ずーーっと昔に検討したが却下した機能。検討した理由は1ピクセル書き込むごとにLock、Unlockが発生することの負荷が心配だったからで、却下した理由は別に遅くなかったからである。実際のところ、CのコードからRubyのコードを呼び出す処理はスタックフレームその他各種情報を構築してVMを起動する必要があるので基本的に遅い。だからInteger#timesするよりもwhileループしたほうが速いわけで、速さを求めるなら逆にRubyで書いたほうがよい可能性もある。
あと、その手の範囲全体を渡すイテレータってのはポストレンダリング的な処理に使うことが多く、それをRubyで書くのだからすなわち速度が要求されていない。起動時にデータを作るのに待たされるという話もあるが、速度が必要であればShaderを使ってもらう感じで。

DXRuby::RenderTargetクラス

フレーム更新ごとに消去されないようにしたい

これは非常に難しい話で、DirectXのテクスチャの事情による。
そもそもDirectX9世代ではビデオメモリとシステムメモリは完全に切り離されていて、DirectX9のテクスチャはざっくり以下の3パターンとなっている。
(a) システムメモリにのみ存在するテクスチャ。CPUから自由に操作でき、GPUから参照できない。描画時に全体を転送するので遅い。デバイスロストの影響を受けない。
(b) ビデオメモリにのみ存在するテクスチャ。CPUから参照できず、GPUから自由に操作できる。描画は高速。デバイスロストで無慈悲に消える。
(c) DirectXが管理するテクスチャ。システムメモリとビデオメモリの両方に存在し、CPUから自由に操作でき、GPUから参照だけできる。描画は高速。CPUで更新すると差分のみ転送する。デバイスロストの影響を受けない。
DXRubyでは、Imageは(c)、RenderTargetは(b)を使っている。つまりRenderTargetはデバイスロストで消える。デバイスロストはOSと他のアプリケーションの都合でいつでも起こりえることになっているので、RenderTargetのデータをデバイスロスト安全にするためには描画のたびにシステムメモリに転送して保存する必要があり、RenderTarget#to_imageでそれが可能ではあるのだが、リファレンスに書いてある通り非常に遅い。(c)の管理テクスチャがGPUから書き込めないのはこの転送の遅さが原因だと思われる。
イメージしてみよう。1画面サイズのRenderTargetに32*32サイズの画像を描画するたびに、1画面サイズの画像がビデオメモリからシステムメモリに転送される。これは恐ろしい。
バイスロストは滅多に起きないが、起きたときに画像が消滅するのではさすがにリスクが高い。ということで、当初は消えなかったRenderTargetのデータを、デバイスロストに対する対策として毎フレーム消えるようにわざわざ変更したのである。RenderTargetのデータを素材として使いまわす際はto_imageするように。Imageオブジェクトの画像はビデオメモリにもあるのでRenderTargetの描画と同等に速いはず。
RenderTarget#to_imageが遅いっつっても全画面のデータを超古いAMDGPUで転送するのに5msかかるとかそんな程度なので、小さいデータで数が少なければいまどき問題にはならない。

DXRuby::Spriteクラス

target=にImageオブジェクトを指定可能にする

SDL2ではソフトウェアレンダラが実装されていて、ハードウェアの描画と同等のことをSurface(システムメモリの画像)にも行うことができる。しかしDXRubyのSpriteはSprite#shader=を使うことでシェーダを経由した描画ができてしまうので、ソフトウェア実装することができない。
一応理屈としては、targetのImageと同じサイズでRenderTargetを生成→targetのImageをRenderTargetに描画→SpriteのtargetをそのRenderTargetに変更→Spriteを描画→RenderTarget#to_image→元のtargetだったImageに画像を転送→targetを戻す、という手順で実装可能ではあるが、おそらく通常の描画と比較して数倍から数十倍オーダーで遅い。
できることはできるが、それができるならImage#draw_exもできるんじゃね?って言われそうで怖い。確かにできることはできるのだが。ちなみに上記のようなコードを書けばRubyでもできる。

drawメソッドが逆方向に機能している

これは俺も思っていて、Sprite#renderを新たに定義してそっちに移行しようかなあ、なんて考えている。もちろん互換性の問題でdrawは残しつつ。フェードアウトさせるような感じで。

DXRuby::Fontクラス

引数にnilを与えたらFont.defaultを使うようにしようかなーとか考えていたのだけど、それはしのかろさん的にはどうなんだろう。リアルに省略するのはハッシュ引数とぶつかってしまうのでできないのだけども。
あと、「今以上にFont情報を取得したい」というのは具体的にどのような情報なのかが知りたかった。その情報がフォントに存在するのなら取得する方法もあるかもしれないし、そうであればRuby側に引き渡すことができるからだ。

DXRuby::Soundクラス

読み込みが遅い

これはたぶんDirectMusicのせいなのではないかと思う。とりあえず起動時の初期化は超遅い。自前のサウンドドライバが作れれば改善可能かもしれないのだが、SDL_Mixerのコードを眺めていたら作れるような気がしなくなってしまった。

Rubyスレッド対応

1.5.1devで導入したスレッド対応は1.4.1に入っている。tkはほとんど使ったことが無いのでよくわからないのだが、現状で使えないということであれば、動かないコードを提供してもらえれば解決策も調べられるかもしれない。

おしまい

以上。わりと気楽に作っているようで、色々なところで壁にぶちあたったり悩んだりしているのだ。まあ、DXRubyにはいろんな機能があるが、できそうなのにできないことにはこれらのわけがあったのである。キリッ

ということで、5日目「DXRuby機能リクエストにお答えする」でした。明日はみれいゆーさんの「Ruby初心者がふれーむわーく:チーム戦のあるサイドビューシステム」です。お楽しみに〜