PDA

View Full Version : How do you write "bulletproof" OpenGL apps?



Duncan Champney
03-30-2008, 05:36 AM
I'm still pretty new to OpenGL.

My app, a high performance fractal renderer for Macintosh called FractalWorks (http://www.pbase.com/duncanc/fractal_images), creates large polygon meshes, and saves them to VRAM as a display list. (the meshes are created using triangle strips.)

The meshes can be arbitrarily large. I support the option to render a very large view of the mesh to disk as a JPEG or TIFF, and have figured out a way to save arbitrary-sized images without running out of VRAM. (By rendering a series of tiles sized to the current window frame and stitching them together in main memory.)

My app currently crashes if the user tries to render a fractal that has too many vertices. That's unacceptable.

I need a way to figure out how big a mesh a user's computer will be able to handle, so I can put up a polite message if they try to create a 3D view of a fractal that's too big.

How do I estimate the amount of VRAM that my display list takes to build and render, and what's a good rule of thumb for how much of total system VRAM I can use without bringing down the machine?

I'm thinking that I could count the number of vertexes I submit, times 3 for the x, y, and z component, times the size of a float, plus 4 bytes for the color of each vertex (RGBA).

I'd like to have 2 messages that I tell the user, depending on how much of VRAM the image will use up.

The first would be something like this: "Your image is quite large. Creating a 3D view will slow down your computer. Would you like to proceed?"

The second would say something like this: "Your image is too large to render in 3D on this computer. Try rendering it on a computer with more video memory."

Also, would I be able to render a larger mesh (albeit more slowly) by NOT creating a display list, but just submitting the triangle strips directly? It's my understanding that when you draw directly, the commands are issued from main memory, not VRAM.

I'm not sure if this post is more appropriate here, or in the Macintosh forum. The crashes I'm experiencing are clearly specific to the drivers on my particular platform, but I would think that the memory management issues would be universal to all machines using OpenGL.

V-man
03-30-2008, 07:01 AM
I would think that the memory management issues would be universal to all machines using OpenGL
The issues are the same : how to know how much VRAM a system has, how big can you make a display list, how big you can make a VBO.

There isn't a platform independent way to know how much VRAM there is and in your case you don't need to know.
The driver should detect there is a problem and should return GL_OUT_OF_MEMORY.

I don't know if you are making 1 huge display list or not. Maybe you should try making many smaller ones.

Duncan Champney
03-30-2008, 07:38 AM
V-man,

I expected the driver to detect an out of memory condition and fail gracefully, but that's not what happens.

It looks like there may be some bugs in the driver on my machine, because when my display list gets too big, first the 3D image goes black. Then, if I increase the size of the mesh even more, the app crashes.

I had similar problems with creating large FBOs with attached renderbuffers (RBs). When the renderbuffer gets too large, I get a black image, then the machine locks up and I have to do a forced shutdown to recover.

I found platform specific calls that let me figure out how much VRAM there is.

I have my code instrumented to check glGetError(), and I'm not getting any errors from the OpenGL driver. It just crashes (or locks up the machine, in the FBO/RB case)

I've added some code to my app to count the number of vertexes and normals I submit in creating the mesh, and do some rough calculations of VRAM used. Here's what I came up with:


# vertexes = (width-1) * height * 2
vertex memory = #vertexes *3 (x,y,z) * 4(sizeof(float))
normal memory = #vertexes *3 (x,y,z) * 4(sizeof(float))
color memory = #vertexes *4 (rgba. I use rgb, but assume 4 bytes for padding)

total memory = (width-1) * height * 2 * 56 bytes.

My machine has 256mb of VRAM. I'm able to create meshes that use nearly twice that much VRAM (a mesh created from 3000x3000 points, which has 17,994,000 vertexes, and should use about 17,994,000 * 56 = 503,832,000 bytes of VRAM.

I assume the driver is swapping parts of my display list in and out of VRAM in order to render it.

I'm quite a newbie regarding polygon meshes and how you set them up. I took some existing code that created a mesh and adapted it. I don't know how to break my mesh into pieces and get the edges to align properly, and have my polygon shading and normals work correctly at the boundaries, with no seams.

I learn best by example. Can you point me at some sample code that creates a large mesh in pieces, and deals with the boundaries?

V-man
03-30-2008, 02:34 PM
It`s not complicated. If you are using glDrawElements and you have an array of 33 indices :

Instead of :
glNewList();
glDrawElements(GL_TRIANGLES, 33, GL_UNSIGNED_SHORT, pointer);
glEndList();

split in 2 :
glNewList();
glDrawElements(GL_TRIANGLES, 10, GL_UNSIGNED_SHORT, pointer);
glEndList();

glNewList();
glDrawElements(GL_TRIANGLES, 23, GL_UNSIGNED_SHORT, pointer+offset);
glEndList();

Duncan Champney
03-30-2008, 06:01 PM
V-man,

I expected the driver to detect an out of memory condition and fail gracefully, but that's not what happens.

It looks like there may be some bugs in the driver on my machine, because when my display list gets too big, first the 3D image goes black. Then, if I increase the size of the mesh even more, the app crashes.

I had similar problems with creating large FBOs with attached renderbuffers (RBs). When the renderbuffer gets too large, I get a black image, then the machine locks up and I have to do a forced shutdown to recover.

I found platform specific calls that let me figure out how much VRAM there is.

I have my code instrumented to check glGetError(), and I'm not getting any errors from the OpenGL driver. It just crashes (or locks up the machine, in the FBO/RB case)

I've added some code to my app to count the number of vertexes and normals I submit in creating the mesh, and do some rough calculations of VRAM used. Here's what I came up with:


# vertexes = (width-1) * height * 2
vertex memory = #vertexes *3 (x,y,z) * 4(sizeof(float))
normal memory = #vertexes *3 (x,y,z) * 4(sizeof(float))
color memory = #vertexes *4 (rgba. I use rgb, but assume 4 bytes for padding)

total memory = (width-1) * height * 2 * 56 bytes.

My machine has 256mb of VRAM. I'm able to create meshes that use nearly twice that much VRAM (a mesh created from 3000x3000 points, which has 17,994,000 vertexes, and should use about 17,994,000 * 56 = 503,832,000 bytes of VRAM.


Very strange.

The crash I'm getting with large display lists doesn't seem to be related to VRAM at all. I've tried the same test on my development machine, a recent Intel MacBook Pro with a GeForce 8600 with 256 mb of video memory, and against an older powerbook with only 64 mb of VRAM. The crash seems to occur at the same mesh size. It's REALLY, REALLY slow on the powerbook because it's swapping memory back and forth between the video card and main memory, but it works. (Building a display list for a mesh for height map with 3000x3500 pixels takes about 5 minutes on the older powerbook, compared to about 18 seconds for the newer MacBook with 256 mb of video memory.

I guess the crash I'm seeing is a limit to the size of a display list, not a VRAM issue.

CatDog
03-30-2008, 07:38 PM
Also, would I be able to render a larger mesh (albeit more slowly) by NOT creating a display list, but just submitting the triangle strips directly? It's my understanding that when you draw directly, the commands are issued from main memory, not VRAM.
You should consider using VBOs (Vertex Buffer Objects). Basically, a VBO opens a "window" to VRAM, so that you are able to write your mesh data directly, without using display lists at all. As with display lists, you need to do this only once.

Like V-man said, you should try to split your mesh into smaller chunks. This is also true for VBOs, but it's more obvious here: managing huge blocks of memory is difficult for the driver, if VRAM is limited. With one huge display list, you can't tell what's happening behind the scenes.

A good idea is to split the mesh in a way that lets you build a bounding volume hierarchy over the chunk array. Then you get the opportunity to do some sophisticated frustum culling, which will speed up things up if chunks are not visible. (For example on camera closeups or when "flying" over a fractal landscape. Also, this could be a starting point for simple level of detail.)

CatDog

Duncan Champney
03-31-2008, 09:22 AM
It`s not complicated. If you are using glDrawElements and you have an array of 33 indices :

Instead of :
glNewList();
glDrawElements(GL_TRIANGLES, 33, GL_UNSIGNED_SHORT, pointer);
glEndList();

split in 2 :
glNewList();
glDrawElements(GL_TRIANGLES, 10, GL_UNSIGNED_SHORT, pointer);
glEndList();

glNewList();
glDrawElements(GL_TRIANGLES, 23, GL_UNSIGNED_SHORT, pointer+offset);
glEndList();


V-man,

In your example code, doesn't glDrawElements(GL_TRIANGLES...) require 3 vertexes for each triangle drawn? If not, what am I missing? If so, how does the 10 element call in your "split in 2" example work? 10 vertexes does not make an even number of triangles.

It's time for a confession. I got my code working without really understanding how it works. I had a working 2D app that I wanted to teach a new trick (3D rendering.)

To draw my polygon mesh, I found some code that drew a triangle mesh from height map data, and adapted it to my needs. This code uses triangle strips (GL_TRIANGLE_STRIP).

First the code builds arrays of vertexes and normals.

Then it goes through nested loops for x and y, rendering the vertexes into polygon strips. The code looks like this:



for (x = 0; x < xMax - 1; x++) // draw the mesh
{
glBegin(mode); //Either GL_TRIANGLE_STRIP or GL_LINE_STRIP
{
for (y = 0; y < yMax; y++)
{
glNormal3fv((GLfloat*)meshNormals + (x+1)*mesh_h*3 + y*3);
[self getColors: the_colors x_value: x+1 y_value: y];
glColor3ubv (the_colors);
glVertex3fv((GLfloat*)meshVertices + (x+1)*mesh_h*3 + y*3);

glNormal3fv((GLfloat*)meshNormals + (x)*mesh_h*3 + y*3);
[self getColors: the_colors x_value: x y_value: y];
glColor3ubv (the_colors);
glVertex3fv((GLfloat*)meshVertices + (x)*mesh_h*3 + y*3);
}
}
glEnd();
}

I can see how one pass through the y loop generates a strip of triangles, from bottom to top of the image, covering a rectangular swath.

This code issues a glBegin/glEnd to start/end each column of the mesh. If you use vertex arrays with triangle strips, what do you do about the end of strips? How do you prevent the last two vertexes from the end of one strip from forming an aberrant triangle with the first point from the beginning of the next strip?

Regarding Vertex Arrays;
I've been reading up on vertex arrays, indexed vertex arrays, and vertex array objects. Clearly Vertex array objects are the way for me to go. First I'll convert my code to create vertex arrays rather than display lists, and then switch over to vertex array objects.

Can you use triangle strips in vertex arrays, or do you have to specify all 3 vertexes for each triangle? (or all 3 vertex indexes, assuming you're using vertex array indexes, which seems the best way to go.)

Once I figure out how to create one big vertex array/array object, the next step is to create several smaller ones. That should let me render a much bigger mesh without overloading the driver trying to draw too big an object at once.

The easiest way to do that would be to make an object consist of one or more whole columns of my mesh. However, another poster mentioned using clipping to avoid portions of my plot that are not shown, and i would think rectangular pieces would work better for that.

Duncan Champney
03-31-2008, 11:22 AM
It`s not complicated. If you are using glDrawElements and you have an array of 33 indices :

Instead of :
glNewList();
glDrawElements(GL_TRIANGLES, 33, GL_UNSIGNED_SHORT, pointer);
glEndList();

split in 2 :
glNewList();
glDrawElements(GL_TRIANGLES, 10, GL_UNSIGNED_SHORT, pointer);
glEndList();

glNewList();
glDrawElements(GL_TRIANGLES, 23, GL_UNSIGNED_SHORT, pointer+offset);
glEndList();


V-man,

In your example code, doesn't glDrawElements(GL_TRIANGLES...) require 3 vertexes for each triangle drawn? If not, what am I missing? If so, how does the 10 element call in your "split in 2" example work? 10 vertexes does not make an even number of triangles.

It's time for a confession. I got my code working without really understanding how it works. I had a working 2D app that I wanted to teach a new trick (3D rendering.)

To draw my polygon mesh, I found some code that drew a triangle mesh from height map data, and adapted it to my needs. This code uses triangle strips (GL_TRIANGLE_STRIP).

First the code builds arrays of vertexes and normals.

Then it goes through nested loops for x and y, rendering the vertexes into polygon strips. The code looks like this:



for (x = 0; x < xMax - 1; x++) // draw the mesh
{
glBegin(mode); //Either GL_TRIANGLE_STRIP or GL_LINE_STRIP
{
for (y = 0; y < yMax; y++)
{
glNormal3fv((GLfloat*)meshNormals + (x+1)*mesh_h*3 + y*3);
[self getColors: the_colors x_value: x+1 y_value: y];
glColor3ubv (the_colors);
glVertex3fv((GLfloat*)meshVertices + (x+1)*mesh_h*3 + y*3);

glNormal3fv((GLfloat*)meshNormals + (x)*mesh_h*3 + y*3);
[self getColors: the_colors x_value: x y_value: y];
glColor3ubv (the_colors);
glVertex3fv((GLfloat*)meshVertices + (x)*mesh_h*3 + y*3);
}
}
glEnd();
}

I can see how one pass through the y loop generates a strip of triangles, from bottom to top of the image, covering a rectangular swath.

This code issues a glBegin/glEnd to start/end each column of the mesh. If you use vertex arrays with triangle strips, what do you do about the end of strips? How do you prevent the last two vertexes from the end of one strip from forming an aberrant triangle with the first point from the beginning of the next strip?

Regarding Vertex Arrays;
I've been reading up on vertex arrays, indexed vertex arrays, and vertex array objects. Clearly Vertex array objects are the way for me to go. First I'll convert my code to create vertex arrays rather than display lists, and then switch over to vertex array objects.

Can you use triangle strips in vertex arrays, or do you have to specify all 3 vertexes for each triangle? (or all 3 vertex indexes, assuming you're using vertex array indexes, which seems the best way to go.)

Once I figure out how to create one big vertex array/array object, the next step is to create several smaller ones. That should let me render a much bigger mesh without overloading the driver trying to draw too big an object at once.

The easiest way to do that would be to make an object consist of one or more whole columns of my mesh. However, another poster mentioned using clipping to avoid portions of my plot that are not shown, and i would think rectangular pieces would work better for that.



Ok, the OpenGL SuperBible didn't cover this very well, but "the red book" has a better section on VBOs that told me about using glMultiDrawElements to specify a series of elements like triangle strips in one pass with a VBO. That makes good sense.

Now, how to break up my mesh into a series of VBOS.

The other poster, CatDog, made mention of breaking my mesh into a series of chunks in order to "build a bounding volume hierarchy over the chunk array". His suggestion makes good sense. I'm thinking I should cut my mesh into a series of rectangular tiles, and then before rendering, make sure each tile is visible. The culling would be especially effective when I'm saving a large 3D view to disk, since I cut that view into tiles.

As I think about it, cutting my mesh into tiles will be quite a bit of work. Instead of having large 2 dimensional arrays of vertexes/normals, I'll need an array of arrays of vertexes/normals, one for each tile. I'll need to handle the boundary case of the small-sized tiles at the end of each row/column (when my mesh size isn't evenly divisible by the tile size). I'll have potentially 4 different sizes of tiles: full size, narrow, and one short and narrow tile for the last corner tile. Gack, that's a lot of special-case code.

It would sure be a lot easier to just cut my mesh into some number of full rows. Then I wouldn't even need to restructure my vertex array. I could just pass a pointer to the beginning of a row when I set up my VBO.

Is the "frustum culling" really worth it, given that my app is not really driven by framerate? I'm leaning towards no, given the amount of work it would involve.

Lord crc
03-31-2008, 02:06 PM
If you're not framerate sensitive, I would try the one-list-per-strip approach first. As you said, it's very easy to implement, and might do the job fine.

Otherwise, I find that using OOP makes the tiling code very easy to implement. Make each tile it's own object, which is initialized by passing the raw data, position and size. Then it would compute the bounding box and generate the display list for rendering. Then simply go through your boxes and issue each tile's Render method, which would perform the frustum check and if needed call the display list.

This can then be easily extended to include a bounding hierarchy, say a quad tree.

CatDog
03-31-2008, 06:12 PM
As I think about it, cutting my mesh into tiles will be quite a bit of work. Instead of having large 2 dimensional arrays of vertexes/normals, I'll need an array of arrays of vertexes/normals, one for each tile. [..] Gack, that's a lot of special-case code.
Special case code? I think you are trying to do several very different things in one step. Separate them!

Here are some thoughts:

1. Your application data is a 2 dimensional array. Since you are doing fractals, I suppose you have something like a height- or color map.

2. You're now converting this map to huge vertex/normal/color triangle strips. Instead of this, convert the map to an indexed triangle list! (At this point, you should be able to draw the mesh with glDrawElements( GL_TRIANGLES, .. ). But we don't want that, except for debugging...)

3. Unlike with strips, it's very easy to work with triangle lists! A triangle list is in fact an array of separate triangles. You can split and reorder them very easily. Group the triangles using subdivision and create a quad- or oct-tree. When done, each leaf should contain a set of triangles: one chunk. No special case code here! Just sorting and reordering triangles.

4. Of course, each chunk can be rendered with glDrawElements( GL_TRIANGLES, .. ) - just like the complete mesh. But all chunks are still in main memory. So now you can create VBOs for each chunk. Or maybe create display lists. (I'd prefer VBOs.)

5. Rendering: on each frame traverse your volume tree and render only those chunks that are seen by the view frustum.


Ok. This outlines one way of doing it. Of course, it's some work. But when you're reaching the end of the easy way, you have to go the long way round, right? ;)

CatDog