Slow Sprite Batch Rendering

Started by
6 comments, last by Dekowta 11 years, 7 months ago
Hi,

I'm fairly new to OpenGL but have been working on it for a little while now.

I'm mainly working on a 2D renderer which uses the more up to date style of OpenGL rather than Immediate mode.

I've managed to create a sprite batching system however I'm not really getting the results that I would expect. I'm roughly getting around 30fps when rendering 5,000 of the same sprite object and around 15fps when rendering 10,000

At the moment the way it works is as followed

- Sprite batch is created which creates the shader, indicies for MultiDrawArrays, 2 VBO and a VAO

- begin is called and sets the alpha mode

- draw is called and checks to see if the texture has been batched already if so it adds the new points to the batch if not it creates a new batch item and adds the points to it. if the max sprites is reached it will draw the batch

- end all the current batch sprites are rendered

header
http://pastebin.com/p20Ehy9s

Cpp file
http://pastebin.com/F8Gz6UQM


I have tried a few things to improve performance but with not success. I expect its the method I'm using and that way that im using openGL thats causing the issue.

Thanks Dekowta
Advertisement
I think your problem may be glMultiDrawArrays. My understanding is that this doesnt really map to the gpu. You will still get one draw call per entry you send to glMultiDrawArrays. The only benefit is reduced driver overhead. You should be able to send all the sprites as a list of quads just using GL QUADS and glDrawArrays. I think this will be much faster.
Your main problem with this code is the way you're updating your VBO. Calling glBufferSubData in this manner is going to lead to pipeline stalls and flushes, and doing it potentially so many times per frame will make things worse. You need to implement proper buffer streaming to get this working well; have a read of this post for a description of the technique.

Sample code:GLuint bufferid;
int buffersize = 0x400000;
int bufferoffset = 0;

void StreamBuffer (void *data, int batchsize)
{
glBindBuffer (GL_ARRAY_BUFFER, bufferid);

if (bufferoffset + batchsize >= buffersize)
{
glBufferData (GL_ARRAY_BUFFER, buffersize, NULL, GL_STREAM_DRAW);
bufferoffset = 0;
}

GLbitfield access = (GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
void *mappeddata = glMapBufferRange (GL_ARRAY_BUFFER, bufferoffset, bufferoffset + batchsize, access);

if (mappeddata)
{
memcpy (mappeddata, data, batchsize);
glUnmapBuffer (GL_ARRAY_BUFFER);
glDrawArrays ( .... );
bufferoffset += batchsize;
}
}

Ideally you wouldn't memcpy here; you'd generate the batch data directly into the pointer returned from glMapBufferRange instead. That's not always possible though, and memcpy is fine for many (if not most) use cases - the key is in avoiding pipeline stalls and the CPU-side overhead from memcpy is going to be very low by comparison.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Thanks for the reply

I'm having a bit of a hard time understanding how to get buffer streaming set up. So far this is what I understand on what I have to do.

in the section where I loop through the iterations in the map I will

- Bind the texture
- Bind the vertex position buffer
- check to see if the bufferoffset + Batch Count is greater than the buffer size
->if so set the buffer to the size of the batch
->reset the bufferoffset to 0
- get the mapbuffer range pointer
- copy over the batch vertex data (if the vertex data is a pointer can I just make mappeddata point to it or do I need to copy data into mappeddata's address using them memcpy)
- unmap the buffer

- Bind the UV buffer
- do the same as the vertex buffer but with the UV coordinates
- unmap the buffer

- then call draw array with GL_QUADS
- increase the bufferoffset by the batch count

- unbind the texture

This is still fine to use with the vertex array as well?

Also post that you linked mentioned buffer-orphaning from what it explained this happens in the if statement check?
You've pretty much got it, yes. It's really just a simple circular buffer; there's no voodoo in it and the only tricky thing is knowing the correct GL calls to use.

You're going to have some added complexity if you're using separate VBOs for positions and texcoords - I'd recommend that you define a vertex struct containing both and interleave them using a single VBO; it'll perform better and make your code much simpler.

Yes, buffer orphaning is what happens in the first "if" check; there are also flags on glMapBufferRange that you can use to accomplish this, but I prefer to use glBufferData (... NULL, ...) - not for any technical reason, just so that I can more easily add a fallback to glBufferSubData if the MapBufferRange call fails (using the same offset and size params). I omitted that from the sample code I posted just for clarity.

Not sure what you mean by vertex arrays here. Old-style vertex arrays or newer VAOs? If the former, there's no need to do this kind of process - you're using memory owned and managed by your program (rather than by the GPU) so there's no resource contention to speak of, and you don't need anything special to handle it.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Humm I don't know what im doing wrong at the moment but its not drawing correctly and if I leave it for a bit glMapBufferRange returns 0 with 1281 a Invalid value.

The Modified Files are as followed though I have only really changed the initalise and render function as well as packing the vertex data into a single struct
.cpp
http://pastebin.com/d778Dt6y
.h
http://pastebin.com/WUHt28Jc

what its rendering
http://oi50.tinypic.com/2hi59jd.jpg

what it should look like (excluding the orange and green character)
http://oi48.tinypic.com/2q86g0o.jpg

O and I meant to say Vertex Array Object rather than just Vertex Array.
You're getting your offsets/etc wrong here - this in particular is not what you want:void* mappedData = glMapBufferRange(GL_ARRAY_BUFFER, m_BufferOffset, m_BufferOffset + currentBatch->spriteCount, access);

I should probably have stated explicitly that the sample code I gave above works in byte sizes, not numbers of vertexes or numbers of sprites, so as a result things need to be adjusted accordingly if you're going to use other units.

So, the second parameter to glMapBufferRange is an offset in bytes, so make it m_BufferOffset * sizeof (MLBatchItem::Vertex) * 4 instead. The third parameter is the size of the range to map (also in bytes), not the end offset of the full range, so it becomes currentBatch->spriteCount * sizeof (MLBatchItem::Vertex) * 4.

You should also make sure that your value of m_BufferSize is equal to (however many sprites fit in your buffer) * sizeof (MLBatchItem::Vertex) * 4; likewise memcpy needs a size in bytes, glDrawArrays takes a number of vertexes, not a number of sprites as it's third param, and your offset needs to be incremented by the number of vertexes, not the number of sprites. There may be a few other places I've missed.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Humm I seem to have it working but there are still a few issues.

I have two sprites A - 128x128 at 100, 100 and B - 230x230 at 400, 100

when I render just A and B, B will render in the coordinates of A but when I render A B B it does the same again but the second B is rendered like normal.

I checked the data in the VBO and it seems to be fine and holds the data that I would expect.

The modified files
.cpp
http://pastebin.com/d778Dt6y
.h
http://pastebin.com/EMFWGEAi

Rendering A and B
http://oi46.tinypic.com/23hau5v.jpg

Rendering A B B
http://oi45.tinypic.com/25s3lt1.jpg


Sorry to keep having issues with it you've helped so much so far.

This topic is closed to new replies.

Advertisement