Skip to main content
GameDev.net gamedev.net
🔒 Locked

Efficient, modern 2D pipeline?

Started by TheAverageUser Jun 28, 2010 at 3:27 AM 7 replies 6.7k views
Original Post
TheAverageUser
TheAverageUser
I'm doing a different project right now, but can't keep my thoughts off that thing. I've written one or two OpenGL-based 2d graphic frameworks so far. I always used OpenGLs immediate mode although knowing it was deprecated and shouldn't be used any more. It was just so damn well fitting the way how vertex data comes in when doing a typical 2d game!

Now, the next time I'll be doing this I won't use immediate mode anymore. I will use VBOs or the DirectX-equivalent to that. The question now is, how to structure all that. Currently, I imagine the following:

-> Allocate one or two VBOs (two in case of VBO double buffering) in the graphics core
--> Wait for draw calls (DrawRect, DrawCircle, ..) and state change calls (SetTexture, SetColor, ...) and queue them in a buffer.
--> When calling "Flip", "Flush" or a similar "frame ended" command: Remove redundant or unneccessary state changes, submit all vertices to the current VBO, then iterate through the buffer and make a DrawArrays for each chunk without state change. In the worst case, there'll be one chunk a triangle, in the best case one chunk per frame.

So far, so good. Or isn't it? If not, tell me! However, some questions come into my mind:

1. How to deal with different vertex formats? For a simple textured rect, I'll need xy, uv per vertex. However, for a multitextured, vertex-colored one with additional z info, it'll be xyz, rgba, u0v0, u1v1, u2v2, ... which i a LOT more. It can't be too good to use the most complex format for ALL cases. Is there a way I can use a single VBO with multiple vertex formats and switch inbetween two DrawArrays-calls?

2. Optimization idea: Z-Prepass. What about drawing the frame two times, once z-sorted from front to back and a second time from back to front? First pass: Z write and Z testing, fully textured and colored with alpha testing for "anything below 1.0". This way, the z prepass only draws completely opaque pixels. Second pass: Draw the scene regularily from back to front. Will this improve performance or just make it worse?

3. The main problem for 2d games: Everything has alpha and usually sorting shapes by state to grow batch sizes isn't possible because of screwing the z order. Does anyone have an idea of how to deal with this? At least, some kind of partial, conditional sorting seems possible: If the current blendmode is "A + B", sorting has no visual effect --> Do it! Are there similar cases in which sorting is okay?
Rattenhirn
Rattenhirn
2d rendering is not fundamentally different from 3d rendering, especially, when using a 3d rendering API...

A typical 2d scene typically has way less vertices and textures than a typical 3d scene, so it is actually easier, unless you want to target really low performance platforms.

We write our vertex data in a buffer and draw it with DirectX's DrawPrimitiveUP.
If you have more complex 2d objects than quads, you may want to consider giving each of them their own buffer, if they don't change a lot.

I really can't give any specific advice, if I don't know what you want to achieve. Also, you state that you've written one or two 2d graphics frameworks already. Figure out what the problems with these were and try to solve them.

Now for your questions:
1) When using user pointers, you can mix and match vertex formats as you want. When using vertex buffers per object, than configure them correctly when you create then. When using a "growing" vertexbuffer for everything, like you suggested, you need one for each vertex format you want to support.

2) Z-Prepass is used for reducing overdraw. If you have a lot of overdraw (which you don't typically have in 2d) it may be worth it. My guess is, that won't be worth it.

3) Since in 2d games, the perspective doesn't typically change at all, you can make sorting within an object (i.e. hair vs. face) static. After that, all you need to do is to sort the objects each frame. Since not much will change, this will typically be very quick.

If you have a lot of object without alpha (also, usually not the case in 2d), it might be worth it to separate them and let the zbuffer take care of the sorting.
TheAverageUser
TheAverageUser
Quote:
Original post by Rattenhirn
I really can't give any specific advice, if I don't know what you want to achieve. Also, you state that you've written one or two 2d graphics frameworks already. Figure out what the problems with these were and try to solve them.


Yeah, I've done that. I created this thread to clarify some thoughts on performance and graphics pipeline design. Since a 2d engine typically has rather simple scenes to manage, you usually don't need a real management behind it. In my current project, this certainly isn't the case. I've got a lot of Objects running with Vertex- and Pixel Shaders, some of them using per-pixel lighting with three textures (Usual Diffuse/Normal/Specular combination). I've run into performance problems due to not having some kind of graphics management to organize draw calls, state changes and sorting. Now I'm trying to figure out something clever in order to not having this problem again in future projects.

I've called it "modern" 2D pipeline not entirely without reason. I'm not talking about the typical 2d platformer with no more than 20 flat fixed-function sprites at once ;)

Quote:
Original post by Rattenhirn
2) Z-Prepass is used for reducing overdraw. If you have a lot of overdraw (which you don't typically have in 2d) it may be worth it. My guess is, that won't be worth it.


Could you specify "a lot of overdraw" a little more? I've thought about early-z especially to reduce overdraw since in an all-alpha'd 2d game everything is drawn from back to front. That sounds like the overdraw-worst case to me but as I've never done 3D engines before I'm maybe out of scale. So, what is "a lot"?

Quote:
Original post by Rattenhirn
3) <...> After that, all you need to do is to sort the objects each frame. Since not much will change, this will typically be very quick.


The sorting itsself isn't much a problem, yes. The problem is the size of draw call batches I can send up to the video card: As pretty much any sprite uses Alpha (typical) I can't just use the Z buffer while sorting to achieve bigger batches. I'll have to sort by the Z value of my objects which will, in worst case, pretty much blow up any possibility to batch *at all*. Though, in most cases I can sort by state at least partially as I've stated above. The question is, if there are more such cases I haven't thought of? Or techniques where this problem doesn't occur at all?
apatriarca
apatriarca
Another thing you may try (depending on your game) is to separate static objects from dynamic ones. This can lead to some performance improvements because it is faster to draw static VBOs (since they aren't updated often and the driver/GPU can do some additional optimizations).

There are several ways to sort the batches to improve performances and it really depends on the game and scene displayed. For example, if your poor performance isn't due to overdraw, then z-sorting your objects will not improve it. The same thing can be said about other kind of optimizations. Have you tried profiling your code with something like glslDevil or gDEBugger?
TheAverageUser
TheAverageUser
Err, well, performance isn't a matter right now anymore, I improved several things in the engine and content and now it's okay. It was just an example and shouldn't be the issue in this thread. I just wanted to discuss the architecture of a general-purpose, high-level 2d graphics engine, utilizing modern hardware best possible. You know, one of these "helps everyone" threads.

However, another idea came into my mind. When using hardware capable of geometry shaders, it should be possible to optimize vertex data transfer in the usual quad drawing routine: Instead of submitting four completely parameterized vertices for each quad, you could submit one vertex with custom attributes for the sprites width, height and rotation. Essentially, some kind of custom point sprites. In plain vertex data, instead of submitting 4x(XYZ-RGBA-UV) you'd submit 1x(XYZ-RGBA-WHR) which sums up to 24 words respectively 7 words. To me, this sounds like a very efficient way to transfer "normal" sprite data. How about the overhead due to the geometry shader code? I've never worked with them as I don't have the hardware to test it.
apatriarca
apatriarca
2D games usually draw a lot less polygons than modern 3D games and I do not think reducing the number of polygons is a very effective way to improve your performance. You are also increasing the computation that the GPU should do to draw your polygons. Depending on your game, it can actually reduce your performance. I think it is really worth it, since you probably need a non-geometric-shader path anyway. I think sorting your sprites (by shader, texture or other) is the most effective way to improve performance. But you should really profile your code to see what's better.
Rattenhirn
Rattenhirn
Quote:
Original post by Fetze
Could you specify "a lot of overdraw" a little more? I've thought about early-z especially to reduce overdraw since in an all-alpha'd 2d game everything is drawn from back to front. That sounds like the overdraw-worst case to me but as I've never done 3D engines before I'm maybe out of scale. So, what is "a lot"?


Normally I would say if you draw 3 times as many pixels as there are on the screen, you can consider using a z-prepass to gain performance. However, you have a lot of alpha blending, which hurts you twice. First, z-prepass helps less on pixels where alpha blending is used and secondly, since you need to use alpha test, you need to sample at least the texture that provides alpha in the z-prepass shader.

If your engine is set up in a way that you can easily add a z-prepass, you should try it, otherwise forget about it.

Quote:
Original post by Rattenhirn
The sorting itsself isn't much a problem, yes. The problem is the size of draw call batches I can send up to the video card: As pretty much any sprite uses Alpha (typical) I can't just use the Z buffer while sorting to achieve bigger batches. I'll have to sort by the Z value of my objects which will, in worst case, pretty much blow up any possibility to batch *at all*. Though, in most cases I can sort by state at least partially as I've stated above. The question is, if there are more such cases I haven't thought of? Or techniques where this problem doesn't occur at all?


I see. What you could do, is figure out, which object overlap each other and then only sort the overlapping one. I'm sure there's a lot of code out there from old dirty rect based 2d engines, that you can have a look at. :)

Another thing that might be worth mentioning: If you have a nVidia GPU, get NVPerfHud and the instrumented drivers (it's free on the nVidia site) and run the frame profiler. It'll tell you where you bottlenecks actually are.
freeworld
freeworld
I use DirectX and the way I have it setup is like so.

I have objects I call renderLists, basically a wrapper around vertexbuffers but stores my custom drawcall struct. and knows how to parse this call and put it into the vertex buffer when it's ready.

When I'm ready to draw I sort all the renderLists by transparent or not textures, then by z value depending on if they are transparent or not. non transparent are sorted front to back with depth testing enabled and transparent are done back to front.

I render the lists in order of creation. I like it this way because I set and extra lists to draw to a rendertarget other than the backbuffer. Then I can use that render target to draw to a different list with a post processing effect.

It works pretty good for me, but i limit the possible shapes to triangles, quads just being two triangles. No lines or points, but I do this to make it simpler and force the optimizations I've made, without sacrificing much flexibility.

Quote:
3. The main problem for 2d games: Everything has alpha and usually sorting shapes by state to grow batch sizes isn't possible because of screwing the z order. Does anyone have an idea of how to deal with this? At least, some kind of partial, conditional sorting seems possible: If the current blendmode is "A + B", sorting has no visual effect --> Do it! Are there similar cases in which sorting is okay?


not all textures have alpha, and actually quite alot can be avoided. ie my draw circle functions takes a box uvcoords and generates a given amount of triangles to create a circle without useing the transparent pixels in a texture.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.