ソフトウェアエンジニアリングでは、グローバルな状態が悪いことは一般的に理解されています。ただし、OpenGLは、グローバルステートの概念を非常に取り入れて設計されています。変更することの多くはプログラム内のすべてに影響します(アクティブシェーディングプログラム、ユニフォーム、アクティブバッファー、glDepthTest ...)
そのため、「グローバルな国家と闘わないで、受け入れる」という哲学を身につけました。 OpenGLプログラムを設計するときは、各関数とオブジェクトがレンダリングされる直前に状態を設定する必要があり、renderコマンドを発行する前に何も仮定できないと想定して設計します。特定の深度テストが必要な場合は設定し、特定のシェーディングプログラムが必要な場合は設定し、特定の均一な値が必要な場合は設定します。明らかに、これは主にヒューリスティックです。あらゆる機能に毎回すべてを設定することは恐ろしい設計になるためです。
しかし、私は(正当な理由により)完全にグローバルステートに反対している開発者と協力していることに気づきました。それらの設計は、小さなオブジェクトのセットをシングルトンとして定義し、可能な限りパラメーターとして値を渡すことを中心に展開します。最終結果として、インテンション(「レンダリング」など)とアクション(glDrawArraysの呼び出しなど)の間に5回以上の関数呼び出しが発生することがあります。
たとえば、アクションを実行するのが簡単、明確、リファクタリングの点で、私にとって最適な場所は、グローバルオブジェクトの関数を呼び出して変数の値を変更することにより、グローバル状態を回避するというこの原則に違反するようです。その変数を渡そうとするのではなく、これは最良の設計についての議論につながりました。
問題は、OpenGLで作業するときは、一般的なヒューリスティックに従い、グローバル状態を可能な限り回避することは良い考えですか、これはルールの例外であり、その設計上、グローバル状態を受け入れることは、この1つのインスタンスの方が良いですか?
OpenGLは、技術的な問題に対する賢明な解決策の1つであったため、グローバルな状態を「受け入れ」ませんでした。ビデオカードとの通信は高価です。ビデオメモリのビットを反転すると、通常のメモリのビットを反転するよりもかなり高価になります。例として、それは非常に重要です。OpenGLの以降のバージョンは即時モードを廃止しました。これは、バッファーを設定する(つまり、保持モード)ため、それらのバッファーからのレンダリングが非常に効率的であり、より複雑な設定に値するものでした。どの程度の通信が行われる必要があるため、レンダリングシステムの構成全体をフレームごとに「渡す」ことは現実的ではありません。
対照的に、多くのWebサーバーの状況では、実際に渡されるのは8バイトのポインターだけなので、リクエストの状態全体を含む非常に複雑なデータ構造を渡すことは珍しくありません。
したがって、グローバル状態の問題は、誰がいつ変更しているかを知るのが難しいことであり、その状態が何であるかを推論するのは難しい場合があります。したがって、やるべきことは、明確に定義されたプロセスで、明確に定義された方法でのみその状態を変更するようにアプリケーションを設定することです。
レンダリングの状態を、たとえばデータベース駆動型アプリケーション。多くの点で、データベースはグローバルな状態です。ただし、そのDBへのインターフェイス、および多くの場合DB自体は、いつ、どのように変更されるかを強制し、データが内部でどのように見えるかを強制します。
似たようなものを実装する簡単な方法の1つは、アプリケーション内でレンダラーの状態を追跡するインターフェイスを用意することです。特定の方法でレンダラーを設定する必要があることがわかっているので、グラフィックスカードに問い合わせる必要なく、インターフェースを確認できます。同様に、レンダラーを最も効率的な方法で正しい状態にするために何を変更する必要があるかを決定することもできます。
3Dグラフィックスエンジンの構築は非常に複雑で幅広いテーマであるため、正確なポインタやコードサンプルを提供することはできません。ただし、基本的な要点は、アプリケーションですべてのグローバル変数を使用するのではなく、3dカードのグローバル状態をCPU側の限定スコーププログラミングモデルと適切にマージする方法を考え出す必要があることです。
あなたの特定の例では、他の開発者がいくつかのシングルトンを定義して渡していると言うと、私は非常に疑わしくなりますが、何が起こっているのかを詳しく見ないと、詳細にコメントすることはできません。
そのため、「グローバルな国家と闘わず、受け入れる」という哲学を身につけました。 OpenGLプログラムを設計するときは、各関数とオブジェクトがレンダリングの直前に状態を設定する必要があり、レンダリングコマンドを発行する前に何も仮定できないことを前提に設計します
実際、私は「グローバルな状態を受け入れる」という考えを、あなたとはまったく逆の方法で解釈しています。あなたがしようとしているのは、副作用を最小限に抑え、参照の透明性に向けて取り組み、時間的な結合を緩めることです。非常に大雑把に言えば、レンダリングする新しいタイプのものが同僚に紹介されたときに、他のものが誤ってレンダリングを開始する原因となるような方法でグローバル状態を台無しにしないようにする必要があります(これは通常、一種のトリップポイントであり、 OpenGL状態の管理不良に関連するバグ)。
私がFilip Milovanović's
コメントから借りることができれば、私がそれを解釈する方法はこれに近いです:
[...]状態の変更にはコストがかかります(ただし、コストは異なります)。バッチで処理されるようにデータを整理することで(たとえば、データを並べ替えたり、タイプ別にデータを個別の配列に保持したり)、それらを最小限に抑えたい場合があります。 )[...]
別の言い方をすれば、GL状態をローカルで単一のオブジェクトをレンダリングする関数と同じように管理することからできるだけ遠くに移動し、代わりに「レンダリングパス」の類推で状態を設定します。特定のパスは、特定のシェーダーとテクスチャのセット、および深度テストのためのいくつかの特定の状態を使用する可能性があります。次に、その特定のレンダリング内でできるだけ多くの関連オブジェクトをレンダリングしようとしますすべて同じ状態を利用できるパス。
そのためには、事前に予期する特定のオブジェクトのレンダリングニーズについて、もう少し考えてコラボレーションする必要があります。また、場合によっては、同じ状態の要件に基づいてこれらを整理してグループ化するデータ構造も必要です。しかし、このようにソフトウェアを構築できれば、パフォーマンスソリューションと最小限の状態(誤)管理の両方の理想的なバランスにつながると思います。また、固定パイプラインから離れるほど、最初に管理するグローバルな状態が大幅に少なくなります(まだ行っていない場合)。
[...](正当な理由により)グローバルな状態に完全に反対する開発者と協力している自分を見つけてください。それらの設計は、小さなオブジェクトのセットをシングルトンとして定義し、可能な限りパラメーターとして値を渡すことを中心に展開します。
最初の2、3のステートメントでは、計算がうまくいきません。状態のあるシングルトンはグローバルステートであるため、ステートレスで変更できない場合を除いて、グローバルステートに反対する人が誰でも喜んでより多くのシングルトンをミックスに導入する理由はわかりません。パラメータの受け渡しとDIは、グローバルの代替手段として私には理にかなっています。
その変数を渡そうとするのではなく、それは最良の設計についての議論につながりました。
物事を渡すことは、それがグローバルの厳密な代替手段である場合、クリーンなコーディングの観点から一般により理想的です。しかし、OpenGLを使用すると、状態を変更するというパフォーマンスの問題と、それが必然的にグローバルになるという事実が組み合わさるので、呼び出しスタックにのみパラメーターを渡す場合、作業に関して少なくともかなりの冗長性があります。最終的にそれらを使用して、レンダリング前にグローバル状態を設定します。デザインの全体的なパフォーマンスコストに関係なく、正確さが最大の関心事である場合は、シングルトンがここに含まれる理由はわかりませんが、それでも妥当なことかもしれません。
前に述べたより理想的なソリューションにたどり着くような非常に最適でないシナリオで作業している場合、チーム間のコラボレーションはかなり緩やかであり、罰則と組み合わされます(これらの組み合わせは残念ながら慣れていない)、それから私が以前に遭遇した解決策がありますが、実際にはそれほど悪くはありませんでした。
私はコードベースで約20年前に巨大なレガシーで純粋な固定パイプラインを使用して作業しましたGLコード、そしてOpenGLコードはどこにでもありました:一般化されたユーティリティライブラリにOpenGL呼び出しがありました。マテリアルライブラリ、ジオメトリライブラリ、至る所にあるGUIコールバック。元の開発者が、バグにパッチを当てて状態をいじっていたということを除いて、カオスが与えられた場合に何が正しくレンダリングされたかを知りません。しかし、新しいレンダリングコードの導入には地雷がつきものであり、より理想的なソリューションに向けて大規模なコードベースをいじろうとする試みは境界線がないように思われたため、同僚はRAIIに依存したかなり実用的なソリューションにたどり着きました。 C++。
彼らはこのようなことをしました:
struct GlLineWidth: noncopyble
{
GlLineWidth(float new_width) {glLineWidth(new_width);}
~GlLineWidth() {glLineWidth(1.0f);}
};
次に、次のような呼び出しを置き換えました。
glLineWidth(3.0f);
... これとともに:
GlLineWidth line_width(3.0f);
代わりにglGet*
を使用して、コンストラクターで状態をキャプチャし、デストラクターで以前の状態を復元することもできます(特にネストされたケースでは、より堅牢になるでしょう)が、私の同僚はそのように設計したと思いますシステムを常にデフォルトの状態にしようとする方法として(例:非常に一時的に設定されていない限り、ワイヤーフレームレンダリングの線幅は常に1であることが期待できます)。
そして、それは最もきれいではありませんが、そのオブジェクトがスコープから外れると、線の幅は1に戻されます。そして、関連するすべてのGLに対してそれを行い、固定パイプラインがいじっていた、これにより、新しいコードを記述し、関数の終了時に(通常または例外の結果として)関数の終了時に元に戻される状態の予測に関して、コードベースをより予測しやすくなりました。
これは、状態の変化を最小限に抑えるという考えをすぐに排除する最悪のシナリオの次善のソリューションですが、実際にはコード(およびその変更)の予測可能性を大幅に改善し、人的エラーの量を減らしました。それでも、最終的にグローバル状態を変更するために使用されるパラメーターのボートロード(またはそれらの多くを示すオブジェクト)を渡そうとするよりも、状態の変更が少ない場合があります。