OpenGLでムカつくこと
Rich Geldreich's Blog: Things that drive me nuts about OpenGL
Valve社員のRich Geldreichが、OpenGLの設計が古臭すぎることについて不満を爆発させている。
OpenGLについてムカつくことを脳内ダンプしてみる(これは個人的な見解であって、Valveや同僚の見解ではない。あと、ここ数年、OpenGLと格闘してきたので、今日は機嫌が悪い)。これを投稿する理由はこうだ。GL APIには再設計が必要だ。というのも、思うに、MantleやD3D12がどうせ昼飯前にOpenGL APIを駆逐してしまうだろうから、この問題については、今考える必要があるのだ。
ここに見れば些細な問題もある。単にAPIのトレースの問題というのもある。しかし、それらの問題が積み重なって、他の開発者にGL APIという環境に飛び込むよう誘うのを躊躇させるほどになっている。
20年もののレガシーは、再設計と大幅な単純化を経る必要がある
コアAPIで互換性なしのゴミ満載のワゴンの周りをぐるぐると。
単純にして、KISS principle適用して、「迷ったら投げ捨てろ」
MantleとD3D12はすみやかに、パフォーマンスと開発者への訴求性という点で、GLを(また!)置き去りにしていくだろう。Global context stateとbinding patternはクソだ。DSA(direct state access)スタイルのAPIが標準となるべきで、また必須であるべきなのだ。
口に苦い良薬をくれてやろう。大半の開発者達は、お手軽な道を選んで、PS4やXBoneのレンダリングコードをD3D21やMantleに移植するだろう。わざわざレンダリングパイプラインを書きなおして、GLコミュニティが最近パフォーマンスアップのために推奨している、超アグレッシブなバッチ処理に書きなおしたりはしねーよ。APIが近代化して簡潔になるまでは、GLは移植ターゲットとしては二級市民の扱いを受けるだろう。
GL context作成地獄
モダンなGLコンテキストを作成するのは怒髪天を突きまなじりことごとく割くほど難しく、ありえんほど間違いやすい(トランポリンコンテキスト?) やり方があまりにも間違いやすく、プラットフォーム(時にはドライバー)ごとに違うので、俺は絶対にglXやwglなんかのAPIを直接使うことはやめて、SDLやGLFW(あるいは、GLEWのようなもので関数/拡張のポインターを取得するもの)のようなライブラリを使うことをおすすめしている。
ちょっと使うだけなのに、膨大なサードパーティライブラリの中からどれかを選んで使えとかいうのが当然になっている現状はクソだ。APIはもっと単純にして標準化して、ただ使うだけでサードパーティライブらいが必要になることがないようにしろよ。
スレッドのコンテキストは暗黙にthisポインターであるかもしれないということ
GetProcAddress()で取得した関数ポインターは、コンテキストに強く結びついているため、グローバルに使えない(GL風に言えば、コンテキスト依存とコンテキスト非依存)。要するに、GetProcAddress()をあるコンテキストで呼び出して、戻り値の関数ポインターを他所で使うと、なにか悪いことが起こるか、単にクラッシュする。
GLってC APIなのかよ? 違うのかよ?
このクソ、もっと単純化して標準化できないのか?
glGet() APIの欠点
これはちょっとトレースの都合かも知れないが、しかし、APPIのトレースが難しいことにより、ツールがクソであるか存在しないとすれば、開発者としては麺道に鳴るので、通常の開発者にも間接的に悪影響を与えるのだ。
glGet() APIシリーズ(glGetIntergerv, glGetTexImage, 等など)は"max_size"引数がない。そのため、ドライバーは渡された引数や、グローバルコンテキストの状態によって、渡されたバッファーをオーバーライトすることが可能である。この関数群は"max_size"引数を受け取るべきであり、max_sizeが小さすぎた場合、関数は失敗するべきである。メモリをオーバーライトすべきではない。
テクスチャーバッファーのサイズを計算するために、ドライバーがグローバルコンテキスト状態に依存して読み書きするのは、クソすぎる。
glGet()に渡せるpname enumには数百種類もある。いくつかは、特定のドライバーでなければ受け付けない。トレーサーとかのデバッグヘルパーを作るとき、あるpname enumで値がいくつ書き込まれるか、あるいは十分な(ロスレスな)型とかいった公式な表が一切ないときている。
それと、APIの初心者には、indexedとnon-indexedなgetsとsetsの挙動は、簡単にわからない。
glGet()メタデータAPIを付け加えるか、表を作るかしろ。
glGetError()
Win32みたいにglSetLastError() APIは存在しないので、トレースが無駄に複雑になる。一度も呼ばないアプリがある。1フレームに一回呼ぶアプリがある。リソース作成の時にしか呼ばないアプリがある。初期化の際に何前回も読んで、その後は二度と呼ばないアプリがある。筆者は、あるい有名なGLアプリが、毎フレームGLエラーを出しているのをみたことがある(これって普通のことか? 開発者は知ってるのか?)
テクスチャーターゲットとかの重要な情報を取得できない
(現在策定中のものもあるってのは知ってるよ。Cass君)。これにより、トレースやスナップショットが、shadowing()により難しくなる。shadowingはglGetError()に影響する(呼び出しが成功したと知るまで、shadowをアップデートできない。呼び出しが成功したかどうかを確かめるには、glGetError()を呼び出す必要がある。これは、コンテキストの現在のGLエラーを吸収してしまうので、トレースされているアプリからみるGLエラーに影響を与えないよう、ヘンテコな仕組みが必要になる)
glGet()をすべて廃止しようという最近の議論について:思うに、すべての状態情報は取得可能であるべきだ(今や当然だろ)、さもなければ、APIはD3D12やMantleみたいに、最高のパフォーマンスのスケーラビリティを念頭に設計されているべきだ。D3D12やMantleのようなAPIによる付加価値は、そういう極端な用途では、理解されている。
glGet()の類を廃止することは、トレーサーとコンテキストスナップショットの仕事をさらに面倒にするだけだ。
DSA(Direct State Access) APIは未だに標準ににないし、使えないしサポートされてない
DSAは、一部のアプリの呼び出しのオーバーヘッドを劇的に改善してくれる(たとえば、Source1のGLバックエンド)。global stateへの依存をやめてくれよ。頼むし、DSAを標準に入れやがれ。
正式規格は2014年に完成しない
XML規格には、依然として強い型付けがされた引数の情報が欠けている。例えば、
<command> <proto>void <name>glBindVertexArray</name></proto> <param><ptype>GLuint</ptype> <name>array</name></param> <glx type="render" opcode="350"/> </command>
筆者の知る限り、apitraceのglapi.pyが、いまだに唯一の公開された信頼できる情報である。
GlFunction(Void, "glBindVertexArray", [(GLarray, "array")]),
glapi.pyは型を"GLarray"にしているところに注目。公式規格は、よくわからない"GLuint"型にしている。
glGet info()を公式規格に、上記のような感じで入れろ。pname enumが返す値はいくつだ? ドライバーが埋め込んでるshadowまで含めてロスレスに値を維持できる型はなんだ? このpnameはindexed版で使っていいのか?
今週のGLSLバージョン地獄
昔のバージョンでは、GLSLバージョンは、最初に定義されたGLバージョンと同じではなかったので、無駄によくわからないことになっていた。これはGLSL拡張(GL拡張ではない)以前の話で、それも含めると、初心者置いてけぼりだ。
D3DXライブラリやツールに相当する標準や公式のものがGLにはない
ドライバーやGLコンテキストに依存しない、テクスチャーや日くせええるフォーマット変換ツール。
KTXフォーマット地獄:KTXフォーマット(DDSのGL版)を読み書きできるツールは少ない上に、常に相互に読み書きできるわけでもない。
開発者は、Direct3DのDXTEXの同等品を、ソース付きで必要としている。
KTXサンプルは、KTXファイルをGLテクスチャーに読み込む方法ぐらいしか示していない。KTXファイルと他のDDSとかPNGとかの標準的なフォーマットとを相互変換するツールが必要だ。
そういうライブラリにはGLSLコンパイラーも含まれているべきだ(HLSLシェーダーをD3DXでコンパイルできるのと同じように)
GL拡張は公式規格へのdiffで書かれている
OpenGL規格エキスパートでもない限り、拡張を理解するのはめちゃくちゃ難しい。
関連:公式規格はあまりにも広範な読者向けに書かれている。ほとんどの規格読者は、これをパースできるほどのエキスパートではない。規格は開発者にやさしい版の規格と、ドライバー開発者のための詳細な規格に分割するべきだ。拡張は規格に対する純粋な差分となるべきではない。そんなんじゃ理解できないだろ。
ドキュメント地獄
20年間ものGL APIのゴミ解説が転がっていて、Google検索のノイズになっている。初心者はクソだったり古臭かったりするドキュメントやサンプルにぶち当たってしまう。
MakeCurrent()地獄
一部の拡張(てめーのことだ、NV bindless texturing!)のせいで、追加の隠し変数のコストが、クッソ高くつくことがあるわ、glBegin/glEndに囲まれた中で呼ぶと、ドライバーをクラッシュさせるわ(GPUすらクラッシュさせる)
この関数呼び出しの挙動とパフォーマンスは、もっとまともに規格化されて、開発者の間で議論されるべきだ。
ドライバーは、APIが未定義の方法で呼び出されたとしても、GPUやCPUをクラッシュさせたりロックアップしたりすんな
いい加減分かれよ。まともなテスターを雇ってドライバーをぶっ叩け。
いやまて、もっとマシな方法があるぜ。APIを構造化して、APIで表現可能な未定義や危険なパターンを削減しやがれ。
複数のコンテキストが絡むオブジェクトの削除、コンテキスト間の参照カウントのルール、ゾンビオブジェクト
削除するオブジェクトが別のコンテキストにも束縛されてたとしたら、まあ、ご愁傷さまってこった。
削除されたオブジェクトに対してglGetを呼び出したら(別の場所で束縛されてたりするので、まだ半分生きてる)、挙動はドライバーごとに異なる。
こういうことが全部、無駄にコードを複雑にしたりオーバーヘッドを生じさせる。
100%信頼できるスナップショットやGLコンテキスト状態の差し戻しがクッソ難しくなる。
世界級の開発者が、知らずのうちにヘマをやらかしている。明らかに、APIやツール環境がぶっ壊れてる証拠だ。
シェーダーコンパイルとリンク地獄
シェーダーのコンパイルとリンクのパフォーマンス上の問題がこれだ。
シェーダープログラムをトークン分割しておくのは有益なことだ。Direct3Dは、そのいい証拠だ。素のGLSLがあふれかえっているのは、開発者がD3Dで書かれたものを移植した結果であるし、その結果としてエンドユーザーは糞遅いロード時間にみまわれていてありえん。そのくせ、GLはいまだにテキストのGLSLシェーダーしか受け付けない。
ドライバーごとに(訳注:シェーダーコンパイルの)パフォーマンスは極端に異なる。あるドライバーでは、シェーダーのコンパイルは、実質無料同然であるが、別のコンパイラーでは、クッソ高くついたりする。
プログラムのリンクは、あほみたいに時間がかかる。
リンク済みプログラムをキャッシュするドライバーもあれば、そうでないドライバーもある。
プログラムをリンクする時間は、予測不可能だ。プログラムがキャッシュされていれば速いが、プログラムがキャッシュされているかどうかを取得する方法がない。そもそも、プログラムのキャッシュをドライバーがサポートしているか取得する方法もない。
マルチスレッドコンパイルをサポートしているドライバーもあれば、そうでないドライバーもある。ドライバーがマルチスレッドコンパイルをサポートしているか取得する方法はない。
APIがクソすぎるため、トレースやスナップショットが困難になっている。シェーダーはリンクされた後にデタッチされる。リンクされたプログラムの状態の多くが、取得不可能だ。トレーサーによるリンク時間の隠匿が必要になる。
D3Dでやってることをコピペしろよ(いいか、D3Dのやり方はちゃんと動くし、開発者も理解できるんだから)
トレース、リプレイ、スナップショット、リストアの難しいAPI
ツール環境にとって悲惨で、最終的にまわり回ってAPIのユーザーを苦しめることになる。
APIは簡単にトレース、リプレイ、スナップショットできることを念頭に設計されるべきだ。さもなければ、MantleやD3D12のように、極端なパフォーマンスとスケーラビリティを重視した設計にすべきだ。
現在、GLはそのどちらの側面も備えていない。あらゆる価値観からみて、悲惨な立ち位置にいる。
API設計者は、物事がいかにして動くべきかとか、俺らめっちゃ賢いからD3Dとはまるっきり違ってたほうがいいんだとかいうことではなく、APIが実際にもたらす価値を考えるべきだ。
GL関数の終わりなき迷路(何千もありやがる!)
おい、本当に何十個ものglVertexAttrib系が必要なのかよ? そもそも、このAPIって誰が使ってんだよ?
APIは再設計と単純化が必要だ。S/N比を上げろ。頼むし。
v3.x APIから移行する際のレガシーの厄介さ
「前方互換」、「互換性」 VS 「コア」プロファイルとかなんとか
一介の開発者は、単にAPIを使ってシェーダーでトライアングルを描画する程度のことに、こんなことを極める必要に迫られるべきじゃねーよ。
「コア」なんて用語は使われるべきですらねーんだ。
パイラインをストールさせずにDISCARDセマンティクスつきでバッファーをロックする信頼できる方法が全ドライバーで提供されるべき
mapフラグって使ってるか? BufferData()をNULLで読んでるか? 両方やってる? どっちかだけ?
どのロックフラグを使ってる、いや、どのフラグ使ってる? いやちょっとまて、ドライバーはフラグを完全に無視してねーかこれ?
D3Dでは当然だったことが、GLでは難しい。エキスパートとなるか、ドライバー開発者と直接コンタクトとれないと厳しい。
これはクッソ重要なんだぞ!
ドライバー開発者に告ぐ。本物のドライバー開発者と、開発者もどきを分かつのは、このへんのところをいかに正しく実装してテストするかにかかっているのだ。この2014年において、パイプラインのストールは許容できん!
BufferSubData()はマルチスレッドドライバーで「多すぎる」データを食わせて呼び出すとストールする
「多すぎる」っていうのがどの程度なのか取得する方法はないときている。4k? 8k? 256k?
パイプラインのストール
公式の方法、非公式の方法を含めて、コールバックとかデバッグメッセージとかで、いつドライバーが諦めて、レンダリングスレッドに馬鹿でかいパイプラインストールをこしらえるのか判断するのかということを、取得する方法がない。
これはレンダリングボトルネックの最大の原因であるにも関わらず、計測のためのツールが一切存在しない(そのようなツールを作るためのAPIもない)
マルチスレッドドライバー地獄
一部の製造業者は、開発者が入念にテストして出荷した後の製品に対して、バグだらけのマルチスレッドドライバーを強制的に個別に自動有効している(なお悪いことに、開発者に告げもせずに「アプリプロファイル」とやらを変更したりしている)
あるマルチスレッドドライバーは、マルチスレッドが有効なときはglGet()が不具合だらけになって、スナップショットが悲惨すぎることになる。
ドライバーがマルチスレッドを使うかどうか取得、設定する公式の方法はない。
トレーサーが有効になっていて、大量のglGet()呼び出し(通常のアプリは行わないこと)を行うということをドライバーに通知する方法もない。
ドアホなマルチスレッドドライバーは、トレーサーがglGet()を発行するとクッソ激遅になった上に停止する(ヒューリスティックか何か使って、自動的にマルチスレッド無効にしろよ!)
一部のドライバーで、タイムスタンプの取得はパイプラインをストールさせる
クロスプラットフォーム環境で、信頼できるGPUプロファイリング目的に使えなくなる。GL規格は、このようなクエリーに対して、いつドライバーはストールを引き起こしていいのかどうかを強く規程すべきだ。無意味にストールするのは、ドライバーのバグ(このちっぽけなAPIがいかに重要であるかを理解していない無能なドライバー開発者のバグ)であると定義すべきだ。
参考のために書くと、NVidiaは正しくやっている。読者がパイプラインクエリーコード周りのドライバーを書いているのならば、頼むし、出荷する前に、お前の実装をNVidiaのドライバーと比較するぐらいのことはしやがれ。
GLは、実質X個の別々のAPIが(ドライバーごとにひとつ、時に、プラットフォームごとにひとつ)、単一のAPIのごとくよりあつまっているということ。
(マルチスレッドモードと非マルチスレッドモードを含む)すべてのドライバーで製品の動作の正しさとパフォーマンスを入念にテストしなければ、GL製品を出荷できない。ドライバーの違いには驚かされるだろう。D3Dで長年やってきた筆者にとって、これは衝撃の事実だ。
これはつまり、Khronosには、何か強制力のあるドライバーのテストや検証をやってもらう必要があるということだろう。GLはWHQL認証のようなものを必要としている。
拡張地獄
GLで喧伝される利点として、拡張のサポートが挙げられる。拡張はAPI全体を蝕むものであり、利点とはならない。
筆者は、この一年半ほど、大小規模の大量のGLのコールストリーム[訳注:一連のGL関数の呼び出しのこと]に関わってきた(それ以前には、筆者はtoglをすべてのドライバーで動かして出荷可能にする仕事をしていた。その2年以上にいかに辛酸を嘗めたかは、筆舌に尽くしがたい)。ドライバー開発者を除けば、筆者は世の中のGL開発者の誰よりも、現場で使われているGLコールストリームに触れてきた。残念ながら、多くの場合、高度でモダンな拡張というのは、かろうじて動くレベルのものだ(時に、ベンダー自身が、その事実を認めることもある)。あるいは、一見かっこよさそうにみえる拡張を使おうとすると、すぐにほとんど使われていない(つまりテストされていない)ドライバーのコードパスに潜り込んで、実用にならないことに気がつくであろう。
コールストリームの研究と仕事を続けるうちに、明らかに、開発者は現在使われていて実行環境でもあるドライバー群に対して、実装する機能としては、大胆なMIN()を行っている。これはたいてい、コアプロファイルv3.xとかで、ひょっとしたら4.xバックエンドの中のとても単純で安全な一部の操作を含むだけだ(あるいは、今がまだ1998年か2003年でもあるかのように、互換GLを使い続けている小さなゲームタイトルとか、だって、必要なものは全部揃っているしね)。開発者は、ほとんどの拡張を、わざわざ使ったりしない(特に、モダンな奴はなおさらだ)。というのも、その手の拡張は、信頼性に足るほど動かない(その機能を実装するドライバーのコードパスは、本物のアプリに対してテストされていないからだ。古典的な鶏と卵問題である)か、単にサポートしているのが立った一つのドライバーだけであるか、あるいはその拡張利用よる付加価値が、製品のテストの規模などを拡大するのに見合わないからだ。
さらに、この点のモダンな拡張は、トレースが極めて難しい。つまり、開発者が使っているデバッグツールに互換性がない。そこで、フォールバック[訳注:拡張を使わない実装]も提供しなければならない。どうせ開発者がフォールバックも実装しなければならないとするならば、最初からそのフォールバックを使った製品を出荷して(全部の環境で動くしな)、拡張なんて気にしなければいいだけの話だ(製品に超すごい付加価値が生まれるのでもなければだ)
というわけで、拡張なしのGLでなければ、信頼性と正しく動く製品を求める開発者の大勢を惹きつけられないのだ。
ドワンゴ広告
この記事はドワンゴ勤務外に会社の電力を無駄使いして書かれた。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0