GroveEngine/docs/DEVELOPER_GUIDE.md
StillHammer 0540fbf526 fix: Resolve bgfx Frame 1 crash on Windows DLL + MinGW GCC 15 compatibility
- Add BGFX_CONFIG_MULTITHREADED=0 to fix TLS crash when bgfx runs from DLL
- Add -include stdint.h for MinGW GCC 15+ compatibility with bgfx third-party code
- Guard SDL2-dependent visual tests with if(SDL2_FOUND)
- Clean up debug logging in BgfxDevice::frame() and BgfxRendererModule::process()
- Re-enable all modules in test_full_stack_interactive.cpp
- Add grove::fs namespace for cross-platform filesystem operations
- Add InputModule C export for feedEvent across DLL boundary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 11:03:06 +07:00

26 KiB

GroveEngine - Developer Guide

Comprehensive guide for building applications with GroveEngine

Table of Contents

  1. Getting Started
  2. Core System
  3. Available Modules
  4. IIO Topics Reference
  5. Complete Application Example
  6. Building Your First Game
  7. Advanced Topics

Getting Started

Prerequisites

  • C++17 compiler (GCC, Clang, or MSVC)
  • CMake 3.20+
  • Git for dependency management

Quick Start

# Clone GroveEngine
git clone <grove-engine-repo> GroveEngine
cd GroveEngine

# Build with all modules
cmake -B build -DGROVE_BUILD_BGFX_RENDERER=ON -DGROVE_BUILD_UI_MODULE=ON -DGROVE_BUILD_INPUT_MODULE=ON
cmake --build build -j4

# Run tests
cd build && ctest --output-on-failure

Documentation Structure


Core System

Architecture Overview

GroveEngine uses a module-based architecture with hot-reload support:

┌─────────────────────────────────────────────────────────────┐
│                    Your Application                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐   │
│  │  Input   │  │   UI     │  │ Renderer │  │  Game    │   │
│  │  Module  │  │  Module  │  │  Module  │  │  Logic   │   │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘   │
│       └─────────────┼─────────────┼─────────────┘          │
│                     │  IIO Pub/Sub System                   │
└─────────────────────┼───────────────────────────────────────┘
                      │
                IntraIOManager

Key Concepts

Component Purpose Documentation
IModule Module interface USER_GUIDE.md
IIO Pub/Sub messaging USER_GUIDE.md
IDataNode Configuration & data USER_GUIDE.md
ModuleLoader Hot-reload system USER_GUIDE.md

Available Modules

BgfxRenderer - 2D Rendering

Status: Production Ready (Phase 7-8 complete)

Multi-backend 2D renderer using bgfx (DirectX 11/12, OpenGL, Vulkan, Metal).

Features

  • Sprite rendering with batching
  • Text rendering with bitmap fonts
  • Tilemap support
  • Particle effects
  • Debug shapes (lines, rectangles)
  • Layer-based Z-ordering
  • Multi-texture batching
  • Headless mode for testing

Configuration

JsonDataNode config("config");
config.setInt("windowWidth", 1920);
config.setInt("windowHeight", 1080);
config.setString("backend", "auto");  // auto, opengl, vulkan, dx11, dx12, metal, noop
config.setString("shaderPath", "./shaders");
config.setBool("vsync", true);
config.setInt("maxSpritesPerBatch", 10000);
config.setInt("nativeWindowHandle", (int)(intptr_t)hwnd);  // Platform window handle

renderer->setConfiguration(config, rendererIO.get(), nullptr);

Rendering a Sprite

// Publish sprite to render
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100.0);
sprite->setDouble("y", 200.0);
sprite->setDouble("scaleX", 1.0);
sprite->setDouble("scaleY", 1.0);
sprite->setDouble("rotation", 0.0);        // Radians
sprite->setInt("color", 0xFFFFFFFF);       // RGBA
sprite->setInt("textureId", playerTexture);
sprite->setInt("layer", 10);               // Z-order (higher = front)
io->publish("render:sprite", std::move(sprite));

Rendering Text

auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 50.0);
text->setDouble("y", 50.0);
text->setString("text", "Score: 100");
text->setDouble("fontSize", 24.0);
text->setInt("color", 0xFFFFFFFF);
text->setInt("layer", 100);                // Text on top
io->publish("render:text", std::move(text));

Camera Control

auto camera = std::make_unique<JsonDataNode>("camera");
camera->setDouble("x", playerX);           // Center camera on player
camera->setDouble("y", playerY);
camera->setDouble("zoom", 1.0);
camera->setInt("viewportX", 0);
camera->setInt("viewportY", 0);
camera->setInt("viewportW", 1920);
camera->setInt("viewportH", 1080);
io->publish("render:camera", std::move(camera));

Full Topic Reference: See IIO Topics - Rendering


UIModule - User Interface

Status: Production Ready (Phase 7 complete)

Complete UI widget system with layout, scrolling, and tooltips.

Available Widgets

Widget Purpose Events
UIButton Clickable button ui:click, ui:action
UILabel Static text -
UIPanel Container -
UICheckbox Toggle checkbox ui:value_changed
UISlider Value slider ui:value_changed
UITextInput Text input field ui:value_changed, ui:text_submitted
UIProgressBar Progress display -
UIImage Image/sprite -
UIScrollPanel Scrollable container ui:scroll
UITooltip Hover tooltip -

Configuration

JsonDataNode uiConfig("config");
uiConfig.setInt("windowWidth", 1920);
uiConfig.setInt("windowHeight", 1080);
uiConfig.setString("layoutFile", "./ui/main_menu.json");  // JSON layout
uiConfig.setInt("baseLayer", 1000);  // UI renders above game (layer 1000+)

uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);

Creating a Button

// In your layout JSON file (ui/main_menu.json)
{
  "type": "UIButton",
  "id": "play_button",
  "x": 100,
  "y": 200,
  "width": 200,
  "height": 50,
  "text": "Play Game",
  "action": "start_game"
}
// In your game module - subscribe to button events
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");

// In process()
while (gameIO->hasMessages() > 0) {
    auto msg = gameIO->pullMessage();

    if (msg.topic == "ui:action") {
        std::string action = msg.data->getString("action", "");
        std::string widgetId = msg.data->getString("widgetId", "");

        if (action == "start_game" && widgetId == "play_button") {
            startGame();
        }
    }
}

Handling Input Events

UIModule automatically consumes input events from InputModule:

// UIModule subscribes to:
// - input:mouse:move
// - input:mouse:button
// - input:mouse:wheel
// - input:keyboard:key
// - input:keyboard:text

// Your game module just subscribes to UI events:
gameIO->subscribe("ui:*");  // All UI events

UI Rendering

UIModule publishes render commands to BgfxRenderer via UIRenderer:

// UIModule automatically publishes:
// - render:sprite (for UI rectangles/images)
// - render:text (for labels/buttons)

// BgfxRenderer consumes these and renders the UI
// Layer management ensures UI renders on top (layer 1000+)

Full Topic Reference: See IIO Topics - UI Events


InputModule - Input Handling

Status: Production Ready (Phase 1 complete)

Cross-platform input handling with SDL2 backend (mouse, keyboard, gamepad).

Features

  • Mouse (move, button, wheel)
  • Keyboard (key events, text input)
  • Thread-safe event buffering
  • Multiple backend support (SDL2, extensible)
  • Hot-reload support

Configuration

JsonDataNode inputConfig("config");
inputConfig.setString("backend", "sdl");
inputConfig.setBool("enableMouse", true);
inputConfig.setBool("enableKeyboard", true);
inputConfig.setBool("enableGamepad", false);  // Phase 2

inputModule->setConfiguration(inputConfig, inputIO.get(), nullptr);

Feeding Events (SDL2)

// In your main loop
SDL_Event event;
while (SDL_PollEvent(&event)) {
    // Feed to InputModule (thread-safe)
    inputModule->feedEvent(&event);

    // Also handle window events
    if (event.type == SDL_QUIT) {
        running = false;
    }
}

// Process InputModule (converts buffered events → IIO messages)
JsonDataNode input("input");
inputModule->process(input);

Consuming Input Events

// Subscribe to input topics
gameIO->subscribe("input:mouse:button");
gameIO->subscribe("input:keyboard:key");

// In process()
while (gameIO->hasMessages() > 0) {
    auto msg = gameIO->pullMessage();

    if (msg.topic == "input:mouse:button") {
        int button = msg.data->getInt("button", 0);  // 0=left, 1=middle, 2=right
        bool pressed = msg.data->getBool("pressed", false);
        double x = msg.data->getDouble("x", 0.0);
        double y = msg.data->getDouble("y", 0.0);

        if (button == 0 && pressed) {
            // Left mouse button pressed at (x, y)
            handleClick(x, y);
        }
    }

    if (msg.topic == "input:keyboard:key") {
        int scancode = msg.data->getInt("scancode", 0);  // SDL_SCANCODE_*
        bool pressed = msg.data->getBool("pressed", false);

        if (scancode == SDL_SCANCODE_SPACE && pressed) {
            playerJump();
        }
    }
}

Full Topic Reference: See IIO Topics - Input Events


IIO Topics Reference

Input Events

Published by InputModule, consumed by UIModule or game logic.

Mouse

Topic Payload Description
input:mouse:move {x: double, y: double} Mouse position (screen coords)
input:mouse:button {button: int, pressed: bool, x: double, y: double} Mouse click (0=left, 1=middle, 2=right)
input:mouse:wheel {delta: double} Mouse wheel (+up, -down)

Keyboard

Topic Payload Description
input:keyboard:key {scancode: int, pressed: bool, repeat: bool, shift: bool, ctrl: bool, alt: bool} Key event (scancode = SDL_SCANCODE_*)
input:keyboard:text {text: string} Text input (UTF-8, for TextInput widgets)

UI Events

Published by UIModule, consumed by game logic.

Topic Payload Description
ui:click {widgetId: string, x: double, y: double} Widget clicked
ui:action {widgetId: string, action: string} Button action triggered
ui:value_changed {widgetId: string, value: variant} Slider, checkbox, or text input changed
ui:text_submitted {widgetId: string, text: string} Text input submitted (Enter key)
ui:hover {widgetId: string, enter: bool} Mouse entered/left widget
ui:scroll {widgetId: string, scrollX: double, scrollY: double} Scroll panel scrolled

Rendering Topics

Consumed by BgfxRenderer, published by UIModule or game logic.

Sprites

Topic Payload Description
render:sprite {x, y, scaleX, scaleY, rotation, u0, v0, u1, v1, color, textureId, layer} Render single sprite
render:sprite:batch {sprites: [array]} Render sprite batch (optimized)

Text

Topic Payload Description
render:text {x, y, text, fontSize, color, layer} Render text

Tilemap

Topic Payload Description
render:tilemap {chunkX, chunkY, tiles: [array], tileSize, textureId, layer} Render tilemap chunk

Particles

Topic Payload Description
render:particle {x, y, velocityX, velocityY, color, lifetime, textureId, layer} Render particle

Camera

Topic Payload Description
render:camera {x, y, zoom, viewportX, viewportY, viewportW, viewportH} Set camera transform

Clear

Topic Payload Description
render:clear {color: int} Set clear color (RGBA)

Debug

Topic Payload Description
render:debug:line {x1, y1, x2, y2, color} Draw debug line
render:debug:rect {x, y, w, h, color, filled} Draw debug rectangle

Complete Application Example

Directory Structure

MyGame/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   └── modules/
│       ├── GameLogic.h
│       └── GameLogic.cpp
├── assets/
│   ├── ui/
│   │   └── main_menu.json
│   └── sprites/
│       └── player.png
└── external/
    └── GroveEngine/  # Git submodule

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyGame VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# GroveEngine + Modules
add_subdirectory(external/GroveEngine)
set(GROVE_BUILD_BGFX_RENDERER ON CACHE BOOL "" FORCE)
set(GROVE_BUILD_UI_MODULE ON CACHE BOOL "" FORCE)
set(GROVE_BUILD_INPUT_MODULE ON CACHE BOOL "" FORCE)

# Main executable
add_executable(mygame src/main.cpp)
target_link_libraries(mygame PRIVATE
    GroveEngine::impl
    SDL2::SDL2
    spdlog::spdlog
)

# Game logic module
add_library(GameLogic SHARED
    src/modules/GameLogic.cpp
)
target_link_libraries(GameLogic PRIVATE
    GroveEngine::impl
    spdlog::spdlog
)
set_target_properties(GameLogic PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
)

main.cpp

#include <grove/ModuleLoader.h>
#include <grove/IntraIOManager.h>
#include <grove/JsonDataNode.h>
#include <SDL2/SDL.h>
#include <iostream>

int main(int argc, char* argv[]) {
    // Initialize SDL
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    SDL_Window* window = SDL_CreateWindow("MyGame", SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_SHOWN);

    // Get native window handle
    SDL_SysWMinfo wmInfo;
    SDL_VERSION(&wmInfo.version);
    SDL_GetWindowWMInfo(window, &wmInfo);
    void* nativeHandle = nullptr;
#ifdef _WIN32
    nativeHandle = wmInfo.info.win.window;  // HWND
#elif __linux__
    nativeHandle = (void*)(uintptr_t)wmInfo.info.x11.window;
#endif

    // Create IIO instances
    auto& ioManager = grove::IntraIOManager::getInstance();
    auto rendererIO = ioManager.createInstance("renderer");
    auto uiIO = ioManager.createInstance("ui");
    auto inputIO = ioManager.createInstance("input");
    auto gameIO = ioManager.createInstance("game");

    // Load modules
    grove::ModuleLoader rendererLoader, uiLoader, inputLoader, gameLoader;

    auto renderer = rendererLoader.load("./modules/BgfxRenderer.dll", "renderer");
    auto uiModule = uiLoader.load("./modules/UIModule.dll", "ui");
    auto inputModule = inputLoader.load("./modules/InputModule.dll", "input");
    auto gameModule = gameLoader.load("./modules/GameLogic.dll", "game");

    // Configure BgfxRenderer
    grove::JsonDataNode rendererConfig("config");
    rendererConfig.setInt("windowWidth", 1920);
    rendererConfig.setInt("windowHeight", 1080);
    rendererConfig.setString("backend", "auto");
    rendererConfig.setInt("nativeWindowHandle", (int)(intptr_t)nativeHandle);
    renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr);

    // Configure UIModule
    grove::JsonDataNode uiConfig("config");
    uiConfig.setInt("windowWidth", 1920);
    uiConfig.setInt("windowHeight", 1080);
    uiConfig.setString("layoutFile", "./assets/ui/main_menu.json");
    uiConfig.setInt("baseLayer", 1000);
    uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr);

    // Configure InputModule
    grove::JsonDataNode inputConfig("config");
    inputConfig.setString("backend", "sdl");
    inputModule->setConfiguration(inputConfig, inputIO.get(), nullptr);

    // Configure GameLogic
    grove::JsonDataNode gameConfig("config");
    gameModule->setConfiguration(gameConfig, gameIO.get(), nullptr);

    // Main loop
    bool running = true;
    Uint64 lastTime = SDL_GetPerformanceCounter();

    while (running) {
        // 1. Handle SDL events
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                running = false;
            }
            inputModule->feedEvent(&event);  // Feed to InputModule
        }

        // 2. Calculate deltaTime
        Uint64 now = SDL_GetPerformanceCounter();
        double deltaTime = (now - lastTime) / (double)SDL_GetPerformanceFrequency();
        lastTime = now;

        // 3. Process all modules
        grove::JsonDataNode input("input");
        input.setDouble("deltaTime", deltaTime);

        inputModule->process(input);   // Input → IIO messages
        uiModule->process(input);      // UI → IIO messages
        gameModule->process(input);    // Game logic
        renderer->process(input);      // Render frame

        // 4. Optional: Hot-reload check
        // (file watcher code here)
    }

    // Cleanup
    renderer->shutdown();
    uiModule->shutdown();
    inputModule->shutdown();
    gameModule->shutdown();

    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}

GameLogic.cpp

#include <grove/IModule.h>
#include <grove/JsonDataNode.h>
#include <grove/IIO.h>
#include <spdlog/spdlog.h>

class GameLogic : public grove::IModule {
public:
    GameLogic() {
        m_logger = spdlog::stdout_color_mt("GameLogic");
    }

    void setConfiguration(const grove::IDataNode& config,
                          grove::IIO* io,
                          grove::ITaskScheduler* scheduler) override {
        m_io = io;

        // Subscribe to UI events
        m_io->subscribe("ui:action");
        m_io->subscribe("ui:click");
    }

    void process(const grove::IDataNode& input) override {
        double deltaTime = input.getDouble("deltaTime", 0.016);

        // Process UI events
        while (m_io->hasMessages() > 0) {
            auto msg = m_io->pullMessage();

            if (msg.topic == "ui:action") {
                std::string action = msg.data->getString("action", "");

                if (action == "start_game") {
                    startGame();
                }
            }
        }

        // Update game logic
        if (m_gameStarted) {
            updatePlayer(deltaTime);
            renderPlayer();
        }
    }

    // ... other IModule methods ...

private:
    void startGame() {
        m_gameStarted = true;
        m_playerX = 960.0;
        m_playerY = 540.0;
        m_logger->info("Game started!");
    }

    void updatePlayer(double deltaTime) {
        // Update player position, etc.
    }

    void renderPlayer() {
        // Publish sprite to renderer
        auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
        sprite->setDouble("x", m_playerX);
        sprite->setDouble("y", m_playerY);
        sprite->setInt("textureId", 0);  // Player texture
        sprite->setInt("layer", 10);
        m_io->publish("render:sprite", std::move(sprite));
    }

    std::shared_ptr<spdlog::logger> m_logger;
    grove::IIO* m_io = nullptr;
    bool m_gameStarted = false;
    double m_playerX = 0.0;
    double m_playerY = 0.0;
};

extern "C" {
    grove::IModule* createModule() { return new GameLogic(); }
    void destroyModule(grove::IModule* m) { delete m; }
}

Interactive Demo - Try It First!

Before reading further, try the full stack interactive demo to see everything in action:

# Windows
run_full_stack_demo.bat

# Linux
./build/tests/test_full_stack_interactive

What it demonstrates:

  • BgfxRenderer rendering sprites and text
  • UIModule with buttons, sliders, panels
  • InputModule capturing mouse and keyboard
  • Complete IIO message flow (input → UI → game → render)
  • Hit testing and click detection (raycasting 2D)
  • Game logic responding to UI events

Interactive controls:

  • Click buttons to spawn/clear sprites
  • Drag slider to change speed
  • Press SPACE to spawn from keyboard
  • Press ESC to exit

See: tests/visual/README_FULL_STACK.md for details.


Building Your First Game

Step-by-Step Tutorial

1. Create Project Structure

mkdir MyGame && cd MyGame
git init
git submodule add <grove-engine-repo> external/GroveEngine
mkdir -p src/modules assets/ui

2. Create CMakeLists.txt

(See Complete Application Example)

3. Create UI Layout

assets/ui/main_menu.json:

{
  "widgets": [
    {
      "type": "UIPanel",
      "id": "main_panel",
      "x": 0,
      "y": 0,
      "width": 1920,
      "height": 1080,
      "color": 2155905279
    },
    {
      "type": "UIButton",
      "id": "play_button",
      "x": 860,
      "y": 500,
      "width": 200,
      "height": 60,
      "text": "Play",
      "action": "start_game"
    },
    {
      "type": "UIButton",
      "id": "quit_button",
      "x": 860,
      "y": 580,
      "width": 200,
      "height": 60,
      "text": "Quit",
      "action": "quit_game"
    }
  ]
}

4. Build and Run

cmake -B build
cmake --build build -j4
./build/mygame

Advanced Topics

Hot-Reload Workflow

# Terminal 1: Run game
./build/mygame

# Terminal 2: Edit and rebuild module
vim src/modules/GameLogic.cpp
cmake --build build --target GameLogic

# Game automatically reloads GameLogic with state preserved!

Performance Optimization

Sprite Batching

// Instead of publishing 100 individual sprites:
for (auto& enemy : enemies) {
    auto sprite = std::make_unique<JsonDataNode>("sprite");
    sprite->setDouble("x", enemy.x);
    // ...
    io->publish("render:sprite", std::move(sprite));  // 100 IIO messages
}

// Publish as batch:
auto batch = std::make_unique<JsonDataNode>("batch");
auto sprites = std::make_unique<JsonDataNode>("sprites");
for (auto& enemy : enemies) {
    auto sprite = std::make_unique<JsonDataNode>("sprite");
    sprite->setDouble("x", enemy.x);
    // ...
    sprites->setChild(enemy.id, std::move(sprite));
}
batch->setChild("sprites", std::move(sprites));
io->publish("render:sprite:batch", std::move(batch));  // 1 IIO message

Low-Frequency Subscriptions

// For non-critical analytics/logging
grove::SubscriptionConfig config;
config.batchInterval = 1000;  // Batch messages for 1 second
io->subscribeLowFreq("analytics:*", config);

Multi-Module Communication Patterns

Request-Response Pattern

// Module A: Request pathfinding
auto request = std::make_unique<JsonDataNode>("request");
request->setString("requestId", "path_123");
request->setDouble("startX", 10.0);
request->setDouble("startY", 20.0);
io->publish("pathfinding:request", std::move(request));

// Module B: Respond with path
moduleB_io->subscribe("pathfinding:request");
// ... compute path ...
auto response = std::make_unique<JsonDataNode>("response");
response->setString("requestId", "path_123");
// ... add path data ...
moduleB_io->publish("pathfinding:response", std::move(response));

// Module A: Receive response
moduleA_io->subscribe("pathfinding:response");

Event Aggregation

// Multiple modules publish events
io->publish("combat:damage", damageData);
io->publish("combat:kill", killData);
io->publish("combat:levelup", levelupData);

// Analytics module aggregates all combat events
analyticsIO->subscribe("combat:*");

Testing Strategies

Headless Testing

// Configure renderer in headless mode
JsonDataNode config("config");
config.setString("backend", "noop");  // No actual rendering
config.setBool("vsync", false);
renderer->setConfiguration(config, io, nullptr);

// Run tests without window
for (int i = 0; i < 1000; i++) {
    // Simulate game logic
    renderer->process(input);
}

Integration Tests

See tests/integration/IT_014_ui_module_integration.cpp for complete example.


Troubleshooting

Common Issues

Module not loading

# Check module exports
nm -D build/modules/GameLogic.so | grep createModule
# Should show: createModule and destroyModule

# Check dependencies
ldd build/modules/GameLogic.so

IIO messages not received

// Verify subscription BEFORE publishing
io->subscribe("render:sprite");  // Must be before publish

// Check topic patterns
io->subscribe("render:*");  // Matches render:sprite, render:text
io->subscribe("render:sprite:*");  // Only matches render:sprite:batch

Hot-reload state loss

// Ensure ALL state is serialized in getState()
std::unique_ptr<IDataNode> MyModule::getState() {
    auto state = std::make_unique<JsonDataNode>("state");

    // DON'T FORGET any member variables!
    state->setInt("score", m_score);
    state->setDouble("playerX", m_playerX);
    state->setDouble("playerY", m_playerY);
    // ...

    return state;
}

Additional Resources


GroveEngine - Build modular, hot-reloadable games with ease 🌳