The 2024 Wheel Reinvention Jam is in 16 days. September 23-29, 2024. More info

Performance of IMGUI

Hi, I'm writing a small UI library in IMGUI style, and I wonder how to reduce the number of draw calls. For example, if I draw a button, it will have a background and a text on top, so the code looks like:

bool button(char *text)
{
  // ..
  drawRect(..);
  drawText(..);
  // ..
}

So if I have a lot of buttons, the render commands will be rect, text, rect, text, .. The problem is that I have to change shaders between rects and text. I would like to be able to draw all rectangles at once first and then all the texts. How can I do that in an IMGUI style? In retained mode I think that we would just maintain the order of the commands and just hand that off to the GPU at once.

I haven't written an immediate mode gui myself, but from what I understand most don't do it like your example. They queue up draw calls to be done in a rendering pass. I guess you could have a first rendering pass for the rectangles and a another for text. Makes sense?

If you're drawing text from font atlas then you can get rectangle drawing very cheaply with same shader. Put a solid (1,1,1,1) color as one pixel into your atlas. And use it's uv coordinats for fake text "glyph" to draw in place of rectangle. Because it is same color the actual size of rectangle does not matter - shader will sample same color across whole rectangle you draw.

Then all it is left is to multiply sampled color with color from vertex attribute - but that part is the same for both font glyphs or your fake rectangle-glyph.

If you open default DearImgui example, for example, from here: https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html

and go to Widgets -> Images you'll see font atlas texture they created. Look above mouse cursor on first row (directly to the right of first triangle). The dot above is this pixel I've talked about.

image.png

The code that uses is to draw rect is here: https://github.com/ocornut/imgui/blob/24dfe6db8ac2ffefce8cf02a6b585a05ebfcbe0a/imgui_draw.cpp#L668-L681

You can see it uses same uv value (TexUvWhitePixel) for all 4 vertices regardless for rectangle size (a/b coordinates).


Edited by Mārtiņš Možeiko on

Thanks, this is an interesting technique. But if we have very specialized shaders for each shape, like circles, rects, what can we do? Do I have to use a single shader for everything? Or maybe I should record the rendering commands and sort them each frame?


Replying to mmozeiko (#26656)

Like Benjipede said, you want to send a single drawcall for all rectangles, and a single drawcall for all text.

In my codebase, every call to drawText and drawRectangle adds a entry in a batch (command buffer), and at the end of the frame I call render, and that fills the index and vertex buffers and do 1 draw call for text, and 1 for rectangles.

I found that this isn't a very good way to do things, because if you have a popup window (for a dropdown list for example) than you want the rectangle and text of the popup to be drawn on top of other things, and you need two other draw calls. A simple solution is to have back and front batches for text and rectangles, but that's just a bit more complication to handle.

So having a single shader and draw call (like mmozeiko said) for every thing is probably better. You can have a look at Ryan Fleury's example: https://www.rfleury.com/p/ui-part-6-rendering


Edited by Simon Anciaux on
Replying to Terans (#26657)

If you start adding more customized shaders for drawing different primitives, then you should do branching on its type. As long as same branch is taken for whole triangle it will be almost for free.

Basically in fragment shader it should look like different cases calculating output color from inputs (uv/screen pos/other attributes) differently:

void main()
{
  if (type == rectangle) {
    out_color = RectColor(..);
  } else if (type == circle) {
    out_color = CircleColor(..);
  } else if (type == whatever) {
    out_color = WhateverColor(..);
}

Edited by Mārtiņš Možeiko on
Replying to Terans (#26657)

Ok so having a single shader that handles everything seems to be the best thing to do.


Edited by Terans on