21

OpenGLVBOの実装で次のマクロが使用されているのを見てきました。

#define BUFFER_OFFSET(i) ((char *)NULL + (i))
//...
glNormalPointer(GL_FLOAT, 32, BUFFER_OFFSET(x));

このマクロがどのように機能するかについて少し詳しく教えてください。関数に置き換えることはできますか?より正確には、NULLポインターをインクリメントした結果はどうなりますか?

4

4 に答える 4

34

OpenGLのひどい歴史を振り返ってみましょう。昔々、OpenGL1.0がありました。あなたは絵を描くのに使っglBeginglEndいました、そしてそれがすべてでした。高速描画が必要な場合は、ディスプレイリストに物を貼り付けます。

次に、誰かがオブジェクトの配列を取得してレンダリングできるようにするという素晴らしいアイデアを思いつきました。このようにして、OpenGL 1.1が誕生しました。これにより、などの機能がもたらされましglVertexPointerた。この関数が「ポインタ」という単語で終わっていることに気付くかもしれません。これは、glDraw*一連の関数の1つが呼び出されたときにアクセスされる実際のメモリへのポインタを取得するためです。

さらに数年早送りします。現在、グラフィックカードはそれ自体で頂点T&Lを実行する機能を備えています(この時点まで、固定機能T&LはCPUによって実行されていました)。これを行う最も効率的な方法は、頂点データをGPUメモリに配置することですが、ディスプレイリストはそのために理想的ではありません。それらはあまりにも隠されており、あなたがそれらで良いパフォーマンスを得ることができるかどうかを知る方法はありません。バッファオブジェクトを入力します。

ただし、ARBには、すべてを可能な限り下位互換性のあるものにするという絶対的なポリシーがあったため(APIの外観がいかに馬鹿げていても)、これを実装する最善の方法は、同じ関数を再度使用することであると判断しました。glVertexPointer今だけ、の動作を「ポインタを取得する」から「バッファオブジェクトからバイトオフセットを取得する」に変更するグローバルスイッチがあります。そのスイッチは、バッファオブジェクトがにバインドされているかどうかGL_ARRAY_BUFFERです。

もちろん、C / C ++に関する限り、関数は依然としてポインターを取ります。また、C / C ++の規則では、整数をポインターとして渡すことはできません。キャストなしではありません。そのため、のようなマクロがBUFFER_OBJECT存在します。これは、整数バイトオフセットをポインタに変換する1つの方法です。

この(char *)NULL部分は、単にNULLポインター(通常はCではavoid*であり、C ++ではリテラル0)を受け取り、それを。に変換しchar*ます。で+ iポインタ演算を実行しますchar*。ヌルポインタは通常ゼロアドレスを持っているので、iそれに追加するとバイトオフセットが増加し、そのi値が渡したバイトオフセットであるポインタが生成されます。

もちろん、C ++仕様では、BUFFER_OBJECTの結果が未定義の動作としてリストされています。それを使用することにより、あなたは本当に合理的なことをするためにコンパイラに依存しています。結局のところ、ゼロである必要NULLはありません。すべての仕様は、それが実装定義のnullポインタ定数であると述べています。ゼロの値である必要はまったくありません。ほとんどの実際のシステムでは、そうなります。しかし、そうする必要はありませ

だから私はキャストを使うだけです。

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48);

どちらの方法でも動作が保証されるわけではありません(int-> ptr-> int変換は条件付きでサポートされており、必須ではありません)。ただし、「BUFFER_OFFSET」と入力するよりも短くなります。GCCとVisualStudioはそれが合理的だと思っているようです。また、NULLマクロの値に依存しません。

個人的には、もし私がもっとC ++の衒学者だったら、それを使うでしょうreinterpret_cast<void*>。でも僕はそうじゃない。

または、古いAPIを捨てて、etを使用することもできます。glVertexAttribFormatal。、これはあらゆる点で優れています。

于 2011-11-27T05:32:51.897 に答える
28
#define BUFFER_OFFSET(i) ((char *)NULL + (i))

技術的には、この操作の結果は未定義であり、マクロは実際には間違っています。説明させてください:

Cは、ポインターを整数、つまり型にキャストできることuintptr_t、およびその方法で取得された整数が元のポインター型にキャストバックされた場合に元のポインターを生成することを定義します(C ++はそれに続きます)。

次に、ポインタ演算があります。つまり、同じオブジェクトを指す2つのポインタがある場合、それらの差をとることができ、(タイプのptrdiff_t)整数になり、その整数を元のポインタのいずれかに加算または減算すると、他の。また、ポインタに1を追加することにより、インデックス付きオブジェクトの次の要素へのポインタが生成されることも定義されています。また、同じオブジェクトのポインタでuintptr_t割った2つの差はsizeof(type pointed to)、減算されるポインタ自体と等しくなければなりません。そして最後になりましたが、uintptr_t値は何でもかまいません。それらは不透明なハンドルでもあり得ます。それらはアドレスである必要はありません(ただし、ほとんどの実装では、それが理にかなっているため、そのようになります)。

これで、悪名高いnullポインタを見ることができます。uintptr_uCは、型値0からforにキャストされるポインターを無効なポインターとして定義します。ソースコードではこれは常に0であることに注意してください。バックエンド側では、コンパイルされたプログラムでは、実際にマシンにそれを表すために使用されるバイナリ値は、まったく異なるものになる可能性があります。通常はそうではありませんが、そうである可能性があります。C ++は同じですが、C ++ではCよりも多くの暗黙的なキャストが許可されていないため、明示的に0をにキャストする必要がありvoid*ます。また、nullポインターはオブジェクトを参照しないため、逆参照されたサイズポインターがないため、nullポインターの演算は未定義です。オブジェクトを参照しないnullポインターは、型付きポインターに適切にキャストするための定義がないことも意味します。

では、これがすべて未定義の場合、なぜこのマクロが機能するのでしょうか。ほとんどの実装(コンパイラを意味する)は非常に騙されやすく、コンパイラコーダは非常に怠惰であるためです。ほとんどの実装におけるポインターの整数値は、バックエンド側のポインター自体の値にすぎません。したがって、nullポインターは実際には0です。nullポインターのポインター演算はチェックされませんが、ほとんどのコンパイラーは、ポインターに何らかの型が割り当てられている場合、それが意味をなさない場合でも、黙って受け入れます。charあなたがそう言いたいのであれば、Cの「ユニットサイズ」タイプです。したがって、キャストでのポインタ演算は、バックエンド側のアドレスでの演算のようなものです。

簡単に言うと、C言語側のオフセットとなるように意図した結果を使用してポインターマジックを実行しようとしても意味がありません。そのようには機能しません。

少し戻って、実際にやろうとしていることを思い出してみましょう。元の問題は、gl…Pointer関数がデータパラメータとしてポインタを受け取ることでしたが、頂点バッファオブジェクトの場合、実際にはバイトベースのオフセットを指定する必要があります。数値であるデータ。Cコンパイラーにとって、関数はポインターを受け取ります(私たちが学んだように不透明なものです)。正しい解決策は、特にVBOで使用するための新しい機能の導入でした(たとえば、gl…Offset私はそれらの導入のために集会を開くつもりだと思います)。代わりに、OpenGLによって定義されたのは、コンパイラーの動作の悪用です。ポインターとそれに相当する整数は、ほとんどのコンパイラーによって同じバイナリ表現として実装されます。したがって、私たちがしなければならないことは、コンパイラーgl…Pointerにポインターの代わりに私たちの番号を使用してそれらの関数を呼び出させることです。

したがって、技術的には、コンパイラに「はい、この変数aは整数だと思います。あなたは正しいと思います。その関数glVertexPointerは、そのデータパラメータに対してのみをvoid*使用します。しかし、推測してください。その整数が生成されました。 "から、void*それをキャストしてから(void*)親指を保持することにより、コンパイラは実際には非常に愚かで、整数値をそのまま渡すことができglVertexPointerます。

したがって、これはすべて、古い関数のシグネチャをなんとか回避することになります。ポインタをキャストするのは私見の汚い方法です。私は少し違うことをします:私は関数の署名を台無しにします:

typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t);
TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer;

myglVertexOffsetこれで、ばかげたキャストを行わずに使用できるようになり、オフセットパラメータが関数に渡され、コンパイラが混乱する可能性があります。これは、私がプログラムで使用する方法でもあります。

于 2011-11-27T10:12:59.137 に答える
2

これは「NULL+int」ではなく、「「charへのポインタ」型へのNULLキャスト」であり、そのポインタをiだけインクリメントします。

はい、それは関数に置き換えることができます-しかし、それが何をするのかわからないのなら、なぜあなたはそれを気にするのですか?最初にそれが何をするかを理解し、次にそれが関数としてより良いかどうかを検討します。

于 2011-11-27T04:53:42.200 に答える
2

openGL頂点属性データは、コンテキストに応じて、メモリ内のポインタまたは頂点バッファオブジェクト内にあるオフセットと同じ関数(glVertexAttribPointer)を介して割り当てられます。

BUFFER_OFFSET()マクロは、整数バイトオフセットをポインターに変換して、コンパイラーがそれをポインター引数として安全に渡すことができるようにするように見えます。「(char *)NULL + i」は、この変換をポインター演算で表します。sizeof(char)== 1と仮定すると、結果は同じビットパターンになるはずです。これがないと、このマクロは失敗します。

単純な再キャストによっても可能ですが、マクロを使用すると、渡される内容がスタイル的に明確になる場合があります。また、32/64ビットの安全性/将来性を確保するためにオーバーフローをトラップするのに便利な場所です。

struct MyVertex { float pos[3]; u8 color[4]; }
// general purpose Macro to find the byte offset of a structure member as an 'int'

#define OFFSET(TYPE, MEMBER) ( (int)&((TYPE*)0)->MEMBER)

// assuming a VBO holding an array of 'MyVertex', 
// specify that color data is held at an offset 12 bytes from the VBO start, for every vertex.
glVertexAttribPointer(
  colorIndex,4, GL_UNSIGNED_BYTE, GL_TRUE,
  sizeof(MyVertex), 
  (GLvoid*) OFFSET(MyVertex, color) // recast offset as pointer 
);
于 2011-11-27T04:54:22.420 に答える