## Breaking Change
IIO API redesigned from manual pull+if-forest to callback dispatch.
All modules must update their subscribe() calls to pass handlers.
### Before (OLD API)
```cpp
io->subscribe("input:mouse");
void process(...) {
while (io->hasMessages()) {
auto msg = io->pullMessage();
if (msg.topic == "input:mouse") {
handleMouse(msg);
} else if (msg.topic == "input:keyboard") {
handleKeyboard(msg);
}
}
}
```
### After (NEW API)
```cpp
io->subscribe("input:mouse", [this](const Message& msg) {
handleMouse(msg);
});
void process(...) {
while (io->hasMessages()) {
io->pullAndDispatch(); // Callbacks invoked automatically
}
}
```
## Changes
**Core API (include/grove/IIO.h)**
- Added: `using MessageHandler = std::function<void(const Message&)>`
- Changed: `subscribe()` now requires `MessageHandler` callback parameter
- Changed: `subscribeLowFreq()` now requires `MessageHandler` callback
- Removed: `pullMessage()`
- Added: `pullAndDispatch()` - pulls and auto-dispatches to handlers
**Implementation (src/IntraIO.cpp)**
- Store callbacks in `Subscription.handler`
- `pullAndDispatch()` matches topic against ALL subscriptions (not just first)
- Fixed: Regex pattern compilation supports both wildcards (*) and regex (.*)
- Performance: ~1000 msg/s throughput (unchanged from before)
**Files Updated**
- 31 test/module files migrated to callback API (via parallel agents)
- 8 documentation files updated (DEVELOPER_GUIDE, USER_GUIDE, module READMEs)
## Bugs Fixed During Migration
1. **pullAndDispatch() early return bug**: Was only calling FIRST matching handler
- Fix: Loop through ALL subscriptions, invoke all matching handlers
2. **Regex pattern compilation bug**: Pattern "player:.*" failed to match
- Fix: Detect ".*" in pattern → use as regex, otherwise escape and convert wildcards
## Testing
✅ test_11_io_system: PASSED (IIO pub/sub, pattern matching, batching)
✅ test_threaded_module_system: 6/6 PASSED
✅ test_threaded_stress: 5/5 PASSED (50 modules, 100x reload, concurrent ops)
✅ test_12_datanode: PASSED
✅ 10 TopicTree scenarios: 10/10 PASSED
✅ benchmark_e2e: ~1000 msg/s throughput
Total: 23+ tests passing
## Performance Impact
No performance regression from callback dispatch:
- IIO throughput: ~1000 msg/s (same as before)
- ThreadedModuleSystem: Speedup ~1.0x (barrier pattern expected)
## Migration Guide
For all modules using IIO:
1. Update subscribe() calls to include handler lambda
2. Replace pullMessage() loops with pullAndDispatch()
3. Move topic-specific logic from if-forest into callbacks
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
9.1 KiB
UIModule - Architecture & Design
Architecture Overview
InputModule → IIO (input:*)
↓
UIModule
(Widget Tree)
↓
UIRenderer (publishes)
↓
IIO (render:*)
↓
BgfxRenderer
All communication happens via IIO topics - no direct module-to-module calls.
Current Limitations
No Direct Data Binding
UIModule does not have built-in data binding. Updates must flow through the game module:
Slider → ui:value_changed → Game Module → ui:set_text → Label
This is intentional to maintain the IIO-based architecture where all communication goes through topics. The game module acts as the central coordinator.
Example:
// Subscribe to slider value changes (in setConfiguration)
gameIO->subscribe("ui:value_changed", [this](const grove::Message& msg) {
std::string widgetId = msg.data->getString("widgetId", "");
if (widgetId == "volume_slider") {
double value = msg.data->getDouble("value", 0);
setVolume(value);
// Update label (must go through game module)
auto updateMsg = std::make_unique<JsonDataNode>("set_text");
updateMsg->setString("id", "volume_label");
updateMsg->setString("text", "Volume: " + std::to_string((int)value) + "%");
m_io->publish("ui:set_text", std::move(updateMsg));
}
});
// In process()
while (gameIO->hasMessages() > 0) {
gameIO->pullAndDispatch(); // Callback invoked automatically
}
Message Latency in Single-Threaded Mode
Current test showcases: ~16ms latency (1 frame @ 60 FPS)
Cause: Messages are queued until the next pullMessage() call in the game loop.
// Single-threaded game loop (test showcase)
while(running) {
handleInput(); // UIModule publishes event
processUIEvents(); // Game receives event (next frame!)
update();
render();
}
Solution: Run modules in separate threads (production architecture).
Threading Model
Current: Single-Threaded (Tests)
Test showcases run all modules in a single thread for simplicity:
void main() {
auto uiModule = loadModule("UIModule");
auto renderer = loadModule("BgfxRenderer");
while(running) {
// All in same thread - sequential execution
processInput();
uiModule->process(deltaTime);
renderer->process(deltaTime);
SDL_GL_SwapWindow(window);
}
}
Latency: ~16ms (next frame polling)
Production: Multi-Threaded
Each module runs in its own thread:
// UIModule thread @ 60 FPS
void uiThread() {
while(running) {
// Receive inputs from queue (filled by InputModule thread)
// Callbacks registered at subscribe() handle dispatch
while(io->hasMessages()) {
io->pullAndDispatch(); // Auto-dispatch to registered callbacks
}
update(deltaTime);
// Publish events (immediately queued to Game thread)
io->publish("ui:value_changed", msg);
sleep(16ms);
}
}
// Game thread @ 60 FPS
void gameThread() {
while(running) {
// Pull messages from queue (latency < 1ms)
// Callbacks registered at subscribe() handle dispatch
while(io->hasMessages()) {
io->pullAndDispatch(); // Auto-dispatch, already in queue!
}
updateGameLogic(deltaTime);
sleep(16ms);
}
}
Latency: < 1ms (just mutex lock + memcpy)
IntraIO Message Delivery
IntraIO uses a queue-based system with push-on-publish, pull-on-consume:
-
Module A publishes:
io->publish("topic", msg)- Message immediately delivered to Module B's queue
- No batching delay (batch thread is for low-freq subscriptions only)
-
Module B pulls:
io->pullMessage()- Returns message from queue
- No network/serialization overhead
With threading: Messages are available in the queue immediately, so the next pullMessage() call retrieves them with minimal latency.
Without threading: All pullMessage() calls happen sequentially in the game loop, so messages wait until the next frame.
Layer Management
UIModule uses layer-based rendering to ensure proper draw order:
- Game sprites: Layer 0-999
- UI base layer: 1000 (configurable via
baseLayerconfig) - UI widgets: baseLayer + widget index
- Tooltips: Highest layer (automatic)
config.setInt("baseLayer", 1000); // UI renders above game
Hot-Reload Support
UIModule fully supports hot-reload with state preservation.
State Preserved
- Widget properties (position, size, colors)
- Widget states (checkbox checked, slider values, text input content)
- Scroll positions
- Widget hierarchy
State Not Preserved
- Transient animation states
- Mouse hover states (recalculated on next mouse move)
- Focus state (recalculated on next interaction)
How It Works
-
Extract State:
nlohmann::json UIModule::extractState() { json state; // Serialize all widget properties return state; } -
Reload Module:
moduleLoader.reload(); // Unload .dll, recompile, reload -
Restore State:
void UIModule::restoreState(const nlohmann::json& state) { // Restore widget properties from JSON }
Performance
Retained Mode Rendering
UIModule uses retained mode to optimize IIO traffic:
Message Reduction:
- Static UI (20 widgets, 0 changes): 100% reduction (0 messages/frame after registration)
- Mostly static (20 widgets, 3 changes): 85% reduction (3 vs 20 messages)
- Fully dynamic (20 widgets, 20 changes): 0% reduction (comparison overhead)
Implementation:
- Widgets cache render state
- Compare against previous state each frame
- Only publish
render:*:updateif changed
See UI_RENDERING.md for details.
Target Performance
- UI update: < 1ms per frame
- Render command generation: < 0.5ms per frame
- Message routing: < 0.1ms per message
- Widget count: Up to 100+ widgets without performance issues
Future Enhancements
Planned Features
Data Binding (Optional)
Link widget properties to game variables with automatic sync:
{
"type": "label",
"text": "${player.health}",
"bindTo": "player.health"
}
Note: Will remain optional to preserve IIO architecture for those who prefer explicit control.
Animations
Tweening, fades, transitions:
{
"type": "panel",
"animations": {
"enter": {"type": "fade", "duration": 0.3},
"exit": {"type": "slide", "direction": "left", "duration": 0.2}
}
}
Flexible Layout
Anchors, constraints, flex, grid:
{
"type": "button",
"anchor": "bottom-right",
"offset": {"x": -20, "y": -20}
}
{
"type": "panel",
"layout": "flex",
"flexDirection": "column",
"gap": 10
}
Drag & Drop
{
"type": "image",
"draggable": true,
"dragGroup": "inventory"
}
Rich Text
Markdown/BBCode formatting:
{
"type": "label",
"text": "**Bold** *italic* `code`",
"richText": true
}
Themes
Swappable style sheets:
{
"theme": "dark",
"themeFile": "themes/dark.json"
}
9-Slice Sprites
Scalable sprite borders:
{
"type": "panel",
"sprite": "panel_border.png",
"sliceMode": "9-slice",
"sliceInsets": {"top": 8, "right": 8, "bottom": 8, "left": 8}
}
Input Validation
Regex patterns for text inputs:
{
"type": "textinput",
"validation": "^[a-zA-Z0-9]+$",
"errorMessage": "Alphanumeric only"
}
Not Planned
These features violate core design principles and will never be added:
- ❌ Direct widget-to-widget communication - All communication must go through IIO topics
- ❌ Embedded game logic in widgets - Widgets are pure UI, game logic stays in game modules
- ❌ Direct renderer access - Widgets publish render commands via IIO, never call renderer directly
- ❌ Direct input polling - Widgets consume
input:*topics, never poll input devices directly
Design Principles
- IIO-First: All communication via topics with callback dispatch, no direct coupling
- Callback Dispatch: Subscribe with handlers, no if-forest dispatch in process()
- Pull-Based Control: Module controls WHEN to process (pullAndDispatch), callbacks handle HOW
- Retained Mode: Cache state, minimize IIO traffic
- Hot-Reload Safe: Full state preservation across reloads
- Thread-Safe: Designed for multi-threaded production use
- Module Independence: UIModule never imports BgfxRenderer or InputModule headers
- Game Logic Separation: Widgets are dumb views, game modules handle logic
Integration with Other Modules
With BgfxRenderer
UIModule → render:sprite:*, render:text:* → BgfxRenderer
No direct dependency. UIModule doesn't know BgfxRenderer exists.
With InputModule
InputModule → input:* → UIModule
No direct dependency. UIModule doesn't know InputModule exists.
With Game Module
Bidirectional via IIO:
- Game →
ui:set_text,ui:set_visible→ UIModule - UIModule →
ui:action,ui:value_changed→ Game
Game module coordinates all interactions.