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/
|
||||||
|
build-*/
|
||||||
|
cmake-build-*/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Logs
|
||||||
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)
|
add_library(GroveEngine::impl ALIAS grove_impl)
|
||||||
endif()
|
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
|
# Testing
|
||||||
option(GROVE_BUILD_TESTS "Build GroveEngine tests" ON)
|
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