- Refactor ShaderManager to use RHI abstraction (no bgfx:: exposed) - Implement Option E: inject ShaderHandle via pass constructors - SpritePass/DebugPass now receive shader in constructor - RenderPass::execute() takes IRHIDevice& for dynamic buffer updates - SpritePass::execute() updates instance buffer from FramePacket - Integrate ShaderManager lifecycle in BgfxRendererModule - Add test_22_bgfx_sprites.cpp (visual test with SDL2) - Add test_22_bgfx_sprites_headless.cpp (headless data structure test) - Update PLAN_BGFX_RENDERER.md with Phase 4 completion and Phase 6.5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
38 KiB
38 KiB
Plan d'implémentation - BgfxRenderer Module
Vue d'ensemble
Module de rendu 2D basé sur bgfx, intégré à GroveEngine comme IModule.
Architecture task-based, MT-ready dès le départ.
Intégration GroveEngine
Le module suit l'architecture GroveEngine standard :
| Aspect | Mécanisme | Usage |
|---|---|---|
| Configuration | IDataNode via setConfiguration() |
Window size, backend, paths, vsync |
| Communication | IIO pub/sub |
Sprites, camera, clear color par frame |
| State | getState()/setState() |
Hot-reload du module renderer |
| Input frame | process(const IDataNode& input) |
deltaTime, frameCount |
Séparation Config vs Messages
Via setConfiguration(const IDataNode& config, IIO* io, ...) - Paramètres statiques :
config.getInt("windowWidth", 1280);
config.getInt("windowHeight", 720);
config.getString("backend", "opengl"); // opengl, vulkan, dx11, metal
config.getString("shaderPath", "./shaders");
config.getBool("vsync", true);
config.getInt("maxSpritesPerBatch", 10000);
config.getInt("frameAllocatorSizeMB", 16);
// Window handle: config.getInt("nativeWindowHandle", 0) ou via WindowModule
Via IIO subscribe/pull dans process() - Données temps réel par frame :
render:sprite → SpriteInstance à dessiner
render:sprite:batch → Batch de sprites (array)
render:tilemap → TilemapChunk
render:text → TextCommand
render:particle → ParticleInstance
render:camera → ViewInfo (position, zoom, viewport)
render:clear → Clear color RGBA
render:debug:line → Debug line
render:debug:rect → Debug rectangle
window:resize → Event resize (si WindowModule séparé)
Architecture globale
modules/
└── BgfxRenderer/
├── BgfxRendererModule.cpp # Point d'entrée IModule
│
├── RHI/ # Render Hardware Interface
│ ├── RHITypes.h # Handles typés, enums
│ ├── RHIDevice.h # Interface device abstraite
│ ├── RHICommandBuffer.h # Command recording (MT-safe)
│ ├── RHIResources.h # Buffer, Texture, Shader descriptors
│ └── BgfxDevice.cpp # Implémentation bgfx
│
├── Frame/ # Gestion multi-frame MT-ready
│ ├── FramePacket.h # Données immuables d'une frame
│ ├── FrameAllocator.h # Allocateur linéaire lock-free
│ └── FrameSync.h # Double/triple buffering
│
├── TaskGraph/ # Système de tâches
│ ├── Task.h # Unité de travail
│ ├── TaskGraph.h # DAG de tâches
│ └── ITaskScheduler.h # Interface scheduler (single/multi)
│
├── RenderGraph/ # Organisation des passes
│ ├── RenderGraph.h # Compilation et exécution passes
│ ├── RenderPass.h # Interface pass abstraite
│ └── PassResources.h # Gestion ressources par pass
│
├── Passes/ # Passes concrètes 2D
│ ├── ClearPass.cpp # Clear du framebuffer
│ ├── SpritePass.cpp # Sprites + batching
│ ├── TilemapPass.cpp # Tilemaps
│ ├── TextPass.cpp # Rendu texte
│ ├── ParticlePass.cpp # Systèmes de particules
│ └── DebugPass.cpp # Debug shapes/lines
│
├── Resources/ # Gestion assets GPU
│ ├── ResourceCache.h # Cache unifié
│ ├── TextureManager.h # Chargement/gestion textures
│ ├── ShaderManager.h # Compilation/cache shaders
│ └── FontManager.h # Fonts + atlas
│
├── Scene/ # Collecte des renderables
│ ├── SceneCollector.h # Collecte depuis IIO
│ ├── SceneProxy.h # Vue immuable pour rendu
│ ├── RenderBatch.h # Batching par texture/shader
│ └── SortKey.h # Tri par layer/depth
│
└── Shaders/ # Sources shaders
├── sprite.sc # Vertex/fragment sprites
├── tilemap.sc # Tilemaps
├── text.sc # Text rendering
└── varying.def.sc # Définitions communes
Phase 1 : Fondations (RHI + Frame)
1.1 RHI Types et Handles
// RHI/RHITypes.h
#pragma once
#include <cstdint>
namespace grove::rhi {
// Handles typés - jamais de bgfx:: exposé hors de BgfxDevice
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 layouts
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] = {};
};
// Descriptors pour création
struct TextureDesc {
uint16_t width = 0;
uint16_t height = 0;
uint8_t mipLevels = 1;
enum Format { 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 { 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
1.2 RHI Device Interface
// RHI/RHIDevice.h
#pragma once
#include "RHITypes.h"
#include <memory>
#include <string>
namespace grove::rhi {
struct DeviceCapabilities {
uint16_t maxTextureSize;
uint16_t maxViews;
uint32_t maxDrawCalls;
bool instancingSupported;
bool computeSupported;
std::string rendererName;
std::string gpuName;
};
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
1.3 Command Buffer (MT-safe)
// RHI/RHICommandBuffer.h
#pragma once
#include "RHITypes.h"
#include <vector>
#include <cstring>
namespace grove::rhi {
// Commandes encodées - POD pour sérialisation
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 - un par thread, write-only pendant l'enregistrement
class RHICommandBuffer {
public:
RHICommandBuffer() = default;
// Non-copiable, movable
RHICommandBuffer(const RHICommandBuffer&) = delete;
RHICommandBuffer& operator=(const RHICommandBuffer&) = delete;
RHICommandBuffer(RHICommandBuffer&&) = default;
RHICommandBuffer& operator=(RHICommandBuffer&&) = default;
// Enregistrement des commandes
void setState(const RenderState& state) {
Command cmd; cmd.type = CommandType::SetState;
cmd.setState.state = state;
commands_.push_back(cmd);
}
void setTexture(uint8_t slot, TextureHandle tex, UniformHandle sampler) {
Command cmd; cmd.type = CommandType::SetTexture;
cmd.setTexture = {slot, tex, sampler};
commands_.push_back(cmd);
}
void 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);
commands_.push_back(cmd);
}
void setVertexBuffer(BufferHandle buffer, uint32_t offset = 0) {
Command cmd; cmd.type = CommandType::SetVertexBuffer;
cmd.setVertexBuffer = {buffer, offset};
commands_.push_back(cmd);
}
void setIndexBuffer(BufferHandle buffer, uint32_t offset = 0, bool is32Bit = false) {
Command cmd; cmd.type = CommandType::SetIndexBuffer;
cmd.setIndexBuffer = {buffer, offset, is32Bit};
commands_.push_back(cmd);
}
void setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count) {
Command cmd; cmd.type = CommandType::SetInstanceBuffer;
cmd.setInstanceBuffer = {buffer, start, count};
commands_.push_back(cmd);
}
void setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
Command cmd; cmd.type = CommandType::SetScissor;
cmd.setScissor = {x, y, w, h};
commands_.push_back(cmd);
}
void draw(uint32_t vertexCount, uint32_t startVertex = 0) {
Command cmd; cmd.type = CommandType::Draw;
cmd.draw = {vertexCount, startVertex};
commands_.push_back(cmd);
}
void drawIndexed(uint32_t indexCount, uint32_t startIndex = 0) {
Command cmd; cmd.type = CommandType::DrawIndexed;
cmd.drawIndexed = {indexCount, startIndex};
commands_.push_back(cmd);
}
void drawInstanced(uint32_t indexCount, uint32_t instanceCount) {
Command cmd; cmd.type = CommandType::DrawInstanced;
cmd.drawInstanced = {indexCount, instanceCount};
commands_.push_back(cmd);
}
void submit(ViewId view, ShaderHandle shader, uint32_t depth = 0) {
Command cmd; cmd.type = CommandType::Submit;
cmd.submit = {view, shader, depth};
commands_.push_back(cmd);
}
// Accès lecture seule pour exécution
const std::vector<Command>& getCommands() const { return commands_; }
void clear() { commands_.clear(); }
size_t size() const { return commands_.size(); }
private:
std::vector<Command> commands_;
};
} // namespace grove::rhi
1.4 Frame Allocator (lock-free)
// Frame/FrameAllocator.h
#pragma once
#include <atomic>
#include <cstdint>
#include <cstddef>
#include <cassert>
namespace grove {
// Allocateur linéaire lock-free, reset chaque frame
class FrameAllocator {
public:
static constexpr size_t DEFAULT_SIZE = 16 * 1024 * 1024; // 16 MB
explicit FrameAllocator(size_t size = DEFAULT_SIZE)
: buffer_(new uint8_t[size]), capacity_(size), offset_(0) {}
~FrameAllocator() { delete[] buffer_; }
// Non-copiable
FrameAllocator(const FrameAllocator&) = delete;
FrameAllocator& operator=(const FrameAllocator&) = delete;
// Allocation thread-safe, lock-free
void* allocate(size_t size, size_t alignment = 16) {
size_t current = offset_.load(std::memory_order_relaxed);
size_t aligned;
do {
aligned = (current + alignment - 1) & ~(alignment - 1);
if (aligned + size > capacity_) {
return nullptr; // Out of memory
}
} while (!offset_.compare_exchange_weak(
current, aligned + size,
std::memory_order_release,
std::memory_order_relaxed));
return buffer_ + aligned;
}
// Allocation typée
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)...);
}
// Allocation array
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 (appelé une fois par frame, single-thread)
void reset() { offset_.store(0, std::memory_order_release); }
// Stats
size_t getUsed() const { return offset_.load(std::memory_order_acquire); }
size_t getCapacity() const { return capacity_; }
private:
uint8_t* buffer_;
size_t capacity_;
std::atomic<size_t> offset_;
};
} // namespace grove
1.5 Frame Packet (données immuables)
// Frame/FramePacket.h
#pragma once
#include "FrameAllocator.h"
#include <cstdint>
#include <span>
namespace grove {
// Données sprite pour le rendu
struct SpriteInstance {
float x, y; // Position
float scaleX, scaleY; // Scale
float rotation; // Radians
float u0, v0, u1, v1; // UVs dans atlas
uint32_t color; // RGBA packed
uint16_t textureId; // Index dans texture array
uint16_t layer; // Z-order
};
// Données tilemap chunk
struct TilemapChunk {
float x, y; // Position du chunk
uint16_t width, height;
uint16_t tileWidth, tileHeight;
uint16_t textureId;
const uint16_t* tiles; // Indices dans tileset
};
// Données texte
struct TextCommand {
float x, y;
const char* text; // Null-terminated, alloué dans FrameAllocator
uint16_t fontId;
uint16_t fontSize;
uint32_t color;
uint16_t layer;
};
// Données particule
struct ParticleInstance {
float x, y;
float vx, vy;
float size;
float life; // 0-1, temps restant
uint32_t color;
uint16_t textureId;
};
// Vue caméra
struct ViewInfo {
float viewMatrix[16];
float projMatrix[16];
float position[2];
float zoom;
uint16_t viewportX, viewportY;
uint16_t viewportW, viewportH;
};
// Packet complet d'une frame - IMMUABLE après construction
struct FramePacket {
uint64_t frameNumber;
float deltaTime;
// Données collectées (read-only pour les passes)
std::span<const SpriteInstance> sprites;
std::span<const TilemapChunk> tilemaps;
std::span<const TextCommand> texts;
std::span<const ParticleInstance> particles;
// Vue principale
ViewInfo mainView;
// Clear color
uint32_t clearColor;
// Allocateur pour données temporaires des passes
FrameAllocator* allocator;
};
} // namespace grove
Phase 2 : Task Graph
2.1 Task Interface
// TaskGraph/Task.h
#pragma once
#include <functional>
#include <string>
#include <cstdint>
namespace grove {
class RHICommandBuffer;
struct FramePacket;
using TaskId = uint32_t;
constexpr TaskId INVALID_TASK_ID = UINT32_MAX;
// Contexte passé à chaque task
struct TaskContext {
const FramePacket* frame;
rhi::RHICommandBuffer* commandBuffer; // Thread-local
uint32_t threadIndex;
};
using TaskFunc = std::function<void(TaskContext&)>;
struct TaskDesc {
std::string name;
TaskFunc func;
std::vector<TaskId> dependencies;
};
} // namespace grove
2.2 Task Graph
// TaskGraph/TaskGraph.h
#pragma once
#include "Task.h"
#include <vector>
#include <memory>
namespace grove {
class ITaskScheduler;
class TaskGraph {
public:
TaskGraph() = default;
// Construction du graph
TaskId addTask(const std::string& name, TaskFunc func);
void addDependency(TaskId before, TaskId after);
// Compilation (tri topologique, détection cycles)
bool compile();
// Exécution
void execute(ITaskScheduler& scheduler, const FramePacket& frame);
// Reset pour prochaine frame
void clear();
// Debug
const std::vector<TaskDesc>& getTasks() const { return tasks_; }
const std::vector<TaskId>& getExecutionOrder() const { return executionOrder_; }
private:
std::vector<TaskDesc> tasks_;
std::vector<TaskId> executionOrder_;
bool compiled_ = false;
};
} // namespace grove
2.3 Task Scheduler Interface
// TaskGraph/ITaskScheduler.h
#pragma once
#include "Task.h"
#include <vector>
#include <memory>
namespace grove {
namespace rhi { class RHICommandBuffer; }
class ITaskScheduler {
public:
virtual ~ITaskScheduler() = default;
// Exécute les tasks et retourne les command buffers générés
virtual std::vector<rhi::RHICommandBuffer> execute(
const std::vector<TaskDesc>& tasks,
const std::vector<TaskId>& order,
const FramePacket& frame
) = 0;
};
// Implémentation single-thread (Phase 1)
class SingleThreadScheduler : public ITaskScheduler {
public:
std::vector<rhi::RHICommandBuffer> execute(
const std::vector<TaskDesc>& tasks,
const std::vector<TaskId>& order,
const FramePacket& frame
) override;
};
// Implémentation multi-thread (Future)
// class ThreadPoolScheduler : public ITaskScheduler { ... };
} // namespace grove
Phase 3 : Render Graph & Passes
3.1 Render Pass Interface
// RenderGraph/RenderPass.h
#pragma once
#include "../RHI/RHICommandBuffer.h"
#include "../Frame/FramePacket.h"
#include <string>
#include <vector>
namespace grove {
class RenderPass {
public:
virtual ~RenderPass() = default;
// Identifiant unique
virtual const char* getName() const = 0;
// Ordre de rendu (plus petit = plus tôt)
virtual uint32_t getSortOrder() const = 0;
// Dépendances (noms des passes qui doivent s'exécuter avant)
virtual std::vector<const char*> getDependencies() const { return {}; }
// Exécution - DOIT être thread-safe
// frame: read-only
// cmd: write-only, thread-local
virtual void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) = 0;
// Setup initial (chargement shaders, création buffers)
virtual void setup(rhi::IRHIDevice& device) = 0;
// Cleanup
virtual void shutdown(rhi::IRHIDevice& device) = 0;
};
} // namespace grove
3.2 Render Graph
// RenderGraph/RenderGraph.h
#pragma once
#include "RenderPass.h"
#include "../TaskGraph/TaskGraph.h"
#include <memory>
#include <vector>
namespace grove {
class RenderGraph {
public:
RenderGraph() = default;
// Enregistrement des passes
void addPass(std::unique_ptr<RenderPass> pass);
// Setup toutes les passes
void setup(rhi::IRHIDevice& device);
// Compile le graph (ordre, dépendances → TaskGraph)
void compile();
// Build le TaskGraph pour une frame
void buildTasks(TaskGraph& taskGraph, const FramePacket& frame);
// Shutdown
void shutdown(rhi::IRHIDevice& device);
private:
std::vector<std::unique_ptr<RenderPass>> passes_;
std::vector<size_t> sortedIndices_;
bool compiled_ = false;
};
} // namespace grove
3.3 Passes concrètes
ClearPass
// Passes/ClearPass.h
#pragma once
#include "../RenderGraph/RenderPass.h"
namespace grove {
class ClearPass : public RenderPass {
public:
const char* getName() const override { return "Clear"; }
uint32_t getSortOrder() const override { return 0; } // Premier
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::RHICommandBuffer& cmd) override;
};
} // namespace grove
SpritePass
// Passes/SpritePass.h
#pragma once
#include "../RenderGraph/RenderPass.h"
namespace grove {
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 shader_;
rhi::BufferHandle quadVB_;
rhi::BufferHandle quadIB_;
rhi::BufferHandle instanceBuffer_;
rhi::UniformHandle textureSampler_;
static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000;
};
} // namespace grove
TilemapPass, TextPass, ParticlePass, DebugPass
Structure similaire avec leurs spécificités.
Phase 4 : Scene Collection (via IIO)
4.1 Scene Collector
Collecte les messages IIO pendant process() et construit le FramePacket.
// Scene/SceneCollector.h
#pragma once
#include "../Frame/FramePacket.h"
#include "grove/IIO.h"
#include <vector>
namespace grove {
class SceneCollector {
public:
SceneCollector() = default;
// Configure les subscriptions IIO (appelé dans setConfiguration)
void setup(IIO* io);
// Collecte tous les messages IIO en début de frame (appelé dans process)
// Pull-based: le module contrôle quand il lit les messages
void collect(IIO* io, float deltaTime);
// Génère le FramePacket immuable pour les passes de rendu
FramePacket finalize(FrameAllocator& allocator);
// Reset pour prochaine frame
void clear();
private:
std::vector<SpriteInstance> sprites_;
std::vector<TilemapChunk> tilemaps_;
std::vector<TextCommand> texts_;
std::vector<ParticleInstance> particles_;
ViewInfo mainView_;
uint32_t clearColor_ = 0x303030FF;
uint64_t frameNumber_ = 0;
// Parse les messages IIO en structures internes
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);
};
} // namespace grove
4.2 Topics IIO et Format Messages
Le renderer subscribe à render:* dans setConfiguration().
| Topic | Format IDataNode | Description |
|---|---|---|
render:sprite |
{x, y, scaleX, scaleY, rotation, u0, v0, u1, v1, color, textureId, layer} |
Un sprite |
render:sprite:batch |
{sprites: [{...}, {...}]} |
Batch de sprites |
render:tilemap |
{x, y, width, height, tileW, tileH, textureId, tiles: [...]} |
Chunk tilemap |
render:text |
{x, y, text, fontId, fontSize, color, layer} |
Texte à afficher |
render:particle |
{x, y, vx, vy, size, life, color, textureId} |
Particule |
render:camera |
{x, y, zoom, viewportX, viewportY, viewportW, viewportH} |
Caméra principale |
render:clear |
{color} |
Clear color (RGBA uint32) |
render:debug:line |
{x1, y1, x2, y2, color} |
Ligne debug |
render:debug:rect |
{x, y, w, h, color, filled} |
Rectangle debug |
4.3 Exemple d'usage (autre module publiant vers renderer)
// Dans un GameModule qui veut dessiner un sprite
void GameModule::process(const IDataNode& input) {
// Créer le message sprite
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", m_playerX);
sprite->setDouble("y", m_playerY);
sprite->setDouble("scaleX", 1.0);
sprite->setDouble("scaleY", 1.0);
sprite->setDouble("rotation", m_playerRotation);
sprite->setDouble("u0", 0.0);
sprite->setDouble("v0", 0.0);
sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0);
sprite->setInt("color", 0xFFFFFFFF);
sprite->setInt("textureId", m_playerTextureId);
sprite->setInt("layer", 10);
// Publier vers le renderer
m_io->publish("render:sprite", std::move(sprite));
}
Phase 5 : Resource Management
5.1 Resource Cache
// Resources/ResourceCache.h
#pragma once
#include "../RHI/RHITypes.h"
#include <unordered_map>
#include <string>
#include <mutex>
namespace grove {
class ResourceCache {
public:
// Thread-safe resource access
rhi::TextureHandle getTexture(const std::string& path);
rhi::ShaderHandle getShader(const std::string& name);
// Chargement (appelé depuis main thread)
void loadTexture(rhi::IRHIDevice& device, const std::string& path);
void loadShader(rhi::IRHIDevice& device, const std::string& name,
const void* vsData, uint32_t vsSize,
const void* fsData, uint32_t fsSize);
// Cleanup
void clear(rhi::IRHIDevice& device);
private:
std::unordered_map<std::string, rhi::TextureHandle> textures_;
std::unordered_map<std::string, rhi::ShaderHandle> shaders_;
mutable std::shared_mutex mutex_;
};
} // namespace grove
Phase 6 : Module Principal
6.1 BgfxRendererModule
// BgfxRendererModule.cpp
#include "grove/IModule.h"
#include "grove/IDataNode.h"
#include "grove/IIO.h"
#include "grove/JsonDataNode.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 <spdlog/spdlog.h>
namespace grove {
class BgfxRendererModule : public IModule {
public:
// ========================================
// IModule Interface
// ========================================
void setConfiguration(const IDataNode& config, IIO* io, ITaskScheduler* scheduler) override {
m_io = io;
m_logger = spdlog::get("BgfxRenderer");
if (!m_logger) m_logger = spdlog::stdout_color_mt("BgfxRenderer");
// Lire config statique 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 = config.getInt("frameAllocatorSizeMB", 16) * 1024 * 1024;
// Window handle (passé via config ou 0 si WindowModule séparé)
void* windowHandle = reinterpret_cast<void*>(
static_cast<uintptr_t>(config.getInt("nativeWindowHandle", 0))
);
// Initialiser les sous-systèmes
m_frameAllocator = std::make_unique<FrameAllocator>(allocatorSize);
m_device = rhi::IRHIDevice::create();
m_device->init(windowHandle, m_width, m_height);
m_renderGraph = std::make_unique<RenderGraph>();
m_renderGraph->setup(*m_device);
m_sceneCollector = std::make_unique<SceneCollector>();
m_sceneCollector->setup(io); // Subscribe à render:*
m_resourceCache = std::make_unique<ResourceCache>();
m_logger->info("BgfxRenderer configured: {}x{} backend={}", m_width, m_height, m_backend);
}
void process(const IDataNode& input) override {
// Lire deltaTime depuis input (fourni par le ModuleSystem)
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016));
// 1. Collecter les messages IIO (pull-based)
m_sceneCollector->collect(m_io, deltaTime);
// 2. Construire le FramePacket immuable
m_frameAllocator->reset();
FramePacket frame = m_sceneCollector->finalize(*m_frameAllocator);
// 3. Exécuter le render graph
m_renderGraph->execute(frame, *m_device);
// 4. Present
m_device->frame();
// 5. Cleanup pour prochaine frame
m_sceneCollector->clear();
m_frameCount++;
}
void shutdown() override {
m_logger->info("BgfxRenderer shutting down, {} frames rendered", m_frameCount);
m_renderGraph->shutdown(*m_device);
m_resourceCache->clear(*m_device);
m_device->shutdown();
}
std::unique_ptr<IDataNode> getState() override {
// État minimal pour hot-reload (le renderer est stateless côté gameplay)
auto state = std::make_unique<JsonDataNode>("state");
state->setInt("frameCount", static_cast<int>(m_frameCount));
// Les resources GPU sont recréées au reload
return state;
}
void setState(const IDataNode& state) override {
m_frameCount = static_cast<uint64_t>(state.getInt("frameCount", 0));
m_logger->info("State restored: frameCount={}", m_frameCount);
}
const IDataNode& getConfiguration() override {
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);
}
return *m_configCache;
}
std::unique_ptr<IDataNode> getHealthStatus() override {
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->getUsed()));
return health;
}
std::string getType() const override { return "bgfx_renderer"; }
bool isIdle() const override { return true; } // Toujours safe pour hot-reload
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 (depuis 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() {
return new grove::BgfxRendererModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}
6.2 Exemple de configuration JSON
{
"windowWidth": 1920,
"windowHeight": 1080,
"backend": "vulkan",
"shaderPath": "./assets/shaders",
"vsync": true,
"maxSpritesPerBatch": 20000,
"frameAllocatorSizeMB": 32,
"nativeWindowHandle": 0
}
Phases d'implémentation
Phase 1 : Squelette ✅ DONE
- Structure fichiers/dossiers
- CMakeLists.txt avec fetch bgfx
- RHITypes.h complet
- RHIDevice interface + BgfxDevice stub
- FrameAllocator
- Module qui compile et se charge
Phase 2 : RHI bgfx ✅ DONE
- BgfxDevice::init/shutdown/frame
- Création textures/buffers/shaders
- RHICommandBuffer execution
- Test: triangle qui s'affiche (test_21_bgfx_triangle)
Phase 3 : Render Graph + Passes ✅ DONE
- RenderGraph avec tri topologique (Kahn's algorithm)
- ClearPass, SpritePass, DebugPass
- Compilation et exécution des passes
- Embedded shaders (vs_color.bin.h, fs_color.bin.h)
Phase 4 : ShaderManager + Intégration ✅ DONE
- ShaderManager refactorisé pour RHI (plus de bgfx:: exposé)
- Injection des shaders via constructeurs des passes (Option E)
- SpritePass::execute avec update instance buffer
- RenderPass::execute prend IRHIDevice& pour updates dynamiques
- Intégration complète dans BgfxRendererModule
Phase 5 : Scene Collection + IIO
- SceneCollector collect() implémentation complète
- Parsing des messages IIO (parseSprite, parseCamera, etc.)
- FramePacket generation depuis données collectées
- Test: sprites via messages IIO end-to-end
Phase 6 : Resource Management
- ResourceCache thread-safe
- TextureManager (chargement async)
- Integration avec SpritePass (textureId → TextureHandle)
Phase 6.5 : Tests Unitaires et Tests d'Intégration
- test_22_bgfx_sprites_headless.cpp (TU structures de données)
- test_22_bgfx_sprites.cpp (TI visuel avec SDL2 - nécessite platform data fix)
- TU ShaderManager (init, getProgram, shutdown)
- TU RenderGraph (addPass, compile, execute order)
- TU FrameAllocator (allocate, reset, overflow)
- TU RHICommandBuffer (recording, getCommands)
- TI SceneCollector (collect depuis IIO mock)
- TI Pipeline complet headless (mock device)
Phase 7 : Passes additionnelles
- TilemapPass
- TextPass (+ font loading avec stb_truetype)
- ParticlePass
- DebugPass lignes/rectangles complets
Phase 8 : Polish
- Resource hot-reload
- State save/restore pour module hot-reload
- Stats/profiling (draw calls, batches, memory)
- Documentation API
Règles strictes
| 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 |
| Dépendances passes explicites | TaskGraph peut paralléliser |
| State serializable | Hot-reload du module |
Dépendances externes
# CMakeLists.txt
FetchContent_Declare(
bgfx
GIT_REPOSITORY https://github.com/bkaradzic/bgfx.cmake.git
GIT_TAG v1.127.8710-464
)
FetchContent_MakeAvailable(bgfx)
# Pour le windowing (optionnel, peut être externe)
find_package(SDL2 REQUIRED)
Décisions d'architecture
| Question | Décision | Raison |
|---|---|---|
| Windowing | Module séparé WindowModule |
Découplage propre, le renderer reçoit le handle via config |
| Config vs Messages | Config = IDataNode, Runtime = IIO |
Aligné avec architecture GroveEngine |
| Window handle | Via config.getInt("nativeWindowHandle") |
Fourni par WindowModule ou application |
| Resize events | Via IIO window:resize |
Event dynamique = message |
Questions ouvertes
- Shaders : Pré-compilés ou compilation runtime via shaderc ?
- Font rendering : stb_truetype ou bibliothèque dédiée ?
- Texture formats : Support DDS/KTX ou juste PNG/JPG via stb_image ?
- WindowModule : Qui le développe ? Dépendance SDL2 ou autre ?
État d'avancement
| Phase | État | Description |
|---|---|---|
| Phase 1 | ✅ DONE | Squelette du module |
| Phase 2 | ✅ DONE | RHI bgfx + triangle test |
| Phase 3 | ✅ DONE | RenderGraph + Passes |
| Phase 4 | ✅ DONE | ShaderManager + Intégration |
| Phase 5 | ⏳ TODO | Scene Collection + IIO |
| Phase 6 | ⏳ TODO | Resource Management |
| Phase 6.5 | ⏳ TODO | Tests Unitaires et Tests d'Intégration |
| Phase 7 | ⏳ TODO | Passes additionnelles |
| Phase 8 | ⏳ TODO | Polish |
Prochaine étape : Phase 5 - Implémenter SceneCollector pour collecter les sprites via IIO.