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:
parent
18a319768d
commit
d63d8d83fa
43
.gitignore
vendored
43
.gitignore
vendored
@ -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
|
||||
*~
|
||||
|
||||
@ -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
113
build_renderer.bat
Normal 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
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
749
docs/USER_GUIDE.md
Normal 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
|
||||
179
modules/BgfxRenderer/BgfxRendererModule.cpp
Normal file
179
modules/BgfxRenderer/BgfxRendererModule.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
81
modules/BgfxRenderer/BgfxRendererModule.h
Normal file
81
modules/BgfxRenderer/BgfxRendererModule.h
Normal 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);
|
||||
}
|
||||
104
modules/BgfxRenderer/CMakeLists.txt
Normal file
104
modules/BgfxRenderer/CMakeLists.txt
Normal 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()
|
||||
44
modules/BgfxRenderer/Frame/FrameAllocator.cpp
Normal file
44
modules/BgfxRenderer/Frame/FrameAllocator.cpp
Normal 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
|
||||
60
modules/BgfxRenderer/Frame/FrameAllocator.h
Normal file
60
modules/BgfxRenderer/Frame/FrameAllocator.h
Normal 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
|
||||
128
modules/BgfxRenderer/Frame/FramePacket.h
Normal file
128
modules/BgfxRenderer/Frame/FramePacket.h
Normal 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
|
||||
25
modules/BgfxRenderer/Passes/ClearPass.cpp
Normal file
25
modules/BgfxRenderer/Passes/ClearPass.cpp
Normal 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
|
||||
21
modules/BgfxRenderer/Passes/ClearPass.h
Normal file
21
modules/BgfxRenderer/Passes/ClearPass.h
Normal 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
|
||||
54
modules/BgfxRenderer/Passes/DebugPass.cpp
Normal file
54
modules/BgfxRenderer/Passes/DebugPass.cpp
Normal 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
|
||||
29
modules/BgfxRenderer/Passes/DebugPass.h
Normal file
29
modules/BgfxRenderer/Passes/DebugPass.h
Normal 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
|
||||
98
modules/BgfxRenderer/Passes/SpritePass.cpp
Normal file
98
modules/BgfxRenderer/Passes/SpritePass.cpp
Normal 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
|
||||
32
modules/BgfxRenderer/Passes/SpritePass.h
Normal file
32
modules/BgfxRenderer/Passes/SpritePass.h
Normal 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
|
||||
264
modules/BgfxRenderer/README.md
Normal file
264
modules/BgfxRenderer/README.md
Normal 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
|
||||
285
modules/BgfxRenderer/RHI/BgfxDevice.cpp
Normal file
285
modules/BgfxRenderer/RHI/BgfxDevice.cpp
Normal 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
|
||||
99
modules/BgfxRenderer/RHI/RHICommandBuffer.cpp
Normal file
99
modules/BgfxRenderer/RHI/RHICommandBuffer.cpp
Normal 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
|
||||
81
modules/BgfxRenderer/RHI/RHICommandBuffer.h
Normal file
81
modules/BgfxRenderer/RHI/RHICommandBuffer.h
Normal 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
|
||||
67
modules/BgfxRenderer/RHI/RHIDevice.h
Normal file
67
modules/BgfxRenderer/RHI/RHIDevice.h
Normal 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
|
||||
106
modules/BgfxRenderer/RHI/RHITypes.h
Normal file
106
modules/BgfxRenderer/RHI/RHITypes.h
Normal 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
|
||||
72
modules/BgfxRenderer/RenderGraph/RenderGraph.cpp
Normal file
72
modules/BgfxRenderer/RenderGraph/RenderGraph.cpp
Normal 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
|
||||
48
modules/BgfxRenderer/RenderGraph/RenderGraph.h
Normal file
48
modules/BgfxRenderer/RenderGraph/RenderGraph.h
Normal 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
|
||||
40
modules/BgfxRenderer/RenderGraph/RenderPass.h
Normal file
40
modules/BgfxRenderer/RenderGraph/RenderPass.h
Normal 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
|
||||
122
modules/BgfxRenderer/Resources/ResourceCache.cpp
Normal file
122
modules/BgfxRenderer/Resources/ResourceCache.cpp
Normal 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
|
||||
47
modules/BgfxRenderer/Resources/ResourceCache.h
Normal file
47
modules/BgfxRenderer/Resources/ResourceCache.h
Normal 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
|
||||
298
modules/BgfxRenderer/Scene/SceneCollector.cpp
Normal file
298
modules/BgfxRenderer/Scene/SceneCollector.cpp
Normal 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
|
||||
64
modules/BgfxRenderer/Scene/SceneCollector.h
Normal file
64
modules/BgfxRenderer/Scene/SceneCollector.h
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user