For practice, I'm writing a Rogue-like. In order to speed up tile rendering I have a dynamic Mesh/MeshBuilder that collects all the vertices of the visible tiles' quads (4 vertices per tile: position, color, UV; and 6 indicies) CPU-side and emits a single draw call of the large tiled triangle mesh. At 600 FPS/0.0017 ms this is fast enough for my purposes.

I recently decided to practice with the various rendering pipeline stages and thought that this would be a great candidate for the geometry stage and have the GPU generate the tiles for me, passing in only a single vertex/index pair per tile.

There are as many as 2000 tiles visible per frame, yet it was no faster than the CPU version despite passing one quarter the amount of vertices and one sixth the indicies across to the GPU.

It's my understanding that it is a best practice to minimize the amount of data passed between the CPU and GPU, and hand off work to the GPU that it is better suited to do.

Am I wrong in this understanding?

It seems like the geometry shader isn't that useful if offloading work to it doesn't have a benefit?

What can be achieved with the geometry shader that other techniques can't do?

  • 1
    \$\begingroup\$ It sounds like you're likely fill rate bound, not draw call or data transfer bandwidth bound. \$\endgroup\$
    – DMGregory
    Dec 4, 2020 at 4:52
  • \$\begingroup\$ @DMGregory I don't know what fill-rate bound means, but that's likely the issue. I increased the visibility so I could view every tile at once and previously the CPU version would choke on 10,000 tiles (1 FPS). The geometry shader version can easily take 60,000 tiles no problem (30 FPS), allowing for as low as 15 FPS I can cram 150k tiles on screen. \$\endgroup\$
    – Casey
    Dec 4, 2020 at 5:56
  • \$\begingroup\$ @DMGregory Follow-up question: gamedev.stackexchange.com/questions/187452/… \$\endgroup\$
    – Casey
    Dec 4, 2020 at 7:32
  • \$\begingroup\$ @DMGregory I want to give you credit for leading me in the right direction. If you'd kindly post an answer explaining what fill-rate bounds means and the fact that I simply wasn't giving the GPU enough work to do (I guess it gets bored easily. :P) I'll accept it. \$\endgroup\$
    – Casey
    Dec 4, 2020 at 7:39

1 Answer 1


This looks like a case of bottlenecks.

When you have a sequence of steps that depend on each other, like...

  • uploading data buffer to the GPU
  • issuing the draw call
  • generating the vertices
  • transforming those vertices to the viewport
  • rasterizing the resulting triangles
  • interpolating values for each fragment
  • rendering each fragment
  • writing the results to the frame buffer

...then the whole pipeline cannot proceed faster than the most time-consuming operation in the sequences. In chemistry this is called the "rate-determining step," while in programming and graphics we call it a "bottleneck".

Making the operations before or after the bottleneck faster won't substantially speed up the whole, because your time is still dominated by the bottlenecked step. (You can sometimes slightly improve the latency this way, but you don't change the total throughput)

One important bottleneck in graphics is "fill rate". There's a limit to how fast the GPU can write the results of an operation out to the frame buffer. You'll often hit this one if you have very simple shaders that draw over large parts of a high-resolution output target, especially if you draw over them again and again (eg. layering lots of translucent blended sprites)

Another common bottleneck is texture bandwidth. There's a limit to how fast the GPU can read, decode, and interpolate texture samples. You'll often hit this one if you sample a lot of large textures in random patterns that make it hard for the texture cache to re-use recent samples.

For a use case like yours, where building and uploading a few thousand quads really isn't very heavy lifting by modern GPU standards, and you're not doing any fancy calculations in your shader that might slow it down, then either of those two bottlenecks above might end up being the slowest part of your pipeline.

Making the generation/upload step faster won't necessarily give you an observable improvement in framerate, because it's not what was costing most of the time in the first place.

If that's the case, then scaling up your problem size by increasing the number of tiles (while keeping the textures and output resolution the same) will tend to make the cost of that generation/upload higher while keeping the texturing/fill rate costs about the same. Eventually you'll hit a tipping point, where now the generation/upload is the slow part.

At that stage, you may start to see a clearer comparison between your CPU and Geometry Shader methods, without the rendering costs common to both solutions muddying the picture. From your comments, it sounds like that's what happened. 🙂


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .