Phase 7 - Text Rendering: - Add BitmapFont with embedded 8x8 CP437 font (ASCII 32-126) - Add TextPass for instanced glyph rendering - Fix SceneCollector::parseText() to copy strings to FrameAllocator Phase 8A - Multi-texture Support: - Add numeric texture ID system in ResourceCache - SpritePass sorts by textureId and batches per texture - Flush batch on texture change for efficient rendering Phase 8B - Tilemap Rendering: - Add TilemapPass for grid-based tile rendering - Support tileData as comma-separated string - Tiles rendered as instanced quads Window Resize: - Handle window resize via process() input - Call bgfx::reset() on size change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
268 lines
9.9 KiB
C++
268 lines
9.9 KiB
C++
#include "BgfxRendererModule.h"
|
|
#include "RHI/RHIDevice.h"
|
|
#include "Frame/FrameAllocator.h"
|
|
#include "Frame/FramePacket.h"
|
|
#include "Shaders/ShaderManager.h"
|
|
#include "RenderGraph/RenderGraph.h"
|
|
#include "Scene/SceneCollector.h"
|
|
#include "Resources/ResourceCache.h"
|
|
#include "Debug/DebugOverlay.h"
|
|
#include "Passes/ClearPass.h"
|
|
#include "Passes/TilemapPass.h"
|
|
#include "Passes/SpritePass.h"
|
|
#include "Passes/TextPass.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)
|
|
// Use double to preserve 64-bit pointer values
|
|
void* windowHandle = reinterpret_cast<void*>(
|
|
static_cast<uintptr_t>(config.getDouble("nativeWindowHandle", 0.0))
|
|
);
|
|
// Display handle (X11 Display* on Linux, 0/nullptr on Windows)
|
|
void* displayHandle = reinterpret_cast<void*>(
|
|
static_cast<uintptr_t>(config.getDouble("nativeDisplayHandle", 0.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, displayHandle, 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);
|
|
|
|
// Initialize shader manager
|
|
m_shaderManager = std::make_unique<ShaderManager>();
|
|
m_shaderManager->init(*m_device, caps.rendererName);
|
|
m_logger->info("ShaderManager initialized with {} programs", m_shaderManager->getProgramCount());
|
|
|
|
// Get shader handles for passes
|
|
rhi::ShaderHandle spriteShader = m_shaderManager->getProgram("sprite");
|
|
rhi::ShaderHandle debugShader = m_shaderManager->getProgram("debug");
|
|
|
|
if (!spriteShader.isValid()) {
|
|
m_logger->error("Failed to load sprite shader");
|
|
return;
|
|
}
|
|
|
|
// Setup render graph with passes (inject shaders via constructors)
|
|
m_logger->info("Creating RenderGraph...");
|
|
m_renderGraph = std::make_unique<RenderGraph>();
|
|
m_renderGraph->addPass(std::make_unique<ClearPass>());
|
|
m_logger->info("Added ClearPass");
|
|
|
|
// Setup resource cache first (needed by passes)
|
|
m_resourceCache = std::make_unique<ResourceCache>();
|
|
|
|
// Create TilemapPass (renders before sprites)
|
|
auto tilemapPass = std::make_unique<TilemapPass>(spriteShader);
|
|
tilemapPass->setResourceCache(m_resourceCache.get());
|
|
m_renderGraph->addPass(std::move(tilemapPass));
|
|
m_logger->info("Added TilemapPass");
|
|
|
|
// Create SpritePass and keep reference for texture binding
|
|
auto spritePass = std::make_unique<SpritePass>(spriteShader);
|
|
m_spritePass = spritePass.get(); // Non-owning reference
|
|
m_spritePass->setResourceCache(m_resourceCache.get());
|
|
m_renderGraph->addPass(std::move(spritePass));
|
|
m_logger->info("Added SpritePass");
|
|
|
|
// Create TextPass (uses sprite shader for glyph quads)
|
|
m_renderGraph->addPass(std::make_unique<TextPass>(spriteShader));
|
|
m_logger->info("Added TextPass");
|
|
|
|
m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
|
|
m_logger->info("Added DebugPass");
|
|
m_renderGraph->setup(*m_device);
|
|
m_logger->info("RenderGraph setup complete");
|
|
m_renderGraph->compile();
|
|
m_logger->info("RenderGraph compiled");
|
|
|
|
// Setup scene collector with IIO subscriptions
|
|
m_sceneCollector = std::make_unique<SceneCollector>();
|
|
m_sceneCollector->setup(io);
|
|
m_logger->info("SceneCollector setup complete");
|
|
|
|
// Setup debug overlay
|
|
m_debugOverlay = std::make_unique<DebugOverlay>();
|
|
bool debugEnabled = config.getBool("debugOverlay", false);
|
|
m_debugOverlay->setEnabled(debugEnabled);
|
|
if (debugEnabled) {
|
|
m_logger->info("Debug overlay enabled");
|
|
}
|
|
|
|
// Load default texture if specified in config
|
|
std::string defaultTexturePath = config.getString("defaultTexture", "");
|
|
if (!defaultTexturePath.empty()) {
|
|
rhi::TextureHandle tex = m_resourceCache->loadTexture(*m_device, defaultTexturePath);
|
|
if (tex.isValid()) {
|
|
m_spritePass->setTexture(tex);
|
|
m_logger->info("Loaded default texture: {}", defaultTexturePath);
|
|
} else {
|
|
m_logger->warn("Failed to load default texture: {}", defaultTexturePath);
|
|
}
|
|
}
|
|
|
|
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));
|
|
|
|
// Check for resize in input
|
|
int newWidth = input.getInt("windowWidth", 0);
|
|
int newHeight = input.getInt("windowHeight", 0);
|
|
if (newWidth > 0 && newHeight > 0 &&
|
|
(static_cast<uint16_t>(newWidth) != m_width || static_cast<uint16_t>(newHeight) != m_height)) {
|
|
m_width = static_cast<uint16_t>(newWidth);
|
|
m_height = static_cast<uint16_t>(newHeight);
|
|
m_device->reset(m_width, m_height);
|
|
m_logger->info("Window resized to {}x{}", m_width, m_height);
|
|
}
|
|
|
|
// 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. Update and render debug overlay
|
|
if (m_debugOverlay) {
|
|
m_debugOverlay->update(deltaTime, static_cast<uint32_t>(frame.spriteCount), 1);
|
|
m_debugOverlay->render(m_width, m_height);
|
|
}
|
|
|
|
// 6. Present
|
|
m_device->frame();
|
|
|
|
// 7. 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_device) {
|
|
m_renderGraph->shutdown(*m_device);
|
|
}
|
|
|
|
if (m_resourceCache && m_device) {
|
|
m_resourceCache->clear(*m_device);
|
|
}
|
|
|
|
if (m_shaderManager && m_device) {
|
|
m_shaderManager->shutdown(*m_device);
|
|
}
|
|
|
|
if (m_device) {
|
|
m_device->shutdown();
|
|
}
|
|
|
|
m_renderGraph.reset();
|
|
m_resourceCache.reset();
|
|
m_shaderManager.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;
|
|
}
|
|
|
|
}
|