# 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 : ```cpp 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 ```cpp // RHI/RHITypes.h #pragma once #include 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 ```cpp // RHI/RHIDevice.h #pragma once #include "RHITypes.h" #include #include 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 create(); }; } // namespace grove::rhi ``` ### 1.3 Command Buffer (MT-safe) ```cpp // RHI/RHICommandBuffer.h #pragma once #include "RHITypes.h" #include #include 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& getCommands() const { return commands_; } void clear() { commands_.clear(); } size_t size() const { return commands_.size(); } private: std::vector commands_; }; } // namespace grove::rhi ``` ### 1.4 Frame Allocator (lock-free) ```cpp // Frame/FrameAllocator.h #pragma once #include #include #include #include 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 T* allocate(Args&&... args) { void* ptr = allocate(sizeof(T), alignof(T)); if (!ptr) return nullptr; return new (ptr) T(std::forward(args)...); } // Allocation array template 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(ptr) + i) T(); } return static_cast(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 offset_; }; } // namespace grove ``` ### 1.5 Frame Packet (données immuables) ```cpp // Frame/FramePacket.h #pragma once #include "FrameAllocator.h" #include #include 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 sprites; std::span tilemaps; std::span texts; std::span 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 ```cpp // TaskGraph/Task.h #pragma once #include #include #include 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; struct TaskDesc { std::string name; TaskFunc func; std::vector dependencies; }; } // namespace grove ``` ### 2.2 Task Graph ```cpp // TaskGraph/TaskGraph.h #pragma once #include "Task.h" #include #include 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& getTasks() const { return tasks_; } const std::vector& getExecutionOrder() const { return executionOrder_; } private: std::vector tasks_; std::vector executionOrder_; bool compiled_ = false; }; } // namespace grove ``` ### 2.3 Task Scheduler Interface ```cpp // TaskGraph/ITaskScheduler.h #pragma once #include "Task.h" #include #include 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 execute( const std::vector& tasks, const std::vector& order, const FramePacket& frame ) = 0; }; // Implémentation single-thread (Phase 1) class SingleThreadScheduler : public ITaskScheduler { public: std::vector execute( const std::vector& tasks, const std::vector& 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 ```cpp // RenderGraph/RenderPass.h #pragma once #include "../RHI/RHICommandBuffer.h" #include "../Frame/FramePacket.h" #include #include 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 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 ```cpp // RenderGraph/RenderGraph.h #pragma once #include "RenderPass.h" #include "../TaskGraph/TaskGraph.h" #include #include namespace grove { class RenderGraph { public: RenderGraph() = default; // Enregistrement des passes void addPass(std::unique_ptr 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> passes_; std::vector sortedIndices_; bool compiled_ = false; }; } // namespace grove ``` ### 3.3 Passes concrètes #### ClearPass ```cpp // 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 ```cpp // 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 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. ```cpp // Scene/SceneCollector.h #pragma once #include "../Frame/FramePacket.h" #include "grove/IIO.h" #include 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 sprites_; std::vector tilemaps_; std::vector texts_; std::vector 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) ```cpp // Dans un GameModule qui veut dessiner un sprite void GameModule::process(const IDataNode& input) { // Créer le message sprite auto sprite = std::make_unique("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 ```cpp // Resources/ResourceCache.h #pragma once #include "../RHI/RHITypes.h" #include #include #include 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 textures_; std::unordered_map shaders_; mutable std::shared_mutex mutex_; }; } // namespace grove ``` --- ## Phase 6 : Module Principal ### 6.1 BgfxRendererModule ```cpp // 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 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(config.getInt("windowWidth", 1280)); m_height = static_cast(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( static_cast(config.getInt("nativeWindowHandle", 0)) ); // Initialiser les sous-systèmes m_frameAllocator = std::make_unique(allocatorSize); m_device = rhi::IRHIDevice::create(); m_device->init(windowHandle, m_width, m_height); m_renderGraph = std::make_unique(); m_renderGraph->setup(*m_device); m_sceneCollector = std::make_unique(); m_sceneCollector->setup(io); // Subscribe à render:* m_resourceCache = std::make_unique(); 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(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 getState() override { // État minimal pour hot-reload (le renderer est stateless côté gameplay) auto state = std::make_unique("state"); state->setInt("frameCount", static_cast(m_frameCount)); // Les resources GPU sont recréées au reload return state; } void setState(const IDataNode& state) override { m_frameCount = static_cast(state.getInt("frameCount", 0)); m_logger->info("State restored: frameCount={}", m_frameCount); } const IDataNode& getConfiguration() override { if (!m_configCache) { m_configCache = std::make_unique("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 getHealthStatus() override { auto health = std::make_unique("health"); health->setString("status", "running"); health->setInt("frameCount", static_cast(m_frameCount)); health->setInt("allocatorUsedBytes", static_cast(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 m_logger; // Core systems std::unique_ptr m_device; std::unique_ptr m_frameAllocator; std::unique_ptr m_renderGraph; std::unique_ptr m_sceneCollector; std::unique_ptr 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 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 ```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 - [x] Structure fichiers/dossiers - [x] CMakeLists.txt avec fetch bgfx - [x] RHITypes.h complet - [x] RHIDevice interface + BgfxDevice stub - [x] FrameAllocator - [x] Module qui compile et se charge ### Phase 2 : RHI bgfx ✅ DONE - [x] BgfxDevice::init/shutdown/frame - [x] Création textures/buffers/shaders - [x] RHICommandBuffer execution - [x] Test: triangle qui s'affiche (test_21_bgfx_triangle) ### Phase 3 : Render Graph + Passes ✅ DONE - [x] RenderGraph avec tri topologique (Kahn's algorithm) - [x] ClearPass, SpritePass, DebugPass - [x] Compilation et exécution des passes - [x] Embedded shaders (vs_color.bin.h, fs_color.bin.h) ### Phase 4 : ShaderManager + Intégration ✅ DONE - [x] ShaderManager refactorisé pour RHI (plus de bgfx:: exposé) - [x] Injection des shaders via constructeurs des passes (Option E) - [x] SpritePass::execute avec update instance buffer - [x] RenderPass::execute prend IRHIDevice& pour updates dynamiques - [x] 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 - [x] test_22_bgfx_sprites_headless.cpp (TU structures de données) - [x] 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 ```cmake # 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 1. **Shaders** : Pré-compilés ou compilation runtime via shaderc ? 2. **Font rendering** : stb_truetype ou bibliothèque dédiée ? 3. **Texture formats** : Support DDS/KTX ou juste PNG/JPG via stb_image ? 4. **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.