描画系」カテゴリーアーカイブ

COM と WRL::ComPtr

これまで中途半端にOpenGLESとMetalを使ったことがあるくらいでDirect3Dはまったくいじったことのないオッサンが「まあMetalとそんな変わらんだろう」といきなりDirect3D12に挑戦して四苦八苦してます。ロートルオジサンにはつらい。。。

で表題の件ですがDirectXのオブジェクトは基本COM。Direct3D12になってもCOM。
D3D12からは言語はC++しかサポートせず、そしてオブジェクトの保持や破棄は全てプログラマが責任持って管理するようになったのにCOMを使う利点って何だろう。。。
もうValkanのようにイチから設計し直して欲しかったがそれならValkanをどうぞということだろうか。

それでCOMですが、アラフィフになってえっCOMって何? なロートルおっさんなので勉強しました。

  • 参照カウント方式。AddRefしたらReleaseする。カウンタがゼロになったら解放される
  • 戻り値や引数でCOMを返却する関数は関数側でAddRefして返す。関数から受け取った側は必要なくなったらReleaseする。

MRC時代のObjective-Cの参照カウンタと似たかんじで、これはOK。

次に WRL::ComPtr。 参照カウンタの面倒を自動化してくれる。すばらしい。

ただ、参照カウンタをどう扱うかのポリシーみたいなやつのドキュメントが見当たらない。どう使っていいのかいまいちピンとこない。しかたないのでComPtrのヘッダをみてみる。

WRL::ComPtrはコンストラクタや代入でCOMオブジェクトを受け取る時に必ずAddRefし、管理しなくなる時に必ずReleaseする。
これは徹底している。参照カウンタが元に戻ることが保証される。
右辺値参照にも対応し、moveする場合は参照カウンタは増えない。

これはなかなか良い。

だけども、Create系関数から返却されたオブジェクトをComPtrで扱おうとするとあれっとなる。
Create系関数から返却されたオブジェクトはすでにAddRefされた状態で返ってくるので、こいつはAddRefせずにReleaseだけ行う。
だけどコンストラクタや代入でWRL::ComPtrに渡すとAddRefしてしまう!!

で、どうするんだろうとサンプルコードなどをみると、ComPtrでCOMオブジェクトを受けるのはこんなカンジになってる

ファッ!?? ってなりますよね。 なりますよね?

&deviceって何??
なんでComPtrのアドレスを直にD3D12CreateDeviceの引数にぶっこんでるの??

と思ってComPtrのヘッダをみると、単項 & 演算子がオーバーロードされていた。こいつがDetails::ComPtrRef<ComPtr<T>>を返す。さらに Details::ComPtrRefの operator InterfaceType**() が呼ばれて、元のComPtrのReleaseAndGetAddressOf()を呼ぶ。。。。 つまり、上記のCreateDeviceのところは

と書くのと同じである。

これでようやく、ああ、ComPtr内部で持ってるオブジェクトのポインタのアドレスを渡して直接オブジェクトを得るんだな。この時AddRefはされないんだな。そしてComPtrのデストラクタで無事Releaseされるんだな。というのが理解できた。

これくらいのことをヘッダ辿って調べるのは一般的なC++erだったらきっと至極当然のことなのであろう。だからWRL::ComPtrのこの辺の挙動を疑問に思うエントリがぐぐっても出てこないんだ。たぶん。
でもロートルおじさんはこの辺の挙動を理解するのに苦労したのでこうして書き留めるよ。

しかしこれ、 &device って書くよりも device.ReleaseAndGetAddressOf() って書く方が圧倒的にわかりやすいよねえ。。。

写真を拡大表示する時にアニメーション途中で大きな画像に挿げ替える場合は最初の画像と置き換える画像とでExifの回転情報を揃えるべし

かなりビミョーすぎる問題

最近は写真のサムネールをタップして大きな画像を表示する時は別画面に遷移するのではなくサムネールがひゅっと拡大するのが流行り。

こんな。

拡大するには、UIViewのアニメーション機能でUIImageViewをちゅちゅーと拡大する。

この時、元の小さなサムネールをそのまま拡大するとボヤけるのでどっかで大きな画像に挿げ替えるんだけど、

  • 小さなサムネールを拡大してから大きな画像に挿げ替える場合、拡大し終わる直前あたりに解像度の足りてないもやっとしたものが表示されてしまう
  • 大きな画像を先に用意し、挿げ替えてから拡大する場合、拡大アニメーションが始まる前に画像ロードの時間のブランクが発生してしまう。iPhone4などの遅い端末だともの凄いブランクになる。

という問題が発生する。
端末を識別し、遅い端末ならアニメーションした後で挿げ替える、速い端末ならアニメーションする前に挿げ替える、というのも手だが正直あんま美しくない。

そこで、

  • とりあえず拡大アニメーションスタート
  • 非同期で同時に大きな画像をロード
  • 大きな画像が取得できたらアニメーション中かどうかおかまいなしに画像を挿げ替える

というのをやってみた。
おおまかなコードはこんな

これを走らせてみると、iPhone5だと大きい画像のロードが瞬時に終わるためアニメーションの最中、しかも早いうちに画像が挿げ替わる。これだと画像のロードを待たずにライムラグ無くアニメーションが始まり、拡大の最中の早いうちに画像が替わるので解像度の低い画像が拡大して見えてしまうこともない。パーフェクト。
iPhone4の場合シングルコアで拡大アニメーションしながらバックグランドタスクをこなすのは酷で、アニメーションが終わってちょい経ってから大きな画像に挿げ替わる。動きとしては「アニメーションが終わった後で画像を挿げ替える」とほとんど同じ。大きな画像を読み込むキューの優先度を低くしてあるのはシングルコア端末でアニメーションの滑らかさを優先するため。

これで不格好な条件分岐かまさずに速い端末でも遅い端末でもどちらでも最適なカンジで大きな画像に挿げ替えることができた。

ばんじゃーい。

で終われば良かったのだが、この「UIViewアニメーションしてる途中のUIImageViewの画像を挿げ替える」って方法に微妙な注意事項が。

結論から言うと、

・挿げ替える前と後の画像で、Exifの回転情報を揃えておかないといけない

っていう。これを守らないと画像を挿げ替えた時点でアニメーション中の画像の位置がおかしなところに飛ぶのだ。

どういうことかというと

  • UIViewのアニメーションはCALayerの機能を使ってる(たぶん)
  • Exif回転情報付きのUIImageは元の向きのままのラスター+回転情報を持っていて、それをUIImageViewで表示する際にCALayerの機能を使って回転を適用している(たぶん)

んで、UIViewのアニメーションしてる途中で画像のExif回転情報が変化するとかたぶん想定されてないので、挿げ替えた画像の回転は適用されるんだけど、アニメーション途中で画像の回転角が変化した時の回転中心。。。は考慮されてなくて、どっか(たぶんアニメーション開始前)を中心に回転してしまう。このため画像の位置が飛んでしまう。

ということが起きている。。。 と思われる。 推測ですがたぶんこんなんでしょう。

大きな画像は通常iPhoneでパシャっと撮った画像。これはラスターはCCDの向きのままでExifに回転情報を入れて向きを回してる。例えば90度回転するExif情報が付いてるとする。
そしてその画像からサムネールを作る場合、よくやるのはCoreGraphics使っての縮小だけど、Exif回転情報付きの画像を縮小すると回転情報を適用済みの縮小画像が得られるのだ。このサムネールは既にラスター自体が回転していてExifによる回転情報がない=回転0度Exif情報が付いてるのと同じ。

というワケでこの2つの画像を挿げ替えると上記の問題が発生してしまうのである。
このパターン(サムネール画像を作成する時に回転適用済みになってしまい回転情報が揃わなくなる)はわりと稀によくあると思うので気をつけて欲しい。まあアニメーションの最中に挿げ替えようとかアレなことしようと思わなければ気をつける必要もないのだが。

対策はとりあえず アニメーション途中で画像を挿げ替える時はExif回転情報を揃えるってのしかないカンジ。パターンとしては

  • 拡大画像もサムネールも両方回転適用済みにしておく
  • サムネールを作成する際に回転適用済みにならないようにする(サムネール画像が元と同じラスターの向きで元と同じExif回転情報を持つようにする)
  • 画像ロードし終わった時にまだアニメーション途中だった場合はアニメーション終わるのを待ってから画像を挿げ替える。←一番確実なんだけど、これだと拡大アニメーションが完了する直前にもやっとした画像が見えちゃうからやっぱりもっと早く挿げ替えたいじゃん?
  • サムネール画像なんか用意しない。全部大きい画像でやる。だから挿げ替えなんか必要ない。←iPhone4だとパフォーマンス遅くて死ぬけどiPhone4見限るならこれもアリか

のいずれか。

2番目の場合、元画像からExif回転情報を落とした画像をまず作り、それを縮小し(同じラスターの向きになる)、その縮小画像に元と同じExif回転情報を付ける。

3番目の場合、アニメーション完了ブロックと画像ロード完了後のメインキューブロックの2つの非同期ブロック同士で情報のやりとりが必要なんだけど、NSMutableDictionary使うのがマイブーム。

同期はこの場合sharedDic弄るのは全部メインキューにしてお手軽解決してる。キューが異なる場合はdispatch_semaphoreあたりで競合しないようにする必要がある。