feat: Add BgfxRenderer module skeleton

- Add complete BgfxRenderer module structure (24 files)
- RHI abstraction layer (no bgfx:: exposed outside BgfxDevice.cpp)
- Frame system with lock-free allocator
- RenderGraph with ClearPass, SpritePass, DebugPass
- SceneCollector for IIO message parsing (render:* topics)
- ResourceCache with thread-safe texture/shader caching
- Full IModule integration (config via IDataNode, comm via IIO)
- CMake with FetchContent for bgfx
- Windows build script (build_renderer.bat)
- Documentation (README.md, USER_GUIDE.md, PLAN_BGFX_RENDERER.md)
- Updated .gitignore for Windows builds

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-26 00:41:55 +08:00
parent 18a319768d
commit d63d8d83fa
30 changed files with 4621 additions and 0 deletions

43
.gitignore vendored
View File

@ -1,2 +1,45 @@
# Build directories
build/
build-*/
cmake-build-*/
out/
# Logs
logs/
*.log
# IDE
.vs/
.vscode/
*.vcxproj.user
*.suo
*.sdf
*.opensdf
.idea/
# Compiled binaries
*.exe
*.dll
*.so
*.dylib
*.a
*.lib
*.o
*.obj
# CMake
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
compile_commands.json
_deps/
# OS
.DS_Store
Thumbs.db
desktop.ini
# Temp
*.tmp
*.swp
*~

View File

@ -156,6 +156,19 @@ if(GROVE_BUILD_IMPLEMENTATIONS)
add_library(GroveEngine::impl ALIAS grove_impl)
endif()
# ============================================================================
# Modules (hot-reloadable .so)
# ============================================================================
option(GROVE_BUILD_MODULES "Build GroveEngine modules" ON)
if(GROVE_BUILD_MODULES)
# BgfxRenderer module (2D rendering via bgfx)
option(GROVE_BUILD_BGFX_RENDERER "Build BgfxRenderer module" OFF)
if(GROVE_BUILD_BGFX_RENDERER)
add_subdirectory(modules/BgfxRenderer)
endif()
endif()
# Testing
option(GROVE_BUILD_TESTS "Build GroveEngine tests" ON)

113
build_renderer.bat Normal file
View File

@ -0,0 +1,113 @@
@echo off
setlocal enabledelayedexpansion
:: ============================================================================
:: GroveEngine - BgfxRenderer Build Script for Windows
:: ============================================================================
echo.
echo ============================================
echo GroveEngine - BgfxRenderer Builder
echo ============================================
echo.
:: Check if cmake is available
where cmake >nul 2>nul
if %ERRORLEVEL% neq 0 (
echo [ERROR] CMake not found in PATH
echo Install CMake from https://cmake.org/download/
echo Or install via: winget install Kitware.CMake
pause
exit /b 1
)
:: Set build directory
set BUILD_DIR=build-win
:: Parse arguments
set CONFIG=Release
set CLEAN=0
set OPEN_VS=0
:parse_args
if "%~1"=="" goto end_parse
if /i "%~1"=="debug" set CONFIG=Debug
if /i "%~1"=="release" set CONFIG=Release
if /i "%~1"=="clean" set CLEAN=1
if /i "%~1"=="vs" set OPEN_VS=1
if /i "%~1"=="--help" goto show_help
if /i "%~1"=="-h" goto show_help
shift
goto parse_args
:end_parse
:: Clean if requested
if %CLEAN%==1 (
echo [INFO] Cleaning build directory...
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
)
:: Configure
echo [INFO] Configuring CMake (%CONFIG%)...
cmake -B %BUILD_DIR% -DGROVE_BUILD_BGFX_RENDERER=ON -DGROVE_BUILD_TESTS=OFF
if %ERRORLEVEL% neq 0 (
echo [ERROR] CMake configuration failed
pause
exit /b 1
)
:: Open VS if requested
if %OPEN_VS%==1 (
echo [INFO] Opening Visual Studio...
start "" "%BUILD_DIR%\GroveEngine.sln"
exit /b 0
)
:: Build
echo.
echo [INFO] Building BgfxRenderer (%CONFIG%)...
cmake --build %BUILD_DIR% --config %CONFIG% --target BgfxRenderer -j
if %ERRORLEVEL% neq 0 (
echo.
echo [ERROR] Build failed
pause
exit /b 1
)
:: Success
echo.
echo ============================================
echo Build successful!
echo ============================================
echo.
echo Output: %BUILD_DIR%\modules\%CONFIG%\libBgfxRenderer.dll
echo.
echo Usage:
echo build_renderer.bat - Build Release
echo build_renderer.bat debug - Build Debug
echo build_renderer.bat clean - Clean and rebuild
echo build_renderer.bat vs - Open in Visual Studio
echo.
exit /b 0
:show_help
echo.
echo Usage: build_renderer.bat [options]
echo.
echo Options:
echo debug Build in Debug mode
echo release Build in Release mode (default)
echo clean Clean build directory before building
echo vs Generate and open Visual Studio solution
echo --help Show this help
echo.
echo Examples:
echo build_renderer.bat
echo build_renderer.bat debug
echo build_renderer.bat clean release
echo build_renderer.bat vs
echo.
exit /b 0

1255
docs/PLAN_BGFX_RENDERER.md Normal file

File diff suppressed because it is too large Load Diff

749
docs/USER_GUIDE.md Normal file
View File

@ -0,0 +1,749 @@
# GroveEngine User Guide
GroveEngine is a C++17 hot-reload module system designed for building modular applications with runtime code replacement capabilities.
## Table of Contents
1. [Overview](#overview)
2. [Core Concepts](#core-concepts)
3. [Project Setup](#project-setup)
4. [Creating Modules](#creating-modules)
5. [Module Lifecycle](#module-lifecycle)
6. [Inter-Module Communication](#inter-module-communication)
7. [Hot-Reload](#hot-reload)
8. [Configuration Management](#configuration-management)
9. [Task Scheduling](#task-scheduling)
10. [API Reference](#api-reference)
---
## Overview
GroveEngine provides:
- **Hot-Reload**: Replace module code at runtime without losing state
- **Modular Architecture**: Self-contained modules with clear interfaces
- **Pub/Sub Communication**: Decoupled inter-module messaging via topics
- **State Preservation**: Automatic state serialization across reloads
- **Configuration Hot-Reload**: Update module configuration without code changes
### Design Philosophy
- Modules contain pure business logic (200-300 lines recommended)
- No infrastructure code in modules (threading, networking, persistence)
- All data via `IDataNode` abstraction (backend agnostic)
- Pull-based message processing (modules control when they read messages)
---
## Core Concepts
### IModule
The base interface all modules implement. Defines the contract for:
- Processing logic (`process()`)
- Configuration (`setConfiguration()`, `getConfiguration()`)
- State management (`getState()`, `setState()`)
- Lifecycle (`shutdown()`)
### IDataNode
Hierarchical data structure for configuration, state, and messages. Supports:
- Typed accessors (`getString()`, `getInt()`, `getDouble()`, `getBool()`)
- Tree navigation (`getChild()`, `getChildNames()`)
- Pattern matching (`getChildrenByNameMatch()`)
### IIO
Pub/Sub communication interface:
- `publish()`: Send messages to topics
- `subscribe()`: Listen to topic patterns
- `pullMessage()`: Consume received messages
### ModuleLoader
Handles dynamic loading of `.so` files:
- `load()`: Load a module from shared library
- `reload()`: Hot-reload with state preservation
- `unload()`: Clean unload
---
## Project Setup
### Directory Structure
```
MyProject/
├── CMakeLists.txt
├── external/
│ └── GroveEngine/ # GroveEngine (submodule or symlink)
├── src/
│ ├── main.cpp
│ └── modules/
│ ├── MyModule.h
│ └── MyModule.cpp
└── config/
└── mymodule.json
```
### CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ============================================================================
# GroveEngine Integration
# ============================================================================
set(GROVE_BUILD_TESTS OFF CACHE BOOL "Disable GroveEngine tests" FORCE)
add_subdirectory(external/GroveEngine)
# ============================================================================
# Main Executable
# ============================================================================
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
GroveEngine::impl
spdlog::spdlog
)
# ============================================================================
# Hot-Reloadable Modules (.so)
# ============================================================================
add_library(MyModule SHARED
src/modules/MyModule.cpp
)
target_link_libraries(MyModule PRIVATE
GroveEngine::impl
spdlog::spdlog
)
set_target_properties(MyModule PROPERTIES
PREFIX "lib"
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
)
# ============================================================================
# Copy config files to build directory
# ============================================================================
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/config/
DESTINATION ${CMAKE_BINARY_DIR}/config)
# ============================================================================
# Convenience targets
# ============================================================================
add_custom_target(modules
DEPENDS MyModule
COMMENT "Building hot-reloadable modules only"
)
```
### Linking GroveEngine
Option 1: Git submodule
```bash
git submodule add <grove-engine-repo> external/GroveEngine
```
Option 2: Symlink (local development)
```bash
ln -s /path/to/GroveEngine external/GroveEngine
```
---
## Creating Modules
### Module Header
```cpp
// src/modules/MyModule.h
#pragma once
#include <grove/IModule.h>
#include <grove/IDataNode.h>
#include <grove/IIO.h>
#include <spdlog/spdlog.h>
#include <memory>
namespace myapp {
class MyModule : public grove::IModule {
public:
MyModule();
// Required IModule interface
void process(const grove::IDataNode& input) override;
void setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) override;
const grove::IDataNode& getConfiguration() override;
std::unique_ptr<grove::IDataNode> getHealthStatus() override;
void shutdown() override;
std::unique_ptr<grove::IDataNode> getState() override;
void setState(const grove::IDataNode& state) override;
std::string getType() const override { return "mymodule"; }
bool isIdle() const override { return true; }
private:
std::shared_ptr<spdlog::logger> m_logger;
std::unique_ptr<grove::IDataNode> m_config;
grove::IIO* m_io = nullptr;
// Module state (will be preserved across hot-reloads)
int m_counter = 0;
std::string m_status;
};
} // namespace myapp
// Required C exports for dynamic loading
extern "C" {
grove::IModule* createModule();
void destroyModule(grove::IModule* module);
}
```
### Module Implementation
```cpp
// src/modules/MyModule.cpp
#include "MyModule.h"
#include <grove/JsonDataNode.h>
#include <spdlog/sinks/stdout_color_sinks.h>
namespace myapp {
MyModule::MyModule() {
m_logger = spdlog::get("MyModule");
if (!m_logger) {
m_logger = spdlog::stdout_color_mt("MyModule");
}
m_config = std::make_unique<grove::JsonDataNode>("config");
}
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
m_config = std::make_unique<grove::JsonDataNode>("config");
// Read configuration values with defaults
m_status = configNode.getString("initialStatus", "ready");
int startCount = configNode.getInt("startCount", 0);
m_logger->info("MyModule configured: status={}, startCount={}",
m_status, startCount);
}
const grove::IDataNode& MyModule::getConfiguration() {
return *m_config;
}
void MyModule::process(const grove::IDataNode& input) {
// Get frame timing from input
double deltaTime = input.getDouble("deltaTime", 0.016);
int frameCount = input.getInt("frameCount", 0);
// Your processing logic here
m_counter++;
// Process incoming messages
while (m_io && m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
m_logger->debug("Received message on topic: {}", msg.topic);
// Handle message...
}
// Publish events if needed
if (m_counter % 100 == 0) {
auto event = std::make_unique<grove::JsonDataNode>("event");
event->setInt("counter", m_counter);
m_io->publish("mymodule:milestone", std::move(event));
}
}
std::unique_ptr<grove::IDataNode> MyModule::getHealthStatus() {
auto status = std::make_unique<grove::JsonDataNode>("health");
status->setString("status", "running");
status->setInt("counter", m_counter);
return status;
}
void MyModule::shutdown() {
m_logger->info("MyModule shutting down, counter={}", m_counter);
}
// ============================================================================
// State Serialization (Critical for Hot-Reload)
// ============================================================================
std::unique_ptr<grove::IDataNode> MyModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
// Serialize all state that must survive hot-reload
state->setInt("counter", m_counter);
state->setString("status", m_status);
m_logger->debug("State saved: counter={}", m_counter);
return state;
}
void MyModule::setState(const grove::IDataNode& state) {
// Restore state after hot-reload
m_counter = state.getInt("counter", 0);
m_status = state.getString("status", "ready");
m_logger->info("State restored: counter={}", m_counter);
}
} // namespace myapp
// ============================================================================
// C Export Functions (Required for dlopen)
// ============================================================================
extern "C" {
grove::IModule* createModule() {
return new myapp::MyModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}
```
---
## Module Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ Module Lifecycle │
└─────────────────────────────────────────────────────────────────┘
1. LOAD
ModuleLoader::load(path, name)
dlopen() → createModule()
setConfiguration(config, io, scheduler)
Module Ready
2. PROCESS (Main Loop)
┌──────────────────────┐
│ process(input) │◄────┐
│ - Read deltaTime │ │
│ - Update state │ │
│ - Pull messages │ │
│ - Publish events │ │
└──────────────────────┘ │
│ │
└────────────────────┘
3. HOT-RELOAD
File change detected
getState() → Save state
unload() → dlclose()
load(path, name, isReload=true)
setConfiguration(config, io, scheduler)
setState(savedState)
Module continues with preserved state
4. SHUTDOWN
shutdown()
destroyModule()
dlclose()
```
---
## Inter-Module Communication
### IIO Pub/Sub System
Modules communicate via topics using publish/subscribe pattern.
#### Publishing Messages
```cpp
void MyModule::process(const grove::IDataNode& input) {
// Create message data
auto data = std::make_unique<grove::JsonDataNode>("data");
data->setString("event", "player_moved");
data->setDouble("x", 100.5);
data->setDouble("y", 200.3);
// Publish to topic
m_io->publish("game:player:position", std::move(data));
}
```
#### Subscribing to Topics
```cpp
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
// Subscribe to specific topic
m_io->subscribe("game:player:*");
// Subscribe with low-frequency batching (for non-critical updates)
grove::SubscriptionConfig config;
config.batchInterval = 1000; // 1 second batches
m_io->subscribeLowFreq("analytics:*", config);
}
```
#### Processing Messages
```cpp
void MyModule::process(const grove::IDataNode& input) {
// Pull-based: module controls when to process messages
while (m_io->hasMessages() > 0) {
grove::Message msg = m_io->pullMessage();
if (msg.topic == "game:player:position") {
double x = msg.data->getDouble("x", 0.0);
double y = msg.data->getDouble("y", 0.0);
// Handle position update...
}
}
}
```
### Topic Patterns
Topics support wildcard matching:
| Pattern | Matches |
|---------|---------|
| `game:player:*` | `game:player:position`, `game:player:health` |
| `economy:*` | `economy:prices`, `economy:trade` |
| `*:error` | `network:error`, `database:error` |
---
## Hot-Reload
### How It Works
1. **File Watcher** detects `.so` modification
2. **State Extraction**: `getState()` serializes module state
3. **Unload**: Old library closed with `dlclose()`
4. **Load**: New library loaded with `dlopen()` (cache bypass via temp copy)
5. **Configure**: `setConfiguration()` called with same config
6. **Restore**: `setState()` restores serialized state
### Implementing Hot-Reload Support
```cpp
// Critical: Serialize ALL state that must survive reload
std::unique_ptr<grove::IDataNode> MyModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
// Primitives
state->setInt("counter", m_counter);
state->setDouble("health", m_health);
state->setString("name", m_name);
state->setBool("active", m_active);
// Complex state: serialize to JSON child nodes
auto entitiesNode = std::make_unique<grove::JsonDataNode>("entities");
for (const auto& entity : m_entities) {
auto entityNode = std::make_unique<grove::JsonDataNode>(entity.id);
entityNode->setDouble("x", entity.x);
entityNode->setDouble("y", entity.y);
entitiesNode->setChild(entity.id, std::move(entityNode));
}
state->setChild("entities", std::move(entitiesNode));
return state;
}
void MyModule::setState(const grove::IDataNode& state) {
// Restore primitives
m_counter = state.getInt("counter", 0);
m_health = state.getDouble("health", 100.0);
m_name = state.getString("name", "default");
m_active = state.getBool("active", true);
// Restore complex state
m_entities.clear();
auto* entitiesNode = state.getChildReadOnly("entities");
if (entitiesNode) {
for (const auto& name : entitiesNode->getChildNames()) {
auto* entityNode = entitiesNode->getChildReadOnly(name);
if (entityNode) {
Entity e;
e.id = name;
e.x = entityNode->getDouble("x", 0.0);
e.y = entityNode->getDouble("y", 0.0);
m_entities.push_back(e);
}
}
}
}
```
### File Watcher Example
```cpp
class FileWatcher {
public:
void watch(const std::string& path) {
if (fs::exists(path)) {
m_lastModified[path] = fs::last_write_time(path);
}
}
bool hasChanged(const std::string& path) {
if (!fs::exists(path)) return false;
auto currentTime = fs::last_write_time(path);
auto it = m_lastModified.find(path);
if (it == m_lastModified.end()) {
m_lastModified[path] = currentTime;
return false;
}
if (currentTime != it->second) {
it->second = currentTime;
return true;
}
return false;
}
private:
std::unordered_map<std::string, fs::file_time_type> m_lastModified;
};
```
---
## Configuration Management
### JSON Configuration Files
```json
// config/mymodule.json
{
"initialStatus": "ready",
"startCount": 0,
"maxItems": 100,
"debugMode": false,
"updateRate": 0.016
}
```
### Loading Configuration
```cpp
std::unique_ptr<grove::JsonDataNode> loadConfig(const std::string& path) {
if (fs::exists(path)) {
std::ifstream file(path);
nlohmann::json j;
file >> j;
return std::make_unique<grove::JsonDataNode>("config", j);
}
// Return empty config with defaults
return std::make_unique<grove::JsonDataNode>("config");
}
```
### Runtime Config Updates
Modules can optionally support runtime configuration changes:
```cpp
bool MyModule::updateConfig(const grove::IDataNode& newConfig) {
// Validate new config
int maxItems = newConfig.getInt("maxItems", 100);
if (maxItems < 1 || maxItems > 10000) {
m_logger->warn("Invalid maxItems: {}", maxItems);
return false;
}
// Apply changes
m_maxItems = maxItems;
m_debugMode = newConfig.getBool("debugMode", false);
m_logger->info("Config updated: maxItems={}", m_maxItems);
return true;
}
```
---
## Task Scheduling
For computationally expensive operations, delegate to `ITaskScheduler`:
```cpp
void MyModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_scheduler = scheduler; // Store reference
}
void MyModule::process(const grove::IDataNode& input) {
// Delegate expensive pathfinding calculation
if (needsPathfinding) {
auto taskData = std::make_unique<grove::JsonDataNode>("task");
taskData->setDouble("startX", unit.x);
taskData->setDouble("startY", unit.y);
taskData->setDouble("targetX", target.x);
taskData->setDouble("targetY", target.y);
taskData->setString("unitId", unit.id);
m_scheduler->scheduleTask("pathfinding", std::move(taskData));
}
// Check for completed tasks
while (m_scheduler->hasCompletedTasks() > 0) {
auto result = m_scheduler->getCompletedTask();
std::string unitId = result->getString("unitId", "");
// Apply pathfinding result...
}
}
```
---
## API Reference
### IDataNode
| Method | Description |
|--------|-------------|
| `getString(name, default)` | Get string property |
| `getInt(name, default)` | Get integer property |
| `getDouble(name, default)` | Get double property |
| `getBool(name, default)` | Get boolean property |
| `setString(name, value)` | Set string property |
| `setInt(name, value)` | Set integer property |
| `setDouble(name, value)` | Set double property |
| `setBool(name, value)` | Set boolean property |
| `hasProperty(name)` | Check if property exists |
| `getChild(name)` | Get child node (transfers ownership) |
| `getChildReadOnly(name)` | Get child node (no ownership transfer) |
| `setChild(name, node)` | Add/replace child node |
| `getChildNames()` | Get names of all children |
### IIO
| Method | Description |
|--------|-------------|
| `publish(topic, data)` | Publish message to topic |
| `subscribe(pattern, config)` | Subscribe to topic pattern |
| `subscribeLowFreq(pattern, config)` | Subscribe with batching |
| `hasMessages()` | Count of pending messages |
| `pullMessage()` | Consume one message |
| `getHealth()` | Get IO health metrics |
### IModule
| Method | Description |
|--------|-------------|
| `process(input)` | Main processing method |
| `setConfiguration(config, io, scheduler)` | Initialize module |
| `getConfiguration()` | Get current config |
| `getState()` | Serialize state for hot-reload |
| `setState(state)` | Restore state after hot-reload |
| `getHealthStatus()` | Get module health report |
| `shutdown()` | Clean shutdown |
| `isIdle()` | Check if safe to hot-reload |
| `getType()` | Get module type identifier |
### ModuleLoader
| Method | Description |
|--------|-------------|
| `load(path, name, isReload)` | Load module from .so |
| `reload(module)` | Hot-reload with state preservation |
| `unload()` | Unload current module |
| `isLoaded()` | Check if module is loaded |
| `getLoadedPath()` | Get path of loaded module |
### IOFactory
| Method | Description |
|--------|-------------|
| `create(type, instanceId)` | Create IO by type string |
| `create(IOType, instanceId)` | Create IO by enum |
| `createFromConfig(config, instanceId)` | Create IO from config |
**IO Types:**
- `"intra"` / `IOType::INTRA` - Same-process (development)
- `"local"` / `IOType::LOCAL` - Same-machine (production single-server)
- `"network"` / `IOType::NETWORK` - Distributed (MMO scale)
---
## Building and Running
```bash
# Configure
cmake -B build
# Build everything
cmake --build build -j4
# Build modules only (for hot-reload workflow)
cmake --build build --target modules
# Run
./build/myapp
```
### Hot-Reload Workflow
1. Start application: `./build/myapp`
2. Edit module source: `src/modules/MyModule.cpp`
3. Rebuild module: `cmake --build build --target modules`
4. Application detects change and hot-reloads automatically
---
## Best Practices
1. **Keep modules small**: 200-300 lines of pure business logic
2. **No infrastructure code**: Let GroveEngine handle threading, persistence
3. **Serialize all state**: Everything in `getState()` survives hot-reload
4. **Use typed accessors**: `getInt()`, `getString()` with sensible defaults
5. **Pull-based messaging**: Process messages in `process()`, not callbacks
6. **Validate config**: Check configuration values in `setConfiguration()`
7. **Log appropriately**: Debug for development, Info for production events

View File

@ -0,0 +1,179 @@
#include "BgfxRendererModule.h"
#include "RHI/RHIDevice.h"
#include "Frame/FrameAllocator.h"
#include "Frame/FramePacket.h"
#include "RenderGraph/RenderGraph.h"
#include "Scene/SceneCollector.h"
#include "Resources/ResourceCache.h"
#include "Passes/ClearPass.h"
#include "Passes/SpritePass.h"
#include "Passes/DebugPass.h"
#include <grove/JsonDataNode.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
namespace grove {
BgfxRendererModule::BgfxRendererModule() = default;
BgfxRendererModule::~BgfxRendererModule() = default;
void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) {
m_io = io;
// Setup logger
m_logger = spdlog::get("BgfxRenderer");
if (!m_logger) {
m_logger = spdlog::stdout_color_mt("BgfxRenderer");
}
// Read static config via IDataNode
m_width = static_cast<uint16_t>(config.getInt("windowWidth", 1280));
m_height = static_cast<uint16_t>(config.getInt("windowHeight", 720));
m_backend = config.getString("backend", "opengl");
m_shaderPath = config.getString("shaderPath", "./shaders");
m_vsync = config.getBool("vsync", true);
m_maxSprites = config.getInt("maxSpritesPerBatch", 10000);
size_t allocatorSize = static_cast<size_t>(config.getInt("frameAllocatorSizeMB", 16)) * 1024 * 1024;
// Window handle (passed via config or 0 if separate WindowModule)
void* windowHandle = reinterpret_cast<void*>(
static_cast<uintptr_t>(config.getInt("nativeWindowHandle", 0))
);
m_logger->info("Initializing BgfxRenderer: {}x{} backend={}", m_width, m_height, m_backend);
// Initialize subsystems
m_frameAllocator = std::make_unique<FrameAllocator>(allocatorSize);
m_device = rhi::IRHIDevice::create();
if (!m_device->init(windowHandle, m_width, m_height)) {
m_logger->error("Failed to initialize RHI device");
return;
}
// Log device capabilities
auto caps = m_device->getCapabilities();
m_logger->info("GPU: {} ({})", caps.gpuName, caps.rendererName);
m_logger->info("Max texture size: {}, Max draw calls: {}", caps.maxTextureSize, caps.maxDrawCalls);
// Setup render graph with passes
m_renderGraph = std::make_unique<RenderGraph>();
m_renderGraph->addPass(std::make_unique<ClearPass>());
m_renderGraph->addPass(std::make_unique<SpritePass>());
m_renderGraph->addPass(std::make_unique<DebugPass>());
m_renderGraph->setup(*m_device);
m_renderGraph->compile();
// Setup scene collector with IIO subscriptions
m_sceneCollector = std::make_unique<SceneCollector>();
m_sceneCollector->setup(io);
// Setup resource cache
m_resourceCache = std::make_unique<ResourceCache>();
m_logger->info("BgfxRenderer initialized successfully");
}
void BgfxRendererModule::process(const IDataNode& input) {
// Read deltaTime from input (provided by ModuleSystem)
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016));
// 1. Collect IIO messages (pull-based)
m_sceneCollector->collect(m_io, deltaTime);
// 2. Build immutable FramePacket
m_frameAllocator->reset();
FramePacket frame = m_sceneCollector->finalize(*m_frameAllocator);
// 3. Set view clear color
m_device->setViewClear(0, frame.clearColor, 1.0f);
m_device->setViewRect(0, 0, 0, m_width, m_height);
m_device->setViewTransform(0, frame.mainView.viewMatrix, frame.mainView.projMatrix);
// 4. Execute render graph
m_renderGraph->execute(frame, *m_device);
// 5. Present
m_device->frame();
// 6. Cleanup for next frame
m_sceneCollector->clear();
m_frameCount++;
}
void BgfxRendererModule::shutdown() {
m_logger->info("BgfxRenderer shutting down, {} frames rendered", m_frameCount);
if (m_renderGraph) {
m_renderGraph->shutdown(*m_device);
}
if (m_resourceCache) {
m_resourceCache->clear(*m_device);
}
if (m_device) {
m_device->shutdown();
}
m_renderGraph.reset();
m_resourceCache.reset();
m_sceneCollector.reset();
m_frameAllocator.reset();
m_device.reset();
}
std::unique_ptr<IDataNode> BgfxRendererModule::getState() {
// Minimal state for hot-reload (renderer is stateless gameplay-wise)
auto state = std::make_unique<JsonDataNode>("state");
state->setInt("frameCount", static_cast<int>(m_frameCount));
// GPU resources are recreated on reload
return state;
}
void BgfxRendererModule::setState(const IDataNode& state) {
m_frameCount = static_cast<uint64_t>(state.getInt("frameCount", 0));
m_logger->info("State restored: frameCount={}", m_frameCount);
}
const IDataNode& BgfxRendererModule::getConfiguration() {
if (!m_configCache) {
m_configCache = std::make_unique<JsonDataNode>("config");
m_configCache->setInt("windowWidth", m_width);
m_configCache->setInt("windowHeight", m_height);
m_configCache->setString("backend", m_backend);
m_configCache->setString("shaderPath", m_shaderPath);
m_configCache->setBool("vsync", m_vsync);
m_configCache->setInt("maxSpritesPerBatch", m_maxSprites);
}
return *m_configCache;
}
std::unique_ptr<IDataNode> BgfxRendererModule::getHealthStatus() {
auto health = std::make_unique<JsonDataNode>("health");
health->setString("status", "running");
health->setInt("frameCount", static_cast<int>(m_frameCount));
health->setInt("allocatorUsedBytes", static_cast<int>(m_frameAllocator ? m_frameAllocator->getUsed() : 0));
health->setInt("textureCount", static_cast<int>(m_resourceCache ? m_resourceCache->getTextureCount() : 0));
health->setInt("shaderCount", static_cast<int>(m_resourceCache ? m_resourceCache->getShaderCount() : 0));
return health;
}
} // namespace grove
// ============================================================================
// C Export (required for dlopen)
// ============================================================================
extern "C" {
grove::IModule* createModule() {
return new grove::BgfxRendererModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}

View File

@ -0,0 +1,81 @@
#pragma once
#include <grove/IModule.h>
#include <grove/IDataNode.h>
#include <grove/IIO.h>
#include <memory>
#include <string>
namespace spdlog { class logger; }
namespace grove {
namespace rhi { class IRHIDevice; }
class FrameAllocator;
class RenderGraph;
class SceneCollector;
class ResourceCache;
// ============================================================================
// BgfxRenderer Module - 2D rendering via bgfx
// ============================================================================
class BgfxRendererModule : public IModule {
public:
BgfxRendererModule();
~BgfxRendererModule() override;
// ========================================
// IModule Interface
// ========================================
void setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) override;
void process(const IDataNode& input) override;
void shutdown() override;
std::unique_ptr<IDataNode> getState() override;
void setState(const IDataNode& state) override;
const IDataNode& getConfiguration() override;
std::unique_ptr<IDataNode> getHealthStatus() override;
std::string getType() const override { return "bgfx_renderer"; }
bool isIdle() const override { return true; }
private:
// Logger
std::shared_ptr<spdlog::logger> m_logger;
// Core systems
std::unique_ptr<rhi::IRHIDevice> m_device;
std::unique_ptr<FrameAllocator> m_frameAllocator;
std::unique_ptr<RenderGraph> m_renderGraph;
std::unique_ptr<SceneCollector> m_sceneCollector;
std::unique_ptr<ResourceCache> m_resourceCache;
// IIO (non-owning)
IIO* m_io = nullptr;
// Config (from IDataNode)
uint16_t m_width = 1280;
uint16_t m_height = 720;
std::string m_backend = "opengl";
std::string m_shaderPath = "./shaders";
bool m_vsync = true;
int m_maxSprites = 10000;
std::unique_ptr<IDataNode> m_configCache;
// Stats
uint64_t m_frameCount = 0;
};
} // namespace grove
// ============================================================================
// C Export (required for dlopen)
// ============================================================================
extern "C" {
grove::IModule* createModule();
void destroyModule(grove::IModule* module);
}

View File

@ -0,0 +1,104 @@
# ============================================================================
# BgfxRenderer Module - CMake Configuration
# ============================================================================
cmake_minimum_required(VERSION 3.20)
# ============================================================================
# Fetch bgfx
# ============================================================================
include(FetchContent)
FetchContent_Declare(
bgfx
GIT_REPOSITORY https://github.com/bkaradzic/bgfx.cmake.git
GIT_TAG v1.127.8710-464
GIT_SHALLOW TRUE
)
# bgfx options
set(BGFX_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(BGFX_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BGFX_INSTALL OFF CACHE BOOL "" FORCE)
set(BGFX_CUSTOM_TARGETS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(bgfx)
# ============================================================================
# BgfxRenderer Shared Library
# ============================================================================
add_library(BgfxRenderer SHARED
# Main module
BgfxRendererModule.cpp
# RHI
RHI/RHICommandBuffer.cpp
RHI/BgfxDevice.cpp
# Frame
Frame/FrameAllocator.cpp
# RenderGraph
RenderGraph/RenderGraph.cpp
# Passes
Passes/ClearPass.cpp
Passes/SpritePass.cpp
Passes/DebugPass.cpp
# Scene
Scene/SceneCollector.cpp
# Resources
Resources/ResourceCache.cpp
)
target_include_directories(BgfxRenderer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../../include
)
target_link_libraries(BgfxRenderer PRIVATE
GroveEngine::impl
bgfx
bx
spdlog::spdlog
)
target_compile_features(BgfxRenderer PRIVATE cxx_std_17)
set_target_properties(BgfxRenderer PROPERTIES
PREFIX "lib"
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
)
# ============================================================================
# Platform-specific settings
# ============================================================================
if(WIN32)
target_compile_definitions(BgfxRenderer PRIVATE
WIN32_LEAN_AND_MEAN
NOMINMAX
)
endif()
if(UNIX AND NOT APPLE)
target_link_libraries(BgfxRenderer PRIVATE
pthread
dl
X11
GL
)
endif()
if(APPLE)
target_link_libraries(BgfxRenderer PRIVATE
"-framework Cocoa"
"-framework QuartzCore"
"-framework Metal"
)
endif()

View File

@ -0,0 +1,44 @@
#include "FrameAllocator.h"
namespace grove {
FrameAllocator::FrameAllocator(size_t size)
: m_buffer(new uint8_t[size])
, m_capacity(size)
, m_offset(0)
{
}
FrameAllocator::~FrameAllocator() {
delete[] m_buffer;
}
void* FrameAllocator::allocate(size_t size, size_t alignment) {
size_t current = m_offset.load(std::memory_order_relaxed);
size_t aligned;
do {
// Align the current offset
aligned = (current + alignment - 1) & ~(alignment - 1);
// Check if we have enough space
if (aligned + size > m_capacity) {
return nullptr; // Out of memory
}
} while (!m_offset.compare_exchange_weak(
current, aligned + size,
std::memory_order_release,
std::memory_order_relaxed));
return m_buffer + aligned;
}
void FrameAllocator::reset() {
m_offset.store(0, std::memory_order_release);
}
size_t FrameAllocator::getUsed() const {
return m_offset.load(std::memory_order_acquire);
}
} // namespace grove

View File

@ -0,0 +1,60 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <cstddef>
#include <new>
namespace grove {
// ============================================================================
// Frame Allocator - Lock-free linear allocator, reset each frame
// ============================================================================
class FrameAllocator {
public:
static constexpr size_t DEFAULT_SIZE = 16 * 1024 * 1024; // 16 MB
explicit FrameAllocator(size_t size = DEFAULT_SIZE);
~FrameAllocator();
// Non-copyable
FrameAllocator(const FrameAllocator&) = delete;
FrameAllocator& operator=(const FrameAllocator&) = delete;
// Thread-safe, lock-free allocation
void* allocate(size_t size, size_t alignment = 16);
// Typed allocation with constructor
template<typename T, typename... Args>
T* allocate(Args&&... args) {
void* ptr = allocate(sizeof(T), alignof(T));
if (!ptr) return nullptr;
return new (ptr) T(std::forward<Args>(args)...);
}
// Array allocation
template<typename T>
T* allocateArray(size_t count) {
void* ptr = allocate(sizeof(T) * count, alignof(T));
if (!ptr) return nullptr;
for (size_t i = 0; i < count; ++i) {
new (static_cast<T*>(ptr) + i) T();
}
return static_cast<T*>(ptr);
}
// Reset (called once per frame, single-thread)
void reset();
// Stats
size_t getUsed() const;
size_t getCapacity() const { return m_capacity; }
private:
uint8_t* m_buffer;
size_t m_capacity;
std::atomic<size_t> m_offset;
};
} // namespace grove

View File

@ -0,0 +1,128 @@
#pragma once
#include <cstdint>
#include <cstddef>
namespace grove {
class FrameAllocator;
// ============================================================================
// Sprite Instance Data
// ============================================================================
struct SpriteInstance {
float x, y; // Position
float scaleX, scaleY; // Scale
float rotation; // Radians
float u0, v0, u1, v1; // UVs in atlas
uint32_t color; // RGBA packed
uint16_t textureId; // Index in texture array
uint16_t layer; // Z-order
};
// ============================================================================
// Tilemap Chunk Data
// ============================================================================
struct TilemapChunk {
float x, y; // Chunk position
uint16_t width, height;
uint16_t tileWidth, tileHeight;
uint16_t textureId;
const uint16_t* tiles; // Tile indices in tileset
size_t tileCount;
};
// ============================================================================
// Text Command Data
// ============================================================================
struct TextCommand {
float x, y;
const char* text; // Null-terminated, allocated in FrameAllocator
uint16_t fontId;
uint16_t fontSize;
uint32_t color;
uint16_t layer;
};
// ============================================================================
// Particle Instance Data
// ============================================================================
struct ParticleInstance {
float x, y;
float vx, vy;
float size;
float life; // 0-1, remaining time
uint32_t color;
uint16_t textureId;
};
// ============================================================================
// Debug Shape Data
// ============================================================================
struct DebugLine {
float x1, y1, x2, y2;
uint32_t color;
};
struct DebugRect {
float x, y, w, h;
uint32_t color;
bool filled;
};
// ============================================================================
// Camera/View Info
// ============================================================================
struct ViewInfo {
float viewMatrix[16];
float projMatrix[16];
float positionX, positionY;
float zoom;
uint16_t viewportX, viewportY;
uint16_t viewportW, viewportH;
};
// ============================================================================
// Frame Packet - IMMUTABLE after construction
// ============================================================================
struct FramePacket {
uint64_t frameNumber;
float deltaTime;
// Collected data (read-only for passes)
const SpriteInstance* sprites;
size_t spriteCount;
const TilemapChunk* tilemaps;
size_t tilemapCount;
const TextCommand* texts;
size_t textCount;
const ParticleInstance* particles;
size_t particleCount;
const DebugLine* debugLines;
size_t debugLineCount;
const DebugRect* debugRects;
size_t debugRectCount;
// Main view
ViewInfo mainView;
// Clear color
uint32_t clearColor;
// Allocator for temporary pass data
FrameAllocator* allocator;
};
} // namespace grove

View File

@ -0,0 +1,25 @@
#include "ClearPass.h"
#include "../RHI/RHIDevice.h"
namespace grove {
void ClearPass::setup(rhi::IRHIDevice& device) {
// No resources needed for clear pass
}
void ClearPass::shutdown(rhi::IRHIDevice& device) {
// Nothing to clean up
}
void ClearPass::execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) {
// Clear is handled via view setup in bgfx
// The clear color is set in BgfxRendererModule before frame execution
// For command buffer approach, we'd record:
// cmd.setClear(frame.clearColor);
// But bgfx handles clear through setViewClear, which is called
// at the beginning of each frame in the main module
}
} // namespace grove

View File

@ -0,0 +1,21 @@
#pragma once
#include "../RenderGraph/RenderPass.h"
namespace grove {
// ============================================================================
// Clear Pass - Clears the framebuffer
// ============================================================================
class ClearPass : public RenderPass {
public:
const char* getName() const override { return "Clear"; }
uint32_t getSortOrder() const override { return 0; } // First pass
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) override;
};
} // namespace grove

View File

@ -0,0 +1,54 @@
#include "DebugPass.h"
#include "../RHI/RHIDevice.h"
namespace grove {
void DebugPass::setup(rhi::IRHIDevice& device) {
// Create dynamic vertex buffer for debug lines
rhi::BufferDesc vbDesc;
vbDesc.type = rhi::BufferDesc::Vertex;
vbDesc.size = MAX_DEBUG_LINES * 2 * sizeof(float) * 6; // 2 verts per line, pos + color
vbDesc.data = nullptr;
vbDesc.dynamic = true;
m_lineVB = device.createBuffer(vbDesc);
// Note: Shader loading will be done via ResourceCache
}
void DebugPass::shutdown(rhi::IRHIDevice& device) {
device.destroy(m_lineVB);
device.destroy(m_lineShader);
}
void DebugPass::execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) {
// Skip if no debug primitives
if (frame.debugLineCount == 0 && frame.debugRectCount == 0) {
return;
}
// Set render state for debug (no blending, no depth)
rhi::RenderState state;
state.blend = rhi::BlendMode::None;
state.cull = rhi::CullMode::None;
state.depthTest = false;
state.depthWrite = false;
cmd.setState(state);
// Build vertex data for lines
// Each line needs 2 vertices with position (x, y, z) and color (r, g, b, a)
if (frame.debugLineCount > 0) {
cmd.setVertexBuffer(m_lineVB);
cmd.draw(static_cast<uint32_t>(frame.debugLineCount * 2));
cmd.submit(0, m_lineShader, 0);
}
// Rectangles are rendered as line loops or filled quads
// For now, just lines (wireframe)
if (frame.debugRectCount > 0) {
// Each rect = 4 lines = 8 vertices
// TODO: Build rect line data and draw
}
}
} // namespace grove

View File

@ -0,0 +1,29 @@
#pragma once
#include "../RenderGraph/RenderPass.h"
#include "../RHI/RHITypes.h"
namespace grove {
// ============================================================================
// Debug Pass - Renders debug lines and shapes
// ============================================================================
class DebugPass : public RenderPass {
public:
const char* getName() const override { return "Debug"; }
uint32_t getSortOrder() const override { return 900; } // Near last
std::vector<const char*> getDependencies() const override { return {"Sprites"}; }
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) override;
private:
rhi::ShaderHandle m_lineShader;
rhi::BufferHandle m_lineVB;
static constexpr uint32_t MAX_DEBUG_LINES = 10000;
};
} // namespace grove

View File

@ -0,0 +1,98 @@
#include "SpritePass.h"
#include "../RHI/RHIDevice.h"
namespace grove {
void SpritePass::setup(rhi::IRHIDevice& device) {
// Create quad vertex buffer (unit quad, instanced)
// Positions: 4 vertices for a quad
float quadVertices[] = {
// pos.x, pos.y, uv.x, uv.y
0.0f, 0.0f, 0.0f, 0.0f, // bottom-left
1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
1.0f, 1.0f, 1.0f, 1.0f, // top-right
0.0f, 1.0f, 0.0f, 1.0f, // top-left
};
rhi::BufferDesc vbDesc;
vbDesc.type = rhi::BufferDesc::Vertex;
vbDesc.size = sizeof(quadVertices);
vbDesc.data = quadVertices;
vbDesc.dynamic = false;
m_quadVB = device.createBuffer(vbDesc);
// Create index buffer
uint16_t quadIndices[] = {
0, 1, 2, // first triangle
0, 2, 3 // second triangle
};
rhi::BufferDesc ibDesc;
ibDesc.type = rhi::BufferDesc::Index;
ibDesc.size = sizeof(quadIndices);
ibDesc.data = quadIndices;
ibDesc.dynamic = false;
m_quadIB = device.createBuffer(ibDesc);
// Create dynamic instance buffer
rhi::BufferDesc instDesc;
instDesc.type = rhi::BufferDesc::Instance;
instDesc.size = MAX_SPRITES_PER_BATCH * sizeof(SpriteInstance);
instDesc.data = nullptr;
instDesc.dynamic = true;
m_instanceBuffer = device.createBuffer(instDesc);
// Create texture sampler uniform
m_textureSampler = device.createUniform("s_texture", 1);
// Note: Shader loading will be done via ResourceCache
// m_shader will be set after shaders are loaded
}
void SpritePass::shutdown(rhi::IRHIDevice& device) {
device.destroy(m_quadVB);
device.destroy(m_quadIB);
device.destroy(m_instanceBuffer);
device.destroy(m_textureSampler);
device.destroy(m_shader);
}
void SpritePass::execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) {
if (frame.spriteCount == 0) {
return;
}
// Set render state for sprites (alpha blending, no depth)
rhi::RenderState state;
state.blend = rhi::BlendMode::Alpha;
state.cull = rhi::CullMode::None;
state.depthTest = false;
state.depthWrite = false;
cmd.setState(state);
// Process sprites in batches
size_t remaining = frame.spriteCount;
size_t offset = 0;
while (remaining > 0) {
size_t batchSize = (remaining > MAX_SPRITES_PER_BATCH)
? MAX_SPRITES_PER_BATCH : remaining;
// Update instance buffer with sprite data
// In a full implementation, we'd sort by texture and batch accordingly
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, static_cast<uint32_t>(offset),
static_cast<uint32_t>(batchSize));
// Submit draw call
cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad
cmd.submit(0, m_shader, 0);
offset += batchSize;
remaining -= batchSize;
}
}
} // namespace grove

View File

@ -0,0 +1,32 @@
#pragma once
#include "../RenderGraph/RenderPass.h"
#include "../RHI/RHITypes.h"
namespace grove {
// ============================================================================
// Sprite Pass - Renders 2D sprites with batching
// ============================================================================
class SpritePass : public RenderPass {
public:
const char* getName() const override { return "Sprites"; }
uint32_t getSortOrder() const override { return 100; }
std::vector<const char*> getDependencies() const override { return {"Clear"}; }
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) override;
private:
rhi::ShaderHandle m_shader;
rhi::BufferHandle m_quadVB;
rhi::BufferHandle m_quadIB;
rhi::BufferHandle m_instanceBuffer;
rhi::UniformHandle m_textureSampler;
static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000;
};
} // namespace grove

View File

@ -0,0 +1,264 @@
# BgfxRenderer Module
Module de rendu 2D pour GroveEngine, basé sur [bgfx](https://github.com/bkaradzic/bgfx).
## Features
- **Abstraction RHI** : Aucune dépendance bgfx exposée hors de `BgfxDevice.cpp`
- **Multi-backend** : DirectX 11/12, OpenGL, Vulkan, Metal (auto-détecté)
- **MT-ready** : Architecture task-based, lock-free frame allocator
- **Hot-reload** : Support complet du hot-reload GroveEngine
- **Batching** : Sprites groupés par texture pour performance
## Architecture
```
BgfxRenderer/
├── BgfxRendererModule.h/.cpp # Point d'entrée IModule
├── RHI/ # Render Hardware Interface
│ ├── RHITypes.h # Handles typés, enums
│ ├── RHIDevice.h # Interface abstraite
│ ├── RHICommandBuffer.h/.cpp # Command recording
│ └── BgfxDevice.cpp # Implémentation bgfx
├── Frame/
│ ├── FrameAllocator.h/.cpp # Allocateur lock-free
│ └── FramePacket.h # Données immuables par frame
├── RenderGraph/
│ ├── RenderPass.h # Interface pass
│ └── RenderGraph.h/.cpp # Gestion des passes
├── Passes/
│ ├── ClearPass.h/.cpp # Clear framebuffer
│ ├── SpritePass.h/.cpp # Sprites + batching
│ └── DebugPass.h/.cpp # Debug lines/shapes
├── Scene/
│ └── SceneCollector.h/.cpp # Collecte depuis IIO
└── Resources/
└── ResourceCache.h/.cpp # Cache textures/shaders
```
## Build
### Windows (recommandé pour le rendu)
```powershell
cd "E:\Users\Alexis Trouvé\Documents\Projets\GroveEngine"
# Build rapide
.\build_renderer.bat
# Ou avec options
.\build_renderer.bat debug # Build Debug
.\build_renderer.bat clean # Clean + rebuild
.\build_renderer.bat vs # Ouvrir Visual Studio
```
### Linux/WSL
```bash
cmake -B build -DGROVE_BUILD_BGFX_RENDERER=ON
cmake --build build -j4
```
Dépendances Linux :
```bash
sudo apt-get install libgl1-mesa-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev
```
## Configuration
Le module est configuré via `IDataNode` dans `setConfiguration()` :
```json
{
"windowWidth": 1280,
"windowHeight": 720,
"backend": "auto",
"shaderPath": "./shaders",
"vsync": true,
"maxSpritesPerBatch": 10000,
"frameAllocatorSizeMB": 16,
"nativeWindowHandle": 0
}
```
| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `windowWidth` | int | 1280 | Largeur fenêtre |
| `windowHeight` | int | 720 | Hauteur fenêtre |
| `backend` | string | "auto" | Backend graphique (auto, opengl, vulkan, dx11, dx12, metal) |
| `shaderPath` | string | "./shaders" | Chemin des shaders compilés |
| `vsync` | bool | true | Synchronisation verticale |
| `maxSpritesPerBatch` | int | 10000 | Sprites max par batch |
| `frameAllocatorSizeMB` | int | 16 | Taille allocateur frame (MB) |
| `nativeWindowHandle` | int | 0 | Handle fenêtre native (HWND, Window, etc.) |
## Communication IIO
Le renderer subscribe à `render:*` et traite les messages suivants :
### Sprites
```cpp
// Topic: render:sprite
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->setDouble("u0", 0.0); // UV min
sprite->setDouble("v0", 0.0);
sprite->setDouble("u1", 1.0); // UV max
sprite->setDouble("v1", 1.0);
sprite->setInt("color", 0xFFFFFFFF); // RGBA
sprite->setInt("textureId", 0);
sprite->setInt("layer", 0); // Z-order
io->publish("render:sprite", std::move(sprite));
```
### Batch de sprites
```cpp
// Topic: render:sprite:batch
auto batch = std::make_unique<JsonDataNode>("batch");
auto sprites = std::make_unique<JsonDataNode>("sprites");
// Ajouter plusieurs sprites comme enfants...
batch->setChild("sprites", std::move(sprites));
io->publish("render:sprite:batch", std::move(batch));
```
### Caméra
```cpp
// Topic: render:camera
auto cam = std::make_unique<JsonDataNode>("camera");
cam->setDouble("x", 0.0);
cam->setDouble("y", 0.0);
cam->setDouble("zoom", 1.0);
cam->setInt("viewportX", 0);
cam->setInt("viewportY", 0);
cam->setInt("viewportW", 1280);
cam->setInt("viewportH", 720);
io->publish("render:camera", std::move(cam));
```
### Clear color
```cpp
// Topic: render:clear
auto clear = std::make_unique<JsonDataNode>("clear");
clear->setInt("color", 0x303030FF); // RGBA
io->publish("render:clear", std::move(clear));
```
### Debug (lignes et rectangles)
```cpp
// Topic: render:debug:line
auto line = std::make_unique<JsonDataNode>("line");
line->setDouble("x1", 0.0);
line->setDouble("y1", 0.0);
line->setDouble("x2", 100.0);
line->setDouble("y2", 100.0);
line->setInt("color", 0xFF0000FF); // Rouge
io->publish("render:debug:line", std::move(line));
// Topic: render:debug:rect
auto rect = std::make_unique<JsonDataNode>("rect");
rect->setDouble("x", 50.0);
rect->setDouble("y", 50.0);
rect->setDouble("w", 100.0);
rect->setDouble("h", 100.0);
rect->setInt("color", 0x00FF00FF); // Vert
rect->setBool("filled", false);
io->publish("render:debug:rect", std::move(rect));
```
### Topics complets
| Topic | Description |
|-------|-------------|
| `render:sprite` | Un sprite |
| `render:sprite:batch` | Batch de sprites |
| `render:tilemap` | Chunk de tilemap |
| `render:text` | Texte à afficher |
| `render:particle` | Particule |
| `render:camera` | Configuration caméra |
| `render:clear` | Clear color |
| `render:debug:line` | Ligne de debug |
| `render:debug:rect` | Rectangle de debug |
## Intégration
### Exemple minimal
```cpp
#include <grove/ModuleLoader.h>
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
int main() {
// Créer le gestionnaire IO
auto ioManager = std::make_unique<IntraIOManager>();
auto io = ioManager->createIO("renderer");
// Charger le module
ModuleLoader loader;
loader.load("./modules/libBgfxRenderer.dll", "renderer");
// Configurer
JsonDataNode config("config");
config.setInt("windowWidth", 1920);
config.setInt("windowHeight", 1080);
config.setInt("nativeWindowHandle", (int)(intptr_t)hwnd); // Ton HWND
auto* module = loader.getModule();
module->setConfiguration(config, io.get(), nullptr);
// Main loop
JsonDataNode input("input");
while (running) {
input.setDouble("deltaTime", deltaTime);
// Envoyer des sprites via IIO
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", playerX);
sprite->setDouble("y", playerY);
sprite->setInt("textureId", 0);
io->publish("render:sprite", std::move(sprite));
// Process (collecte IIO + rendu)
module->process(input);
}
module->shutdown();
return 0;
}
```
## Règles d'architecture
| Règle | Raison |
|-------|--------|
| **Zéro `bgfx::` hors de `BgfxDevice.cpp`** | Abstraction propre, changement backend possible |
| **FramePacket const dans les passes** | Thread-safety, pas de mutation pendant render |
| **CommandBuffer par thread** | Pas de lock pendant l'encoding |
| **Handles, jamais de pointeurs raw** | Indirection = safe pour relocation |
| **Allocation via FrameAllocator** | Lock-free, reset gratuit chaque frame |
## TODO
- [ ] Chargement textures (stb_image)
- [ ] Compilation shaders (shaderc ou pré-compilés)
- [ ] TilemapPass
- [ ] TextPass + fonts (stb_truetype)
- [ ] ParticlePass
- [ ] Resize handling (window:resize)
- [ ] Multi-view support
- [ ] Render targets / post-process
## Dépendances
- **bgfx** : Téléchargé automatiquement via CMake FetchContent
- **GroveEngine::impl** : Core engine (IModule, IIO, IDataNode)
- **spdlog** : Logging

View File

@ -0,0 +1,285 @@
#include "RHIDevice.h"
#include "RHICommandBuffer.h"
// bgfx includes - ONLY in this file
#include <bgfx/bgfx.h>
#include <bgfx/platform.h>
#include <bx/math.h>
#include <unordered_map>
namespace grove::rhi {
// ============================================================================
// Bgfx Device Implementation
// ============================================================================
class BgfxDevice : public IRHIDevice {
public:
BgfxDevice() = default;
~BgfxDevice() override = default;
bool init(void* nativeWindowHandle, uint16_t width, uint16_t height) override {
m_width = width;
m_height = height;
bgfx::Init init;
init.type = bgfx::RendererType::Count; // Auto-select
init.resolution.width = width;
init.resolution.height = height;
init.resolution.reset = BGFX_RESET_VSYNC;
init.platformData.nwh = nativeWindowHandle;
if (!bgfx::init(init)) {
return false;
}
// Set debug flags in debug builds
#ifdef _DEBUG
bgfx::setDebug(BGFX_DEBUG_TEXT);
#endif
// Set default view clear
bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x303030FF, 1.0f, 0);
bgfx::setViewRect(0, 0, 0, width, height);
m_initialized = true;
return true;
}
void shutdown() override {
if (m_initialized) {
bgfx::shutdown();
m_initialized = false;
}
}
void reset(uint16_t width, uint16_t height) override {
m_width = width;
m_height = height;
bgfx::reset(width, height, BGFX_RESET_VSYNC);
bgfx::setViewRect(0, 0, 0, width, height);
}
DeviceCapabilities getCapabilities() const override {
DeviceCapabilities caps;
const bgfx::Caps* bgfxCaps = bgfx::getCaps();
caps.maxTextureSize = static_cast<uint16_t>(bgfxCaps->limits.maxTextureSize);
caps.maxViews = static_cast<uint16_t>(bgfxCaps->limits.maxViews);
caps.maxDrawCalls = bgfxCaps->limits.maxDrawCalls;
caps.instancingSupported = bgfxCaps->supported & BGFX_CAPS_INSTANCING;
caps.computeSupported = bgfxCaps->supported & BGFX_CAPS_COMPUTE;
caps.rendererName = bgfx::getRendererName(bgfxCaps->rendererType);
caps.gpuName = bgfxCaps->vendorId != BGFX_PCI_ID_NONE
? std::to_string(bgfxCaps->vendorId) : "Unknown";
return caps;
}
// ========================================
// Resource Creation
// ========================================
TextureHandle createTexture(const TextureDesc& desc) override {
bgfx::TextureFormat::Enum format = toBgfxFormat(desc.format);
bgfx::TextureHandle handle = bgfx::createTexture2D(
desc.width, desc.height,
desc.mipLevels > 1,
1, // layers
format,
BGFX_TEXTURE_NONE | BGFX_SAMPLER_NONE,
desc.data ? bgfx::copy(desc.data, desc.dataSize) : nullptr
);
TextureHandle result;
result.id = handle.idx;
return result;
}
BufferHandle createBuffer(const BufferDesc& desc) override {
BufferHandle result;
if (desc.type == BufferDesc::Vertex) {
bgfx::VertexBufferHandle vb;
if (desc.dynamic) {
bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer(
desc.size,
bgfx::VertexLayout(), // Will be set at draw time
BGFX_BUFFER_ALLOW_RESIZE
);
// Store as dynamic (high bit set)
result.id = dvb.idx | 0x8000;
} else {
vb = bgfx::createVertexBuffer(
desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(nullptr, desc.size),
bgfx::VertexLayout()
);
result.id = vb.idx;
}
} else if (desc.type == BufferDesc::Index) {
if (desc.dynamic) {
bgfx::DynamicIndexBufferHandle dib = bgfx::createDynamicIndexBuffer(
desc.size / sizeof(uint16_t),
BGFX_BUFFER_ALLOW_RESIZE
);
result.id = dib.idx | 0x8000;
} else {
bgfx::IndexBufferHandle ib = bgfx::createIndexBuffer(
desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(nullptr, desc.size)
);
result.id = ib.idx;
}
} else { // Instance buffer - treated as vertex buffer
bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer(
desc.size,
bgfx::VertexLayout(),
BGFX_BUFFER_ALLOW_RESIZE
);
result.id = dvb.idx | 0x8000;
}
return result;
}
ShaderHandle createShader(const ShaderDesc& desc) override {
bgfx::ShaderHandle vs = bgfx::createShader(
bgfx::copy(desc.vsData, desc.vsSize)
);
bgfx::ShaderHandle fs = bgfx::createShader(
bgfx::copy(desc.fsData, desc.fsSize)
);
bgfx::ProgramHandle program = bgfx::createProgram(vs, fs, true);
ShaderHandle result;
result.id = program.idx;
return result;
}
UniformHandle createUniform(const char* name, uint8_t numVec4s) override {
bgfx::UniformHandle uniform = bgfx::createUniform(
name,
numVec4s == 1 ? bgfx::UniformType::Vec4 : bgfx::UniformType::Mat4
);
UniformHandle result;
result.id = uniform.idx;
return result;
}
// ========================================
// Resource Destruction
// ========================================
void destroy(TextureHandle handle) override {
if (handle.isValid()) {
bgfx::TextureHandle h = { handle.id };
bgfx::destroy(h);
}
}
void destroy(BufferHandle handle) override {
if (handle.isValid()) {
bool isDynamic = (handle.id & 0x8000) != 0;
uint16_t idx = handle.id & 0x7FFF;
if (isDynamic) {
bgfx::DynamicVertexBufferHandle h = { idx };
bgfx::destroy(h);
} else {
bgfx::VertexBufferHandle h = { idx };
bgfx::destroy(h);
}
}
}
void destroy(ShaderHandle handle) override {
if (handle.isValid()) {
bgfx::ProgramHandle h = { handle.id };
bgfx::destroy(h);
}
}
void destroy(UniformHandle handle) override {
if (handle.isValid()) {
bgfx::UniformHandle h = { handle.id };
bgfx::destroy(h);
}
}
// ========================================
// Dynamic Updates
// ========================================
void updateBuffer(BufferHandle handle, const void* data, uint32_t size) override {
if (!handle.isValid()) return;
bool isDynamic = (handle.id & 0x8000) != 0;
uint16_t idx = handle.id & 0x7FFF;
if (isDynamic) {
bgfx::DynamicVertexBufferHandle h = { idx };
bgfx::update(h, 0, bgfx::copy(data, size));
}
// Static buffers cannot be updated
}
void updateTexture(TextureHandle handle, const void* data, uint32_t size) override {
if (!handle.isValid()) return;
bgfx::TextureHandle h = { handle.id };
bgfx::updateTexture2D(h, 0, 0, 0, 0, m_width, m_height, bgfx::copy(data, size));
}
// ========================================
// View Setup
// ========================================
void setViewClear(ViewId id, uint32_t rgba, float depth) override {
bgfx::setViewClear(id, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, rgba, depth, 0);
}
void setViewRect(ViewId id, uint16_t x, uint16_t y, uint16_t w, uint16_t h) override {
bgfx::setViewRect(id, x, y, w, h);
}
void setViewTransform(ViewId id, const float* view, const float* proj) override {
bgfx::setViewTransform(id, view, proj);
}
// ========================================
// Frame
// ========================================
void frame() override {
bgfx::frame();
}
private:
uint16_t m_width = 0;
uint16_t m_height = 0;
bool m_initialized = false;
static bgfx::TextureFormat::Enum toBgfxFormat(TextureDesc::Format format) {
switch (format) {
case TextureDesc::RGBA8: return bgfx::TextureFormat::RGBA8;
case TextureDesc::RGB8: return bgfx::TextureFormat::RGB8;
case TextureDesc::R8: return bgfx::TextureFormat::R8;
case TextureDesc::DXT1: return bgfx::TextureFormat::BC1;
case TextureDesc::DXT5: return bgfx::TextureFormat::BC3;
default: return bgfx::TextureFormat::RGBA8;
}
}
};
// ============================================================================
// Factory
// ============================================================================
std::unique_ptr<IRHIDevice> IRHIDevice::create() {
return std::make_unique<BgfxDevice>();
}
} // namespace grove::rhi

View File

@ -0,0 +1,99 @@
#include "RHICommandBuffer.h"
namespace grove::rhi {
void RHICommandBuffer::setState(const RenderState& state) {
Command cmd;
cmd.type = CommandType::SetState;
cmd.setState.state = state;
m_commands.push_back(cmd);
}
void RHICommandBuffer::setTexture(uint8_t slot, TextureHandle tex, UniformHandle sampler) {
Command cmd;
cmd.type = CommandType::SetTexture;
cmd.setTexture.slot = slot;
cmd.setTexture.texture = tex;
cmd.setTexture.sampler = sampler;
m_commands.push_back(cmd);
}
void RHICommandBuffer::setUniform(UniformHandle uniform, const float* data, uint8_t numVec4s) {
Command cmd;
cmd.type = CommandType::SetUniform;
cmd.setUniform.uniform = uniform;
cmd.setUniform.numVec4s = numVec4s;
std::memcpy(cmd.setUniform.data, data, numVec4s * 16);
m_commands.push_back(cmd);
}
void RHICommandBuffer::setVertexBuffer(BufferHandle buffer, uint32_t offset) {
Command cmd;
cmd.type = CommandType::SetVertexBuffer;
cmd.setVertexBuffer.buffer = buffer;
cmd.setVertexBuffer.offset = offset;
m_commands.push_back(cmd);
}
void RHICommandBuffer::setIndexBuffer(BufferHandle buffer, uint32_t offset, bool is32Bit) {
Command cmd;
cmd.type = CommandType::SetIndexBuffer;
cmd.setIndexBuffer.buffer = buffer;
cmd.setIndexBuffer.offset = offset;
cmd.setIndexBuffer.is32Bit = is32Bit;
m_commands.push_back(cmd);
}
void RHICommandBuffer::setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count) {
Command cmd;
cmd.type = CommandType::SetInstanceBuffer;
cmd.setInstanceBuffer.buffer = buffer;
cmd.setInstanceBuffer.start = start;
cmd.setInstanceBuffer.count = count;
m_commands.push_back(cmd);
}
void RHICommandBuffer::setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
Command cmd;
cmd.type = CommandType::SetScissor;
cmd.setScissor.x = x;
cmd.setScissor.y = y;
cmd.setScissor.w = w;
cmd.setScissor.h = h;
m_commands.push_back(cmd);
}
void RHICommandBuffer::draw(uint32_t vertexCount, uint32_t startVertex) {
Command cmd;
cmd.type = CommandType::Draw;
cmd.draw.vertexCount = vertexCount;
cmd.draw.startVertex = startVertex;
m_commands.push_back(cmd);
}
void RHICommandBuffer::drawIndexed(uint32_t indexCount, uint32_t startIndex) {
Command cmd;
cmd.type = CommandType::DrawIndexed;
cmd.drawIndexed.indexCount = indexCount;
cmd.drawIndexed.startIndex = startIndex;
m_commands.push_back(cmd);
}
void RHICommandBuffer::drawInstanced(uint32_t indexCount, uint32_t instanceCount) {
Command cmd;
cmd.type = CommandType::DrawInstanced;
cmd.drawInstanced.indexCount = indexCount;
cmd.drawInstanced.instanceCount = instanceCount;
m_commands.push_back(cmd);
}
void RHICommandBuffer::submit(ViewId view, ShaderHandle shader, uint32_t depth) {
Command cmd;
cmd.type = CommandType::Submit;
cmd.submit.view = view;
cmd.submit.shader = shader;
cmd.submit.depth = depth;
m_commands.push_back(cmd);
}
} // namespace grove::rhi

View File

@ -0,0 +1,81 @@
#pragma once
#include "RHITypes.h"
#include <vector>
#include <cstring>
namespace grove::rhi {
// ============================================================================
// Command Types - POD for serialization
// ============================================================================
enum class CommandType : uint8_t {
SetState,
SetTexture,
SetUniform,
SetVertexBuffer,
SetIndexBuffer,
SetInstanceBuffer,
SetScissor,
Draw,
DrawIndexed,
DrawInstanced,
Submit
};
struct Command {
CommandType type;
union {
struct { RenderState state; } setState;
struct { uint8_t slot; TextureHandle texture; UniformHandle sampler; } setTexture;
struct { UniformHandle uniform; float data[16]; uint8_t numVec4s; } setUniform;
struct { BufferHandle buffer; uint32_t offset; } setVertexBuffer;
struct { BufferHandle buffer; uint32_t offset; bool is32Bit; } setIndexBuffer;
struct { BufferHandle buffer; uint32_t start; uint32_t count; } setInstanceBuffer;
struct { uint16_t x, y, w, h; } setScissor;
struct { uint32_t vertexCount; uint32_t startVertex; } draw;
struct { uint32_t indexCount; uint32_t startIndex; } drawIndexed;
struct { uint32_t indexCount; uint32_t instanceCount; } drawInstanced;
struct { ViewId view; ShaderHandle shader; uint32_t depth; } submit;
};
};
// ============================================================================
// Command Buffer - One per thread, write-only during recording
// ============================================================================
class RHICommandBuffer {
public:
RHICommandBuffer() = default;
// Non-copyable, movable
RHICommandBuffer(const RHICommandBuffer&) = delete;
RHICommandBuffer& operator=(const RHICommandBuffer&) = delete;
RHICommandBuffer(RHICommandBuffer&&) = default;
RHICommandBuffer& operator=(RHICommandBuffer&&) = default;
// Command recording
void setState(const RenderState& state);
void setTexture(uint8_t slot, TextureHandle tex, UniformHandle sampler);
void setUniform(UniformHandle uniform, const float* data, uint8_t numVec4s);
void setVertexBuffer(BufferHandle buffer, uint32_t offset = 0);
void setIndexBuffer(BufferHandle buffer, uint32_t offset = 0, bool is32Bit = false);
void setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count);
void setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void draw(uint32_t vertexCount, uint32_t startVertex = 0);
void drawIndexed(uint32_t indexCount, uint32_t startIndex = 0);
void drawInstanced(uint32_t indexCount, uint32_t instanceCount);
void submit(ViewId view, ShaderHandle shader, uint32_t depth = 0);
// Read-only access for execution
const std::vector<Command>& getCommands() const { return m_commands; }
void clear() { m_commands.clear(); }
size_t size() const { return m_commands.size(); }
private:
std::vector<Command> m_commands;
};
} // namespace grove::rhi

View File

@ -0,0 +1,67 @@
#pragma once
#include "RHITypes.h"
#include <memory>
#include <string>
namespace grove::rhi {
// ============================================================================
// Device Capabilities
// ============================================================================
struct DeviceCapabilities {
uint16_t maxTextureSize = 0;
uint16_t maxViews = 0;
uint32_t maxDrawCalls = 0;
bool instancingSupported = false;
bool computeSupported = false;
std::string rendererName;
std::string gpuName;
};
// ============================================================================
// RHI Device Interface - Abstract GPU access
// ============================================================================
class IRHIDevice {
public:
virtual ~IRHIDevice() = default;
// Lifecycle
virtual bool init(void* nativeWindowHandle, uint16_t width, uint16_t height) = 0;
virtual void shutdown() = 0;
virtual void reset(uint16_t width, uint16_t height) = 0;
// Capabilities
virtual DeviceCapabilities getCapabilities() const = 0;
// Resource creation
virtual TextureHandle createTexture(const TextureDesc& desc) = 0;
virtual BufferHandle createBuffer(const BufferDesc& desc) = 0;
virtual ShaderHandle createShader(const ShaderDesc& desc) = 0;
virtual UniformHandle createUniform(const char* name, uint8_t numVec4s) = 0;
// Resource destruction
virtual void destroy(TextureHandle handle) = 0;
virtual void destroy(BufferHandle handle) = 0;
virtual void destroy(ShaderHandle handle) = 0;
virtual void destroy(UniformHandle handle) = 0;
// Dynamic updates
virtual void updateBuffer(BufferHandle handle, const void* data, uint32_t size) = 0;
virtual void updateTexture(TextureHandle handle, const void* data, uint32_t size) = 0;
// View setup
virtual void setViewClear(ViewId id, uint32_t rgba, float depth) = 0;
virtual void setViewRect(ViewId id, uint16_t x, uint16_t y, uint16_t w, uint16_t h) = 0;
virtual void setViewTransform(ViewId id, const float* view, const float* proj) = 0;
// Frame
virtual void frame() = 0;
// Factory
static std::unique_ptr<IRHIDevice> create();
};
} // namespace grove::rhi

View File

@ -0,0 +1,106 @@
#pragma once
#include <cstdint>
namespace grove::rhi {
// ============================================================================
// Typed Handles - Never expose bgfx:: outside BgfxDevice.cpp
// ============================================================================
struct TextureHandle {
uint16_t id = UINT16_MAX;
bool isValid() const { return id != UINT16_MAX; }
};
struct BufferHandle {
uint16_t id = UINT16_MAX;
bool isValid() const { return id != UINT16_MAX; }
};
struct ShaderHandle {
uint16_t id = UINT16_MAX;
bool isValid() const { return id != UINT16_MAX; }
};
struct UniformHandle {
uint16_t id = UINT16_MAX;
bool isValid() const { return id != UINT16_MAX; }
};
struct FramebufferHandle {
uint16_t id = UINT16_MAX;
bool isValid() const { return id != UINT16_MAX; }
};
using ViewId = uint16_t;
// ============================================================================
// Render States
// ============================================================================
enum class BlendMode : uint8_t {
None,
Alpha,
Additive,
Multiply
};
enum class CullMode : uint8_t {
None,
CW,
CCW
};
struct RenderState {
BlendMode blend = BlendMode::Alpha;
CullMode cull = CullMode::None;
bool depthTest = false;
bool depthWrite = false;
};
// ============================================================================
// Vertex Layout
// ============================================================================
struct VertexLayout {
enum Attrib : uint8_t {
Position, // float3
TexCoord0, // float2
Color0, // uint32 RGBA
Normal, // float3
Count
};
uint32_t stride = 0;
uint16_t offsets[Attrib::Count] = {};
bool has[Attrib::Count] = {};
};
// ============================================================================
// Resource Descriptors
// ============================================================================
struct TextureDesc {
uint16_t width = 0;
uint16_t height = 0;
uint8_t mipLevels = 1;
enum Format : uint8_t { RGBA8, RGB8, R8, DXT1, DXT5 } format = RGBA8;
const void* data = nullptr;
uint32_t dataSize = 0;
};
struct BufferDesc {
uint32_t size = 0;
const void* data = nullptr;
bool dynamic = false;
enum Type : uint8_t { Vertex, Index, Instance } type = Vertex;
};
struct ShaderDesc {
const void* vsData = nullptr;
uint32_t vsSize = 0;
const void* fsData = nullptr;
uint32_t fsSize = 0;
};
} // namespace grove::rhi

View File

@ -0,0 +1,72 @@
#include "RenderGraph.h"
#include "../RHI/RHIDevice.h"
#include <algorithm>
#include <unordered_map>
#include <stdexcept>
namespace grove {
void RenderGraph::addPass(std::unique_ptr<RenderPass> pass) {
m_passes.push_back(std::move(pass));
m_compiled = false;
}
void RenderGraph::setup(rhi::IRHIDevice& device) {
for (auto& pass : m_passes) {
pass->setup(device);
}
}
void RenderGraph::compile() {
if (m_compiled) return;
// Build name to index map
std::unordered_map<std::string, size_t> nameToIndex;
for (size_t i = 0; i < m_passes.size(); ++i) {
nameToIndex[m_passes[i]->getName()] = i;
}
// Create sorted indices based on sort order and dependencies
m_sortedIndices.clear();
m_sortedIndices.reserve(m_passes.size());
for (size_t i = 0; i < m_passes.size(); ++i) {
m_sortedIndices.push_back(i);
}
// Sort by sort order (topological sort would be more complete but this is simpler)
std::sort(m_sortedIndices.begin(), m_sortedIndices.end(),
[this](size_t a, size_t b) {
return m_passes[a]->getSortOrder() < m_passes[b]->getSortOrder();
});
m_compiled = true;
}
void RenderGraph::execute(const FramePacket& frame, rhi::IRHIDevice& device) {
if (!m_compiled) {
compile();
}
// Single command buffer for single-threaded execution
rhi::RHICommandBuffer cmdBuffer;
// Execute passes in order
for (size_t idx : m_sortedIndices) {
m_passes[idx]->execute(frame, cmdBuffer);
}
// TODO: Execute command buffer on device
// For now, passes directly call bgfx through the device
}
void RenderGraph::shutdown(rhi::IRHIDevice& device) {
for (auto& pass : m_passes) {
pass->shutdown(device);
}
m_passes.clear();
m_sortedIndices.clear();
m_compiled = false;
}
} // namespace grove

View File

@ -0,0 +1,48 @@
#pragma once
#include "RenderPass.h"
#include <memory>
#include <vector>
namespace grove {
namespace rhi { class IRHIDevice; }
// ============================================================================
// Render Graph - Manages pass ordering and execution
// ============================================================================
class RenderGraph {
public:
RenderGraph() = default;
~RenderGraph() = default;
// Non-copyable
RenderGraph(const RenderGraph&) = delete;
RenderGraph& operator=(const RenderGraph&) = delete;
// Pass registration
void addPass(std::unique_ptr<RenderPass> pass);
// Setup all passes
void setup(rhi::IRHIDevice& device);
// Compile the graph (order, dependencies)
void compile();
// Execute all passes for a frame
void execute(const FramePacket& frame, rhi::IRHIDevice& device);
// Shutdown all passes
void shutdown(rhi::IRHIDevice& device);
// Accessors
size_t getPassCount() const { return m_passes.size(); }
private:
std::vector<std::unique_ptr<RenderPass>> m_passes;
std::vector<size_t> m_sortedIndices;
bool m_compiled = false;
};
} // namespace grove

View File

@ -0,0 +1,40 @@
#pragma once
#include "../RHI/RHICommandBuffer.h"
#include "../Frame/FramePacket.h"
#include <vector>
namespace grove {
namespace rhi { class IRHIDevice; }
// ============================================================================
// Render Pass Interface
// ============================================================================
class RenderPass {
public:
virtual ~RenderPass() = default;
// Unique identifier
virtual const char* getName() const = 0;
// Render order (lower = earlier)
virtual uint32_t getSortOrder() const = 0;
// Dependencies (names of passes that must execute before)
virtual std::vector<const char*> getDependencies() const { return {}; }
// Execution - MUST be thread-safe
// frame: read-only
// cmd: write-only, thread-local
virtual void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) = 0;
// Initial setup (load shaders, create buffers)
virtual void setup(rhi::IRHIDevice& device) = 0;
// Cleanup
virtual void shutdown(rhi::IRHIDevice& device) = 0;
};
} // namespace grove

View File

@ -0,0 +1,122 @@
#include "ResourceCache.h"
#include "../RHI/RHIDevice.h"
namespace grove {
rhi::TextureHandle ResourceCache::getTexture(const std::string& path) const {
std::shared_lock lock(m_mutex);
auto it = m_textures.find(path);
if (it != m_textures.end()) {
return it->second;
}
return rhi::TextureHandle{}; // Invalid handle
}
rhi::ShaderHandle ResourceCache::getShader(const std::string& name) const {
std::shared_lock lock(m_mutex);
auto it = m_shaders.find(name);
if (it != m_shaders.end()) {
return it->second;
}
return rhi::ShaderHandle{}; // Invalid handle
}
rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std::string& path) {
// Check if already loaded
{
std::shared_lock lock(m_mutex);
auto it = m_textures.find(path);
if (it != m_textures.end()) {
return it->second;
}
}
// Load texture data from file
// TODO: Use stb_image or similar to load actual texture files
// For now, create a placeholder 1x1 white texture
uint32_t whitePixel = 0xFFFFFFFF;
rhi::TextureDesc desc;
desc.width = 1;
desc.height = 1;
desc.mipLevels = 1;
desc.format = rhi::TextureDesc::RGBA8;
desc.data = &whitePixel;
desc.dataSize = sizeof(whitePixel);
rhi::TextureHandle handle = device.createTexture(desc);
// Store in cache
{
std::unique_lock lock(m_mutex);
m_textures[path] = handle;
}
return handle;
}
rhi::ShaderHandle ResourceCache::loadShader(rhi::IRHIDevice& device, const std::string& name,
const void* vsData, uint32_t vsSize,
const void* fsData, uint32_t fsSize) {
// Check if already loaded
{
std::shared_lock lock(m_mutex);
auto it = m_shaders.find(name);
if (it != m_shaders.end()) {
return it->second;
}
}
rhi::ShaderDesc desc;
desc.vsData = vsData;
desc.vsSize = vsSize;
desc.fsData = fsData;
desc.fsSize = fsSize;
rhi::ShaderHandle handle = device.createShader(desc);
// Store in cache
{
std::unique_lock lock(m_mutex);
m_shaders[name] = handle;
}
return handle;
}
bool ResourceCache::hasTexture(const std::string& path) const {
std::shared_lock lock(m_mutex);
return m_textures.find(path) != m_textures.end();
}
bool ResourceCache::hasShader(const std::string& name) const {
std::shared_lock lock(m_mutex);
return m_shaders.find(name) != m_shaders.end();
}
void ResourceCache::clear(rhi::IRHIDevice& device) {
std::unique_lock lock(m_mutex);
for (auto& [path, handle] : m_textures) {
device.destroy(handle);
}
m_textures.clear();
for (auto& [name, handle] : m_shaders) {
device.destroy(handle);
}
m_shaders.clear();
}
size_t ResourceCache::getTextureCount() const {
std::shared_lock lock(m_mutex);
return m_textures.size();
}
size_t ResourceCache::getShaderCount() const {
std::shared_lock lock(m_mutex);
return m_shaders.size();
}
} // namespace grove

View File

@ -0,0 +1,47 @@
#pragma once
#include "../RHI/RHITypes.h"
#include <unordered_map>
#include <string>
#include <shared_mutex>
namespace grove {
namespace rhi { class IRHIDevice; }
// ============================================================================
// Resource Cache - Thread-safe texture and shader cache
// ============================================================================
class ResourceCache {
public:
ResourceCache() = default;
// Thread-safe resource access (returns invalid handle if not found)
rhi::TextureHandle getTexture(const std::string& path) const;
rhi::ShaderHandle getShader(const std::string& name) const;
// Loading (called from main thread)
rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path);
rhi::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name,
const void* vsData, uint32_t vsSize,
const void* fsData, uint32_t fsSize);
// Check if resource exists
bool hasTexture(const std::string& path) const;
bool hasShader(const std::string& name) const;
// Cleanup
void clear(rhi::IRHIDevice& device);
// Stats
size_t getTextureCount() const;
size_t getShaderCount() const;
private:
std::unordered_map<std::string, rhi::TextureHandle> m_textures;
std::unordered_map<std::string, rhi::ShaderHandle> m_shaders;
mutable std::shared_mutex m_mutex;
};
} // namespace grove

View File

@ -0,0 +1,298 @@
#include "SceneCollector.h"
#include "grove/IIO.h"
#include "grove/IDataNode.h"
#include "../Frame/FrameAllocator.h"
#include <cstring>
namespace grove {
void SceneCollector::setup(IIO* io) {
// Subscribe to all render topics
io->subscribe("render:*");
// Initialize default view (will be overridden by camera messages)
initDefaultView(1280, 720);
}
void SceneCollector::collect(IIO* io, float deltaTime) {
m_deltaTime = deltaTime;
m_frameNumber++;
// Pull all pending messages
while (io->hasMessages() > 0) {
Message msg = io->pullMessage();
if (!msg.data) continue;
// Route message based on topic
if (msg.topic == "render:sprite") {
parseSprite(*msg.data);
}
else if (msg.topic == "render:sprite:batch") {
parseSpriteBatch(*msg.data);
}
else if (msg.topic == "render:tilemap") {
parseTilemap(*msg.data);
}
else if (msg.topic == "render:text") {
parseText(*msg.data);
}
else if (msg.topic == "render:particle") {
parseParticle(*msg.data);
}
else if (msg.topic == "render:camera") {
parseCamera(*msg.data);
}
else if (msg.topic == "render:clear") {
parseClear(*msg.data);
}
else if (msg.topic == "render:debug:line") {
parseDebugLine(*msg.data);
}
else if (msg.topic == "render:debug:rect") {
parseDebugRect(*msg.data);
}
}
}
FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
FramePacket packet;
packet.frameNumber = m_frameNumber;
packet.deltaTime = m_deltaTime;
packet.clearColor = m_clearColor;
packet.mainView = m_mainView;
packet.allocator = &allocator;
// Copy sprites to frame allocator
if (!m_sprites.empty()) {
SpriteInstance* sprites = allocator.allocateArray<SpriteInstance>(m_sprites.size());
if (sprites) {
std::memcpy(sprites, m_sprites.data(), m_sprites.size() * sizeof(SpriteInstance));
packet.sprites = sprites;
packet.spriteCount = m_sprites.size();
}
} else {
packet.sprites = nullptr;
packet.spriteCount = 0;
}
// Copy tilemaps
if (!m_tilemaps.empty()) {
TilemapChunk* tilemaps = allocator.allocateArray<TilemapChunk>(m_tilemaps.size());
if (tilemaps) {
std::memcpy(tilemaps, m_tilemaps.data(), m_tilemaps.size() * sizeof(TilemapChunk));
packet.tilemaps = tilemaps;
packet.tilemapCount = m_tilemaps.size();
}
} else {
packet.tilemaps = nullptr;
packet.tilemapCount = 0;
}
// Copy texts
if (!m_texts.empty()) {
TextCommand* texts = allocator.allocateArray<TextCommand>(m_texts.size());
if (texts) {
std::memcpy(texts, m_texts.data(), m_texts.size() * sizeof(TextCommand));
packet.texts = texts;
packet.textCount = m_texts.size();
}
} else {
packet.texts = nullptr;
packet.textCount = 0;
}
// Copy particles
if (!m_particles.empty()) {
ParticleInstance* particles = allocator.allocateArray<ParticleInstance>(m_particles.size());
if (particles) {
std::memcpy(particles, m_particles.data(), m_particles.size() * sizeof(ParticleInstance));
packet.particles = particles;
packet.particleCount = m_particles.size();
}
} else {
packet.particles = nullptr;
packet.particleCount = 0;
}
// Copy debug lines
if (!m_debugLines.empty()) {
DebugLine* lines = allocator.allocateArray<DebugLine>(m_debugLines.size());
if (lines) {
std::memcpy(lines, m_debugLines.data(), m_debugLines.size() * sizeof(DebugLine));
packet.debugLines = lines;
packet.debugLineCount = m_debugLines.size();
}
} else {
packet.debugLines = nullptr;
packet.debugLineCount = 0;
}
// Copy debug rects
if (!m_debugRects.empty()) {
DebugRect* rects = allocator.allocateArray<DebugRect>(m_debugRects.size());
if (rects) {
std::memcpy(rects, m_debugRects.data(), m_debugRects.size() * sizeof(DebugRect));
packet.debugRects = rects;
packet.debugRectCount = m_debugRects.size();
}
} else {
packet.debugRects = nullptr;
packet.debugRectCount = 0;
}
return packet;
}
void SceneCollector::clear() {
m_sprites.clear();
m_tilemaps.clear();
m_texts.clear();
m_particles.clear();
m_debugLines.clear();
m_debugRects.clear();
}
// ============================================================================
// Message Parsing
// ============================================================================
void SceneCollector::parseSprite(const IDataNode& data) {
SpriteInstance sprite;
sprite.x = static_cast<float>(data.getDouble("x", 0.0));
sprite.y = static_cast<float>(data.getDouble("y", 0.0));
sprite.scaleX = static_cast<float>(data.getDouble("scaleX", 1.0));
sprite.scaleY = static_cast<float>(data.getDouble("scaleY", 1.0));
sprite.rotation = static_cast<float>(data.getDouble("rotation", 0.0));
sprite.u0 = static_cast<float>(data.getDouble("u0", 0.0));
sprite.v0 = static_cast<float>(data.getDouble("v0", 0.0));
sprite.u1 = static_cast<float>(data.getDouble("u1", 1.0));
sprite.v1 = static_cast<float>(data.getDouble("v1", 1.0));
sprite.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF));
sprite.textureId = static_cast<uint16_t>(data.getInt("textureId", 0));
sprite.layer = static_cast<uint16_t>(data.getInt("layer", 0));
m_sprites.push_back(sprite);
}
void SceneCollector::parseSpriteBatch(const IDataNode& data) {
// Get sprites child node and iterate
IDataNode* spritesNode = data.getChildReadOnly("sprites");
if (!spritesNode) return;
for (const auto& name : spritesNode->getChildNames()) {
IDataNode* spriteData = spritesNode->getChildReadOnly(name);
if (spriteData) {
parseSprite(*spriteData);
}
}
}
void SceneCollector::parseTilemap(const IDataNode& data) {
TilemapChunk chunk;
chunk.x = static_cast<float>(data.getDouble("x", 0.0));
chunk.y = static_cast<float>(data.getDouble("y", 0.0));
chunk.width = static_cast<uint16_t>(data.getInt("width", 0));
chunk.height = static_cast<uint16_t>(data.getInt("height", 0));
chunk.tileWidth = static_cast<uint16_t>(data.getInt("tileW", 16));
chunk.tileHeight = static_cast<uint16_t>(data.getInt("tileH", 16));
chunk.textureId = static_cast<uint16_t>(data.getInt("textureId", 0));
chunk.tiles = nullptr; // TODO: Parse tile array
chunk.tileCount = 0;
m_tilemaps.push_back(chunk);
}
void SceneCollector::parseText(const IDataNode& data) {
TextCommand text;
text.x = static_cast<float>(data.getDouble("x", 0.0));
text.y = static_cast<float>(data.getDouble("y", 0.0));
text.text = nullptr; // TODO: Copy string to frame allocator
text.fontId = static_cast<uint16_t>(data.getInt("fontId", 0));
text.fontSize = static_cast<uint16_t>(data.getInt("fontSize", 16));
text.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF));
text.layer = static_cast<uint16_t>(data.getInt("layer", 0));
m_texts.push_back(text);
}
void SceneCollector::parseParticle(const IDataNode& data) {
ParticleInstance particle;
particle.x = static_cast<float>(data.getDouble("x", 0.0));
particle.y = static_cast<float>(data.getDouble("y", 0.0));
particle.vx = static_cast<float>(data.getDouble("vx", 0.0));
particle.vy = static_cast<float>(data.getDouble("vy", 0.0));
particle.size = static_cast<float>(data.getDouble("size", 1.0));
particle.life = static_cast<float>(data.getDouble("life", 1.0));
particle.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF));
particle.textureId = static_cast<uint16_t>(data.getInt("textureId", 0));
m_particles.push_back(particle);
}
void SceneCollector::parseCamera(const IDataNode& data) {
m_mainView.positionX = static_cast<float>(data.getDouble("x", 0.0));
m_mainView.positionY = static_cast<float>(data.getDouble("y", 0.0));
m_mainView.zoom = static_cast<float>(data.getDouble("zoom", 1.0));
m_mainView.viewportX = static_cast<uint16_t>(data.getInt("viewportX", 0));
m_mainView.viewportY = static_cast<uint16_t>(data.getInt("viewportY", 0));
m_mainView.viewportW = static_cast<uint16_t>(data.getInt("viewportW", 1280));
m_mainView.viewportH = static_cast<uint16_t>(data.getInt("viewportH", 720));
// TODO: Compute view and projection matrices from camera params
}
void SceneCollector::parseClear(const IDataNode& data) {
m_clearColor = static_cast<uint32_t>(data.getInt("color", 0x303030FF));
}
void SceneCollector::parseDebugLine(const IDataNode& data) {
DebugLine line;
line.x1 = static_cast<float>(data.getDouble("x1", 0.0));
line.y1 = static_cast<float>(data.getDouble("y1", 0.0));
line.x2 = static_cast<float>(data.getDouble("x2", 0.0));
line.y2 = static_cast<float>(data.getDouble("y2", 0.0));
line.color = static_cast<uint32_t>(data.getInt("color", 0xFF0000FF));
m_debugLines.push_back(line);
}
void SceneCollector::parseDebugRect(const IDataNode& data) {
DebugRect rect;
rect.x = static_cast<float>(data.getDouble("x", 0.0));
rect.y = static_cast<float>(data.getDouble("y", 0.0));
rect.w = static_cast<float>(data.getDouble("w", 0.0));
rect.h = static_cast<float>(data.getDouble("h", 0.0));
rect.color = static_cast<uint32_t>(data.getInt("color", 0x00FF00FF));
rect.filled = data.getBool("filled", false);
m_debugRects.push_back(rect);
}
void SceneCollector::initDefaultView(uint16_t width, uint16_t height) {
m_mainView.positionX = 0.0f;
m_mainView.positionY = 0.0f;
m_mainView.zoom = 1.0f;
m_mainView.viewportX = 0;
m_mainView.viewportY = 0;
m_mainView.viewportW = width;
m_mainView.viewportH = height;
// Identity view matrix
for (int i = 0; i < 16; ++i) {
m_mainView.viewMatrix[i] = (i % 5 == 0) ? 1.0f : 0.0f;
}
// Orthographic projection matrix (2D)
// Maps (0,0)-(width,height) to (-1,-1)-(1,1)
std::memset(m_mainView.projMatrix, 0, sizeof(m_mainView.projMatrix));
m_mainView.projMatrix[0] = 2.0f / width;
m_mainView.projMatrix[5] = -2.0f / height; // Y-flip for top-left origin
m_mainView.projMatrix[10] = 1.0f;
m_mainView.projMatrix[12] = -1.0f;
m_mainView.projMatrix[13] = 1.0f;
m_mainView.projMatrix[15] = 1.0f;
}
} // namespace grove

View File

@ -0,0 +1,64 @@
#pragma once
#include "../Frame/FramePacket.h"
#include <vector>
#include <string>
namespace grove {
class IIO;
class IDataNode;
class FrameAllocator;
// ============================================================================
// Scene Collector - Gathers render data from IIO messages
// ============================================================================
class SceneCollector {
public:
SceneCollector() = default;
// Configure IIO subscriptions (called in setConfiguration)
void setup(IIO* io);
// Collect all IIO messages at frame start (called in process)
// Pull-based: module controls when to read messages
void collect(IIO* io, float deltaTime);
// Generate immutable FramePacket for render passes
FramePacket finalize(FrameAllocator& allocator);
// Reset for next frame
void clear();
private:
// Staging buffers (filled during collect, copied to FramePacket in finalize)
std::vector<SpriteInstance> m_sprites;
std::vector<TilemapChunk> m_tilemaps;
std::vector<TextCommand> m_texts;
std::vector<ParticleInstance> m_particles;
std::vector<DebugLine> m_debugLines;
std::vector<DebugRect> m_debugRects;
// View state
ViewInfo m_mainView;
uint32_t m_clearColor = 0x303030FF;
uint64_t m_frameNumber = 0;
float m_deltaTime = 0.0f;
// Message parsing helpers
void parseSprite(const IDataNode& data);
void parseSpriteBatch(const IDataNode& data);
void parseTilemap(const IDataNode& data);
void parseText(const IDataNode& data);
void parseParticle(const IDataNode& data);
void parseCamera(const IDataNode& data);
void parseClear(const IDataNode& data);
void parseDebugLine(const IDataNode& data);
void parseDebugRect(const IDataNode& data);
// Initialize default view
void initDefaultView(uint16_t width, uint16_t height);
};
} // namespace grove