As flohofwoe suggests in a sibling, combining an imgui-like API with an actual retained widget tree would be something of the best of both worlds. That is indeed pretty close to what the Crochet prototype does. We're not trying to exactly match the imgui but it's pretty close. In particular, getting actions back from widgets (like button presses) is the return value from calling the widget-creating function. That's a notable difference from, say, Jetpack Compose, which is fairly imgui-like on widget creation but pretty much uses callbacks for those actions. Crochet does solve the page tearing problem (though the current implementation is hacky, don't look too closely under the hood).
Another note - the Dear ImGui implementation has become a lot more sophisticated over the years, and is no longer a simplistic implementation of the immediate mode GUI concept. It has unique id's for widgets, its text processing has become better, etc.
The important feature of immediate mode UIs is that there's only one sequential piece of code for UI creation, updating and input handling. All I'm saying as the framework user is basically "this is what I want the UI to look like in the current frame".
E.g. if the UI that's described in the current frame by user code is completely identical to the last frame's UI, exactly "nothing" would happen, the internal widget tree wouldn't change, and nothing would be rendered.
It is possible but you would need some severe API changes... you could either ask the users to use unique names for each widget, or something similar like storing references between frames. Alternatively you'd have to force "same order between frames" (similar to React does with Hooks).
The difference from React would be representational. In React you explicitly build this data structure as something resembling a DOM tree and handle input by attaching callbacks to nodes, but I think this representation is just convenient for web development rather than fundamental to the approach.
You might still need a way to manage updates to the state the GUI depends on and re-render when it changes for some other reason than an input operation.
It also caught me by surprise, and would catch many others where seemingly normal controls, that just happen to have the same text would be identified with the same state, as the text (contents) are initially used to point to this state.
If this is the case, then it seems entirely possible to have an "internal retained mode", and might even help with some extra stuff like focus management and accessibility.
Feel free to check my blog and send me an email if you are interested. I have work in progress version for Windows and Linux, but no documentation yet. It will be released(free and open-source) by end of this year.
Consider this trivial counterexample:
int counter = 0;
label("count: %d\n", counter);
if(button("increase")) { counter += 1; }
label("count: %d\n", counter);
This is, of course, a silly little example but the same issue will arise in many practical use cases and would be much more complicated.Most applications "solve" this by rendering new frames continuously, so the inconsistent state is only shown for one frame and will quickly disappear. This, however, has terrible power consumption implications if nothing of interest is occuring on the screen.
SkyAlt would "solve" your example by quickly redrawing the frame. The power consumption is good because it's redrawing only when the key is pressed or the mouse is moving, otherwise one frame every two seconds, so CPU is most of the time under 1% and my 3.6GHz CPU is running on 1.4GHz.
//initialization
int counter=0;
//each frame
label("count: %d\n", counter);
label("count again, in case you missed the other label: %d\n", counter);
if(button("increase")) { counter += 1; }
or, if the order of widgets is constrained, to buffer values: //initialization
int counter_old=0;
int counter_new=0;
//each frame
counter_old = counter_new;
label("count: %d\n", counter_old);
if(button("increase")) { counter_new = counter_old+1; }
if(button("decrease")) { counter_new = counter_old-1; }
label("count again, in case you missed the other label: %d\n", counter_old);It's also very inefficient to continuously redraw, which is not a concern for games, but not in general.
Games can render huge, vibrant, dynamic 3D worlds at 60fps and increasingly at >120Hz rates. Web browsers lag if a piece of dust falls on the wrong spot.
I agree with you in theory. But in practice retained mode is a mountain range or complexity and doesn’t actually perform better.
I think people dramatically overestimate the cost to perform simple UI layout. There’s no reason that CPU time should be more than 1ms single-thread for most apps. GPU time should be similarly small. Modern smartphone supercomputers are FAST!
Your retained-mode GUI framework isn't doing dirty rectangles anymore anyway; it's going to redraw everything anyway. (Not every frame, but upon receiving an event.) With modern GPUs, redrawing a whole frame is super cheap, even with integrated graphics.
The gain, then, is going to come from not having to rebuild the scene graph every frame. And modern imgui implementations will cache the scene graph+layout anyway, and only rebuild the parts they need to. Which is slightly more CPU-intensive, but not appreciably so.
Except that a a retained mode GUI can easily tell that no redraw is necessary and it will be a no-op.
> With modern GPUs, redrawing a whole frame is super cheap, even with integrated graphics.
It's cheap as in fast, but it's not cheap in power consumption having to wake up the GPU from sleep state to redraw what's already on the screen.
This matters with battery powered devices.
> The gain, then, is going to come from not having to rebuild the scene graph every frame.
Rebuilding a scene graph that's based on CSS-style flexbox layout is REALLY fast. It's a linear time operation in the worst case and a lot of the work can be just skipped if the layout doesn't need updating.
Check out Raph Levien's talk which I linked to in my top level posting. He discusses the implementation details to make this smoking fast.
Text layout is by far the most time consuming operation in rebuilding a GUI scene graph.
The way this is done in the Druid UI library is probably a lot faster than what ImGui does, if you exclude the time it takes to do text layout with HarfBuzz.
All of this is pretty moot point, because the most important factor in performance and battery consumption is not to wake up the GPU to do unnecessary work.
How is that different from imgui exactly? Both dirty rect invalidation and the following redraw skipping is incredibly easy to do (and has been done, see microui)
> Rebuilding a scene graph that's based on CSS-style flexbox layout is REALLY fast. It's a linear time operation in the worst case and a lot of the work can be just skipped if the layout doesn't need updating.
I'm not sure what are the numbers there, but my fullscreen imgui application (You can see it here https://twitter.com/DoctorGester/status/1022147094558269446 and there are views with more complex layouts, rich text rendering, etc) hacked on top of DearIMGUI takes 0.2ms (200 microseconds) per frame, that is including all the logic and submission of commands to the GPU backend. That's before any optimization or multithreading independent views. Do you really get much faster than that?
Does this refer to the one presented in the top-level comment or a retained-mode GUI framework in general (like GTK)? Because it's usually not true for the latter.
Writing a desktop app using ImGui I had the same problems. In many instances it can be resolved by re-ordering operations such that in a frame you first update the state and then render it, but this isn't always possible or easy. For those cases I eventually resorted to having a requestFrame() function (which posts an empty event to the input queue at the end of the frame, causing another frame to be rendered immediately).
Suppose that the widget functions apply one of two different strategies depending on the call context: 1. Render the widgets and 2. Evaluate the layout and process input accordingly. You invoke the GUI code in the second context on every input, and after that invoke it in the first context only if the calls in the second context potentially resulted in a change. This minimizes re-rendering and eliminates page tearing while allowing you to re-render reactively to events.
You will of course still need some kind of basic state tracking to make the GUI re-render to reflect changes that are not triggered by input.
In general, I agree, though. The right tool for the right job.
Edit: I guess you could partially solve this problem by double-buffering the application state, so during the first draw you update a state copy, and during the second draw you use the updated state. You won't get any "tearing" but you still need to draw twice.
There is no problem in redrawing everything from state every frame. There is no problems in having "half frame be old state" - even at 30 fps it is not noticeable.
We use a separate update() and gui() path though. So all entities will get an update opportunity even if the gui is hidden. And state is kept in a config that is beautiful to use... Maybe the issues here comes from not having those parts.
But is rendering twice really that much of a problem if you only have to do it on a state-changing event (even if that includes things like hover events or live data)? Even games that run with 30-60 FPS aren't really a problem anymore on weak hardware so why would a GUI that updates "once in a blue moon" (relatively speaking) be?
Apart from that, I don't know how Dear ImGui or other immediate mode GUI frameworks do it but if a framework handles page tearing with double rendering it could skip the actual paint (i.e. pixel drawing) in the first render. In case the user wants to avoid some needless work (like rendering labels or graphs that never affect state) in the first render the framework could also pass the calling context (update or paint) to the render function.
g_pSwapChain->Present(1, 0); // Present with vsync
//g_pSwapChain->Present(0, 0); // Present without vsync
So you can control this by setting the first argument to 1 or 0.
You describe the UI by making a bunch of function calls, each of which immediately renders a piece of the UI and returns any relevant events (e.g. render a button, return whether it has been clicked). But this means that any event that should change how the UI is rendered may have missed its chance- what if the changed part of the UI has already had its function called that frame?
The "ideal" dataflow, as far as this problem is concerned, would be to fully process all the events in a frame (from raw input, to widget-level events like button presses, to application-specific stuff) before rendering anything.
One of the benefit of IMGUI-style is to simplify user-data binding and avoid user-data sync/duplication, but that's entirely decorelated with layout consideration. You could perfectly have an editor manipulating widgets properties or their layout info (e.g. pos/size/anchor) somewhere and have the function fetch and use those infos.
If a widget exist in an user interface editor, regardless of the UI paradigm, some code will interact with that widget (read/write the data or bind actions to some callback). So what are you describing and what exists everywhere are merely a way to edit properties and layout, which can be done in IMGUI style, and actual binding requires code either way.
> or dynamically generate user interface resources from other programs.
Err... of course you can, and you are free to express them in whichever format is more adequate and convenient from your application. Generating user interface resources doesn't mean "generating code", that would be silly because the interesting part of that code is the data/function binding, and that's the one that makes less sense to magically generate.
> You can't iterate quickly on user interface development, because the user interface specification is a program that you have to recompile, not data that you can reload.
Then I'm sorry for you if recompiling and reloading code is a slow process in your workflow today. Many languages including C++ have hot-reloading techniques, and it is the standard for any so-called "scripting" languages like Python, Lua, Javascript to be easy to reload. _Because_ IMGUI have less state and less spread editing UI touches less code, less data structures, less layers and therefore it is easy to have that dynamic editing. With Dear ImGui I can add temporary tooling UI _inside a random function deep into the callstack_, reload the code while running, then remove that code when I'm done.
I wish there was a retained mode GUI library that would be as easy to integrate in your game and graphics apps as ImGui (or nuklear) is.
On my spare time, I've been working on a retained mode GUI layout and rendering framework (not a complete GUI toolkit) that uses a DOM-like tree, CSS style flexbox layouts and produces a vertex buffer for rendering like ImGui does.
Unfortunately life gets in the way, and all I have to show for my project is a small prototype in C and another one in Rust (can't decide which I like more) that draws some text and rectangles with a fancy layout. Code is not public at the moment but I'm willing to share it with anyone (on GitHub) who responds with a thoughtful comment or a good question.
I have been following Raph Levien's fantastic work on Druid and Rust GUIs which has been very inspirational, this video in particular [0].
Where Raph is focused on native GUI rendering and high fidelity text output, I've only focused on rendering in a GPU application using shaders and only very basic text layout with simple kerning (like Dear ImGui).
As Raph points out in his later talks on the topic, layout and rendering are only half of the GUI puzzle. The other half is maintaining the state of the application, usually through some kind of widget tree.
Leave a comment if you're interested, I'd be happy to chat even though it doesn't seem like my projects are going anywhere anytime soon.
[0] https://www.youtube.com/watch?v=4YTfxresvS8 Data Oriented GUI in Rust by Raph Levien