Immediate Mode (API)
Immediate mode is an API design approach, where the handling of state by the client is minimized. Instead, calls to the API encode both state changes and functionality or control flow.
On the other side of the spectrum is the retained mode approach, where state such as layout, hierarchies or styles typically need to be specified and initialized beforehand in another part of the codebase.
Immediate mode APIs most commonly come in the form of immediate mode GUIs.
Immediate Mode vs Retained Mode (UI Example)
A (mostly) pure immediate mode approach to a UI in C:
int main() { while (app_running) /* application loop */ { /* update */ { // ... gui_update(); // collect input } /* runs every frame */ label("Would you like to save your changes?"); // pushes rendering of text to command buffer if (button("Yes")) // pushes rendering of button to cmd_buf, returns true when pushed { app->save(); app_running = 0; } if (button("No")) { app_running = 0; } /* render */ { // ... gui_render(); // render from command buffer } } }
What a retained mode approach could look like in C++:
/* in ui.cpp */ class UI { public: UI(...) { /* construct buttons, labels, etc. */ save_prompt = UILabel("Would you like to save your changes?"); /* NOTE: since retained mode often works via callbacks, these now somehow need access to state (e.g. by passing it to the UI constructor, making the state global, etc.). In the imgui, this state was directly available just from being in the same scope */ yes_button = UIButton("Yes", []() { app->save(); app_running = false; } ); no_button = UIButton("No", []() { app_running = false; }); } void UpdateUI(); void RenderUI(); private: UILabel save_prompt; UIButton yes_button; UIButton no_button; } /* in main.cpp */ int main() { /* construct on start up */ UI ui(...); while (app_running) /* application loop */ { /* update */ { ui.UpdateUI(); } /* render */ { ui.RenderUI(); } } }
Implicitly Building Data Structures
Following datastructures can be build implicitly in an immediate mode API:
- Stack
- Linked Lists
- Stacks + Queues (via linked lists)
- Ring buffer (via circular linked lists)
- Trees (via linked lists)
Stack
Using for-loops, one can push a structure onto a stack with every opening for-loop. The top of the stack is available under the same name in every scope.
typedef struct thing_t { /* state of thing_t */ // ... /* used for stack implementation */ int i = 0; } thing_t; // explicit building of a thing_t stack for (thing_t thing = {0}; thing.i == 0; thing.i++) { /* stack: |thing|... */ operate(thing, 4.0f); for (thing_t thing_old = thing, thing_2 = {0}; thing_2.i == 0; (thing = thing_old, thing_2.i += 1)) { /* stack: |thing|thing|... */ operate(thing, 3.2f); transform(thing, 5.f); for (thing_t thing_old = thing, thing_3 = {0}; thing_3.i == 0; (thing = thing_old, thing_3.i += 1)) { /* stack: |thing|thing|thing|... */ // ... } /* stack: |thing|thing|... */ } /* stack: |thing|... */ } /* stack: || */
The implicit immediate mode API could look like this:
// implicit building of a thing_t stack scope_begin_thing(...) /* parameters can be used to construct thing_t */ { /* stack: |thing|... */ operate(4.0f); // implicit passing of thing via macro push_thing(...) /* contains parameters for the new thing_t */ { /* stack: |thing|thing|... */ operate(3.2f); // transforms only top of stack called thing transform(5.f); push_thing(...) { /* stack: |thing|thing|thing| */ } } }
Tree
An n-ary tree can be build in the form of a binary first-child-next-sibling tree:
/* Tree structure: child-sibling-sibling | | | child-sibling-sibling | child-sibling-sibling */ scope_start() // child { operation(5.5); // sibling transformation(); // sibling scope_child() // child { button(); // sibling button(); // sibling button(); // sibling box() // sibling { /* children of box */ text(); } } }
Use Cases
- UI
- Rendering
- Plotting
- Texture Generation
- HTTP Client
- JSON Serialization
Examples
Immediate-mode Dialogue System
Design Idea:
/* Usage, runs every frame */ dialogue_t dialogue; dialogue_box(dialogue) { text("Die monster.", bold(), shake(0.3)) { text("You don't belong in this world!", speed(1.4)); } text("It was not by my hand that I am once again given flesh.", font("evil.ttf")) { linebreak(); text("I was called here by humans who wish to pay me tribute.", italic()); } text("Tribute!?!", shake(0.5), bold(), color(RED)) { text("You steal men's souls and make them your slaves!"); } text("Perhaps the same could be said of all religions", italic(), speed(0.5)) { ellipsis(); } text("Your words are as empty as your soul! Mankind ill needs a savior such as you!", glowing()); text("What is a man?") { pause(3); text("A miserable little pile of secrets.", shake(1.0)); } } // dialogue_t filled with all relevant data at this point, either... // - all relevant data to render the text that should be currently rendered (position, style, color, ...) // - or a fully rendered texture that is transparent besides the text
Encapsulating Operations
TextureOp ops[] = { {TextureOpKind_Fill, .color = 0x340f}, {TextureOpKind_Grunge}, {TextureOpKind_Noise, .color = 0x100a, .value = 2.5f}, }; Texture tex = MakeTexture(ops, ArrayCount(ops)); TextureOp ops[] = { Fill(0x340f), Grunge(), Noise(0x100a, 2.5f), }; Texture tex = MakeTexture(ops, ArrayCount(ops)); #define MakeTexture(...) MakeTexture_((TextureOp[]){__VA_ARGS__}, ArrayCount((TextureOp[]){__VA_ARGS__})) /* usage code */ Texture tex = MakeTexture(Fill(0x340f), Grunge(), Noise(0x100a, 2.5f));