diff --git a/include/grove/IIO.h b/include/grove/IIO.h index 94d43d5..1210c32 100644 --- a/include/grove/IIO.h +++ b/include/grove/IIO.h @@ -26,6 +26,17 @@ struct Message { std::string topic; std::unique_ptr data; uint64_t timestamp; + + // Default constructor + Message() = default; + + // Move constructor and assignment (unique_ptr is move-only) + Message(Message&&) = default; + Message& operator=(Message&&) = default; + + // Delete copy (unique_ptr cannot be copied) + Message(const Message&) = delete; + Message& operator=(const Message&) = delete; }; struct IOHealth { diff --git a/include/grove/IntraIO.h b/include/grove/IntraIO.h index e2f74e9..cc0b99c 100644 --- a/include/grove/IntraIO.h +++ b/include/grove/IntraIO.h @@ -70,6 +70,15 @@ private: std::chrono::high_resolution_clock::time_point lastBatch; std::unordered_map batchedMessages; // For replaceable messages std::vector accumulatedMessages; // For non-replaceable messages + + // Default constructor + Subscription() = default; + + // Move-only (Message contains unique_ptr) + Subscription(Subscription&&) = default; + Subscription& operator=(Subscription&&) = default; + Subscription(const Subscription&) = delete; + Subscription& operator=(const Subscription&) = delete; }; std::vector highFreqSubscriptions; diff --git a/modules/BgfxRenderer/BgfxRendererModule.cpp b/modules/BgfxRenderer/BgfxRendererModule.cpp index 49bff50..f3c9bd4 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.cpp +++ b/modules/BgfxRenderer/BgfxRendererModule.cpp @@ -1,320 +1,309 @@ -#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/ParticlePass.h" -#include "Passes/DebugPass.h" - -#include -#include -#include - -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(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 = static_cast(config.getInt("frameAllocatorSizeMB", 16)) * 1024 * 1024; - - // Window handle (passed via config or 0 if separate WindowModule) - // Use double to preserve 64-bit pointer values - // Also try getInt as fallback for compatibility with older code that uses setInt - void* windowHandle = nullptr; - double handleDouble = config.getDouble("nativeWindowHandle", 0.0); - if (handleDouble != 0.0) { - windowHandle = reinterpret_cast(static_cast(handleDouble)); - } else { - // Fallback: try reading as int (for 32-bit handles or compatibility) - int handleInt = config.getInt("nativeWindowHandle", 0); - if (handleInt != 0) { - windowHandle = reinterpret_cast(static_cast(static_cast(handleInt))); - m_logger->warn("nativeWindowHandle passed as int - consider using setDouble for 64-bit handles"); - } - } - - // Display handle (X11 Display* on Linux, 0/nullptr on Windows) - void* displayHandle = nullptr; - double displayDouble = config.getDouble("nativeDisplayHandle", 0.0); - if (displayDouble != 0.0) { - displayHandle = reinterpret_cast(static_cast(displayDouble)); - } - - m_logger->info("Initializing BgfxRenderer: {}x{} backend={}", m_width, m_height, m_backend); - - // Initialize subsystems - m_frameAllocator = std::make_unique(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(); - 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(); - m_renderGraph->addPass(std::make_unique()); - m_logger->info("Added ClearPass"); - - // Setup resource cache first (needed by passes) - m_resourceCache = std::make_unique(); - - // Create TilemapPass (renders before sprites) - auto tilemapPass = std::make_unique(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(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(spriteShader)); - m_logger->info("Added TextPass"); - - // Create ParticlePass (uses sprite shader, renders after sprites with additive blending) - auto particlePass = std::make_unique(spriteShader); - particlePass->setResourceCache(m_resourceCache.get()); - m_renderGraph->addPass(std::move(particlePass)); - m_logger->info("Added ParticlePass"); - - m_renderGraph->addPass(std::make_unique(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 and correct dimensions - m_sceneCollector = std::make_unique(); - m_sceneCollector->setup(io, m_width, m_height); - m_logger->info("SceneCollector setup complete with dimensions {}x{}", m_width, m_height); - - // Setup debug overlay - m_debugOverlay = std::make_unique(); - 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()) { - uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, defaultTexturePath); - if (texId > 0) { - rhi::TextureHandle tex = m_resourceCache->getTextureById(texId); - m_spritePass->setTexture(tex); - m_logger->info("Loaded default texture: {} (id={})", defaultTexturePath, texId); - } else { - m_logger->warn("Failed to load default texture: {}", defaultTexturePath); - } - } - - // Load additional textures (texture1, texture2, etc.) - for (int i = 1; i <= 10; ++i) { - std::string key = "texture" + std::to_string(i); - std::string path = config.getString(key, ""); - if (!path.empty()) { - uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, path); - if (texId > 0) { - m_logger->info("Loaded texture: {} (id={})", path, texId); - } else { - m_logger->warn("Failed to load texture: {}", path); - } - } - } - - m_logger->info("BgfxRenderer initialized successfully"); -} - -void BgfxRendererModule::process(const IDataNode& input) { - // Validate device - if (!m_device) { - m_logger->error("BgfxRenderer::process called but device is not initialized"); - return; - } - - // Read deltaTime from input (provided by ModuleSystem) - float deltaTime = static_cast(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(newWidth) != m_width || static_cast(newHeight) != m_height)) { - m_width = static_cast(newWidth); - m_height = static_cast(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) - if (m_sceneCollector && m_io) { - m_sceneCollector->collect(m_io, deltaTime); - } - - // 2. Build immutable FramePacket - if (!m_frameAllocator || !m_sceneCollector) { - m_logger->error("BgfxRenderer::process - frameAllocator or sceneCollector not initialized"); - return; - } - 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(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 BgfxRendererModule::getState() { - // Minimal state for hot-reload (renderer is stateless gameplay-wise) - auto state = std::make_unique("state"); - state->setInt("frameCount", static_cast(m_frameCount)); - // GPU resources are recreated on reload - return state; -} - -void BgfxRendererModule::setState(const IDataNode& state) { - m_frameCount = static_cast(state.getInt("frameCount", 0)); - m_logger->info("State restored: frameCount={}", m_frameCount); -} - -const IDataNode& BgfxRendererModule::getConfiguration() { - 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); - m_configCache->setString("shaderPath", m_shaderPath); - m_configCache->setBool("vsync", m_vsync); - m_configCache->setInt("maxSpritesPerBatch", m_maxSprites); - } - return *m_configCache; -} - -std::unique_ptr BgfxRendererModule::getHealthStatus() { - auto health = std::make_unique("health"); - health->setString("status", "running"); - health->setInt("frameCount", static_cast(m_frameCount)); - health->setInt("allocatorUsedBytes", static_cast(m_frameAllocator ? m_frameAllocator->getUsed() : 0)); - health->setInt("textureCount", static_cast(m_resourceCache ? m_resourceCache->getTextureCount() : 0)); - health->setInt("shaderCount", static_cast(m_resourceCache ? m_resourceCache->getShaderCount() : 0)); - return health; -} - -} // namespace grove - -// ============================================================================ -// C Export (required for dlopen/LoadLibrary) -// ============================================================================ - -#ifdef _WIN32 -#define GROVE_MODULE_EXPORT __declspec(dllexport) -#else -#define GROVE_MODULE_EXPORT -#endif - -extern "C" { - -GROVE_MODULE_EXPORT grove::IModule* createModule() { - return new grove::BgfxRendererModule(); -} - -GROVE_MODULE_EXPORT void destroyModule(grove::IModule* module) { - delete module; -} - -} +#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/ParticlePass.h" +#include "Passes/DebugPass.h" + +#include +#include +#include + +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(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 = static_cast(config.getInt("frameAllocatorSizeMB", 16)) * 1024 * 1024; + + // Window handle (passed via config or 0 if separate WindowModule) + // Use double to preserve 64-bit pointer values + // Also try getInt as fallback for compatibility with older code that uses setInt + void* windowHandle = nullptr; + double handleDouble = config.getDouble("nativeWindowHandle", 0.0); + if (handleDouble != 0.0) { + windowHandle = reinterpret_cast(static_cast(handleDouble)); + } else { + // Fallback: try reading as int (for 32-bit handles or compatibility) + int handleInt = config.getInt("nativeWindowHandle", 0); + if (handleInt != 0) { + windowHandle = reinterpret_cast(static_cast(static_cast(handleInt))); + m_logger->warn("nativeWindowHandle passed as int - consider using setDouble for 64-bit handles"); + } + } + + // Display handle (X11 Display* on Linux, 0/nullptr on Windows) + void* displayHandle = nullptr; + double displayDouble = config.getDouble("nativeDisplayHandle", 0.0); + if (displayDouble != 0.0) { + displayHandle = reinterpret_cast(static_cast(displayDouble)); + } + + m_logger->info("Initializing BgfxRenderer: {}x{} backend={}", m_width, m_height, m_backend); + + // Initialize subsystems + m_frameAllocator = std::make_unique(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(); + 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(); + m_renderGraph->addPass(std::make_unique()); + m_logger->info("Added ClearPass"); + + // Setup resource cache first (needed by passes) + m_resourceCache = std::make_unique(); + + // Create TilemapPass (renders before sprites) + auto tilemapPass = std::make_unique(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(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(spriteShader)); + m_logger->info("Added TextPass"); + + // Create ParticlePass (uses sprite shader, renders after sprites with additive blending) + auto particlePass = std::make_unique(spriteShader); + particlePass->setResourceCache(m_resourceCache.get()); + m_renderGraph->addPass(std::move(particlePass)); + m_logger->info("Added ParticlePass"); + + m_renderGraph->addPass(std::make_unique(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 and correct dimensions + m_sceneCollector = std::make_unique(); + m_sceneCollector->setup(io, m_width, m_height); + m_logger->info("SceneCollector setup complete with dimensions {}x{}", m_width, m_height); + + // Setup debug overlay + m_debugOverlay = std::make_unique(); + 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()) { + uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, defaultTexturePath); + if (texId > 0) { + rhi::TextureHandle tex = m_resourceCache->getTextureById(texId); + m_spritePass->setTexture(tex); + m_logger->info("Loaded default texture: {} (id={})", defaultTexturePath, texId); + } else { + m_logger->warn("Failed to load default texture: {}", defaultTexturePath); + } + } + + // Load additional textures (texture1, texture2, etc.) + for (int i = 1; i <= 10; ++i) { + std::string key = "texture" + std::to_string(i); + std::string path = config.getString(key, ""); + if (!path.empty()) { + uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, path); + if (texId > 0) { + m_logger->info("Loaded texture: {} (id={})", path, texId); + } else { + m_logger->warn("Failed to load texture: {}", path); + } + } + } + + m_logger->info("BgfxRenderer initialized successfully"); +} + +void BgfxRendererModule::process(const IDataNode& input) { + if (!m_device) { + m_logger->error("Device not initialized"); + return; + } + + // Reset frame allocator for this frame + if (m_frameAllocator) { + m_frameAllocator->reset(); + } + + // Collect scene data from IIO messages and prepare frame packet + if (m_sceneCollector && m_renderGraph && m_frameAllocator && m_io) { + // Get delta time from input (or default to 16ms) + float deltaTime = static_cast(input.getDouble("deltaTime", 0.016)); + + // Collect all IIO messages for this frame + m_sceneCollector->collect(m_io, deltaTime); + + // Generate immutable FramePacket for render passes + FramePacket packet = m_sceneCollector->finalize(*m_frameAllocator); + + // Apply view transform (projection matrix for 2D rendering) + m_device->setViewClear(0, packet.clearColor, 1.0f); + m_device->setViewRect(0, packet.mainView.viewportX, packet.mainView.viewportY, + packet.mainView.viewportW, packet.mainView.viewportH); + m_device->setViewTransform(0, packet.mainView.viewMatrix, packet.mainView.projMatrix); + + // Execute render graph with collected scene data + m_renderGraph->execute(packet, *m_device); + + // Clear staging buffers for next frame + m_sceneCollector->clear(); + } + + // Present frame + m_device->frame(); + + 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 BgfxRendererModule::getState() { + // Minimal state for hot-reload (renderer is stateless gameplay-wise) + auto state = std::make_unique("state"); + state->setInt("frameCount", static_cast(m_frameCount)); + // GPU resources are recreated on reload + return state; +} + +void BgfxRendererModule::setState(const IDataNode& state) { + m_frameCount = static_cast(state.getInt("frameCount", 0)); + m_logger->info("State restored: frameCount={}", m_frameCount); +} + +const IDataNode& BgfxRendererModule::getConfiguration() { + 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); + m_configCache->setString("shaderPath", m_shaderPath); + m_configCache->setBool("vsync", m_vsync); + m_configCache->setInt("maxSpritesPerBatch", m_maxSprites); + } + return *m_configCache; +} + +std::unique_ptr BgfxRendererModule::getHealthStatus() { + auto health = std::make_unique("health"); + health->setString("status", "running"); + health->setInt("frameCount", static_cast(m_frameCount)); + health->setInt("allocatorUsedBytes", static_cast(m_frameAllocator ? m_frameAllocator->getUsed() : 0)); + health->setInt("textureCount", static_cast(m_resourceCache ? m_resourceCache->getTextureCount() : 0)); + health->setInt("shaderCount", static_cast(m_resourceCache ? m_resourceCache->getShaderCount() : 0)); + return health; +} + +} // namespace grove + +// ============================================================================ +// C Export (required for dlopen/LoadLibrary) +// Skip when building as static library to avoid multiple definition errors +// ============================================================================ + +#ifndef GROVE_MODULE_STATIC + +#ifdef _WIN32 +#define GROVE_MODULE_EXPORT __declspec(dllexport) +#else +#define GROVE_MODULE_EXPORT +#endif + +extern "C" { + +GROVE_MODULE_EXPORT grove::IModule* createModule() { + return new grove::BgfxRendererModule(); +} + +GROVE_MODULE_EXPORT void destroyModule(grove::IModule* module) { + delete module; +} + +} + +#endif // GROVE_MODULE_STATIC diff --git a/modules/BgfxRenderer/BgfxRendererModule.h b/modules/BgfxRenderer/BgfxRendererModule.h index 9e7a965..64b4aa0 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.h +++ b/modules/BgfxRenderer/BgfxRendererModule.h @@ -83,7 +83,13 @@ private: // C Export (required for dlopen) // ============================================================================ +#ifdef _WIN32 +#define GROVE_MODULE_EXPORT __declspec(dllexport) +#else +#define GROVE_MODULE_EXPORT +#endif + extern "C" { - grove::IModule* createModule(); - void destroyModule(grove::IModule* module); + GROVE_MODULE_EXPORT grove::IModule* createModule(); + GROVE_MODULE_EXPORT void destroyModule(grove::IModule* module); } diff --git a/modules/BgfxRenderer/CMakeLists.txt b/modules/BgfxRenderer/CMakeLists.txt index 6610d56..4ca5033 100644 --- a/modules/BgfxRenderer/CMakeLists.txt +++ b/modules/BgfxRenderer/CMakeLists.txt @@ -19,12 +19,15 @@ FetchContent_Declare( # CRITICAL: Disable bgfx multithreading BEFORE fetching to avoid TLS issues when running from a DLL on Windows # Without this, bgfx::frame() crashes on Frame 1 due to render thread/DLL memory conflicts -add_compile_definitions(BGFX_CONFIG_MULTITHREADED=0) +# Must use CMAKE_CXX_FLAGS to ensure the definition is passed to ALL compilation units including bgfx +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBGFX_CONFIG_MULTITHREADED=0") +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DBGFX_CONFIG_MULTITHREADED=0") # Fix for MinGW GCC 15+ - glslang headers don't include # Use stdint.h (C header, works in C++ too) to ensure uint32_t is defined if(MINGW) - add_compile_options(-include stdint.h) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include stdint.h") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -include stdint.h") endif() # bgfx options @@ -36,6 +39,17 @@ set(BGFX_CUSTOM_TARGETS OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(bgfx) +# CRITICAL: Force single-threaded mode on bgfx targets AFTER they're created +# This is necessary because CMAKE_CXX_FLAGS might not be applied to FetchContent targets +# Without this, bgfx::frame() crashes in DLL context due to render thread issues +if(TARGET bgfx) + target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_MULTITHREADED=0) + message(STATUS "BGFX: Forcing BGFX_CONFIG_MULTITHREADED=0 on bgfx target") +endif() +if(TARGET bx) + target_compile_definitions(bx PRIVATE BGFX_CONFIG_MULTITHREADED=0) +endif() + # ============================================================================ # BgfxRenderer Shared Library # ============================================================================ @@ -128,6 +142,89 @@ if(APPLE) ) endif() +# ============================================================================ +# BgfxRenderer STATIC Library (for Windows where DLL+bgfx doesn't work) +# ============================================================================ +# On Windows, bgfx crashes when loaded from a DLL due to threading/TLS issues. +# This static library allows linking bgfx directly into the executable while +# still using the IModule interface. + +add_library(BgfxRenderer_static STATIC + # Main module + BgfxRendererModule.cpp + + # RHI + RHI/RHICommandBuffer.cpp + RHI/BgfxDevice.cpp + + # Frame + Frame/FrameAllocator.cpp + + # RenderGraph + RenderGraph/RenderGraph.cpp + + # Shaders + Shaders/ShaderManager.cpp + + # Passes + Passes/ClearPass.cpp + Passes/TilemapPass.cpp + Passes/SpritePass.cpp + Passes/TextPass.cpp + Passes/ParticlePass.cpp + Passes/DebugPass.cpp + + # Text + Text/BitmapFont.cpp + + # Scene + Scene/SceneCollector.cpp + + # Resources + Resources/ResourceCache.cpp + Resources/TextureLoader.cpp + + # Debug + Debug/DebugOverlay.cpp +) + +target_include_directories(BgfxRenderer_static PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include + ${bgfx_SOURCE_DIR}/bimg/3rdparty +) + +target_link_libraries(BgfxRenderer_static PUBLIC + GroveEngine::impl + bgfx + bx + spdlog::spdlog +) + +target_compile_features(BgfxRenderer_static PRIVATE cxx_std_17) + +# Mark as static build to skip C exports (avoids multiple definition errors) +target_compile_definitions(BgfxRenderer_static PRIVATE GROVE_MODULE_STATIC) + +if(WIN32) + target_compile_definitions(BgfxRenderer_static PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX + ) +endif() + +if(UNIX AND NOT APPLE) + target_link_libraries(BgfxRenderer_static PUBLIC pthread dl X11 GL) +endif() + +if(APPLE) + target_link_libraries(BgfxRenderer_static PUBLIC + "-framework Cocoa" + "-framework QuartzCore" + "-framework Metal" + ) +endif() + # ============================================================================ # Shader Compilation # ============================================================================ diff --git a/modules/BgfxRenderer/Passes/DebugPass.cpp b/modules/BgfxRenderer/Passes/DebugPass.cpp index f584dcc..40666f2 100644 --- a/modules/BgfxRenderer/Passes/DebugPass.cpp +++ b/modules/BgfxRenderer/Passes/DebugPass.cpp @@ -1,8 +1,27 @@ #include "DebugPass.h" #include "../RHI/RHIDevice.h" +#include "../Frame/FramePacket.h" +#include +#include +#include namespace grove { +// Vertex format: x, y, z, r, g, b, a (7 floats = 28 bytes) +struct DebugVertex { + float x, y, z; + float r, g, b, a; +}; +static_assert(sizeof(DebugVertex) == 28, "DebugVertex must be 28 bytes"); + +// Helper to convert packed RGBA color to floats +static void unpackColor(uint32_t color, float& r, float& g, float& b, float& a) { + r = static_cast((color >> 24) & 0xFF) / 255.0f; + g = static_cast((color >> 16) & 0xFF) / 255.0f; + b = static_cast((color >> 8) & 0xFF) / 255.0f; + a = static_cast(color & 0xFF) / 255.0f; +} + DebugPass::DebugPass(rhi::ShaderHandle shader) : m_lineShader(shader) { @@ -10,11 +29,14 @@ DebugPass::DebugPass(rhi::ShaderHandle shader) void DebugPass::setup(rhi::IRHIDevice& device) { // Create dynamic vertex buffer for debug lines + // Each line = 2 vertices, each rect = 4 lines = 8 vertices + // Buffer size: MAX_DEBUG_LINES * 2 vertices * 28 bytes rhi::BufferDesc vbDesc; vbDesc.type = rhi::BufferDesc::Vertex; - vbDesc.size = MAX_DEBUG_LINES * 2 * sizeof(float) * 6; // 2 verts per line, pos + color + vbDesc.size = MAX_DEBUG_LINES * 2 * sizeof(DebugVertex); vbDesc.data = nullptr; vbDesc.dynamic = true; + vbDesc.layout = rhi::BufferDesc::PosColor; // vec3 pos + vec4 color m_lineVB = device.createBuffer(vbDesc); } @@ -24,37 +46,93 @@ void DebugPass::shutdown(rhi::IRHIDevice& device) { } void DebugPass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) { + static int logCounter = 0; + // Skip if no debug primitives if (frame.debugLineCount == 0 && frame.debugRectCount == 0) { + if (logCounter++ % 60 == 0) { + spdlog::debug("[DebugPass] No primitives (lines={}, rects={})", frame.debugLineCount, frame.debugRectCount); + } return; } - // Set render state for debug (no blending, no depth) + // Skip if shader is invalid + if (!m_lineShader.isValid()) { + spdlog::warn("[DebugPass] Shader invalid!"); + return; + } + + // Log periodically + if (logCounter++ % 60 == 0) { + spdlog::info("[DebugPass] Drawing {} lines, {} rects (shader={})", + frame.debugLineCount, frame.debugRectCount, m_lineShader.id); + } + + // Build vertex data for all debug primitives + std::vector vertices; + + // Reserve space: lines (2 verts each) + rects (8 verts each for wireframe) + size_t totalVertices = frame.debugLineCount * 2 + frame.debugRectCount * 8; + vertices.reserve(totalVertices); + + // Add line vertices + for (size_t i = 0; i < frame.debugLineCount; ++i) { + const DebugLine& line = frame.debugLines[i]; + float r, g, b, a; + unpackColor(line.color, r, g, b, a); + + // Vertex 1 + vertices.push_back({line.x1, line.y1, 0.0f, r, g, b, a}); + // Vertex 2 + vertices.push_back({line.x2, line.y2, 0.0f, r, g, b, a}); + } + + // Add rect vertices as 4 lines (wireframe) + for (size_t i = 0; i < frame.debugRectCount; ++i) { + const DebugRect& rect = frame.debugRects[i]; + float r, g, b, a; + unpackColor(rect.color, r, g, b, a); + + float x1 = rect.x; + float y1 = rect.y; + float x2 = rect.x + rect.w; + float y2 = rect.y + rect.h; + + // Line 1: top + vertices.push_back({x1, y1, 0.0f, r, g, b, a}); + vertices.push_back({x2, y1, 0.0f, r, g, b, a}); + // Line 2: right + vertices.push_back({x2, y1, 0.0f, r, g, b, a}); + vertices.push_back({x2, y2, 0.0f, r, g, b, a}); + // Line 3: bottom + vertices.push_back({x2, y2, 0.0f, r, g, b, a}); + vertices.push_back({x1, y2, 0.0f, r, g, b, a}); + // Line 4: left + vertices.push_back({x1, y2, 0.0f, r, g, b, a}); + vertices.push_back({x1, y1, 0.0f, r, g, b, a}); + } + + if (vertices.empty()) { + return; + } + + // Update dynamic vertex buffer + device.updateBuffer(m_lineVB, vertices.data(), + static_cast(vertices.size() * sizeof(DebugVertex))); + + // Set render state for debug lines rhi::RenderState state; state.blend = rhi::BlendMode::None; state.cull = rhi::CullMode::None; + state.primitive = rhi::PrimitiveType::Lines; 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) { - // TODO: Build line vertex data from frame.debugLines and update buffer - // device.updateBuffer(m_lineVB, lineVertices, lineVertexDataSize); - cmd.setVertexBuffer(m_lineVB); - cmd.draw(static_cast(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 - (void)device; // Will be used when implementing rect rendering - } + // Draw all lines + cmd.setVertexBuffer(m_lineVB); + cmd.draw(static_cast(vertices.size())); + cmd.submit(0, m_lineShader, 0); } } // namespace grove diff --git a/modules/BgfxRenderer/Passes/SpritePass.cpp b/modules/BgfxRenderer/Passes/SpritePass.cpp index 65e2740..162fee7 100644 --- a/modules/BgfxRenderer/Passes/SpritePass.cpp +++ b/modules/BgfxRenderer/Passes/SpritePass.cpp @@ -4,6 +4,7 @@ #include "../Resources/ResourceCache.h" #include #include +#include namespace grove { @@ -58,15 +59,19 @@ void SpritePass::setup(rhi::IRHIDevice& device) { // Create texture sampler uniform (must match shader: s_texColor) m_textureSampler = device.createUniform("s_texColor", 1); - // Create default white 1x1 texture (used when no texture is bound) - uint32_t whitePixel = 0xFFFFFFFF; // RGBA white + // Create default white 4x4 texture (used when no texture is bound) + // Some drivers have issues with 1x1 textures + uint32_t whitePixels[16]; + for (int i = 0; i < 16; ++i) whitePixels[i] = 0xFFFFFFFF; // RGBA white rhi::TextureDesc texDesc; - texDesc.width = 1; - texDesc.height = 1; + texDesc.width = 4; + texDesc.height = 4; texDesc.format = rhi::TextureDesc::RGBA8; - texDesc.data = &whitePixel; - texDesc.dataSize = sizeof(whitePixel); + texDesc.data = whitePixels; + texDesc.dataSize = sizeof(whitePixels); m_defaultTexture = device.createTexture(texDesc); + + spdlog::info("SpritePass: defaultTexture valid={} (4x4 white)", m_defaultTexture.isValid()); } void SpritePass::shutdown(rhi::IRHIDevice& device) { @@ -91,11 +96,9 @@ void SpritePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd, } void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) { - if (frame.spriteCount == 0) { - return; - } + if (frame.spriteCount == 0) return; - // Set render state for sprites (alpha blending, no depth) + // Set render state ONCE (like TextPass does) rhi::RenderState state; state.blend = rhi::BlendMode::Alpha; state.cull = rhi::CullMode::None; @@ -103,90 +106,35 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi: state.depthWrite = false; cmd.setState(state); - // Build sorted indices by layer (primary) and textureId (secondary) for batching + // Sort sprites by layer for correct draw order m_sortedIndices.clear(); m_sortedIndices.reserve(frame.spriteCount); for (size_t i = 0; i < frame.spriteCount; ++i) { m_sortedIndices.push_back(static_cast(i)); } - - // Sort by layer first (ascending: layer 0 = background, rendered first) - // Then by textureId to batch sprites on the same layer std::sort(m_sortedIndices.begin(), m_sortedIndices.end(), [&frame](uint32_t a, uint32_t b) { - const SpriteInstance& sa = frame.sprites[a]; - const SpriteInstance& sb = frame.sprites[b]; - if (sa.layer != sb.layer) { - return sa.layer < sb.layer; - } - return sa.textureId < sb.textureId; + return frame.sprites[a].layer < frame.sprites[b].layer; }); - // Process sprites in batches by texture - // Use transient buffers for proper multi-batch rendering - uint32_t batchStart = 0; - while (batchStart < frame.spriteCount) { - // Find the end of current batch (same texture) - uint16_t currentTexId = static_cast(frame.sprites[m_sortedIndices[batchStart]].textureId); - uint32_t batchEnd = batchStart + 1; - - while (batchEnd < frame.spriteCount) { - uint16_t nextTexId = static_cast(frame.sprites[m_sortedIndices[batchEnd]].textureId); - if (nextTexId != currentTexId) { - break; // Texture changed, flush this batch - } - ++batchEnd; - } - - uint32_t batchCount = batchEnd - batchStart; - - // Resolve texture handle for this batch - rhi::TextureHandle batchTexture; - if (currentTexId == 0 || !m_resourceCache) { - batchTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture; - } else { - batchTexture = m_resourceCache->getTextureById(currentTexId); - if (!batchTexture.isValid()) { - batchTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture; - } - } - - // Allocate transient instance buffer for this batch - rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(batchCount); - - if (transientBuffer.isValid()) { - // Copy sprite data to transient buffer - SpriteInstance* dest = static_cast(transientBuffer.data); - for (uint32_t i = 0; i < batchCount; ++i) { - dest[i] = frame.sprites[m_sortedIndices[batchStart + i]]; - } - - // Re-set state for each batch to ensure clean state - cmd.setState(state); - - // Set buffers and draw - cmd.setVertexBuffer(m_quadVB); - cmd.setIndexBuffer(m_quadIB); - cmd.setTransientInstanceBuffer(transientBuffer, 0, batchCount); - cmd.setTexture(0, batchTexture, m_textureSampler); - cmd.drawInstanced(6, batchCount); - cmd.submit(0, m_shader, 0); - } else { - // Fallback: use dynamic buffer (may have issues with multiple batches) - // This should only happen if GPU runs out of transient memory - std::vector batchData; - batchData.reserve(batchCount); - for (uint32_t i = 0; i < batchCount; ++i) { - batchData.push_back(frame.sprites[m_sortedIndices[batchStart + i]]); - } - device.updateBuffer(m_instanceBuffer, batchData.data(), - static_cast(batchData.size() * sizeof(SpriteInstance))); - cmd.setInstanceBuffer(m_instanceBuffer, 0, batchCount); - flushBatch(device, cmd, batchTexture, batchCount); - } - - batchStart = batchEnd; + // Copy sorted sprites to temporary buffer (like TextPass does with glyphs) + std::vector sortedSprites; + sortedSprites.reserve(frame.spriteCount); + for (uint32_t idx : m_sortedIndices) { + sortedSprites.push_back(frame.sprites[idx]); } + + // Update dynamic instance buffer with ALL sprites (like TextPass) + device.updateBuffer(m_instanceBuffer, sortedSprites.data(), + static_cast(sortedSprites.size() * sizeof(SpriteInstance))); + + // Set buffers and draw ALL sprites in ONE call (like TextPass) + cmd.setVertexBuffer(m_quadVB); + cmd.setIndexBuffer(m_quadIB); + cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast(sortedSprites.size())); + cmd.setTexture(0, m_defaultTexture, m_textureSampler); + cmd.drawInstanced(6, static_cast(sortedSprites.size())); + cmd.submit(0, m_shader, 0); } } // namespace grove diff --git a/modules/BgfxRenderer/Passes/TilemapPass.cpp b/modules/BgfxRenderer/Passes/TilemapPass.cpp index b6a1a76..250b390 100644 --- a/modules/BgfxRenderer/Passes/TilemapPass.cpp +++ b/modules/BgfxRenderer/Passes/TilemapPass.cpp @@ -121,9 +121,9 @@ void TilemapPass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi size_t tileX = t % chunk.width; size_t tileY = t / chunk.width; - // Calculate world position - float worldX = chunk.x + tileX * chunk.tileWidth; - float worldY = chunk.y + tileY * chunk.tileHeight; + // Calculate world position (add 0.5 tile offset because sprite shader centers quads) + float worldX = chunk.x + (tileX + 0.5f) * chunk.tileWidth; + float worldY = chunk.y + (tileY + 0.5f) * chunk.tileHeight; // Calculate UV coords from tile index // tileIndex-1 because 0 is empty, actual tiles start at 1 diff --git a/modules/BgfxRenderer/RHI/BgfxDevice.cpp b/modules/BgfxRenderer/RHI/BgfxDevice.cpp index 2fc92a5..f41ccac 100644 --- a/modules/BgfxRenderer/RHI/BgfxDevice.cpp +++ b/modules/BgfxRenderer/RHI/BgfxDevice.cpp @@ -1,584 +1,609 @@ -#include "RHIDevice.h" -#include "RHICommandBuffer.h" - -// CRITICAL: Force single-threaded mode BEFORE including bgfx -// This avoids TLS (Thread-Local Storage) crashes when bgfx runs in a DLL on Windows -#ifndef BGFX_CONFIG_MULTITHREADED -#define BGFX_CONFIG_MULTITHREADED 0 -#endif - -// bgfx includes - ONLY in this file -#include -#include -#include - -#include -#include - -namespace grove::rhi { - -// ============================================================================ -// Bgfx Device Implementation -// ============================================================================ - -class BgfxDevice : public IRHIDevice { -public: - BgfxDevice() = default; - ~BgfxDevice() override = default; - - bool init(void* nativeWindowHandle, void* nativeDisplayHandle, uint16_t width, uint16_t height) override { - m_width = width; - m_height = height; - - bgfx::Init init; - // Let bgfx auto-select the best renderer (D3D11 on Windows) - init.type = bgfx::RendererType::Count; - init.resolution.width = width; - init.resolution.height = height; - init.resolution.reset = BGFX_RESET_VSYNC; - - // Set platform data - init.platformData.nwh = nativeWindowHandle; - init.platformData.ndt = nativeDisplayHandle; // X11 Display* on Linux, nullptr on Windows - - if (!bgfx::init(init)) { - return false; - } - - // Note: Debug text is enabled only when DebugOverlay is active - // Don't enable it by default as it can cause issues on some platforms - // bgfx::setDebug(BGFX_DEBUG_TEXT); - - // 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(bgfxCaps->limits.maxTextureSize); - caps.maxViews = static_cast(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) { - // Build vertex layout based on layout type - bgfx::VertexLayout layout; - if (desc.layout == BufferDesc::PosColor) { - // vec3 position + vec4 color (7 floats = 28 bytes per vertex) - layout.begin() - .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) - .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float, true) // normalized - .end(); - } else { - // Raw bytes - use 1-byte layout - layout.begin().add(bgfx::Attrib::Position, 1, bgfx::AttribType::Uint8).end(); - } - - if (desc.dynamic) { - bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( - desc.layout == BufferDesc::PosColor ? desc.size / layout.getStride() : desc.size, - layout, - BGFX_BUFFER_ALLOW_RESIZE - ); - result.id = dvb.idx | 0x8000; - } else { - // Use bgfx::copy instead of bgfx::makeRef to ensure data is copied - // This avoids potential issues with DLL memory visibility on Windows - bgfx::VertexBufferHandle vb = bgfx::createVertexBuffer( - desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::copy(s_emptyBuffer, 1), - layout - ); - 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 { - // Use bgfx::copy instead of bgfx::makeRef to ensure data is copied - bgfx::IndexBufferHandle ib = bgfx::createIndexBuffer( - desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::copy(s_emptyBuffer, 1) - ); - result.id = ib.idx; - } - } else { // Instance buffer - treated as dynamic vertex buffer - // Instance buffer layout: 5 x vec4 = 80 bytes per instance - // i_data0 (TEXCOORD7), i_data1 (TEXCOORD6), i_data2 (TEXCOORD5), - // i_data3 (TEXCOORD4), i_data4 (TEXCOORD3) - bgfx::VertexLayout layout; - layout.begin() - .add(bgfx::Attrib::TexCoord7, 4, bgfx::AttribType::Float) // i_data0: pos.xy, scale.xy - .add(bgfx::Attrib::TexCoord6, 4, bgfx::AttribType::Float) // i_data1: rotation, uv0.xy, unused - .add(bgfx::Attrib::TexCoord5, 4, bgfx::AttribType::Float) // i_data2: uv1.xy, unused, unused - .add(bgfx::Attrib::TexCoord4, 4, bgfx::AttribType::Float) // i_data3: reserved - .add(bgfx::Attrib::TexCoord3, 4, bgfx::AttribType::Float) // i_data4: color rgba - .end(); - // 80 bytes per instance - uint32_t instanceCount = desc.size / 80; - bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( - instanceCount, - layout, - 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)); - } - - // ======================================== - // Transient Instance Buffers - // ======================================== - - TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) override { - TransientInstanceBuffer result; - - constexpr uint16_t INSTANCE_STRIDE = 80; // 5 x vec4 - - // Check if we have space in the pool - if (m_transientPoolCount >= MAX_TRANSIENT_BUFFERS) { - return result; // Pool full, return invalid - } - - // Check if bgfx has enough transient memory - if (bgfx::getAvailInstanceDataBuffer(count, INSTANCE_STRIDE) < count) { - return result; // Not enough memory - } - - // Allocate from bgfx - uint16_t poolIndex = m_transientPoolCount++; - bgfx::allocInstanceDataBuffer(&m_transientPool[poolIndex], count, INSTANCE_STRIDE); - - result.data = m_transientPool[poolIndex].data; - result.size = count * INSTANCE_STRIDE; - result.count = count; - result.stride = INSTANCE_STRIDE; - result.poolIndex = poolIndex; - - return result; - } - - // ======================================== - // 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 { - // Ensure view 0 is processed even if nothing was rendered to it - bgfx::touch(0); - - // Present frame - bgfx::frame(); - - // Reset transient pool for next frame - m_transientPoolCount = 0; - } - - void executeCommandBuffer(const RHICommandBuffer& cmdBuffer) override { - // Reset transient instance state for this command buffer execution - m_useTransientInstance = false; - m_currentTransientIndex = UINT16_MAX; - - // Track current state for bgfx calls - RenderState currentState; - BufferHandle currentVB; - BufferHandle currentIB; - BufferHandle currentInstBuffer; - uint32_t instStart = 0; - uint32_t instCount = 0; - - // Store texture state to apply at draw time (not immediately) - TextureHandle pendingTexture; - UniformHandle pendingSampler; - uint8_t pendingTextureSlot = 0; - bool hasTexture = false; - - for (const Command& cmd : cmdBuffer.getCommands()) { - switch (cmd.type) { - case CommandType::SetState: { - currentState = cmd.setState.state; - // Build bgfx state flags - uint64_t state = BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A; - - switch (currentState.blend) { - case BlendMode::Alpha: - state |= BGFX_STATE_BLEND_ALPHA; - break; - case BlendMode::Additive: - state |= BGFX_STATE_BLEND_ADD; - break; - case BlendMode::Multiply: - state |= BGFX_STATE_BLEND_MULTIPLY; - break; - case BlendMode::None: - default: - break; - } - - switch (currentState.cull) { - case CullMode::CW: - state |= BGFX_STATE_CULL_CW; - break; - case CullMode::CCW: - state |= BGFX_STATE_CULL_CCW; - break; - case CullMode::None: - default: - break; - } - - if (currentState.depthTest) { - state |= BGFX_STATE_DEPTH_TEST_LESS; - } - if (currentState.depthWrite) { - state |= BGFX_STATE_WRITE_Z; - } - - bgfx::setState(state); - break; - } - - case CommandType::SetTexture: { - // Store texture state - apply at draw time, not immediately - // This ensures texture is set after all other state is configured - pendingTexture = cmd.setTexture.texture; - pendingSampler = cmd.setTexture.sampler; - pendingTextureSlot = cmd.setTexture.slot; - hasTexture = true; - break; - } - - case CommandType::SetUniform: { - bgfx::UniformHandle uniform = { cmd.setUniform.uniform.id }; - bgfx::setUniform(uniform, cmd.setUniform.data, cmd.setUniform.numVec4s); - break; - } - - case CommandType::SetVertexBuffer: { - currentVB = cmd.setVertexBuffer.buffer; - break; - } - - case CommandType::SetIndexBuffer: { - currentIB = cmd.setIndexBuffer.buffer; - break; - } - - case CommandType::SetInstanceBuffer: { - currentInstBuffer = cmd.setInstanceBuffer.buffer; - instStart = cmd.setInstanceBuffer.start; - instCount = cmd.setInstanceBuffer.count; - m_useTransientInstance = false; - break; - } - - case CommandType::SetTransientInstanceBuffer: { - m_currentTransientIndex = cmd.setTransientInstanceBuffer.poolIndex; - instStart = cmd.setTransientInstanceBuffer.start; - instCount = cmd.setTransientInstanceBuffer.count; - m_useTransientInstance = true; - break; - } - - case CommandType::SetScissor: { - bgfx::setScissor(cmd.setScissor.x, cmd.setScissor.y, - cmd.setScissor.w, cmd.setScissor.h); - break; - } - - case CommandType::Draw: { - // Set vertex buffer before draw - if (currentVB.isValid()) { - bool isDynamic = (currentVB.id & 0x8000) != 0; - uint16_t idx = currentVB.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicVertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h, 0, cmd.draw.vertexCount); - } else { - bgfx::VertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h, cmd.draw.startVertex, cmd.draw.vertexCount); - } - } - break; - } - - case CommandType::DrawIndexed: { - // Set vertex and index buffers before draw - if (currentVB.isValid()) { - bool isDynamic = (currentVB.id & 0x8000) != 0; - uint16_t idx = currentVB.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicVertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h); - } else { - bgfx::VertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h); - } - } - if (currentIB.isValid()) { - bool isDynamic = (currentIB.id & 0x8000) != 0; - uint16_t idx = currentIB.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicIndexBufferHandle h = { idx }; - bgfx::setIndexBuffer(h, cmd.drawIndexed.startIndex, cmd.drawIndexed.indexCount); - } else { - bgfx::IndexBufferHandle h = { idx }; - bgfx::setIndexBuffer(h, cmd.drawIndexed.startIndex, cmd.drawIndexed.indexCount); - } - } - break; - } - - case CommandType::DrawInstanced: { - // Set vertex, index, and instance buffers - if (currentVB.isValid()) { - bool isDynamic = (currentVB.id & 0x8000) != 0; - uint16_t idx = currentVB.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicVertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h); - } else { - bgfx::VertexBufferHandle h = { idx }; - bgfx::setVertexBuffer(0, h); - } - } - if (currentIB.isValid()) { - bool isDynamic = (currentIB.id & 0x8000) != 0; - uint16_t idx = currentIB.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicIndexBufferHandle h = { idx }; - bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount); - } else { - bgfx::IndexBufferHandle h = { idx }; - bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount); - } - } - // Set instance buffer (either dynamic or transient) - if (m_useTransientInstance && m_currentTransientIndex < m_transientPoolCount) { - // Transient instance buffer from pool - bgfx::setInstanceDataBuffer(&m_transientPool[m_currentTransientIndex], instStart, instCount); - } else if (currentInstBuffer.isValid()) { - bool isDynamic = (currentInstBuffer.id & 0x8000) != 0; - uint16_t idx = currentInstBuffer.id & 0x7FFF; - if (isDynamic) { - bgfx::DynamicVertexBufferHandle h = { idx }; - bgfx::setInstanceDataBuffer(h, instStart, instCount); - } - } - break; - } - - case CommandType::Submit: { - // Apply pending texture right before submit - if (hasTexture) { - bgfx::TextureHandle tex = { pendingTexture.id }; - bgfx::UniformHandle sampler = { pendingSampler.id }; - bgfx::setTexture(pendingTextureSlot, sampler, tex); - } - bgfx::ProgramHandle program = { cmd.submit.shader.id }; - bgfx::submit(cmd.submit.view, program, cmd.submit.depth); - // Reset texture state after submit (consumed) - hasTexture = false; - break; - } - } - } - } - -private: - uint16_t m_width = 0; - uint16_t m_height = 0; - bool m_initialized = false; - - // Transient instance buffer pool (reset each frame) - static constexpr uint16_t MAX_TRANSIENT_BUFFERS = 256; - bgfx::InstanceDataBuffer m_transientPool[MAX_TRANSIENT_BUFFERS] = {}; - uint16_t m_transientPoolCount = 0; - - // Transient buffer state for command execution - bool m_useTransientInstance = false; - uint16_t m_currentTransientIndex = UINT16_MAX; - - // Empty buffer for null data fallback in buffer creation - inline static const uint8_t s_emptyBuffer[1] = {0}; - - 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::create() { - return std::make_unique(); -} - -} // namespace grove::rhi +#include "RHIDevice.h" +#include "RHICommandBuffer.h" + +// CRITICAL: Force single-threaded mode BEFORE including bgfx +// This avoids TLS (Thread-Local Storage) crashes when bgfx runs in a DLL on Windows +#ifndef BGFX_CONFIG_MULTITHREADED +#define BGFX_CONFIG_MULTITHREADED 0 +#endif + +// bgfx includes - ONLY in this file +#include +#include +#include + +#include +#include + +namespace grove::rhi { + +// ============================================================================ +// Bgfx Device Implementation +// ============================================================================ + +class BgfxDevice : public IRHIDevice { +public: + BgfxDevice() = default; + ~BgfxDevice() override = default; + + bool init(void* nativeWindowHandle, void* nativeDisplayHandle, uint16_t width, uint16_t height) override { + m_width = width; + m_height = height; + + // IMPORTANT: On Windows, we MUST call bgfx::renderFrame() before bgfx::init() to force + // single-threaded mode. This is required because: + // 1. In multi-threaded mode, bgfx starts a render thread that pumps Windows message queue + // 2. This conflicts with SDL_PollEvent which also pumps the message queue + // 3. The conflict causes crashes on frame 2 + // With single-threaded mode, bgfx::frame() does all the work synchronously. +#ifdef _WIN32 + // bgfx::renderFrame(); // Disabled - test_bgfx_minimal_win works without it +#endif + + bgfx::Init init; + init.type = bgfx::RendererType::Direct3D11; + init.resolution.width = width; + init.resolution.height = height; + init.resolution.reset = BGFX_RESET_VSYNC; + + // Set platform data + init.platformData.nwh = nativeWindowHandle; + init.platformData.ndt = nativeDisplayHandle; // X11 Display* on Linux, nullptr on Windows + + if (!bgfx::init(init)) { + return false; + } + + // Note: Debug text is enabled only when DebugOverlay is active + // Don't enable it by default as it can cause issues on some platforms + // bgfx::setDebug(BGFX_DEBUG_TEXT); + + // Set default view clear - BRIGHT RED for debugging + bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0xFF0000FF, 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(bgfxCaps->limits.maxTextureSize); + caps.maxViews = static_cast(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) { + // Build vertex layout based on layout type + bgfx::VertexLayout layout; + if (desc.layout == BufferDesc::PosColor) { + // vec3 position + vec4 color (7 floats = 28 bytes per vertex) + layout.begin() + .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) + .add(bgfx::Attrib::Color0, 4, bgfx::AttribType::Float, true) // normalized + .end(); + } else { + // Raw bytes - use 1-byte layout + layout.begin().add(bgfx::Attrib::Position, 1, bgfx::AttribType::Uint8).end(); + } + + if (desc.dynamic) { + bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( + desc.layout == BufferDesc::PosColor ? desc.size / layout.getStride() : desc.size, + layout, + BGFX_BUFFER_ALLOW_RESIZE + ); + result.id = dvb.idx | 0x8000; + } else { + // Use bgfx::copy instead of bgfx::makeRef to ensure data is copied + // This avoids potential issues with DLL memory visibility on Windows + bgfx::VertexBufferHandle vb = bgfx::createVertexBuffer( + desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::copy(s_emptyBuffer, 1), + layout + ); + 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 { + // Use bgfx::copy instead of bgfx::makeRef to ensure data is copied + bgfx::IndexBufferHandle ib = bgfx::createIndexBuffer( + desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::copy(s_emptyBuffer, 1) + ); + result.id = ib.idx; + } + } else { // Instance buffer - treated as dynamic vertex buffer + // Instance buffer layout: 5 x vec4 = 80 bytes per instance + // i_data0 (TEXCOORD7), i_data1 (TEXCOORD6), i_data2 (TEXCOORD5), + // i_data3 (TEXCOORD4), i_data4 (TEXCOORD3) + bgfx::VertexLayout layout; + layout.begin() + .add(bgfx::Attrib::TexCoord7, 4, bgfx::AttribType::Float) // i_data0: pos.xy, scale.xy + .add(bgfx::Attrib::TexCoord6, 4, bgfx::AttribType::Float) // i_data1: rotation, uv0.xy, unused + .add(bgfx::Attrib::TexCoord5, 4, bgfx::AttribType::Float) // i_data2: uv1.xy, unused, unused + .add(bgfx::Attrib::TexCoord4, 4, bgfx::AttribType::Float) // i_data3: reserved + .add(bgfx::Attrib::TexCoord3, 4, bgfx::AttribType::Float) // i_data4: color rgba + .end(); + // 80 bytes per instance + uint32_t instanceCount = desc.size / 80; + bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( + instanceCount, + layout, + 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)); + } + + // ======================================== + // Transient Instance Buffers + // ======================================== + + TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) override { + TransientInstanceBuffer result; + + constexpr uint16_t INSTANCE_STRIDE = 80; // 5 x vec4 + + // Check if we have space in the pool + if (m_transientPoolCount >= MAX_TRANSIENT_BUFFERS) { + return result; // Pool full, return invalid + } + + // Check if bgfx has enough transient memory + if (bgfx::getAvailInstanceDataBuffer(count, INSTANCE_STRIDE) < count) { + return result; // Not enough memory + } + + // Allocate from bgfx + uint16_t poolIndex = m_transientPoolCount++; + bgfx::allocInstanceDataBuffer(&m_transientPool[poolIndex], count, INSTANCE_STRIDE); + + result.data = m_transientPool[poolIndex].data; + result.size = count * INSTANCE_STRIDE; + result.count = count; + result.stride = INSTANCE_STRIDE; + result.poolIndex = poolIndex; + + return result; + } + + // ======================================== + // 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 { + // Ensure view 0 is processed even if nothing was rendered to it + bgfx::touch(0); + + // Present frame + // Note: bgfx must be linked statically on Windows to avoid TLS/threading crashes. + // Use BgfxRenderer_static library instead of BgfxRenderer DLL. + bgfx::frame(); + + // Reset transient pool for next frame + m_transientPoolCount = 0; + } + + void executeCommandBuffer(const RHICommandBuffer& cmdBuffer) override { + // Reset transient instance state for this command buffer execution + m_useTransientInstance = false; + m_currentTransientIndex = UINT16_MAX; + + // Track current state for bgfx calls + RenderState currentState; + BufferHandle currentVB; + BufferHandle currentIB; + BufferHandle currentInstBuffer; + uint32_t instStart = 0; + uint32_t instCount = 0; + + // Store texture state to apply at draw time (not immediately) + TextureHandle pendingTexture; + UniformHandle pendingSampler; + uint8_t pendingTextureSlot = 0; + bool hasTexture = false; + + for (const Command& cmd : cmdBuffer.getCommands()) { + switch (cmd.type) { + case CommandType::SetState: { + currentState = cmd.setState.state; + // Build bgfx state flags + uint64_t state = BGFX_STATE_WRITE_RGB | BGFX_STATE_WRITE_A; + + switch (currentState.blend) { + case BlendMode::Alpha: + state |= BGFX_STATE_BLEND_ALPHA; + break; + case BlendMode::Additive: + state |= BGFX_STATE_BLEND_ADD; + break; + case BlendMode::Multiply: + state |= BGFX_STATE_BLEND_MULTIPLY; + break; + case BlendMode::None: + default: + break; + } + + switch (currentState.cull) { + case CullMode::CW: + state |= BGFX_STATE_CULL_CW; + break; + case CullMode::CCW: + state |= BGFX_STATE_CULL_CCW; + break; + case CullMode::None: + default: + break; + } + + // Primitive type + switch (currentState.primitive) { + case PrimitiveType::Lines: + state |= BGFX_STATE_PT_LINES; + break; + case PrimitiveType::Points: + state |= BGFX_STATE_PT_POINTS; + break; + case PrimitiveType::Triangles: + default: + // Triangles is default, no flag needed + break; + } + + if (currentState.depthTest) { + state |= BGFX_STATE_DEPTH_TEST_LESS; + } + if (currentState.depthWrite) { + state |= BGFX_STATE_WRITE_Z; + } + + bgfx::setState(state); + break; + } + + case CommandType::SetTexture: { + // Store texture state - apply at draw time, not immediately + // This ensures texture is set after all other state is configured + pendingTexture = cmd.setTexture.texture; + pendingSampler = cmd.setTexture.sampler; + pendingTextureSlot = cmd.setTexture.slot; + hasTexture = true; + break; + } + + case CommandType::SetUniform: { + bgfx::UniformHandle uniform = { cmd.setUniform.uniform.id }; + bgfx::setUniform(uniform, cmd.setUniform.data, cmd.setUniform.numVec4s); + break; + } + + case CommandType::SetVertexBuffer: { + currentVB = cmd.setVertexBuffer.buffer; + break; + } + + case CommandType::SetIndexBuffer: { + currentIB = cmd.setIndexBuffer.buffer; + break; + } + + case CommandType::SetInstanceBuffer: { + currentInstBuffer = cmd.setInstanceBuffer.buffer; + instStart = cmd.setInstanceBuffer.start; + instCount = cmd.setInstanceBuffer.count; + m_useTransientInstance = false; + break; + } + + case CommandType::SetTransientInstanceBuffer: { + m_currentTransientIndex = cmd.setTransientInstanceBuffer.poolIndex; + instStart = cmd.setTransientInstanceBuffer.start; + instCount = cmd.setTransientInstanceBuffer.count; + m_useTransientInstance = true; + break; + } + + case CommandType::SetScissor: { + bgfx::setScissor(cmd.setScissor.x, cmd.setScissor.y, + cmd.setScissor.w, cmd.setScissor.h); + break; + } + + case CommandType::Draw: { + // Set vertex buffer before draw + if (currentVB.isValid()) { + bool isDynamic = (currentVB.id & 0x8000) != 0; + uint16_t idx = currentVB.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicVertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h, 0, cmd.draw.vertexCount); + } else { + bgfx::VertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h, cmd.draw.startVertex, cmd.draw.vertexCount); + } + } + break; + } + + case CommandType::DrawIndexed: { + // Set vertex and index buffers before draw + if (currentVB.isValid()) { + bool isDynamic = (currentVB.id & 0x8000) != 0; + uint16_t idx = currentVB.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicVertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h); + } else { + bgfx::VertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h); + } + } + if (currentIB.isValid()) { + bool isDynamic = (currentIB.id & 0x8000) != 0; + uint16_t idx = currentIB.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicIndexBufferHandle h = { idx }; + bgfx::setIndexBuffer(h, cmd.drawIndexed.startIndex, cmd.drawIndexed.indexCount); + } else { + bgfx::IndexBufferHandle h = { idx }; + bgfx::setIndexBuffer(h, cmd.drawIndexed.startIndex, cmd.drawIndexed.indexCount); + } + } + break; + } + + case CommandType::DrawInstanced: { + // Set vertex, index, and instance buffers + if (currentVB.isValid()) { + bool isDynamic = (currentVB.id & 0x8000) != 0; + uint16_t idx = currentVB.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicVertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h); + } else { + bgfx::VertexBufferHandle h = { idx }; + bgfx::setVertexBuffer(0, h); + } + } + if (currentIB.isValid()) { + bool isDynamic = (currentIB.id & 0x8000) != 0; + uint16_t idx = currentIB.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicIndexBufferHandle h = { idx }; + bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount); + } else { + bgfx::IndexBufferHandle h = { idx }; + bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount); + } + } + // Set instance buffer (either dynamic or transient) + if (m_useTransientInstance && m_currentTransientIndex < m_transientPoolCount) { + // Transient instance buffer from pool + bgfx::setInstanceDataBuffer(&m_transientPool[m_currentTransientIndex], instStart, instCount); + } else if (currentInstBuffer.isValid()) { + bool isDynamic = (currentInstBuffer.id & 0x8000) != 0; + uint16_t idx = currentInstBuffer.id & 0x7FFF; + if (isDynamic) { + bgfx::DynamicVertexBufferHandle h = { idx }; + bgfx::setInstanceDataBuffer(h, instStart, instCount); + } + } + break; + } + + case CommandType::Submit: { + // Apply pending texture right before submit + if (hasTexture) { + bgfx::TextureHandle tex = { pendingTexture.id }; + bgfx::UniformHandle sampler = { pendingSampler.id }; + bgfx::setTexture(pendingTextureSlot, sampler, tex); + } + bgfx::ProgramHandle program = { cmd.submit.shader.id }; + bgfx::submit(cmd.submit.view, program, cmd.submit.depth); + // Reset texture state after submit (consumed) + hasTexture = false; + break; + } + } + } + } + +private: + uint16_t m_width = 0; + uint16_t m_height = 0; + bool m_initialized = false; + + // Transient instance buffer pool (reset each frame) + static constexpr uint16_t MAX_TRANSIENT_BUFFERS = 256; + bgfx::InstanceDataBuffer m_transientPool[MAX_TRANSIENT_BUFFERS] = {}; + uint16_t m_transientPoolCount = 0; + + // Transient buffer state for command execution + bool m_useTransientInstance = false; + uint16_t m_currentTransientIndex = UINT16_MAX; + + // Empty buffer for null data fallback in buffer creation + inline static const uint8_t s_emptyBuffer[1] = {0}; + + 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::create() { + return std::make_unique(); +} + +} // namespace grove::rhi diff --git a/modules/BgfxRenderer/RHI/RHITypes.h b/modules/BgfxRenderer/RHI/RHITypes.h index af621dc..c3e97e5 100644 --- a/modules/BgfxRenderer/RHI/RHITypes.h +++ b/modules/BgfxRenderer/RHI/RHITypes.h @@ -65,9 +65,16 @@ enum class CullMode : uint8_t { CCW }; +enum class PrimitiveType : uint8_t { + Triangles, + Lines, + Points +}; + struct RenderState { BlendMode blend = BlendMode::Alpha; CullMode cull = CullMode::None; + PrimitiveType primitive = PrimitiveType::Triangles; bool depthTest = false; bool depthWrite = false; }; diff --git a/modules/BgfxRenderer/Scene/SceneCollector.cpp b/modules/BgfxRenderer/Scene/SceneCollector.cpp index cc550b8..4adba25 100644 --- a/modules/BgfxRenderer/Scene/SceneCollector.cpp +++ b/modules/BgfxRenderer/Scene/SceneCollector.cpp @@ -3,6 +3,7 @@ #include "grove/IDataNode.h" #include "../Frame/FrameAllocator.h" #include +#include namespace grove { @@ -196,6 +197,7 @@ void SceneCollector::parseSprite(const IDataNode& data) { sprite.y = static_cast(data.getDouble("y", 0.0)); sprite.scaleX = static_cast(data.getDouble("scaleX", 1.0)); sprite.scaleY = static_cast(data.getDouble("scaleY", 1.0)); + // i_data1 sprite.rotation = static_cast(data.getDouble("rotation", 0.0)); sprite.u0 = static_cast(data.getDouble("u0", 0.0)); @@ -366,8 +368,13 @@ void SceneCollector::parseDebugRect(const IDataNode& data) { DebugRect rect; rect.x = static_cast(data.getDouble("x", 0.0)); rect.y = static_cast(data.getDouble("y", 0.0)); - rect.w = static_cast(data.getDouble("w", 0.0)); - rect.h = static_cast(data.getDouble("h", 0.0)); + // Accept both "w"/"h" and "width"/"height" for convenience + double w = data.getDouble("w", 0.0); + if (w == 0.0) w = data.getDouble("width", 0.0); + double h = data.getDouble("h", 0.0); + if (h == 0.0) h = data.getDouble("height", 0.0); + rect.w = static_cast(w); + rect.h = static_cast(h); rect.color = static_cast(data.getInt("color", 0x00FF00FF)); rect.filled = data.getBool("filled", false); diff --git a/modules/BgfxRenderer/Shaders/ShaderManager.cpp b/modules/BgfxRenderer/Shaders/ShaderManager.cpp index 742a819..de508ea 100644 --- a/modules/BgfxRenderer/Shaders/ShaderManager.cpp +++ b/modules/BgfxRenderer/Shaders/ShaderManager.cpp @@ -124,6 +124,11 @@ void ShaderManager::loadSpriteShader(rhi::IRHIDevice& device, const std::string& vsSize = sizeof(vs_sprite_mtl); fsData = fs_sprite_mtl; fsSize = sizeof(fs_sprite_mtl); + } else if (rendererName == "Direct3D 11" || rendererName == "Direct3D 12") { + vsData = vs_sprite_dx11; + vsSize = sizeof(vs_sprite_dx11); + fsData = fs_sprite_dx11; + fsSize = sizeof(fs_sprite_dx11); } else { // Fallback to Vulkan (most common in WSL2) vsData = vs_sprite_spv; diff --git a/modules/BgfxRenderer/Shaders/fs_sprite.bin.h b/modules/BgfxRenderer/Shaders/fs_sprite.bin.h index 6f4e4aa..8be6378 100644 --- a/modules/BgfxRenderer/Shaders/fs_sprite.bin.h +++ b/modules/BgfxRenderer/Shaders/fs_sprite.bin.h @@ -155,3 +155,38 @@ static const uint8_t fs_sprite_mtl[] = { 0x0a, 0x7d, 0x0a, 0x0a, 0x00, 0x00, 0x20, 0x00 }; static const unsigned int fs_sprite_mtl_len = sizeof(fs_sprite_mtl); + +// D3D11 bytecode - compiled with shaderc for Shader Model 5.0 +static const uint8_t fs_sprite_dx11[464] = +{ + 0x46, 0x53, 0x48, 0x0b, 0x01, 0x83, 0xf2, 0xe1, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x0a, 0x73, // FSH............s + 0x5f, 0x74, 0x65, 0x78, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x30, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, // _texColor0...... + 0x00, 0x00, 0x00, 0x0a, 0x73, 0x5f, 0x74, 0x65, 0x78, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x30, 0x01, // ....s_texColor0. + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x44, 0x58, 0x42, 0x43, // ............DXBC + 0x20, 0x19, 0x9e, 0xef, 0x46, 0xd7, 0xd2, 0x3f, 0xe3, 0xed, 0x65, 0x99, 0xfb, 0x76, 0x3c, 0xf8, // ...F..?..e..v<. + 0x01, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, // ............,... + 0xa0, 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x49, 0x53, 0x47, 0x4e, 0x6c, 0x00, 0x00, 0x00, // ........ISGNl... + 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........P....... + 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, // ................ + 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, // ................ + 0x01, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........b....... + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, // ................ + 0x53, 0x56, 0x5f, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x43, 0x4f, 0x4c, 0x4f, // SV_POSITION.COLO + 0x52, 0x00, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x00, 0xab, 0x4f, 0x53, 0x47, 0x4e, // R.TEXCOORD..OSGN + 0x2c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, // ,........... ... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + 0x0f, 0x00, 0x00, 0x00, 0x53, 0x56, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54, 0x00, 0xab, 0xab, // ....SV_TARGET... + 0x53, 0x48, 0x45, 0x58, 0xb4, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, // SHEX....P...-... + 0x6a, 0x88, 0x00, 0x01, 0x5a, 0x00, 0x00, 0x03, 0x00, 0x60, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // j...Z....`...... + 0x58, 0x18, 0x00, 0x04, 0x00, 0x70, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, // X....p......UU.. + 0x62, 0x10, 0x00, 0x03, 0xf2, 0x10, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x62, 0x10, 0x00, 0x03, // b...........b... + 0x32, 0x10, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x03, 0xf2, 0x20, 0x10, 0x00, // 2.......e.... .. + 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x8b, // ....h.......E... + 0xc2, 0x00, 0x00, 0x80, 0x43, 0x55, 0x15, 0x00, 0xf2, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // ....CU.......... + 0x46, 0x10, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x46, 0x7e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // F.......F~...... + 0x00, 0x60, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0xf2, 0x00, 0x10, 0x00, // .`......6....... + 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, // ....F.......8... + 0xf2, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // . ......F....... + 0x46, 0x1e, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // F.......>....... +}; +static const unsigned int fs_sprite_dx11_len = sizeof(fs_sprite_dx11); diff --git a/modules/BgfxRenderer/Shaders/vs_sprite.bin.h b/modules/BgfxRenderer/Shaders/vs_sprite.bin.h index 66a66da..b7f134d 100644 --- a/modules/BgfxRenderer/Shaders/vs_sprite.bin.h +++ b/modules/BgfxRenderer/Shaders/vs_sprite.bin.h @@ -373,3 +373,112 @@ static const uint8_t vs_sprite_mtl[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x40, 0x00 }; static const unsigned int vs_sprite_mtl_len = sizeof(vs_sprite_mtl); + +// D3D11 bytecode - compiled with shaderc for Shader Model 5.0 +static const uint8_t vs_sprite_dx11[1633] = +{ + 0x56, 0x53, 0x48, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x01, 0x83, 0xf2, 0xe1, 0x01, 0x00, 0x0a, 0x75, // VSH............u + 0x5f, 0x76, 0x69, 0x65, 0x77, 0x50, 0x72, 0x6f, 0x6a, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, // _viewProj....... + 0x00, 0x00, 0x00, 0x28, 0x06, 0x00, 0x00, 0x44, 0x58, 0x42, 0x43, 0xdb, 0xbf, 0xa7, 0xd1, 0xaa, // ...(...DXBC..... + 0x42, 0xf6, 0x9b, 0xee, 0xb1, 0xb3, 0xd0, 0x4c, 0xe4, 0xb7, 0x78, 0x01, 0x00, 0x00, 0x00, 0x28, // B......L..x....( + 0x06, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x70, // .......,.......p + 0x01, 0x00, 0x00, 0x49, 0x53, 0x47, 0x4e, 0xc8, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x08, // ...ISGN......... + 0x00, 0x00, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xb6, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, // ................ + 0x03, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // ................ + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x06, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0f, // ................ + 0x0f, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // ................ + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x0f, 0x01, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x04, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0f, // ................ + 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // ................ + 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0x43, 0x4f, 0x4c, 0x4f, 0x52, // ...........COLOR + 0x00, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x54, 0x45, 0x58, 0x43, 0x4f, 0x4f, // .POSITION.TEXCOO + 0x52, 0x44, 0x00, 0x4f, 0x53, 0x47, 0x4e, 0x6c, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x08, // RD.OSGNl........ + 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, // ...P............ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x00, // ................ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0f, // ................ + 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // ...b............ + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x0c, 0x00, 0x00, 0x53, 0x56, 0x5f, 0x50, 0x4f, // ...........SV_PO + 0x53, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x00, 0x43, 0x4f, 0x4c, 0x4f, 0x52, 0x00, 0x54, 0x45, 0x58, // SITION.COLOR.TEX + 0x43, 0x4f, 0x4f, 0x52, 0x44, 0x00, 0xab, 0x53, 0x48, 0x45, 0x58, 0xb0, 0x04, 0x00, 0x00, 0x50, // COORD..SHEX....P + 0x00, 0x01, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x6a, 0x88, 0x00, 0x01, 0x59, 0x00, 0x00, 0x04, 0x46, // ...,...j...Y...F + 0x8e, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x03, 0xf2, // . ........._.... + 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x03, 0x32, 0x10, 0x10, 0x00, 0x01, // ......._...2.... + 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x03, 0xf2, 0x10, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x5f, // ..._..........._ + 0x00, 0x00, 0x03, 0xf2, 0x10, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x03, 0x12, // ..........._.... + 0x10, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x03, 0xf2, 0x10, 0x10, 0x00, 0x06, // ......._........ + 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x04, 0xf2, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...g.... ....... + 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x03, 0xf2, 0x20, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x65, // ...e.... ......e + 0x00, 0x00, 0x03, 0x32, 0x20, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x02, 0x05, // ...2 ......h.... + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x32, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, // ...6...2.......F + 0x10, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0xc2, 0x00, 0x10, 0x00, 0x00, // .......6........ + 0x00, 0x00, 0x00, 0xa6, 0x1e, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x12, // ...........6.... + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x10, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, // ...............6 + 0x00, 0x00, 0x05, 0x12, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1a, 0x10, 0x10, 0x00, 0x03, // ................ + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x22, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2a, // ...6...".......* + 0x10, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x12, 0x00, 0x10, 0x00, 0x03, // .......6........ + 0x00, 0x00, 0x00, 0x3a, 0x10, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x22, // ...:.......6..." + 0x00, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0a, 0x10, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x4d, // ...............M + 0x00, 0x00, 0x06, 0x00, 0xd0, 0x00, 0x00, 0x22, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0a, // ......."........ + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x00, 0x06, 0x12, 0x00, 0x10, 0x00, 0x01, // .......M........ + 0x00, 0x00, 0x00, 0x00, 0xd0, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x36, // ...............6 + 0x00, 0x00, 0x08, 0xc2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, // ............@... + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0xbf, 0x00, 0x00, 0x00, 0xbf, 0x00, // ................ + 0x00, 0x00, 0x07, 0xc2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa6, 0x0e, 0x10, 0x00, 0x01, // ................ + 0x00, 0x00, 0x00, 0x06, 0x14, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x42, // ...........8...B + 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, // ...............* + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x82, 0x00, 0x10, 0x00, 0x02, // .......8........ + 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x01, // ...........:.... + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x06, 0x82, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3a, // ...6...........: + 0x00, 0x10, 0x80, 0x41, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x12, // ...A............ + 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x2a, // .......:.......* + 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x12, 0x00, 0x10, 0x00, 0x01, // .......8........ + 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x2a, 0x00, 0x10, 0x00, 0x01, // ...........*.... + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x22, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1a, // ...8..."........ + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // .......:........ + 0x00, 0x00, 0x07, 0x22, 0x00, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x01, // ..."............ + 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0xc2, // ...........8.... + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, // ................ + 0x04, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x32, 0x00, 0x10, 0x00, 0x00, // ...........2.... + 0x00, 0x00, 0x00, 0x46, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe6, 0x0a, 0x10, 0x00, 0x00, // ...F............ + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x08, 0xf2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, // ...8............ + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x8e, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .......F. ...... + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x08, 0xf2, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, // ...8...........V + 0x05, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x8e, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // .......F. ...... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xf2, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, // ...............F + 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, // .......F.......8 + 0x00, 0x00, 0x0b, 0xf2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x8e, 0x20, 0x00, 0x00, // ...........F. .. + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ........@....... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xf2, // ................ + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, // .......F.......F + 0x0e, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x0b, 0xf2, 0x00, 0x10, 0x00, 0x01, // .......8........ + 0x00, 0x00, 0x00, 0x46, 0x8e, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, // ...F. .......... + 0x40, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x80, 0x3f, 0x00, // @.....?...?...?. + 0x00, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x07, 0xf2, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, // ..?............F + 0x0e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x36, // .......F.......6 + 0x00, 0x00, 0x05, 0x12, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x02, // ................ + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x22, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x1a, // ...6..."........ + 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x12, 0x00, 0x10, 0x00, 0x03, // .......6........ + 0x00, 0x00, 0x00, 0x0a, 0x00, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x22, // ...........6..." + 0x00, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x36, // ...............6 + 0x00, 0x00, 0x05, 0x32, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x10, 0x10, 0x00, 0x01, // ...2.......F.... + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x06, 0xc2, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, // ...6............ + 0x04, 0x10, 0x80, 0x41, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xc2, // ...A............ + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa6, 0x0e, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, // ................ + 0x04, 0x10, 0x00, 0x03, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0x32, 0x00, 0x10, 0x00, 0x01, // .......8...2.... + 0x00, 0x00, 0x00, 0xe6, 0x0a, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x00, 0x10, 0x00, 0x01, // ...........F.... + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x32, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, // .......2.......F + 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36, // .......F.......6 + 0x00, 0x00, 0x05, 0x32, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x00, 0x10, 0x00, 0x01, // ...2.......F.... + 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x07, 0xf2, 0x00, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x46, // ...8...........F + 0x1e, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x1e, 0x10, 0x00, 0x06, 0x00, 0x00, 0x00, 0x36, // .......F.......6 + 0x00, 0x00, 0x05, 0xf2, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x0e, 0x10, 0x00, 0x00, // .... ......F.... + 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0xf2, 0x20, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, // ...6.... ......F + 0x0e, 0x10, 0x00, 0x02, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x05, 0x32, 0x20, 0x10, 0x00, 0x02, // .......6...2 ... + 0x00, 0x00, 0x00, 0x46, 0x00, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x01, 0x00, // ...F.......>.... + 0x07, 0x05, 0x00, 0x01, 0x00, 0x17, 0x00, 0x16, 0x00, 0x15, 0x00, 0x14, 0x00, 0x13, 0x00, 0x40, // ...............@ + 0x00, // . +}; +static const unsigned int vs_sprite_dx11_len = sizeof(vs_sprite_dx11); diff --git a/modules/InputModule/CMakeLists.txt b/modules/InputModule/CMakeLists.txt index 4c951f9..a64146b 100644 --- a/modules/InputModule/CMakeLists.txt +++ b/modules/InputModule/CMakeLists.txt @@ -1,6 +1,28 @@ # InputModule - Input capture and conversion module # Converts native input events (SDL, GLFW, etc.) to IIO messages +# Find SDL2 - REQUIRED for InputModule to function +find_package(SDL2 QUIET) + +# Check if SDL2 is available +set(SDL2_USABLE FALSE) +if(SDL2_FOUND) + set(SDL2_USABLE TRUE) + message(STATUS "InputModule: SDL2 found via find_package") +elseif(UNIX AND EXISTS "/usr/include/SDL2/SDL.h") + set(SDL2_USABLE TRUE) + message(STATUS "InputModule: SDL2 found at system path (Linux)") +endif() + +if(NOT SDL2_USABLE) + message(WARNING "InputModule: SDL2 not found - InputModule will NOT be built") + message(STATUS " On Windows MinGW, install via: pacman -S mingw-w64-ucrt-x86_64-SDL2") + message(STATUS " On Ubuntu/Debian, install via: apt install libsdl2-dev") + # Set a variable so parent can know InputModule was skipped + set(GROVE_INPUT_MODULE_SKIPPED TRUE PARENT_SCOPE) + return() +endif() + add_library(InputModule SHARED InputModule.cpp Core/InputState.cpp @@ -8,17 +30,13 @@ add_library(InputModule SHARED Backends/SDLBackend.cpp ) -target_include_directories(InputModule - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} - PRIVATE - ${CMAKE_SOURCE_DIR}/include - /usr/include/SDL2 -) - -# Try to find SDL2, but don't fail if not found (use system paths) -find_package(SDL2 QUIET) - if(SDL2_FOUND) + target_include_directories(InputModule + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ) + target_link_libraries(InputModule PRIVATE GroveEngine::impl @@ -27,7 +45,14 @@ if(SDL2_FOUND) spdlog::spdlog ) else() - # Fallback to system SDL2 + # Linux system SDL2 + target_include_directories(InputModule + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE + ${CMAKE_SOURCE_DIR}/include + /usr/include/SDL2 + ) + target_link_libraries(InputModule PRIVATE GroveEngine::impl diff --git a/modules/UIModule/CMakeLists.txt b/modules/UIModule/CMakeLists.txt index d3f33ca..fe4376c 100644 --- a/modules/UIModule/CMakeLists.txt +++ b/modules/UIModule/CMakeLists.txt @@ -70,3 +70,56 @@ if(UNIX AND NOT APPLE) dl ) endif() + +# ============================================================================ +# UIModule Static Library (for tests that can't use DLL loading) +# ============================================================================ + +add_library(UIModule_static STATIC + # Main module + UIModule.cpp + + # Core + Core/UITree.cpp + Core/UILayout.cpp + Core/UIContext.cpp + Core/UIStyle.cpp + Core/UITooltip.cpp + + # Widgets + Widgets/UIPanel.cpp + Widgets/UILabel.cpp + Widgets/UIButton.cpp + Widgets/UIImage.cpp + Widgets/UISlider.cpp + Widgets/UICheckbox.cpp + Widgets/UIProgressBar.cpp + Widgets/UITextInput.cpp + Widgets/UIScrollPanel.cpp + + # Rendering + Rendering/UIRenderer.cpp +) + +target_include_directories(UIModule_static PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +target_link_libraries(UIModule_static PUBLIC + GroveEngine::impl + spdlog::spdlog + nlohmann_json::nlohmann_json +) + +target_compile_features(UIModule_static PRIVATE cxx_std_17) + +# Mark as static build to skip C exports (avoids multiple definition errors) +target_compile_definitions(UIModule_static PRIVATE GROVE_MODULE_STATIC) + +if(WIN32) + target_compile_definitions(UIModule_static PRIVATE + WIN32_LEAN_AND_MEAN + NOMINMAX + ) +endif() diff --git a/modules/UIModule/Core/UIContext.h b/modules/UIModule/Core/UIContext.h index 2344886..bc1ccb1 100644 --- a/modules/UIModule/Core/UIContext.h +++ b/modules/UIModule/Core/UIContext.h @@ -48,7 +48,8 @@ public: keyCode = 0; keyChar = 0; mouseWheelDelta = 0.0f; - hoveredWidgetId.clear(); + // Note: hoveredWidgetId is NOT cleared here - it persists + // and is updated by hit testing during updateUI() } /** diff --git a/modules/UIModule/Core/UITree.cpp b/modules/UIModule/Core/UITree.cpp index f59f17b..641f1f1 100644 --- a/modules/UIModule/Core/UITree.cpp +++ b/modules/UIModule/Core/UITree.cpp @@ -66,61 +66,60 @@ void UITree::registerDefaultWidgets() { button->onClick = node.getString("onClick", ""); button->enabled = node.getBool("enabled", true); + // Helper lambda to parse a button style + auto parseButtonStyle = [](IDataNode* styleNode, ButtonStyle& style) { + if (!styleNode) return; + + std::string bgColorStr = styleNode->getString("bgColor", ""); + if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) { + style.bgColor = static_cast(std::stoul(bgColorStr, nullptr, 16)); + } + std::string textColorStr = styleNode->getString("textColor", ""); + if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) { + style.textColor = static_cast(std::stoul(textColorStr, nullptr, 16)); + } + std::string borderColorStr = styleNode->getString("borderColor", ""); + if (borderColorStr.size() >= 2 && (borderColorStr.substr(0, 2) == "0x" || borderColorStr.substr(0, 2) == "0X")) { + style.borderColor = static_cast(std::stoul(borderColorStr, nullptr, 16)); + } + style.borderWidth = static_cast(styleNode->getDouble("borderWidth", style.borderWidth)); + style.borderRadius = static_cast(styleNode->getDouble("borderRadius", style.borderRadius)); + style.textureId = styleNode->getInt("textureId", 0); + style.useTexture = style.textureId > 0; + }; + // Parse style (const_cast safe for read-only operations) auto& mutableNode = const_cast(node); if (auto* style = mutableNode.getChildReadOnly("style")) { // Normal style if (auto* normalStyle = style->getChildReadOnly("normal")) { - std::string bgColorStr = normalStyle->getString("bgColor", "0x444444FF"); - if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) { - button->normalStyle.bgColor = static_cast(std::stoul(bgColorStr, nullptr, 16)); - } - std::string textColorStr = normalStyle->getString("textColor", "0xFFFFFFFF"); - if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) { - button->normalStyle.textColor = static_cast(std::stoul(textColorStr, nullptr, 16)); - } + parseButtonStyle(normalStyle, button->normalStyle); } // Hover style if (auto* hoverStyle = style->getChildReadOnly("hover")) { - std::string bgColorStr = hoverStyle->getString("bgColor", "0x666666FF"); - if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) { - button->hoverStyle.bgColor = static_cast(std::stoul(bgColorStr, nullptr, 16)); - } - std::string textColorStr = hoverStyle->getString("textColor", "0xFFFFFFFF"); - if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) { - button->hoverStyle.textColor = static_cast(std::stoul(textColorStr, nullptr, 16)); - } + parseButtonStyle(hoverStyle, button->hoverStyle); + button->hoverStyleSet = true; } // Pressed style if (auto* pressedStyle = style->getChildReadOnly("pressed")) { - std::string bgColorStr = pressedStyle->getString("bgColor", "0x333333FF"); - if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) { - button->pressedStyle.bgColor = static_cast(std::stoul(bgColorStr, nullptr, 16)); - } - std::string textColorStr = pressedStyle->getString("textColor", "0xFFFFFFFF"); - if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) { - button->pressedStyle.textColor = static_cast(std::stoul(textColorStr, nullptr, 16)); - } + parseButtonStyle(pressedStyle, button->pressedStyle); + button->pressedStyleSet = true; } // Disabled style if (auto* disabledStyle = style->getChildReadOnly("disabled")) { - std::string bgColorStr = disabledStyle->getString("bgColor", "0x222222FF"); - if (bgColorStr.size() >= 2 && (bgColorStr.substr(0, 2) == "0x" || bgColorStr.substr(0, 2) == "0X")) { - button->disabledStyle.bgColor = static_cast(std::stoul(bgColorStr, nullptr, 16)); - } - std::string textColorStr = disabledStyle->getString("textColor", "0x666666FF"); - if (textColorStr.size() >= 2 && (textColorStr.substr(0, 2) == "0x" || textColorStr.substr(0, 2) == "0X")) { - button->disabledStyle.textColor = static_cast(std::stoul(textColorStr, nullptr, 16)); - } + parseButtonStyle(disabledStyle, button->disabledStyle); } // Font size from style root button->fontSize = static_cast(style->getDouble("fontSize", 16.0)); } + // Auto-generate hover/pressed styles if not explicitly set + button->generateDefaultStyles(); + return button; }); diff --git a/modules/UIModule/Rendering/UIRenderer.cpp b/modules/UIModule/Rendering/UIRenderer.cpp index 0ff8cf1..ae087de 100644 --- a/modules/UIModule/Rendering/UIRenderer.cpp +++ b/modules/UIModule/Rendering/UIRenderer.cpp @@ -1,5 +1,6 @@ #include "UIRenderer.h" #include +#include namespace grove { @@ -10,11 +11,19 @@ UIRenderer::UIRenderer(IIO* io) void UIRenderer::drawRect(float x, float y, float w, float h, uint32_t color) { if (!m_io) return; + // DEBUG: Log color being sent + static uint32_t lastLoggedColor = 0; + if (color != lastLoggedColor && (color == 0xFF0000FF || color == 0x00FF00FF)) { + spdlog::info("UIRenderer::drawRect color=0x{:08X} at ({}, {})", color, x, y); + lastLoggedColor = color; + } + auto sprite = std::make_unique("sprite"); - sprite->setDouble("x", static_cast(x)); - sprite->setDouble("y", static_cast(y)); - sprite->setDouble("width", static_cast(w)); - sprite->setDouble("height", static_cast(h)); + // Position at center of rect (sprite shader centers quads) + sprite->setDouble("x", static_cast(x + w * 0.5f)); + sprite->setDouble("y", static_cast(y + h * 0.5f)); + sprite->setDouble("scaleX", static_cast(w)); + sprite->setDouble("scaleY", static_cast(h)); sprite->setInt("color", static_cast(color)); sprite->setInt("textureId", 0); // White/solid color texture sprite->setInt("layer", nextLayer()); @@ -40,10 +49,11 @@ void UIRenderer::drawSprite(float x, float y, float w, float h, int textureId, u if (!m_io) return; auto sprite = std::make_unique("sprite"); - sprite->setDouble("x", static_cast(x)); - sprite->setDouble("y", static_cast(y)); - sprite->setDouble("width", static_cast(w)); - sprite->setDouble("height", static_cast(h)); + // Position at center of sprite (sprite shader centers quads) + sprite->setDouble("x", static_cast(x + w * 0.5f)); + sprite->setDouble("y", static_cast(y + h * 0.5f)); + sprite->setDouble("scaleX", static_cast(w)); + sprite->setDouble("scaleY", static_cast(h)); sprite->setInt("color", static_cast(color)); sprite->setInt("textureId", textureId); sprite->setInt("layer", nextLayer()); diff --git a/modules/UIModule/UIModule.cpp b/modules/UIModule/UIModule.cpp index 59c8e0d..a3b49c7 100644 --- a/modules/UIModule/UIModule.cpp +++ b/modules/UIModule/UIModule.cpp @@ -424,8 +424,11 @@ std::unique_ptr UIModule::getHealthStatus() { // ============================================================================ // C Export (required for dlopen/LoadLibrary) +// Skip when building as static library to avoid multiple definition errors // ============================================================================ +#ifndef GROVE_MODULE_STATIC + #ifdef _WIN32 #define GROVE_MODULE_EXPORT __declspec(dllexport) #else @@ -443,3 +446,5 @@ GROVE_MODULE_EXPORT void destroyModule(grove::IModule* module) { } } + +#endif // GROVE_MODULE_STATIC diff --git a/modules/UIModule/Widgets/UIButton.cpp b/modules/UIModule/Widgets/UIButton.cpp index 92af57a..cc70d59 100644 --- a/modules/UIModule/Widgets/UIButton.cpp +++ b/modules/UIModule/Widgets/UIButton.cpp @@ -2,6 +2,8 @@ #include "../Core/UIContext.h" #include "../Rendering/UIRenderer.h" #include +#include +#include namespace grove { @@ -30,8 +32,12 @@ void UIButton::update(UIContext& ctx, float deltaTime) { void UIButton::render(UIRenderer& renderer) { const ButtonStyle& style = getCurrentStyle(); - // Render background rectangle - renderer.drawRect(absX, absY, width, height, style.bgColor); + // Render background (texture or solid color) + if (style.useTexture && style.textureId > 0) { + renderer.drawSprite(absX, absY, width, height, style.textureId, style.bgColor); + } else { + renderer.drawRect(absX, absY, width, height, style.bgColor); + } // Render border if specified if (style.borderWidth > 0.0f) { @@ -53,6 +59,39 @@ void UIButton::render(UIRenderer& renderer) { renderChildren(renderer); } +void UIButton::generateDefaultStyles() { + // If hover style wasn't explicitly set, lighten normal color + if (!hoverStyleSet) { + hoverStyle = normalStyle; + hoverStyle.bgColor = adjustBrightness(normalStyle.bgColor, 1.2f); + } + + // If pressed style wasn't explicitly set, darken normal color + if (!pressedStyleSet) { + pressedStyle = normalStyle; + pressedStyle.bgColor = adjustBrightness(normalStyle.bgColor, 0.7f); + } + + // Disabled style: desaturate and dim + disabledStyle = normalStyle; + disabledStyle.bgColor = adjustBrightness(normalStyle.bgColor, 0.5f); + disabledStyle.textColor = 0x888888FF; +} + +uint32_t UIButton::adjustBrightness(uint32_t color, float factor) { + uint8_t r = (color >> 24) & 0xFF; + uint8_t g = (color >> 16) & 0xFF; + uint8_t b = (color >> 8) & 0xFF; + uint8_t a = color & 0xFF; + + // Adjust RGB, clamp to 0-255 + r = static_cast(std::min(255.0f, std::max(0.0f, r * factor))); + g = static_cast(std::min(255.0f, std::max(0.0f, g * factor))); + b = static_cast(std::min(255.0f, std::max(0.0f, b * factor))); + + return (r << 24) | (g << 16) | (b << 8) | a; +} + bool UIButton::containsPoint(float px, float py) const { return px >= absX && px < absX + width && py >= absY && py < absY + height; diff --git a/modules/UIModule/Widgets/UIButton.h b/modules/UIModule/Widgets/UIButton.h index 0172101..1c18701 100644 --- a/modules/UIModule/Widgets/UIButton.h +++ b/modules/UIModule/Widgets/UIButton.h @@ -25,6 +25,8 @@ struct ButtonStyle { uint32_t borderColor = 0x000000FF; float borderWidth = 0.0f; float borderRadius = 0.0f; + int textureId = 0; // 0 = no texture (solid color), >0 = texture ID + bool useTexture = false; }; /** @@ -71,16 +73,34 @@ public: ButtonStyle pressedStyle; ButtonStyle disabledStyle; + // Track if styles were explicitly set (for auto-generation) + bool hoverStyleSet = false; + bool pressedStyleSet = false; + // Current state ButtonState state = ButtonState::Normal; bool isHovered = false; bool isPressed = false; + /** + * @brief Auto-generate hover/pressed styles from normal style + * Call this after setting normalStyle if hover/pressed weren't explicitly set + */ + void generateDefaultStyles(); + private: /** * @brief Get the appropriate style for current state */ const ButtonStyle& getCurrentStyle() const; + + /** + * @brief Adjust color brightness + * @param color RGBA color + * @param factor >1 to lighten, <1 to darken + * @return Adjusted color + */ + static uint32_t adjustBrightness(uint32_t color, float factor); }; } // namespace grove diff --git a/modules/UIModule/Widgets/UIPanel.cpp b/modules/UIModule/Widgets/UIPanel.cpp index 8b2f89d..c38108d 100644 --- a/modules/UIModule/Widgets/UIPanel.cpp +++ b/modules/UIModule/Widgets/UIPanel.cpp @@ -2,6 +2,7 @@ #include "../Core/UIContext.h" #include "../Core/UILayout.h" #include "../Rendering/UIRenderer.h" +#include namespace grove { diff --git a/src/DebugEngine.cpp b/src/DebugEngine.cpp index f08a333..c1787d6 100644 --- a/src/DebugEngine.cpp +++ b/src/DebugEngine.cpp @@ -425,7 +425,7 @@ void DebugEngine::processClientMessages() { logger->trace("📨 Client {} has {} pending message(s)", i, messageCount); // Process a few messages per frame to avoid blocking - int messagesToProcess = std::min(messageCount, 5); + int messagesToProcess = (std::min)(messageCount, 5); for (int j = 0; j < messagesToProcess; ++j) { try { @@ -452,7 +452,7 @@ void DebugEngine::processCoordinatorMessages() { logger->trace("📨 Coordinator has {} pending message(s)", messageCount); // Process coordinator messages with higher priority - int messagesToProcess = std::min(messageCount, 10); + int messagesToProcess = (std::min)(messageCount, 10); for (int i = 0; i < messagesToProcess; ++i) { try { diff --git a/src/IntraIOManager.cpp b/src/IntraIOManager.cpp index e276203..80f5e8e 100644 --- a/src/IntraIOManager.cpp +++ b/src/IntraIOManager.cpp @@ -14,8 +14,8 @@ IntraIOManager::IntraIOManager() { logger->info("🌐🔗 IntraIOManager created - Central message router initialized"); // TEMPORARY: Disable batch thread to debug Windows crash - batchThreadRunning = true; - batchThread = std::thread(&IntraIOManager::batchFlushLoop, this); + batchThreadRunning = false; + // batchThread = std::thread(&IntraIOManager::batchFlushLoop, this); logger->info("⚠️ Batch flush thread DISABLED (debugging Windows crash)"); } @@ -26,14 +26,30 @@ IntraIOManager::~IntraIOManager() { if (batchThread.joinable()) { batchThread.join(); } - logger->info("🛑 Batch flush thread stopped"); - // Get stats before locking to avoid recursive lock - auto stats = getRoutingStats(); - logger->info("📊 Final routing stats:"); - logger->info(" Total routed messages: {}", stats["total_routed_messages"].get()); - logger->info(" Total routes: {}", stats["total_routes"].get()); - logger->info(" Active instances: {}", stats["active_instances"].get()); + // IMPORTANT: During static destruction order on Windows (especially MinGW GCC 15), + // spdlog's registry may be destroyed BEFORE this singleton destructor runs. + // Using a destroyed logger causes STATUS_STACK_BUFFER_OVERRUN (0xc0000409). + // We must check if our logger is still valid before using it. + bool loggerValid = false; + try { + // Check if spdlog registry still exists and our logger is registered + loggerValid = logger && spdlog::get(logger->name()) != nullptr; + } catch (...) { + // spdlog registry may throw during destruction + loggerValid = false; + } + + if (loggerValid) { + logger->info("🛑 Batch flush thread stopped"); + + // Get stats before locking to avoid recursive lock + auto stats = getRoutingStats(); + logger->info("📊 Final routing stats:"); + logger->info(" Total routed messages: {}", stats["total_routed_messages"].get()); + logger->info(" Total routes: {}", stats["total_routes"].get()); + logger->info(" Active instances: {}", stats["active_instances"].get()); + } { std::unique_lock lock(managerMutex); // WRITE - exclusive access needed @@ -48,7 +64,9 @@ IntraIOManager::~IntraIOManager() { batchBuffers.clear(); } - logger->info("🌐🔗 IntraIOManager destroyed"); + if (loggerValid) { + logger->info("🌐🔗 IntraIOManager destroyed"); + } } std::shared_ptr IntraIOManager::createInstance(const std::string& instanceId) { diff --git a/src/JsonDataNode.cpp b/src/JsonDataNode.cpp index 91fca12..6d143bd 100644 --- a/src/JsonDataNode.cpp +++ b/src/JsonDataNode.cpp @@ -35,29 +35,92 @@ std::unique_ptr JsonDataNode::getChild(const std::string& name) { } IDataNode* JsonDataNode::getChildReadOnly(const std::string& name) { + // First check if we already have this child node created auto it = m_children.find(name); - if (it == m_children.end()) { - return nullptr; + if (it != m_children.end()) { + return it->second.get(); } - // Return raw pointer without copying - valid as long as parent exists - return it->second.get(); + + // If not found in m_children, check if m_data has this key as an object/array + // and create a child node on demand + if (m_data.is_object() && m_data.contains(name)) { + const auto& childData = m_data[name]; + if (childData.is_object() || childData.is_array()) { + // Create a new child node from the JSON data + auto newChild = std::make_unique(name, childData, this, m_readOnly); + m_children[name] = std::move(newChild); + return m_children[name].get(); + } + } + + // Handle array access by numeric index (e.g., "0", "1", "2") + if (m_data.is_array()) { + try { + size_t index = std::stoul(name); + if (index < m_data.size()) { + const auto& childData = m_data[index]; + if (childData.is_object() || childData.is_array()) { + auto newChild = std::make_unique(name, childData, this, m_readOnly); + m_children[name] = std::move(newChild); + return m_children[name].get(); + } + } + } catch (...) { + // Not a valid numeric index, ignore + } + } + + return nullptr; } std::vector JsonDataNode::getChildNames() { std::vector names; - names.reserve(m_children.size()); + + // First, add names from m_children (already materialized nodes) for (const auto& [name, _] : m_children) { names.push_back(name); } + + // Also include object keys from m_data that haven't been materialized yet + if (m_data.is_object()) { + for (auto& [key, value] : m_data.items()) { + if (value.is_object() || value.is_array()) { + // Only add if not already in the list + if (m_children.find(key) == m_children.end()) { + names.push_back(key); + } + } + } + } + return names; } bool JsonDataNode::hasChildren() { - return !m_children.empty(); + if (!m_children.empty()) { + return true; + } + // Check if m_data has any object/array values + if (m_data.is_object()) { + for (auto& [key, value] : m_data.items()) { + if (value.is_object() || value.is_array()) { + return true; + } + } + } + return false; } bool JsonDataNode::hasChild(const std::string& name) const { - return m_children.find(name) != m_children.end(); + if (m_children.find(name) != m_children.end()) { + return true; + } + // Check if m_data has this key as an object/array + if (m_data.is_object() && m_data.contains(name)) { + const auto& val = m_data[name]; + return val.is_object() || val.is_array(); + } + return false; } // ======================================== diff --git a/src/SequentialModuleSystem.cpp b/src/SequentialModuleSystem.cpp index d60e5cf..e5609e3 100644 --- a/src/SequentialModuleSystem.cpp +++ b/src/SequentialModuleSystem.cpp @@ -13,8 +13,20 @@ SequentialModuleSystem::SequentialModuleSystem() { } SequentialModuleSystem::~SequentialModuleSystem() { - // Guard against logger being invalid during static destruction order - if (logger) { + // IMPORTANT: During static destruction order on Windows (especially MinGW GCC 15), + // spdlog's registry may be destroyed BEFORE this destructor runs. + // Using a destroyed logger causes STATUS_STACK_BUFFER_OVERRUN (0xc0000409). + // We must check if our logger is still valid before using it. + bool loggerValid = false; + try { + // Check if spdlog registry still exists and our logger is registered + loggerValid = logger && spdlog::get(logger->name()) != nullptr; + } catch (...) { + // spdlog registry may throw during destruction + loggerValid = false; + } + + if (loggerValid) { logger->info("🔧 SequentialModuleSystem destructor called"); if (module) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18a4082..2faccfe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,28 @@ # Hot-reload test suite +# ================================================================================ +# SDL2 Detection (used by visual tests, demos, and InputModule-dependent tests) +# ================================================================================ +find_package(SDL2 QUIET) + +# Determine if SDL2 is available (either via find_package or system install on Linux) +set(SDL2_AVAILABLE FALSE) +if(SDL2_FOUND) + set(SDL2_AVAILABLE TRUE) + message(STATUS "SDL2 found via find_package") +elseif(UNIX AND EXISTS "/usr/include/SDL2/SDL.h") + set(SDL2_AVAILABLE TRUE) + message(STATUS "SDL2 found at system path /usr/include/SDL2") +else() + message(STATUS "SDL2 not found - visual tests and SDL2-dependent demos will be disabled") + message(STATUS " On Windows MinGW, install via: pacman -S mingw-w64-ucrt-x86_64-SDL2") + message(STATUS " On Ubuntu/Debian, install via: apt install libsdl2-dev") +endif() + +# ================================================================================ +# Test Modules +# ================================================================================ + # Test module as shared library (.so) for hot-reload add_library(TestModule SHARED modules/TestModule.cpp @@ -678,48 +701,79 @@ if(GROVE_BUILD_BGFX_RENDERER) # CTest integration grove_add_test(BgfxRHI test_20_bgfx_rhi ${CMAKE_CURRENT_BINARY_DIR}) - # Test 21: Visual Triangle Test (requires SDL2 and display) - find_package(SDL2 QUIET) - if(SDL2_FOUND OR EXISTS "/usr/include/SDL2/SDL.h") + # Test 21+: Visual Tests (requires SDL2 and display) + if(SDL2_AVAILABLE) + # Test 21: Visual Triangle Test add_executable(test_21_bgfx_triangle visual/test_bgfx_triangle.cpp ) - target_include_directories(test_21_bgfx_triangle PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_21_bgfx_triangle PRIVATE - bgfx - bx - SDL2 - pthread - dl - X11 - GL - ) + if(SDL2_FOUND) + target_link_libraries(test_21_bgfx_triangle PRIVATE + bgfx + bx + SDL2::SDL2 + ) + else() + # Linux system SDL2 + target_include_directories(test_21_bgfx_triangle PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_21_bgfx_triangle PRIVATE + bgfx + bx + SDL2 + pthread + dl + X11 + GL + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_21_bgfx_triangle' enabled (run manually)") + # Minimal bgfx test for Windows (no DLL) + if(SDL2_FOUND) + # WIN32 makes it a Windows app; SDL2main provides WinMain->main conversion + add_executable(test_bgfx_minimal_win WIN32 + visual/test_bgfx_minimal_win.cpp + ) + target_link_libraries(test_bgfx_minimal_win PRIVATE + bgfx + bx + SDL2::SDL2main + SDL2::SDL2 + ) + message(STATUS "Minimal bgfx test 'test_bgfx_minimal_win' enabled") + endif() + # Test 22: Sprite Integration Test (requires SDL2, display, and BgfxRenderer module) add_executable(test_22_bgfx_sprites visual/test_bgfx_sprites.cpp ) - target_include_directories(test_22_bgfx_sprites PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_22_bgfx_sprites PRIVATE - GroveEngine::impl - bgfx - bx - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_22_bgfx_sprites PRIVATE + GroveEngine::impl + bgfx + bx + SDL2::SDL2 + ) + else() + target_include_directories(test_22_bgfx_sprites PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_22_bgfx_sprites PRIVATE + GroveEngine::impl + bgfx + bx + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_22_bgfx_sprites' enabled (run manually)") @@ -729,17 +783,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_23_bgfx_sprites_visual.cpp ) - target_include_directories(test_23_bgfx_sprites_visual PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_23_bgfx_sprites_visual PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_23_bgfx_sprites_visual PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_23_bgfx_sprites_visual PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_23_bgfx_sprites_visual PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_23_bgfx_sprites_visual' enabled (run manually)") @@ -750,17 +810,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_24_ui_basic.cpp ) - target_include_directories(test_24_ui_basic PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_24_ui_basic PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_24_ui_basic PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_24_ui_basic PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_24_ui_basic PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_24_ui_basic' enabled (run manually)") @@ -770,17 +836,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_25_ui_layout.cpp ) - target_include_directories(test_25_ui_layout PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_25_ui_layout PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_25_ui_layout PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_25_ui_layout PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_25_ui_layout PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_25_ui_layout' enabled (run manually)") @@ -790,17 +862,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_26_ui_buttons.cpp ) - target_include_directories(test_26_ui_buttons PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_26_ui_buttons PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_26_ui_buttons PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_26_ui_buttons PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_26_ui_buttons PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_26_ui_buttons' enabled (run manually)") @@ -810,17 +888,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_28_ui_scroll.cpp ) - target_include_directories(test_28_ui_scroll PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_28_ui_scroll PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_28_ui_scroll PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_28_ui_scroll PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_28_ui_scroll PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_28_ui_scroll' enabled (run manually)") @@ -830,17 +914,23 @@ if(GROVE_BUILD_BGFX_RENDERER) visual/test_29_ui_advanced.cpp ) - target_include_directories(test_29_ui_advanced PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_29_ui_advanced PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_29_ui_advanced PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_29_ui_advanced PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_29_ui_advanced PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display) message(STATUS "Visual test 'test_29_ui_advanced' enabled (run manually)") @@ -853,17 +943,26 @@ if(GROVE_BUILD_BGFX_RENDERER) ) target_include_directories(test_30_input_module PRIVATE - /usr/include/SDL2 ${CMAKE_SOURCE_DIR}/modules ) - target_link_libraries(test_30_input_module PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) + if(SDL2_FOUND) + target_link_libraries(test_30_input_module PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_include_directories(test_30_input_module PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_30_input_module PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + endif() # Not added to CTest (requires display and user interaction) message(STATUS "Visual test 'test_30_input_module' enabled (run manually)") @@ -877,16 +976,28 @@ if(GROVE_BUILD_BGFX_RENDERER) target_include_directories(test_full_stack_interactive PRIVATE ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer ) - # Platform-specific SDL2 and window system libraries + # Platform-specific: On Windows, link BgfxRenderer statically (bgfx DLL issues) if(WIN32) + # Static linking for BgfxRenderer - required on Windows + # bgfx crashes when loaded from DLL due to threading/TLS issues + target_compile_definitions(test_full_stack_interactive PRIVATE USE_STATIC_BGFX) target_link_libraries(test_full_stack_interactive PRIVATE GroveEngine::impl + BgfxRenderer_static SDL2::SDL2 spdlog::spdlog ) + # MSVC: Use console subsystem for main() entry point + if(MSVC) + set_target_properties(test_full_stack_interactive PROPERTIES + LINK_FLAGS "/SUBSYSTEM:CONSOLE" + ) + endif() else() + # Dynamic linking on Linux/Mac - works fine target_include_directories(test_full_stack_interactive PRIVATE /usr/include/SDL2 ) @@ -1192,23 +1303,151 @@ message(STATUS "Integration test 'IT_015_input_ui_integration_minimal' enabled ( # UIModule Interactive Showcase Demo # ============================================ -if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER) +if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER AND SDL2_AVAILABLE) add_executable(demo_ui_showcase demo/demo_ui_showcase.cpp ) - target_link_libraries(demo_ui_showcase PRIVATE - GroveEngine::core - GroveEngine::impl - SDL2 - pthread - dl - ) - - # Add X11 on Linux for SDL window integration - if(UNIX AND NOT APPLE) - target_link_libraries(demo_ui_showcase PRIVATE X11) + if(SDL2_FOUND) + target_link_libraries(demo_ui_showcase PRIVATE + GroveEngine::core + GroveEngine::impl + SDL2::SDL2 + ) + else() + target_link_libraries(demo_ui_showcase PRIVATE + GroveEngine::core + GroveEngine::impl + SDL2 + pthread + dl + ) + # Add X11 on Linux for SDL window integration + if(UNIX AND NOT APPLE) + target_link_libraries(demo_ui_showcase PRIVATE X11) + endif() endif() message(STATUS "UIModule showcase demo 'demo_ui_showcase' enabled") endif() + +# Minimal test using BgfxRenderer_static only (no DLL loading) +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER) + add_executable(test_bgfx_static_only WIN32 + visual/test_bgfx_static_only.cpp + ) + target_include_directories(test_bgfx_static_only PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ) + target_link_libraries(test_bgfx_static_only PRIVATE + SDL2::SDL2main + SDL2::SDL2 + GroveEngine::impl + BgfxRenderer_static + spdlog::spdlog + ) + message(STATUS "Minimal BgfxRenderer_static test 'test_bgfx_static_only' enabled") +endif() + +# Test using BgfxDevice directly (no BgfxRendererModule) +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER) + add_executable(test_bgfx_device_only WIN32 + visual/test_bgfx_device_only.cpp + ) + target_include_directories(test_bgfx_device_only PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ) + target_link_libraries(test_bgfx_device_only PRIVATE + SDL2::SDL2main + SDL2::SDL2 + BgfxRenderer_static + spdlog::spdlog + ) + message(STATUS "BgfxDevice direct test 'test_bgfx_device_only' enabled") +endif() + +# ================================================================================ +# Complete Showcases (Renderer and UI demonstrations) +# ================================================================================ + +# BgfxRenderer Complete Showcase - all rendering features +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND SDL2_AVAILABLE) + add_executable(test_renderer_showcase WIN32 + visual/test_renderer_showcase.cpp + ) + target_include_directories(test_renderer_showcase PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ) + target_link_libraries(test_renderer_showcase PRIVATE + SDL2::SDL2main + SDL2::SDL2 + GroveEngine::impl + BgfxRenderer_static + spdlog::spdlog + ) + message(STATUS "BgfxRenderer showcase 'test_renderer_showcase' enabled") +endif() + +# UIModule Complete Showcase - all UI widgets +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE) + add_executable(test_ui_showcase WIN32 + visual/test_ui_showcase.cpp + ) + target_include_directories(test_ui_showcase PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ${CMAKE_SOURCE_DIR}/modules/UIModule + ) + target_link_libraries(test_ui_showcase PRIVATE + SDL2::SDL2main + SDL2::SDL2 + GroveEngine::impl + BgfxRenderer_static + UIModule_static + spdlog::spdlog + ) + message(STATUS "UIModule showcase 'test_ui_showcase' enabled") +endif() + +# Sprite Rendering Diagnostic Test - isolates IIO sprite pipeline +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND SDL2_AVAILABLE) + add_executable(test_sprite_debug WIN32 + visual/test_sprite_debug.cpp + ) + target_include_directories(test_sprite_debug PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ) + target_link_libraries(test_sprite_debug PRIVATE + SDL2::SDL2main + SDL2::SDL2 + GroveEngine::impl + BgfxRenderer_static + spdlog::spdlog + ) + message(STATUS "Sprite diagnostic 'test_sprite_debug' enabled") +endif() + +# Single Button Test - one UIButton only +if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE) + add_executable(test_single_button WIN32 + visual/test_single_button.cpp + ) + target_include_directories(test_single_button PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ${CMAKE_SOURCE_DIR}/modules/BgfxRenderer + ${CMAKE_SOURCE_DIR}/modules/UIModule + ) + target_link_libraries(test_single_button PRIVATE + SDL2::SDL2main + SDL2::SDL2 + GroveEngine::impl + BgfxRenderer_static + UIModule_static + spdlog::spdlog + ) + message(STATUS "Single button test 'test_single_button' enabled") +endif() diff --git a/tests/visual/test_bgfx_device_only.cpp b/tests/visual/test_bgfx_device_only.cpp new file mode 100644 index 0000000..2cdaaa1 --- /dev/null +++ b/tests/visual/test_bgfx_device_only.cpp @@ -0,0 +1,90 @@ +/** + * Test using BgfxDevice directly via IRHIDevice::create() + * No ShaderManager, no RenderGraph - just init + frame + */ + +#include +#include +#include +#include + +#include "RHI/RHIDevice.h" + +#include +#include + +using namespace grove::rhi; + +int main(int argc, char* argv[]) { + auto logger = spdlog::stdout_color_mt("Main"); + spdlog::set_level(spdlog::level::info); + + logger->info("=== BgfxDevice Direct Test ==="); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + logger->error("SDL_Init failed: {}", SDL_GetError()); + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "BgfxDevice Direct Test", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 800, 600, + SDL_WINDOW_SHOWN + ); + + if (!window) { + logger->error("SDL_CreateWindow failed"); + SDL_Quit(); + return 1; + } + + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + logger->info("Window handle: {}", (void*)wmi.info.win.window); + + // Create BgfxDevice via factory + auto device = IRHIDevice::create(); + + logger->info("Initializing BgfxDevice..."); + if (!device->init(wmi.info.win.window, nullptr, 800, 600)) { + logger->error("BgfxDevice init failed"); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + logger->info("BgfxDevice initialized, starting loop..."); + + bool running = true; + int frameCount = 0; + + while (running && frameCount < 60) { + logger->info("Frame {} start", frameCount); + spdlog::default_logger()->flush(); + + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + } + + // Just frame() - exactly like test_bgfx_minimal_win + device->frame(); + + logger->info("Frame {} complete", frameCount); + frameCount++; + } + + logger->info("Rendered {} frames", frameCount); + + device->shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + + logger->info("=== Test complete ==="); + return 0; +} diff --git a/tests/visual/test_bgfx_minimal_win.cpp b/tests/visual/test_bgfx_minimal_win.cpp new file mode 100644 index 0000000..ebb144d --- /dev/null +++ b/tests/visual/test_bgfx_minimal_win.cpp @@ -0,0 +1,91 @@ +/** + * Minimal bgfx test for Windows - no DLL, just renders a red screen + */ + +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) { + std::cout << "=== Minimal bgfx test ===\n"; + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "bgfx minimal test", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 800, 600, + SDL_WINDOW_SHOWN + ); + + if (!window) { + std::cerr << "SDL_CreateWindow failed\n"; + SDL_Quit(); + return 1; + } + + // Get native window handle + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + // Setup bgfx + bgfx::Init init; + init.type = bgfx::RendererType::Direct3D11; + init.resolution.width = 800; + init.resolution.height = 600; + init.resolution.reset = BGFX_RESET_VSYNC; + +#ifdef _WIN32 + init.platformData.nwh = wmi.info.win.window; +#endif + + std::cout << "Initializing bgfx with D3D11...\n"; + + if (!bgfx::init(init)) { + std::cerr << "bgfx::init failed\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + const bgfx::Caps* caps = bgfx::getCaps(); + std::cout << "Renderer: " << bgfx::getRendererName(caps->rendererType) << "\n"; + + // Set bright red clear color + bgfx::setViewClear(0, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0xFF0000FF, 1.0f, 0); + bgfx::setViewRect(0, 0, 0, 800, 600); + + std::cout << "Running for 3 seconds...\n"; + + Uint32 start = SDL_GetTicks(); + bool running = true; + int frames = 0; + + while (running && (SDL_GetTicks() - start) < 3000) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + } + + bgfx::touch(0); + bgfx::frame(); + frames++; + } + + std::cout << "Rendered " << frames << " frames\n"; + + bgfx::shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "=== Test complete ===\n"; + return 0; +} diff --git a/tests/visual/test_bgfx_static_only.cpp b/tests/visual/test_bgfx_static_only.cpp new file mode 100644 index 0000000..c981799 --- /dev/null +++ b/tests/visual/test_bgfx_static_only.cpp @@ -0,0 +1,188 @@ +/** + * Minimal test using BgfxRenderer_static - no DLLs loaded + * Tests if BgfxRendererModule works in isolation + */ + +#include +#include +#include +#include +#include + +#include "BgfxRendererModule.h" +#include +#include +#include +#include +#include + +using namespace grove; + +int main(int argc, char* argv[]) { + // Setup logging + auto logger = spdlog::stdout_color_mt("Main"); + spdlog::set_level(spdlog::level::info); + + logger->info("=== BgfxRenderer Static Only Test ==="); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + logger->error("SDL_Init failed: {}", SDL_GetError()); + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "BgfxRenderer Static Test", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 800, 600, + SDL_WINDOW_SHOWN + ); + + if (!window) { + logger->error("SDL_CreateWindow failed: {}", SDL_GetError()); + SDL_Quit(); + return 1; + } + + // Get native window handle + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + logger->info("Window created, handle: {}", (void*)wmi.info.win.window); + + // Create IO instances from singleton manager + // NOTE: Must use singleton because IntraIO::publish/subscribe use IntraIOManager::getInstance() + // IMPORTANT: Need SEPARATE instances for publisher and subscriber because messages + // are not delivered back to the sender (see IntraIOManager::routeMessage) + auto rendererIOPtr = IntraIOManager::getInstance().createInstance("renderer"); + IIO* rendererIO = rendererIOPtr.get(); + + // Separate "game" instance for publishing render commands + auto gameIOPtr = IntraIOManager::getInstance().createInstance("game"); + IIO* gameIO = gameIOPtr.get(); + + // Create and configure renderer + auto renderer = std::make_unique(); + + JsonDataNode config("config"); + config.setDouble("nativeWindowHandle", static_cast(reinterpret_cast(wmi.info.win.window))); + config.setInt("windowWidth", 800); + config.setInt("windowHeight", 600); + config.setString("backend", "d3d11"); + config.setString("shaderPath", "./shaders"); + config.setBool("vsync", true); + + logger->info("Configuring BgfxRendererModule..."); + renderer->setConfiguration(config, rendererIO, nullptr); + + logger->info("BgfxRendererModule configured"); + + // Main loop + logger->info("Starting main loop..."); + bool running = true; + int frameCount = 0; + Uint64 start = SDL_GetPerformanceCounter(); + + // Setup camera for orthographic projection (required for pixel-space rendering) + { + auto cam = std::make_unique("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", 800); + cam->setInt("viewportH", 600); + gameIO->publish("render:camera", std::move(cam)); + } + + while (running && frameCount < 300) { // Run for 300 frames (~5 sec) + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + } + + // Send render commands via IIO before processing + // Draw debug rectangles (colored boxes without texture) + float t = frameCount * 0.02f; + + // Moving red rectangle + { + auto rect = std::make_unique("rect"); + rect->setDouble("x", 100 + std::sin(t) * 50); + rect->setDouble("y", 100); + rect->setDouble("width", 100); + rect->setDouble("height", 100); + rect->setInt("color", 0xFF0000FF); // Red, alpha=255 + rect->setBool("filled", true); + gameIO->publish("render:debug:rect", std::move(rect)); + } + + // Moving green rectangle + { + auto rect = std::make_unique("rect"); + rect->setDouble("x", 300); + rect->setDouble("y", 200 + std::cos(t) * 50); + rect->setDouble("width", 80); + rect->setDouble("height", 80); + rect->setInt("color", 0x00FF00FF); // Green + rect->setBool("filled", true); + gameIO->publish("render:debug:rect", std::move(rect)); + } + + // Rotating blue rectangle + { + auto rect = std::make_unique("rect"); + rect->setDouble("x", 500); + rect->setDouble("y", 300); + rect->setDouble("width", 120); + rect->setDouble("height", 60); + rect->setInt("color", 0x0088FFFF); // Blue + rect->setBool("filled", true); + gameIO->publish("render:debug:rect", std::move(rect)); + } + + // Draw debug lines + { + auto line = std::make_unique("line"); + line->setDouble("x1", 50); + line->setDouble("y1", 50); + line->setDouble("x2", 750); + line->setDouble("y2", 550); + line->setInt("color", 0xFFFF00FF); // Yellow + gameIO->publish("render:debug:line", std::move(line)); + } + + // Process renderer + JsonDataNode input("input"); + input.setDouble("deltaTime", 0.016); + input.setInt("frameCount", frameCount); + + renderer->process(input); + + frameCount++; + + // Log every 60 frames + if (frameCount % 60 == 0) { + logger->info("Frame {}", frameCount); + } + } + + Uint64 end = SDL_GetPerformanceCounter(); + double elapsed = (end - start) / (double)SDL_GetPerformanceFrequency(); + + logger->info("Rendered {} frames in {:.2f}s ({:.1f} FPS)", frameCount, elapsed, frameCount / elapsed); + + // Cleanup + logger->info("Shutting down..."); + renderer->shutdown(); + IntraIOManager::getInstance().removeInstance("renderer"); + IntraIOManager::getInstance().removeInstance("game"); + SDL_DestroyWindow(window); + SDL_Quit(); + + logger->info("=== Test complete ==="); + return 0; +} diff --git a/tests/visual/test_full_stack_interactive.cpp b/tests/visual/test_full_stack_interactive.cpp index 86a77ad..d74ff1b 100644 --- a/tests/visual/test_full_stack_interactive.cpp +++ b/tests/visual/test_full_stack_interactive.cpp @@ -11,6 +11,10 @@ * - Mouse: Click buttons, drag sliders * - Keyboard: Type in text input, press Space to spawn sprites * - ESC: Exit + * + * Build modes: + * - USE_STATIC_BGFX: Link BgfxRenderer statically (required on Windows) + * - Default: Load BgfxRenderer as DLL (works on Linux/Mac) */ #include @@ -34,6 +38,11 @@ #include #endif +// Static linking for BgfxRenderer (required on Windows due to bgfx DLL issues) +#ifdef USE_STATIC_BGFX +#include "BgfxRendererModule.h" +#endif + // Function pointer type for feedEvent (loaded from DLL) typedef void (*FeedEventFunc)(grove::IModule*, const void*); @@ -64,9 +73,9 @@ public: sprite.x += sprite.vx * deltaTime; sprite.y += sprite.vy * deltaTime; - // Bounce off walls - if (sprite.x < 0 || sprite.x > 1920) sprite.vx = -sprite.vx; - if (sprite.y < 0 || sprite.y > 1080) sprite.vy = -sprite.vy; + // Bounce off walls (1280x720 window) + if (sprite.x < 0 || sprite.x > 1280) sprite.vx = -sprite.vx; + if (sprite.y < 0 || sprite.y > 720) sprite.vy = -sprite.vy; } // Process events @@ -201,11 +210,13 @@ int main(int argc, char* argv[]) { return 1; } - // Create window + // Create window (windowed mode, not fullscreen) + const int WINDOW_WIDTH = 1280; + const int WINDOW_HEIGHT = 720; SDL_Window* window = SDL_CreateWindow( "GroveEngine - Full Stack Demo", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - 1920, 1080, + WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE ); @@ -239,37 +250,49 @@ int main(int argc, char* argv[]) { auto gameIO = ioManager.createInstance("game"); // Load modules - ModuleLoader rendererLoader, uiLoader, inputLoader; + ModuleLoader uiLoader, inputLoader; +#ifndef USE_STATIC_BGFX + ModuleLoader rendererLoader; +#endif - std::string rendererPath = "./modules/BgfxRenderer.dll"; std::string uiPath = "./modules/UIModule.dll"; std::string inputPath = "./modules/InputModule.dll"; #ifndef _WIN32 - rendererPath = "./modules/libBgfxRenderer.so"; uiPath = "./modules/libUIModule.so"; inputPath = "./modules/libInputModule.so"; #endif logger->info("Loading modules..."); - // Load BgfxRenderer + // Load/Create BgfxRenderer std::unique_ptr renderer; +#ifdef USE_STATIC_BGFX + // Static linking: instantiate directly (required on Windows) + renderer = std::make_unique(); + logger->info("✅ BgfxRenderer created (static)"); +#else + // Dynamic linking: load from DLL + std::string rendererPath = "./modules/BgfxRenderer.dll"; + #ifndef _WIN32 + rendererPath = "./modules/libBgfxRenderer.so"; + #endif try { renderer = rendererLoader.load(rendererPath, "renderer"); - logger->info("✅ BgfxRenderer loaded"); + logger->info("✅ BgfxRenderer loaded (dynamic)"); } catch (const std::exception& e) { logger->error("Failed to load BgfxRenderer: {}", e.what()); SDL_DestroyWindow(window); SDL_Quit(); return 1; } +#endif // Configure BgfxRenderer JsonDataNode rendererConfig("config"); - rendererConfig.setInt("windowWidth", 1920); - rendererConfig.setInt("windowHeight", 1080); - rendererConfig.setString("backend", "auto"); + rendererConfig.setInt("windowWidth", WINDOW_WIDTH); + rendererConfig.setInt("windowHeight", WINDOW_HEIGHT); + rendererConfig.setString("backend", "opengl"); // Force OpenGL instead of D3D11 rendererConfig.setBool("vsync", true); rendererConfig.setInt("nativeWindowHandle", (int)(intptr_t)nativeHandle); renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr); @@ -289,8 +312,8 @@ int main(int argc, char* argv[]) { // Configure UIModule with inline layout JsonDataNode uiConfig("config"); - uiConfig.setInt("windowWidth", 1920); - uiConfig.setInt("windowHeight", 1080); + uiConfig.setInt("windowWidth", WINDOW_WIDTH); + uiConfig.setInt("windowHeight", WINDOW_HEIGHT); uiConfig.setInt("baseLayer", 1000); // Create inline layout @@ -456,11 +479,18 @@ int main(int argc, char* argv[]) { int frameCount = 0; logger->info("Entering main loop..."); + spdlog::default_logger()->flush(); while (running) { + logger->info("Frame {} start", frameCount); + spdlog::default_logger()->flush(); + logger->info(" SDL_PollEvent..."); + spdlog::default_logger()->flush(); // Handle SDL events SDL_Event event; while (SDL_PollEvent(&event)) { + logger->info(" Event type: {}", event.type); + spdlog::default_logger()->flush(); if (event.type == SDL_QUIT) { running = false; } @@ -469,6 +499,8 @@ int main(int argc, char* argv[]) { } // Feed to InputModule via exported C function + logger->info(" feedEventFunc..."); + spdlog::default_logger()->flush(); feedEventFunc(inputModuleBase.get(), &event); } @@ -485,11 +517,18 @@ int main(int argc, char* argv[]) { input.setDouble("deltaTime", deltaTime); input.setInt("frameCount", frameCount); + logger->info("Processing input module..."); inputModuleBase->process(input); + logger->info("Processing UI module..."); uiModule->process(input); + logger->info("Updating game logic..."); gameLogic.update((float)deltaTime); + logger->info("Rendering game..."); gameLogic.render(rendererIO.get()); + logger->info("Processing renderer..."); + spdlog::default_logger()->flush(); renderer->process(input); + logger->info("Frame {} complete", frameCount); frameCount++; } diff --git a/tests/visual/test_full_stack_interactive.cpp.orig b/tests/visual/test_full_stack_interactive.cpp.orig new file mode 100644 index 0000000..5378109 --- /dev/null +++ b/tests/visual/test_full_stack_interactive.cpp.orig @@ -0,0 +1,546 @@ +/** + * Visual Test: Full Stack Interactive Demo + * + * Demonstrates complete integration of: + * - BgfxRenderer (2D rendering) + * - UIModule (widgets) + * - InputModule (mouse + keyboard) + * - Game logic responding to UI events + * + * Controls: + * - Mouse: Click buttons, drag sliders + * - Keyboard: Type in text input, press Space to spawn sprites + * - ESC: Exit + * + * Build modes: + * - USE_STATIC_BGFX: Link BgfxRenderer statically (required on Windows) + * - Default: Load BgfxRenderer as DLL (works on Linux/Mac) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +// Static linking for BgfxRenderer (required on Windows due to bgfx DLL issues) +#ifdef USE_STATIC_BGFX +#include "BgfxRendererModule.h" +#endif + +// Function pointer type for feedEvent (loaded from DLL) +typedef void (*FeedEventFunc)(grove::IModule*, const void*); + +using namespace grove; + +// Simple game state +struct Sprite { + float x, y; + float vx, vy; + uint32_t color; +}; + +class GameLogic { +public: + GameLogic(IIO* io) : m_io(io) { + m_logger = spdlog::stdout_color_mt("GameLogic"); + + // Subscribe to UI events + m_io->subscribe("ui:click"); + m_io->subscribe("ui:action"); + m_io->subscribe("ui:value_changed"); + m_io->subscribe("input:keyboard:key"); + } + + void update(float deltaTime) { + // Update sprites + for (auto& sprite : m_sprites) { + sprite.x += sprite.vx * deltaTime; + sprite.y += sprite.vy * deltaTime; + + // Bounce off walls (1280x720 window) + if (sprite.x < 0 || sprite.x > 1280) sprite.vx = -sprite.vx; + if (sprite.y < 0 || sprite.y > 720) sprite.vy = -sprite.vy; + } + + // Process events + while (m_io->hasMessages() > 0) { + auto msg = m_io->pullMessage(); + + if (msg.topic == "ui:action") { + std::string action = msg.data->getString("action", ""); + m_logger->info("UI Action: {}", action); + + if (action == "spawn_sprite") { + spawnSprite(); + } else if (action == "clear_sprites") { + m_sprites.clear(); + m_logger->info("Cleared all sprites"); + } else if (action == "toggle_background") { + m_darkBackground = !m_darkBackground; + m_logger->info("Background: {}", m_darkBackground ? "Dark" : "Light"); + } + } + else if (msg.topic == "ui:value_changed") { + std::string widgetId = msg.data->getString("widgetId", ""); + + if (widgetId == "speed_slider") { + m_spawnSpeed = static_cast(msg.data->getDouble("value", 100.0)); + m_logger->info("Spawn speed: {}", m_spawnSpeed); + } + } + else if (msg.topic == "input:keyboard:key") { + int scancode = msg.data->getInt("scancode", 0); + bool pressed = msg.data->getBool("pressed", false); + + if (pressed && scancode == SDL_SCANCODE_SPACE) { + spawnSprite(); + } + } + } + } + + void render(IIO* rendererIO) { + // Publish clear color + auto clear = std::make_unique("clear"); + clear->setInt("color", m_darkBackground ? 0x1a1a1aFF : 0x303030FF); + rendererIO->publish("render:clear", std::move(clear)); + + // Render sprites + int layer = 5; + for (const auto& sprite : m_sprites) { + auto spriteNode = std::make_unique("sprite"); + spriteNode->setDouble("x", sprite.x); + spriteNode->setDouble("y", sprite.y); + spriteNode->setDouble("scaleX", 32.0); + spriteNode->setDouble("scaleY", 32.0); + spriteNode->setDouble("rotation", 0.0); + spriteNode->setDouble("u0", 0.0); + spriteNode->setDouble("v0", 0.0); + spriteNode->setDouble("u1", 1.0); + spriteNode->setDouble("v1", 1.0); + spriteNode->setInt("color", sprite.color); + spriteNode->setInt("textureId", 0); // White texture + spriteNode->setInt("layer", layer); + rendererIO->publish("render:sprite", std::move(spriteNode)); + } + + // Render sprite count + auto text = std::make_unique("text"); + text->setDouble("x", 20.0); + text->setDouble("y", 20.0); + text->setString("text", "Sprites: " + std::to_string(m_sprites.size()) + " (Press SPACE to spawn)"); + text->setDouble("fontSize", 24.0); + text->setInt("color", 0xFFFFFFFF); + text->setInt("layer", 2000); // Above UI + rendererIO->publish("render:text", std::move(text)); + } + +private: + void spawnSprite() { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution posX(100.0f, 1820.0f); + std::uniform_real_distribution posY(100.0f, 980.0f); + std::uniform_real_distribution vel(-1.0f, 1.0f); + std::uniform_int_distribution colorDist(0x80000000, 0xFFFFFFFF); + + Sprite sprite; + sprite.x = posX(gen); + sprite.y = posY(gen); + sprite.vx = vel(gen) * m_spawnSpeed; + sprite.vy = vel(gen) * m_spawnSpeed; + sprite.color = colorDist(gen) | 0xFF; // Force full alpha + + m_sprites.push_back(sprite); + m_logger->info("Spawned sprite at ({}, {})", sprite.x, sprite.y); + } + + IIO* m_io; + std::shared_ptr m_logger; + std::vector m_sprites; + float m_spawnSpeed = 100.0f; + bool m_darkBackground = false; +}; + +#undef main // Undefine SDL's main macro for Windows + +int main(int argc, char* argv[]) { + // Setup logging to both console AND file + try { + auto console_sink = std::make_shared(); + auto file_sink = std::make_shared("full_stack_demo.log", true); + + std::vector sinks {console_sink, file_sink}; + auto logger = std::make_shared("Main", sinks.begin(), sinks.end()); + spdlog::register_logger(logger); + spdlog::set_default_logger(logger); + spdlog::set_level(spdlog::level::info); + spdlog::flush_on(spdlog::level::info); // Auto-flush pour pas perdre de logs + } catch (const std::exception& e) { + std::cerr << "Failed to setup logging: " << e.what() << "\n"; + return 1; + } + + auto logger = spdlog::get("Main"); + + logger->info("=============================================="); + logger->info(" Full Stack Interactive Demo"); + logger->info("=============================================="); + logger->info("Log file: full_stack_demo.log"); + + // Initialize SDL + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { + logger->error("SDL_Init failed: {}", SDL_GetError()); + return 1; + } + + // Create window (windowed mode, not fullscreen) + const int WINDOW_WIDTH = 1280; + const int WINDOW_HEIGHT = 720; + SDL_Window* window = SDL_CreateWindow( + "GroveEngine - Full Stack Demo", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + WINDOW_WIDTH, WINDOW_HEIGHT, + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE + ); + + if (!window) { + logger->error("SDL_CreateWindow failed: {}", SDL_GetError()); + SDL_Quit(); + return 1; + } + + // Get native window handle + SDL_SysWMinfo wmInfo; + SDL_VERSION(&wmInfo.version); + SDL_GetWindowWMInfo(window, &wmInfo); + void* nativeHandle = nullptr; + +#ifdef _WIN32 + nativeHandle = wmInfo.info.win.window; +#elif defined(__linux__) + nativeHandle = (void*)(uintptr_t)wmInfo.info.x11.window; +#elif defined(__APPLE__) + nativeHandle = wmInfo.info.cocoa.window; +#endif + + logger->info("Native window handle: {}", nativeHandle); + + // Create IIO instances + auto& ioManager = IntraIOManager::getInstance(); + auto rendererIO = ioManager.createInstance("renderer"); + auto uiIO = ioManager.createInstance("ui"); + auto inputIO = ioManager.createInstance("input"); + auto gameIO = ioManager.createInstance("game"); + + // Load modules + ModuleLoader uiLoader, inputLoader; +#ifndef USE_STATIC_BGFX + ModuleLoader rendererLoader; +#endif + + std::string uiPath = "./modules/UIModule.dll"; + std::string inputPath = "./modules/InputModule.dll"; + +#ifndef _WIN32 + uiPath = "./modules/libUIModule.so"; + inputPath = "./modules/libInputModule.so"; +#endif + + logger->info("Loading modules..."); + + // Load/Create BgfxRenderer + std::unique_ptr renderer; +#ifdef USE_STATIC_BGFX + // Static linking: instantiate directly (required on Windows) + renderer = std::make_unique(); + logger->info("✅ BgfxRenderer created (static)"); +#else + // Dynamic linking: load from DLL + std::string rendererPath = "./modules/BgfxRenderer.dll"; + #ifndef _WIN32 + rendererPath = "./modules/libBgfxRenderer.so"; + #endif + try { + renderer = rendererLoader.load(rendererPath, "renderer"); + logger->info("✅ BgfxRenderer loaded (dynamic)"); + } catch (const std::exception& e) { + logger->error("Failed to load BgfxRenderer: {}", e.what()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } +#endif + + // Configure BgfxRenderer + JsonDataNode rendererConfig("config"); + rendererConfig.setInt("windowWidth", WINDOW_WIDTH); + rendererConfig.setInt("windowHeight", WINDOW_HEIGHT); + rendererConfig.setString("backend", "opengl"); // Force OpenGL instead of D3D11 + rendererConfig.setBool("vsync", true); + rendererConfig.setInt("nativeWindowHandle", (int)(intptr_t)nativeHandle); + renderer->setConfiguration(rendererConfig, rendererIO.get(), nullptr); + + // Load UIModule + std::unique_ptr uiModule; + try { + uiModule = uiLoader.load(uiPath, "ui"); + logger->info("✅ UIModule loaded"); + } catch (const std::exception& e) { + logger->error("Failed to load UIModule: {}", e.what()); + renderer->shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Configure UIModule with inline layout + JsonDataNode uiConfig("config"); + uiConfig.setInt("windowWidth", WINDOW_WIDTH); + uiConfig.setInt("windowHeight", WINDOW_HEIGHT); + uiConfig.setInt("baseLayer", 1000); + + // Create inline layout + auto layout = std::make_unique("layout"); + auto widgets = std::make_unique("widgets"); + + // Panel background + auto panel = std::make_unique("panel"); + panel->setString("type", "UIPanel"); + panel->setString("id", "control_panel"); + panel->setInt("x", 20); + panel->setInt("y", 80); + panel->setInt("width", 300); + panel->setInt("height", 300); + panel->setInt("color", 0x404040CC); // Semi-transparent gray + widgets->setChild("panel", std::move(panel)); + + // Title label + auto title = std::make_unique("title"); + title->setString("type", "UILabel"); + title->setString("id", "title_label"); + title->setInt("x", 40); + title->setInt("y", 100); + title->setInt("width", 260); + title->setInt("height", 40); + title->setString("text", "Control Panel"); + title->setInt("fontSize", 28); + title->setInt("color", 0xFFFFFFFF); + widgets->setChild("title", std::move(title)); + + // Spawn button + auto spawnBtn = std::make_unique("spawn_button"); + spawnBtn->setString("type", "UIButton"); + spawnBtn->setString("id", "spawn_button"); + spawnBtn->setInt("x", 40); + spawnBtn->setInt("y", 160); + spawnBtn->setInt("width", 120); + spawnBtn->setInt("height", 40); + spawnBtn->setString("text", "Spawn"); + spawnBtn->setString("action", "spawn_sprite"); + spawnBtn->setInt("fontSize", 20); + widgets->setChild("spawn_button", std::move(spawnBtn)); + + // Clear button + auto clearBtn = std::make_unique("clear_button"); + clearBtn->setString("type", "UIButton"); + clearBtn->setString("id", "clear_button"); + clearBtn->setInt("x", 180); + clearBtn->setInt("y", 160); + clearBtn->setInt("width", 120); + clearBtn->setInt("height", 40); + clearBtn->setString("text", "Clear"); + clearBtn->setString("action", "clear_sprites"); + clearBtn->setInt("fontSize", 20); + widgets->setChild("clear_button", std::move(clearBtn)); + + // Speed slider + auto slider = std::make_unique("speed_slider"); + slider->setString("type", "UISlider"); + slider->setString("id", "speed_slider"); + slider->setInt("x", 40); + slider->setInt("y", 220); + slider->setInt("width", 260); + slider->setInt("height", 30); + slider->setDouble("min", 10.0); + slider->setDouble("max", 500.0); + slider->setDouble("value", 100.0); + slider->setString("orientation", "horizontal"); + widgets->setChild("speed_slider", std::move(slider)); + + // Speed label + auto speedLabel = std::make_unique("speed_label"); + speedLabel->setString("type", "UILabel"); + speedLabel->setString("id", "speed_label"); + speedLabel->setInt("x", 40); + speedLabel->setInt("y", 260); + speedLabel->setInt("width", 260); + speedLabel->setInt("height", 30); + speedLabel->setString("text", "Speed: 100"); + speedLabel->setInt("fontSize", 18); + speedLabel->setInt("color", 0xCCCCCCFF); + widgets->setChild("speed_label", std::move(speedLabel)); + + // Background toggle button + auto bgBtn = std::make_unique("bg_button"); + bgBtn->setString("type", "UIButton"); + bgBtn->setString("id", "bg_button"); + bgBtn->setInt("x", 40); + bgBtn->setInt("y", 310); + bgBtn->setInt("width", 260); + bgBtn->setInt("height", 40); + bgBtn->setString("text", "Toggle Background"); + bgBtn->setString("action", "toggle_background"); + bgBtn->setInt("fontSize", 18); + widgets->setChild("bg_button", std::move(bgBtn)); + + layout->setChild("widgets", std::move(widgets)); + uiConfig.setChild("layout", std::move(layout)); + + uiModule->setConfiguration(uiConfig, uiIO.get(), nullptr); + + // Load InputModule + std::unique_ptr inputModuleBase; + FeedEventFunc feedEventFunc = nullptr; + try { + inputModuleBase = inputLoader.load(inputPath, "input"); + logger->info("✅ InputModule loaded"); + + // Get the feedEvent function from the DLL +#ifdef _WIN32 + HMODULE inputDll = LoadLibraryA(inputPath.c_str()); + if (inputDll) { + feedEventFunc = (FeedEventFunc)GetProcAddress(inputDll, "feedEventToInputModule"); + if (!feedEventFunc) { + logger->warn("feedEventToInputModule not found in InputModule.dll"); + } + } +#else + void* inputDll = dlopen(inputPath.c_str(), RTLD_NOW); + if (inputDll) { + feedEventFunc = (FeedEventFunc)dlsym(inputDll, "feedEventToInputModule"); + } +#endif + } catch (const std::exception& e) { + logger->error("Failed to load InputModule: {}", e.what()); + uiModule->shutdown(); + renderer->shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + if (!feedEventFunc) { + logger->error("Failed to get feedEventToInputModule function"); + uiModule->shutdown(); + renderer->shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + // Configure InputModule + JsonDataNode inputConfig("config"); + inputConfig.setString("backend", "sdl"); + inputConfig.setBool("enableMouse", true); + inputConfig.setBool("enableKeyboard", true); + inputModuleBase->setConfiguration(inputConfig, inputIO.get(), nullptr); + + // Create game logic + GameLogic gameLogic(gameIO.get()); + + logger->info("\n=============================================="); + logger->info("Demo started! Controls:"); + logger->info(" - Click buttons to spawn/clear sprites"); + logger->info(" - Drag slider to change speed"); + logger->info(" - Press SPACE to spawn sprite"); + logger->info(" - Press ESC to exit"); + logger->info("==============================================\n"); + + // Main loop + bool running = true; + Uint64 lastTime = SDL_GetPerformanceCounter(); + int frameCount = 0; + + logger->info("Entering main loop..."); + spdlog::default_logger()->flush(); + + while (running) { + logger->info("Frame {} start", frameCount); + spdlog::default_logger()->flush(); + // Handle SDL events + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + running = false; + } + else if (event.type == SDL_KEYDOWN && event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { + running = false; + } + + // Feed to InputModule via exported C function + feedEventFunc(inputModuleBase.get(), &event); + } + + // Calculate deltaTime + Uint64 now = SDL_GetPerformanceCounter(); + double deltaTime = (now - lastTime) / (double)SDL_GetPerformanceFrequency(); + lastTime = now; + + // Clamp deltaTime to avoid huge jumps + if (deltaTime > 0.1) deltaTime = 0.016; + + // Process modules + JsonDataNode input("input"); + input.setDouble("deltaTime", deltaTime); + input.setInt("frameCount", frameCount); + + logger->info("Processing input module..."); + inputModuleBase->process(input); + logger->info("Processing UI module..."); + uiModule->process(input); + logger->info("Updating game logic..."); + gameLogic.update((float)deltaTime); + logger->info("Rendering game..."); + gameLogic.render(rendererIO.get()); + logger->info("Processing renderer..."); + spdlog::default_logger()->flush(); + renderer->process(input); + logger->info("Frame {} complete", frameCount); + + frameCount++; + } + + // Cleanup + logger->info("\nShutting down..."); + inputModuleBase->shutdown(); + uiModule->shutdown(); + renderer->shutdown(); + + ioManager.removeInstance("renderer"); + ioManager.removeInstance("ui"); + ioManager.removeInstance("input"); + ioManager.removeInstance("game"); + + SDL_DestroyWindow(window); + SDL_Quit(); + + logger->info("✅ Demo exited cleanly"); + return 0; +} diff --git a/tests/visual/test_renderer_showcase.cpp b/tests/visual/test_renderer_showcase.cpp new file mode 100644 index 0000000..c132b02 --- /dev/null +++ b/tests/visual/test_renderer_showcase.cpp @@ -0,0 +1,660 @@ +/** + * BgfxRenderer Complete Showcase + * + * Demonstrates ALL rendering features of BgfxRendererModule: + * - Sprites (static, animated, colored, layered, rotated, scaled) + * - Text (different sizes, colors, multi-line) + * - Particles (with life, velocity, size, additive blending) + * - Tilemap (simple grid with tile indices) + * - Debug primitives (lines, rectangles wireframe and filled) + * - Camera (orthographic projection, panning, zooming) + * - Clear color + * + * Controls: + * - Arrow keys: Move camera + * - +/-: Zoom in/out + * - SPACE: Spawn explosion particles + * - C: Cycle clear color + * - ESC: Exit + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "BgfxRendererModule.h" +#include +#include +#include +#include +#include + +using namespace grove; + +// Particle structure for CPU-side simulation +struct Particle { + float x, y; + float vx, vy; + float size; + float life; + float maxLife; + uint32_t color; + bool alive; +}; + +// Simple tilemap data (10x8 grid) +static const int TILEMAP_WIDTH = 10; +static const int TILEMAP_HEIGHT = 8; +static const int TILE_SIZE = 32; +static uint16_t tilemapData[TILEMAP_WIDTH * TILEMAP_HEIGHT] = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 2, 2, 0, 0, 3, 3, 0, 1, + 1, 0, 2, 2, 0, 0, 3, 3, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 4, 4, 4, 4, 4, 4, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +}; + +// Clear colors to cycle through +static const uint32_t clearColors[] = { + 0x1a1a2eFF, // Dark blue-gray + 0x16213eFF, // Navy blue + 0x0f3460FF, // Deep blue + 0x1e5128FF, // Forest green + 0x2c3333FF, // Dark teal + 0x3d0000FF, // Dark red +}; +static const int numClearColors = sizeof(clearColors) / sizeof(clearColors[0]); + +class RendererShowcase { +public: + RendererShowcase() + : m_rng(std::random_device{}()) + { + m_particles.reserve(500); + } + + bool init(SDL_Window* window) { + // Get native window handle + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + m_logger = spdlog::stdout_color_mt("Showcase"); + spdlog::set_level(spdlog::level::info); + + m_logger->info("=== BgfxRenderer Complete Showcase ==="); + + // Create IIO instances - IMPORTANT: separate for publisher and subscriber + // Keep shared_ptr alive, use IIO* abstract interface + m_rendererIOPtr = IntraIOManager::getInstance().createInstance("renderer"); + m_gameIOPtr = IntraIOManager::getInstance().createInstance("game"); + m_rendererIO = m_rendererIOPtr.get(); + m_gameIO = m_gameIOPtr.get(); + + // Create and configure renderer + m_renderer = std::make_unique(); + + JsonDataNode config("config"); + config.setDouble("nativeWindowHandle", static_cast(reinterpret_cast(wmi.info.win.window))); + config.setInt("windowWidth", 1024); + config.setInt("windowHeight", 768); + config.setString("backend", "d3d11"); + config.setBool("vsync", true); + + // Load textures - try multiple paths for flexibility + // Works from build/ or build/tests/ + config.setString("texture1", "../assets/textures/1f440.png"); // Eye emoji + config.setString("texture2", "../assets/textures/5oxaxt1vo2f91.jpg"); // Image + + m_renderer->setConfiguration(config, m_rendererIO, nullptr); + + m_logger->info("Renderer initialized"); + m_logger->info("Controls:"); + m_logger->info(" Arrows: Move camera"); + m_logger->info(" +/-: Zoom"); + m_logger->info(" SPACE: Spawn particles"); + m_logger->info(" C: Cycle clear color"); + m_logger->info(" ESC: Exit"); + + return true; + } + + void handleInput(SDL_Event& e) { + if (e.type == SDL_KEYDOWN) { + switch (e.key.keysym.sym) { + case SDLK_LEFT: m_cameraVX = -200.0f; break; + case SDLK_RIGHT: m_cameraVX = 200.0f; break; + case SDLK_UP: m_cameraVY = -200.0f; break; + case SDLK_DOWN: m_cameraVY = 200.0f; break; + case SDLK_PLUS: + case SDLK_EQUALS: + m_cameraZoom = std::min(4.0f, m_cameraZoom * 1.1f); + break; + case SDLK_MINUS: + m_cameraZoom = std::max(0.25f, m_cameraZoom / 1.1f); + break; + case SDLK_SPACE: + spawnExplosion(512.0f + m_cameraX, 400.0f + m_cameraY); + break; + case SDLK_c: + m_clearColorIndex = (m_clearColorIndex + 1) % numClearColors; + break; + } + } + else if (e.type == SDL_KEYUP) { + switch (e.key.keysym.sym) { + case SDLK_LEFT: + case SDLK_RIGHT: + m_cameraVX = 0.0f; + break; + case SDLK_UP: + case SDLK_DOWN: + m_cameraVY = 0.0f; + break; + } + } + } + + void update(float dt) { + m_time += dt; + m_frameCount++; + + // Update camera position + m_cameraX += m_cameraVX * dt; + m_cameraY += m_cameraVY * dt; + + // Update particles + updateParticles(dt); + } + + void render() { + // 1. Set clear color + sendClearColor(); + + // 2. Set camera + sendCamera(); + + // 3. Render tilemap (background layer) + sendTilemap(); + + // 4. Render sprites (multiple layers) + sendSprites(); + + // 5. Render particles + sendParticles(); + + // 6. Render text + sendText(); + + // 7. Render debug primitives + sendDebugPrimitives(); + + // Process frame + JsonDataNode input("input"); + input.setDouble("deltaTime", 0.016); + input.setInt("frameCount", m_frameCount); + m_renderer->process(input); + } + + void shutdown() { + m_renderer->shutdown(); + IntraIOManager::getInstance().removeInstance("renderer"); + IntraIOManager::getInstance().removeInstance("game"); + m_logger->info("Showcase shutdown complete"); + } + + int getFrameCount() const { return m_frameCount; } + +private: + void sendClearColor() { + auto clear = std::make_unique("clear"); + clear->setInt("color", clearColors[m_clearColorIndex]); + m_gameIO->publish("render:clear", std::move(clear)); + } + + void sendCamera() { + auto cam = std::make_unique("camera"); + cam->setDouble("x", m_cameraX); + cam->setDouble("y", m_cameraY); + cam->setDouble("zoom", m_cameraZoom); + cam->setInt("viewportX", 0); + cam->setInt("viewportY", 0); + cam->setInt("viewportW", 1024); + cam->setInt("viewportH", 768); + m_gameIO->publish("render:camera", std::move(cam)); + } + + void sendTilemap() { + auto tilemap = std::make_unique("tilemap"); + tilemap->setDouble("x", 50.0); + tilemap->setDouble("y", 400.0); + tilemap->setInt("width", TILEMAP_WIDTH); + tilemap->setInt("height", TILEMAP_HEIGHT); + tilemap->setInt("tileW", TILE_SIZE); + tilemap->setInt("tileH", TILE_SIZE); + tilemap->setInt("textureId", 0); + + // Convert tilemap to comma-separated string + std::string tileData; + for (int i = 0; i < TILEMAP_WIDTH * TILEMAP_HEIGHT; ++i) { + if (i > 0) tileData += ","; + tileData += std::to_string(tilemapData[i]); + } + tilemap->setString("tileData", tileData); + + m_gameIO->publish("render:tilemap", std::move(tilemap)); + } + + void sendSprites() { + // Layer 0: Background sprites with TEXTURE 2 (image) + for (int i = 0; i < 5; ++i) { + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 100 + i * 150); + sprite->setDouble("y", 50); + sprite->setDouble("scaleX", 120.0); + sprite->setDouble("scaleY", 90.0); + sprite->setDouble("rotation", 0.0); + sprite->setInt("color", 0xFFFFFFFF); // White (no tint) + sprite->setInt("layer", 0); + sprite->setInt("textureId", 2); // Image texture + m_gameIO->publish("render:sprite", std::move(sprite)); + } + + // Layer 5: Bouncing EYE EMOJIS with texture 1 + for (int i = 0; i < 5; ++i) { + float offset = std::sin(m_time * 2.0f + i * 1.2f) * 40.0f; + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 100 + i * 180); + sprite->setDouble("y", 200 + offset); + sprite->setDouble("scaleX", 64.0); + sprite->setDouble("scaleY", 64.0); + sprite->setDouble("rotation", 0.0); + sprite->setInt("color", 0xFFFFFFFF); // No tint + sprite->setInt("layer", 5); + sprite->setInt("textureId", 1); // Eye emoji + m_gameIO->publish("render:sprite", std::move(sprite)); + } + + // Layer 10: Rotating eye emoji + { + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 700); + sprite->setDouble("y", 200); + sprite->setDouble("scaleX", 100.0); + sprite->setDouble("scaleY", 100.0); + sprite->setDouble("rotation", m_time); // Radians + sprite->setInt("color", 0xFFFFFFFF); + sprite->setInt("layer", 10); + sprite->setInt("textureId", 1); // Eye emoji + m_gameIO->publish("render:sprite", std::move(sprite)); + } + + // Layer 15: Scaling/pulsing image + { + float scale = 80.0f + std::sin(m_time * 3.0f) * 30.0f; + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 850); + sprite->setDouble("y", 200); + sprite->setDouble("scaleX", scale); + sprite->setDouble("scaleY", scale * 0.75f); + sprite->setDouble("rotation", 0.0); + sprite->setInt("color", 0xFFFFFFFF); + sprite->setInt("layer", 15); + sprite->setInt("textureId", 2); // Image + m_gameIO->publish("render:sprite", std::move(sprite)); + } + + // Layer 20: Tinted sprites (color overlay on texture) + { + uint32_t colors[] = { 0xFF8888FF, 0x88FF88FF, 0x8888FFFF, 0xFFFF88FF }; + for (int i = 0; i < 4; ++i) { + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 100 + i * 100); + sprite->setDouble("y", 320); + sprite->setDouble("scaleX", 80.0); + sprite->setDouble("scaleY", 80.0); + sprite->setDouble("rotation", 0.0); + sprite->setInt("color", colors[i]); // Tinted + sprite->setInt("layer", 20); + sprite->setInt("textureId", 1); // Eye emoji with color tint + m_gameIO->publish("render:sprite", std::move(sprite)); + } + } + + // Layer 25: Grid of small images + for (int row = 0; row < 2; ++row) { + for (int col = 0; col < 4; ++col) { + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 550 + col * 70); + sprite->setDouble("y", 320 + row * 55); + sprite->setDouble("scaleX", 60.0); + sprite->setDouble("scaleY", 45.0); + sprite->setDouble("rotation", 0.0); + sprite->setInt("color", 0xFFFFFFFF); + sprite->setInt("layer", 25); + sprite->setInt("textureId", 2); // Image + m_gameIO->publish("render:sprite", std::move(sprite)); + } + } + } + + void sendParticles() { + for (const auto& p : m_particles) { + if (!p.alive) continue; + + auto particle = std::make_unique("particle"); + particle->setDouble("x", p.x); + particle->setDouble("y", p.y); + particle->setDouble("vx", p.vx); + particle->setDouble("vy", p.vy); + particle->setDouble("size", p.size); + particle->setDouble("life", p.life / p.maxLife); // Normalized 0-1 + particle->setInt("color", p.color); + particle->setInt("textureId", 0); + m_gameIO->publish("render:particle", std::move(particle)); + } + } + + void sendText() { + // Title (large) + { + auto text = std::make_unique("text"); + text->setDouble("x", 10); + text->setDouble("y", 10); + text->setString("text", "BgfxRenderer Showcase"); + text->setInt("fontSize", 32); + text->setInt("color", 0xFFFFFFFF); + text->setInt("layer", 100); + m_gameIO->publish("render:text", std::move(text)); + } + + // Info text + { + auto text = std::make_unique("text"); + text->setDouble("x", 10); + text->setDouble("y", 50); + char buf[128]; + snprintf(buf, sizeof(buf), "Frame: %d Camera: (%.0f, %.0f) Zoom: %.2fx", + m_frameCount, m_cameraX, m_cameraY, m_cameraZoom); + text->setString("text", buf); + text->setInt("fontSize", 16); + text->setInt("color", 0xAAAAAAFF); + text->setInt("layer", 100); + m_gameIO->publish("render:text", std::move(text)); + } + + // Particles count + { + auto text = std::make_unique("text"); + text->setDouble("x", 10); + text->setDouble("y", 70); + char buf[64]; + int aliveCount = 0; + for (const auto& p : m_particles) if (p.alive) aliveCount++; + snprintf(buf, sizeof(buf), "Particles: %d", aliveCount); + text->setString("text", buf); + text->setInt("fontSize", 16); + text->setInt("color", 0xFFCC00FF); + text->setInt("layer", 100); + m_gameIO->publish("render:text", std::move(text)); + } + + // Controls help + { + auto text = std::make_unique("text"); + text->setDouble("x", 750); + text->setDouble("y", 730); + text->setString("text", "SPACE: Particles | C: Color | Arrows: Pan | +/-: Zoom"); + text->setInt("fontSize", 14); + text->setInt("color", 0x888888FF); + text->setInt("layer", 100); + m_gameIO->publish("render:text", std::move(text)); + } + + // Multi-line text demo + { + auto text = std::make_unique("text"); + text->setDouble("x", 700); + text->setDouble("y", 300); + text->setString("text", "Multi-line\nText Demo\nWith Colors!"); + text->setInt("fontSize", 20); + text->setInt("color", 0x00FF88FF); + text->setInt("layer", 100); + m_gameIO->publish("render:text", std::move(text)); + } + } + + void sendDebugPrimitives() { + // Grid lines + for (int i = 0; i <= 10; ++i) { + // Vertical lines + auto vline = std::make_unique("line"); + vline->setDouble("x1", 50 + i * 32); + vline->setDouble("y1", 400); + vline->setDouble("x2", 50 + i * 32); + vline->setDouble("y2", 400 + TILEMAP_HEIGHT * 32); + vline->setInt("color", 0x444444FF); + m_gameIO->publish("render:debug:line", std::move(vline)); + } + for (int i = 0; i <= 8; ++i) { + // Horizontal lines + auto hline = std::make_unique("line"); + hline->setDouble("x1", 50); + hline->setDouble("y1", 400 + i * 32); + hline->setDouble("x2", 50 + TILEMAP_WIDTH * 32); + hline->setDouble("y2", 400 + i * 32); + hline->setInt("color", 0x444444FF); + m_gameIO->publish("render:debug:line", std::move(hline)); + } + + // Diagonal line (animated) + { + auto line = std::make_unique("line"); + float offset = std::sin(m_time) * 50.0f; + line->setDouble("x1", 500); + line->setDouble("y1", 450 + offset); + line->setDouble("x2", 700); + line->setDouble("y2", 550 - offset); + line->setInt("color", 0xFFFF00FF); // Yellow + m_gameIO->publish("render:debug:line", std::move(line)); + } + + // Wireframe rectangle + { + auto rect = std::make_unique("rect"); + rect->setDouble("x", 750); + rect->setDouble("y", 400); + rect->setDouble("w", 100); + rect->setDouble("h", 80); + rect->setInt("color", 0x00FFFFFF); // Cyan + rect->setBool("filled", false); + m_gameIO->publish("render:debug:rect", std::move(rect)); + } + + // Filled rectangle (pulsing) + { + float pulse = (std::sin(m_time * 2.0f) + 1.0f) * 0.5f; + uint8_t alpha = static_cast(128 + pulse * 127); + uint32_t color = 0xFF4444FF | (alpha); + + auto rect = std::make_unique("rect"); + rect->setDouble("x", 870); + rect->setDouble("y", 400); + rect->setDouble("w", 80); + rect->setDouble("h", 80); + rect->setInt("color", color); + rect->setBool("filled", true); + m_gameIO->publish("render:debug:rect", std::move(rect)); + } + + // Crosshair at center + { + float cx = 512.0f, cy = 384.0f; + auto hline = std::make_unique("line"); + hline->setDouble("x1", cx - 20); + hline->setDouble("y1", cy); + hline->setDouble("x2", cx + 20); + hline->setDouble("y2", cy); + hline->setInt("color", 0xFF0000FF); + m_gameIO->publish("render:debug:line", std::move(hline)); + + auto vline = std::make_unique("line"); + vline->setDouble("x1", cx); + vline->setDouble("y1", cy - 20); + vline->setDouble("x2", cx); + vline->setDouble("y2", cy + 20); + vline->setInt("color", 0xFF0000FF); + m_gameIO->publish("render:debug:line", std::move(vline)); + } + } + + void spawnExplosion(float x, float y) { + std::uniform_real_distribution angleDist(0.0f, 2.0f * 3.14159f); + std::uniform_real_distribution speedDist(50.0f, 200.0f); + std::uniform_real_distribution sizeDist(5.0f, 20.0f); + std::uniform_real_distribution lifeDist(0.5f, 2.0f); + + uint32_t colors[] = { + 0xFF4444FF, 0xFF8844FF, 0xFFCC44FF, 0xFFFF44FF, + 0xFF6644FF, 0xFFAA00FF + }; + std::uniform_int_distribution colorDist(0, 5); + + for (int i = 0; i < 50; ++i) { + Particle p; + float angle = angleDist(m_rng); + float speed = speedDist(m_rng); + p.x = x; + p.y = y; + p.vx = std::cos(angle) * speed; + p.vy = std::sin(angle) * speed; + p.size = sizeDist(m_rng); + p.life = lifeDist(m_rng); + p.maxLife = p.life; + p.color = colors[colorDist(m_rng)]; + p.alive = true; + + // Find dead particle slot or add new + bool found = false; + for (auto& existing : m_particles) { + if (!existing.alive) { + existing = p; + found = true; + break; + } + } + if (!found) { + m_particles.push_back(p); + } + } + + m_logger->info("Spawned explosion at ({:.0f}, {:.0f})", x, y); + } + + void updateParticles(float dt) { + for (auto& p : m_particles) { + if (!p.alive) continue; + + p.x += p.vx * dt; + p.y += p.vy * dt; + p.vy += 100.0f * dt; // Gravity + p.life -= dt; + + if (p.life <= 0.0f) { + p.alive = false; + } + } + } + + std::shared_ptr m_logger; + std::unique_ptr m_renderer; + std::shared_ptr m_rendererIOPtr; // Keep shared_ptr alive + std::shared_ptr m_gameIOPtr; // Keep shared_ptr alive + IIO* m_rendererIO = nullptr; // Abstract interface + IIO* m_gameIO = nullptr; // Abstract interface + + float m_time = 0.0f; + int m_frameCount = 0; + + // Camera + float m_cameraX = 0.0f; + float m_cameraY = 0.0f; + float m_cameraVX = 0.0f; + float m_cameraVY = 0.0f; + float m_cameraZoom = 1.0f; + + // Clear color + int m_clearColorIndex = 0; + + // Particles + std::vector m_particles; + std::mt19937 m_rng; +}; + +int main(int argc, char* argv[]) { + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "BgfxRenderer Showcase", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 1024, 768, + SDL_WINDOW_SHOWN + ); + + if (!window) { + std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl; + SDL_Quit(); + return 1; + } + + RendererShowcase showcase; + if (!showcase.init(window)) { + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + bool running = true; + Uint64 lastTime = SDL_GetPerformanceCounter(); + + while (running) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || + (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + showcase.handleInput(e); + } + + Uint64 now = SDL_GetPerformanceCounter(); + float dt = static_cast(now - lastTime) / SDL_GetPerformanceFrequency(); + lastTime = now; + + showcase.update(dt); + showcase.render(); + + // Cap to ~60 FPS + SDL_Delay(1); + } + + Uint64 endTime = SDL_GetPerformanceCounter(); + int frames = showcase.getFrameCount(); + + showcase.shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "Rendered " << frames << " frames" << std::endl; + return 0; +} diff --git a/tests/visual/test_single_button.cpp b/tests/visual/test_single_button.cpp new file mode 100644 index 0000000..fd474a0 --- /dev/null +++ b/tests/visual/test_single_button.cpp @@ -0,0 +1,221 @@ +/** + * Test: Single UIButton with interaction + * - Bouton rouge + * - Feedback visuel hover (orange) + * - Feedback visuel pressed (rouge foncé) + * - Logs des événements + */ + +#include +#include +#include +#include + +#include "BgfxRendererModule.h" +#include "UIModule/UIModule.h" +#include +#include +#include +#include +#include + +using namespace grove; + +int main(int argc, char* argv[]) { + spdlog::set_level(spdlog::level::info); + auto logger = spdlog::stdout_color_mt("ButtonTest"); + + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed" << std::endl; + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "Single Button Test", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 800, 600, SDL_WINDOW_SHOWN + ); + + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + // Create IIO instances + auto rendererIO = IntraIOManager::getInstance().createInstance("renderer"); + auto uiIO = IntraIOManager::getInstance().createInstance("ui"); + auto gameIO = IntraIOManager::getInstance().createInstance("game"); + + // Subscribe to UI events for logging + gameIO->subscribe("ui:hover"); + gameIO->subscribe("ui:click"); + gameIO->subscribe("ui:action"); + + // Initialize BgfxRenderer + auto renderer = std::make_unique(); + { + JsonDataNode config("config"); + config.setDouble("nativeWindowHandle", + static_cast(reinterpret_cast(wmi.info.win.window))); + config.setInt("windowWidth", 800); + config.setInt("windowHeight", 600); + renderer->setConfiguration(config, rendererIO.get(), nullptr); + } + + // Initialize UIModule with ONE button - proper style structure + auto ui = std::make_unique(); + { + JsonDataNode config("config"); + config.setInt("windowWidth", 800); + config.setInt("windowHeight", 600); + + /* + * COMMENT CA MARCHE: + * + * 1. UIModule charge le layout JSON et crée les widgets + * 2. Chaque frame: + * - UIModule reçoit les events input (mouse, keyboard) via IIO + * - UIModule update les widgets (hover detection, click handling) + * - UIModule appelle render() sur chaque widget + * - UIButton::render() publie un sprite via IIO topic "render:sprite" + * - BgfxRenderer reçoit le sprite et le dessine + * + * 3. Les styles du bouton: + * - normal: état par défaut + * - hover: quand la souris est dessus + * - pressed: quand on clique + * - disabled: quand enabled=false + * + * 4. Les événements publiés par UIModule: + * - ui:hover - quand on entre/sort d'un widget + * - ui:click - quand on clique + * - ui:action - quand un bouton avec onClick est cliqué + */ + + nlohmann::json layoutJson = { + {"id", "root"}, + {"type", "panel"}, + {"x", 0}, {"y", 0}, + {"width", 800}, {"height", 600}, + {"style", { + {"bgColor", "0x1A1A2EFF"} // Fond sombre + }}, + {"children", { + { + {"id", "btn_test"}, + {"type", "button"}, + {"x", 250}, + {"y", 250}, + {"width", 300}, + {"height", 100}, + {"text", "CLICK ME!"}, + {"onClick", "test_action"}, + {"style", { + {"fontSize", 24.0}, + {"normal", { + {"bgColor", "0xE74C3CFF"}, // Rouge + {"textColor", "0xFFFFFFFF"}, // Blanc + {"borderRadius", 8.0} + }}, + {"hover", { + {"bgColor", "0xF39C12FF"}, // Orange (hover) + {"textColor", "0xFFFFFFFF"}, + {"borderRadius", 8.0} + }}, + {"pressed", { + {"bgColor", "0xC0392BFF"}, // Rouge foncé (pressed) + {"textColor", "0xFFFFFFFF"}, + {"borderRadius", 8.0} + }} + }} + } + }} + }; + + auto layoutNode = std::make_unique("layout", layoutJson); + config.setChild("layout", std::move(layoutNode)); + + ui->setConfiguration(config, uiIO.get(), nullptr); + } + + logger->info("=== SINGLE BUTTON TEST ==="); + logger->info("- Bouton ROUGE au centre"); + logger->info("- Hover = ORANGE"); + logger->info("- Click = ROUGE FONCE"); + logger->info("- Les events sont loggés ci-dessous"); + logger->info("Press ESC to exit\n"); + + bool running = true; + while (running) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || + (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + + // Forward mouse events to UI via IIO + // IMPORTANT: Publish from gameIO (not uiIO) because IIO doesn't deliver to self + if (e.type == SDL_MOUSEMOTION) { + auto mouseMsg = std::make_unique("mouse"); + mouseMsg->setDouble("x", static_cast(e.motion.x)); + mouseMsg->setDouble("y", static_cast(e.motion.y)); + gameIO->publish("input:mouse:move", std::move(mouseMsg)); + } + else if (e.type == SDL_MOUSEBUTTONDOWN || e.type == SDL_MOUSEBUTTONUP) { + auto mouseMsg = std::make_unique("mouse"); + mouseMsg->setInt("button", e.button.button); + mouseMsg->setBool("pressed", e.type == SDL_MOUSEBUTTONDOWN); + mouseMsg->setDouble("x", static_cast(e.button.x)); + mouseMsg->setDouble("y", static_cast(e.button.y)); + gameIO->publish("input:mouse:button", std::move(mouseMsg)); + + // Log press/release + logger->info("[INPUT] Mouse {} at ({}, {})", + e.type == SDL_MOUSEBUTTONDOWN ? "PRESSED" : "RELEASED", + e.button.x, e.button.y); + } + } + + // Check for UI events + while (gameIO->hasMessages() > 0) { + auto msg = gameIO->pullMessage(); + + if (msg.topic == "ui:hover") { + std::string widgetId = msg.data->getString("widgetId", ""); + bool enter = msg.data->getBool("enter", false); + logger->info("[UI EVENT] HOVER {} widget '{}'", + enter ? "ENTER" : "LEAVE", widgetId); + } + else if (msg.topic == "ui:click") { + std::string widgetId = msg.data->getString("widgetId", ""); + logger->info("[UI EVENT] CLICK on widget '{}'", widgetId); + } + else if (msg.topic == "ui:action") { + std::string action = msg.data->getString("action", ""); + std::string widgetId = msg.data->getString("widgetId", ""); + logger->info("[UI EVENT] ACTION '{}' from widget '{}'", action, widgetId); + } + } + + JsonDataNode input("input"); + input.setDouble("deltaTime", 0.016); + + // Process UI (publishes sprites) + ui->process(input); + + // Render + renderer->process(input); + + SDL_Delay(16); + } + + ui->shutdown(); + renderer->shutdown(); + IntraIOManager::getInstance().removeInstance("renderer"); + IntraIOManager::getInstance().removeInstance("ui"); + IntraIOManager::getInstance().removeInstance("game"); + + SDL_DestroyWindow(window); + SDL_Quit(); + return 0; +} diff --git a/tests/visual/test_sprite_debug.cpp b/tests/visual/test_sprite_debug.cpp new file mode 100644 index 0000000..249088f --- /dev/null +++ b/tests/visual/test_sprite_debug.cpp @@ -0,0 +1,221 @@ +/** + * Diagnostic Test: Sprite Rendering Debug + * + * Ce test isole le problème de rendu des sprites UI en comparant: + * 1. Sprites hardcodés directement dans SpritePass + * 2. Sprites envoyés via IIO (comme UIRenderer le fait) + * + * Objectif: Identifier EXACTEMENT où le pipeline échoue + */ + +#include +#include +#include +#include + +#include "BgfxRendererModule.h" +#include +#include +#include +#include +#include + +using namespace grove; + +class SpriteDiagnostic { +public: + bool init(SDL_Window* window) { + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + m_logger = spdlog::stdout_color_mt("SpriteDiag"); + spdlog::set_level(spdlog::level::debug); + + m_logger->info("=== DIAGNOSTIC: Sprite Rendering ==="); + + // Create IIO instances + m_rendererIO = IntraIOManager::getInstance().createInstance("renderer_diag"); + m_testIO = IntraIOManager::getInstance().createInstance("test_publisher"); + + // Initialize renderer + m_renderer = std::make_unique(); + JsonDataNode config("config"); + config.setDouble("nativeWindowHandle", + static_cast(reinterpret_cast(wmi.info.win.window))); + config.setInt("windowWidth", 800); + config.setInt("windowHeight", 600); + config.setString("backend", "d3d11"); + config.setBool("vsync", true); + m_renderer->setConfiguration(config, m_rendererIO.get(), nullptr); + + m_logger->info("Renderer initialized"); + return true; + } + + void runTest() { + m_logger->info("\n========================================"); + m_logger->info("TEST: Publishing sprites via IIO"); + m_logger->info("========================================\n"); + + // Publish camera first (like UIModule does) + { + auto cam = std::make_unique("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", 800); + cam->setInt("viewportH", 600); + m_testIO->publish("render:camera", std::move(cam)); + m_logger->info("[PUB] Camera: viewport 800x600"); + } + + // TEST 1: Sprite at known position (should be visible) + { + auto sprite = std::make_unique("sprite"); + // Position at CENTER of screen (400, 300) + sprite->setDouble("x", 400.0); // Center X + sprite->setDouble("y", 300.0); // Center Y + sprite->setDouble("scaleX", 200.0); // 200px wide + sprite->setDouble("scaleY", 150.0); // 150px tall + sprite->setInt("color", 0xFF0000FF); // Red, full alpha + sprite->setInt("textureId", 0); + sprite->setInt("layer", 100); + + m_logger->info("[PUB] Sprite 1: pos=({},{}) scale=({},{}) color=0x{:08X}", + 400.0, 300.0, 200.0, 150.0, 0xFF0000FF); + + m_testIO->publish("render:sprite", std::move(sprite)); + } + + // TEST 2: Sprite at top-left corner + { + auto sprite = std::make_unique("sprite"); + sprite->setDouble("x", 100.0); + sprite->setDouble("y", 100.0); + sprite->setDouble("scaleX", 100.0); + sprite->setDouble("scaleY", 100.0); + sprite->setInt("color", 0x00FF00FF); // Green + sprite->setInt("textureId", 0); + sprite->setInt("layer", 101); + + m_logger->info("[PUB] Sprite 2: pos=({},{}) scale=({},{}) color=0x{:08X}", + 100.0, 100.0, 100.0, 100.0, 0x00FF00FF); + + m_testIO->publish("render:sprite", std::move(sprite)); + } + + // TEST 3: Sprite simulating UIButton (like UIRenderer::drawRect) + { + float btnX = 50.0f, btnY = 400.0f; + float btnW = 200.0f, btnH = 50.0f; + + auto sprite = std::make_unique("sprite"); + // UIRenderer centers the sprite + sprite->setDouble("x", static_cast(btnX + btnW * 0.5f)); + sprite->setDouble("y", static_cast(btnY + btnH * 0.5f)); + sprite->setDouble("scaleX", static_cast(btnW)); + sprite->setDouble("scaleY", static_cast(btnH)); + sprite->setInt("color", 0x0984E3FF); // Blue (like primary button) + sprite->setInt("textureId", 0); + sprite->setInt("layer", 1000); // UI layer + + m_logger->info("[PUB] Button Sprite: pos=({},{}) scale=({},{}) color=0x{:08X}", + btnX + btnW * 0.5f, btnY + btnH * 0.5f, btnW, btnH, 0x0984E3FF); + + m_testIO->publish("render:sprite", std::move(sprite)); + } + + // Now render + m_logger->info("\n[RENDER] Processing frame..."); + + JsonDataNode input("input"); + input.setDouble("deltaTime", 0.016); + input.setInt("frameCount", m_frameCount++); + + m_renderer->process(input); + + m_logger->info("[RENDER] Frame complete\n"); + } + + void shutdown() { + m_renderer->shutdown(); + IntraIOManager::getInstance().removeInstance("renderer_diag"); + IntraIOManager::getInstance().removeInstance("test_publisher"); + m_logger->info("Diagnostic test complete"); + } + + int getFrameCount() const { return m_frameCount; } + +private: + std::shared_ptr m_logger; + std::unique_ptr m_renderer; + std::shared_ptr m_rendererIO; + std::shared_ptr m_testIO; + int m_frameCount = 0; +}; + +int main(int argc, char* argv[]) { + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; + return 1; + } + + SDL_Window* window = SDL_CreateWindow( + "Sprite Rendering Diagnostic", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 800, 600, + SDL_WINDOW_SHOWN + ); + + if (!window) { + std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl; + SDL_Quit(); + return 1; + } + + SpriteDiagnostic diag; + if (!diag.init(window)) { + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + std::cout << "\n=== SPRITE DIAGNOSTIC TEST ===" << std::endl; + std::cout << "Expected: 3 colored rectangles" << std::endl; + std::cout << " - RED (200x150) at center of screen" << std::endl; + std::cout << " - GREEN (100x100) at top-left" << std::endl; + std::cout << " - BLUE (200x50) at bottom-left (simulating UI button)" << std::endl; + std::cout << "\nPress ESC to exit\n" << std::endl; + + bool running = true; + int frameCount = 0; + const int maxFrames = 300; // Run for ~5 seconds + + while (running && frameCount < maxFrames) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || + (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + } + + diag.runTest(); + frameCount++; + + SDL_Delay(16); + } + + std::cout << "\nRendered " << frameCount << " frames" << std::endl; + std::cout << "If you saw the colored rectangles: IIO routing works!" << std::endl; + std::cout << "If blank/red background only: Problem in SceneCollector or SpritePass" << std::endl; + + diag.shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} diff --git a/tests/visual/test_ui_showcase.cpp b/tests/visual/test_ui_showcase.cpp new file mode 100644 index 0000000..19686c7 --- /dev/null +++ b/tests/visual/test_ui_showcase.cpp @@ -0,0 +1,649 @@ +/** + * UIModule Complete Showcase + * + * Demonstrates ALL widgets of UIModule: + * - UIPanel (containers, backgrounds) + * - UIButton (click actions, hover states) + * - UILabel (static text, different sizes) + * - UICheckbox (toggle states) + * - UISlider (horizontal values) + * - UITextInput (text entry) + * - UIProgressBar (animated progress) + * - UIScrollPanel (scrollable content) + * + * Uses BgfxRenderer for rendering. + * Uses manual SDL input converted to IIO messages. + * + * Controls: + * - Mouse: Interact with UI widgets + * - ESC: Exit + */ + +#include +#include +#include +#include +#include +#include + +#include "BgfxRendererModule.h" +#include "UIModule.h" +#include +#include +#include +#include +#include +#include + +using namespace grove; + +// Create the UI layout JSON - must be a single root widget with children +static nlohmann::json createUILayout() { + nlohmann::json root; + + // Root panel (full screen background) + root["type"] = "panel"; + root["id"] = "root"; + root["x"] = 0; + root["y"] = 0; + root["width"] = 1024; + root["height"] = 768; + root["style"] = {{"bgColor", "0x1a1a2eFF"}}; + + // Children array (like test_single_button) + nlohmann::json children = nlohmann::json::array(); + + // Title label + children.push_back({ + {"type", "label"}, + {"id", "title"}, + {"x", 350}, {"y", 20}, + {"width", 400}, {"height", 50}, + {"text", "UIModule Showcase"}, + {"style", {{"fontSize", 36}, {"color", "0xFFFFFFFF"}}} + }); + + // Subtitle + children.push_back({ + {"type", "label"}, + {"id", "subtitle"}, + {"x", 380}, {"y", 65}, + {"width", 300}, {"height", 30}, + {"text", "Interactive Widget Demo"}, + {"style", {{"fontSize", 16}, {"color", "0x888888FF"}}} + }); + + // === LEFT COLUMN: Buttons Panel === + children.push_back({ + {"type", "panel"}, + {"id", "buttons_panel"}, + {"x", 30}, {"y", 120}, + {"width", 300}, {"height", 280}, + {"style", {{"bgColor", "0x2d3436FF"}}} + }); + + children.push_back({ + {"type", "label"}, + {"id", "buttons_title"}, + {"x", 45}, {"y", 130}, + {"width", 200}, {"height", 30}, + {"text", "Buttons"}, + {"style", {{"fontSize", 20}, {"color", "0xFFFFFFFF"}}} + }); + + // Button 1 - Primary (with very contrasting hover for testing) + children.push_back({ + {"type", "button"}, + {"id", "btn_primary"}, + {"x", 50}, {"y", 170}, + {"width", 260}, {"height", 45}, + {"text", "Primary Action"}, + {"onClick", "action_primary"}, + {"style", { + {"normal", {{"bgColor", "0x0984e3FF"}, {"textColor", "0xFFFFFFFF"}}}, + {"hover", {{"bgColor", "0xFF0000FF"}, {"textColor", "0xFFFFFFFF"}}}, // BRIGHT RED + {"pressed", {{"bgColor", "0x00FF00FF"}, {"textColor", "0xFFFFFFFF"}}} // BRIGHT GREEN + }} + }); + + // Button 2 - Success + children.push_back({ + {"type", "button"}, + {"id", "btn_success"}, + {"x", 50}, {"y", 225}, + {"width", 260}, {"height", 45}, + {"text", "Success Button"}, + {"onClick", "action_success"}, + {"style", { + {"normal", {{"bgColor", "0x00b894FF"}, {"textColor", "0xFFFFFFFF"}}}, + {"hover", {{"bgColor", "0x55efc4FF"}, {"textColor", "0xFFFFFFFF"}}}, + {"pressed", {{"bgColor", "0x009874FF"}, {"textColor", "0xFFFFFFFF"}}} + }} + }); + + // Button 3 - Warning + children.push_back({ + {"type", "button"}, + {"id", "btn_warning"}, + {"x", 50}, {"y", 280}, + {"width", 260}, {"height", 45}, + {"text", "Warning Button"}, + {"onClick", "action_warning"}, + {"style", { + {"normal", {{"bgColor", "0xfdcb6eFF"}, {"textColor", "0x2d3436FF"}}}, + {"hover", {{"bgColor", "0xffeaa7FF"}, {"textColor", "0x2d3436FF"}}}, + {"pressed", {{"bgColor", "0xd4a84eFF"}, {"textColor", "0x2d3436FF"}}} + }} + }); + + // Button 4 - Danger + children.push_back({ + {"type", "button"}, + {"id", "btn_danger"}, + {"x", 50}, {"y", 335}, + {"width", 260}, {"height", 45}, + {"text", "Danger Button"}, + {"onClick", "action_danger"}, + {"style", { + {"normal", {{"bgColor", "0xd63031FF"}, {"textColor", "0xFFFFFFFF"}}}, + {"hover", {{"bgColor", "0xff7675FF"}, {"textColor", "0xFFFFFFFF"}}}, + {"pressed", {{"bgColor", "0xa62021FF"}, {"textColor", "0xFFFFFFFF"}}} + }} + }); + + // === MIDDLE COLUMN: Inputs Panel === + children.push_back({ + {"type", "panel"}, + {"id", "inputs_panel"}, + {"x", 362}, {"y", 120}, + {"width", 300}, {"height", 280}, + {"style", {{"bgColor", "0x2d3436FF"}}} + }); + + children.push_back({ + {"type", "label"}, + {"id", "inputs_title"}, + {"x", 377}, {"y", 130}, + {"width", 200}, {"height", 30}, + {"text", "Input Widgets"}, + {"style", {{"fontSize", 20}, {"color", "0xFFFFFFFF"}}} + }); + + // Checkboxes + children.push_back({ + {"type", "checkbox"}, + {"id", "chk_option1"}, + {"x", 382}, {"y", 175}, + {"width", 200}, {"height", 24}, + {"checked", false}, + {"text", "Enable feature A"} + }); + + children.push_back({ + {"type", "checkbox"}, + {"id", "chk_option2"}, + {"x", 382}, {"y", 210}, + {"width", 200}, {"height", 24}, + {"checked", true}, + {"text", "Enable feature B"} + }); + + // Slider + children.push_back({ + {"type", "label"}, + {"id", "slider_label"}, + {"x", 382}, {"y", 255}, + {"width", 200}, {"height", 20}, + {"text", "Volume: 50%"}, + {"style", {{"fontSize", 14}, {"color", "0xAAAAAAFF"}}} + }); + + children.push_back({ + {"type", "slider"}, + {"id", "volume_slider"}, + {"x", 382}, {"y", 280}, + {"width", 260}, {"height", 24}, + {"min", 0.0}, + {"max", 100.0}, + {"value", 50.0} + }); + + // Text input + children.push_back({ + {"type", "label"}, + {"id", "textinput_label"}, + {"x", 382}, {"y", 320}, + {"width", 200}, {"height", 20}, + {"text", "Username:"}, + {"style", {{"fontSize", 14}, {"color", "0xAAAAAAFF"}}} + }); + + children.push_back({ + {"type", "textinput"}, + {"id", "username_input"}, + {"x", 382}, {"y", 345}, + {"width", 260}, {"height", 35}, + {"placeholder", "Enter username..."} + }); + + // === RIGHT COLUMN: Progress Panel === + children.push_back({ + {"type", "panel"}, + {"id", "progress_panel"}, + {"x", 694}, {"y", 120}, + {"width", 300}, {"height", 280}, + {"style", {{"bgColor", "0x2d3436FF"}}} + }); + + children.push_back({ + {"type", "label"}, + {"id", "progress_title"}, + {"x", 709}, {"y", 130}, + {"width", 200}, {"height", 30}, + {"text", "Progress Bars"}, + {"style", {{"fontSize", 20}, {"color", "0xFFFFFFFF"}}} + }); + + // Progress bars + children.push_back({ + {"type", "label"}, + {"x", 714}, {"y", 170}, + {"width", 200}, {"height", 20}, + {"text", "Loading..."}, + {"style", {{"fontSize", 14}, {"color", "0xAAAAAAFF"}}} + }); + + children.push_back({ + {"type", "progressbar"}, + {"id", "progress_loading"}, + {"x", 714}, {"y", 195}, + {"width", 260}, {"height", 20}, + {"value", 0.35} + }); + + children.push_back({ + {"type", "label"}, + {"x", 714}, {"y", 230}, + {"width", 200}, {"height", 20}, + {"text", "Download: 75%"}, + {"style", {{"fontSize", 14}, {"color", "0xAAAAAAFF"}}} + }); + + children.push_back({ + {"type", "progressbar"}, + {"id", "progress_download"}, + {"x", 714}, {"y", 255}, + {"width", 260}, {"height", 20}, + {"value", 0.75} + }); + + children.push_back({ + {"type", "label"}, + {"x", 714}, {"y", 290}, + {"width", 200}, {"height", 20}, + {"text", "Health: 45%"}, + {"style", {{"fontSize", 14}, {"color", "0xAAAAAAFF"}}} + }); + + children.push_back({ + {"type", "progressbar"}, + {"id", "progress_health"}, + {"x", 714}, {"y", 315}, + {"width", 260}, {"height", 25}, + {"value", 0.45} + }); + + // === BOTTOM: Info Panel === + children.push_back({ + {"type", "panel"}, + {"id", "info_panel"}, + {"x", 30}, {"y", 420}, + {"width", 964}, {"height", 320}, + {"style", {{"bgColor", "0x2d3436FF"}}} + }); + + children.push_back({ + {"type", "label"}, + {"id", "info_title"}, + {"x", 45}, {"y", 430}, + {"width", 400}, {"height", 30}, + {"text", "UI Showcase - Click buttons and interact!"}, + {"style", {{"fontSize", 20}, {"color", "0xFFFFFFFF"}}} + }); + + children.push_back({ + {"type", "label"}, + {"x", 45}, {"y", 470}, + {"width", 900}, {"height", 250}, + {"text", "This showcase demonstrates UIModule widgets:\n- Panels (containers)\n- Labels (text)\n- Buttons (4 styles with hover/press states)\n- Checkboxes (toggles)\n- Sliders (value input)\n- Text Input (keyboard entry)\n- Progress Bars (3 examples)"}, + {"style", {{"fontSize", 16}, {"color", "0xAAAAAAFF"}}} + }); + + root["children"] = children; + return root; +} + +class UIShowcase { +public: + UIShowcase() = default; + + bool init(SDL_Window* window) { + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + SDL_GetWindowWMInfo(window, &wmi); + + m_logger = spdlog::stdout_color_mt("UIShowcase"); + spdlog::set_level(spdlog::level::info); + + m_logger->info("=== UIModule Complete Showcase ==="); + + // Create IIO instances - keep shared_ptr alive, use IIO* abstract interface + // renderer: subscribes to render:* topics (from UI and game) + // ui: subscribes to input:* topics, publishes ui:* events + // input: publishes input:* topics + // game: subscribes to ui:* events, can publish render:* directly + m_rendererIOPtr = IntraIOManager::getInstance().createInstance("renderer"); + m_uiIOPtr = IntraIOManager::getInstance().createInstance("ui_module"); + m_inputIOPtr = IntraIOManager::getInstance().createInstance("input"); + m_gameIOPtr = IntraIOManager::getInstance().createInstance("game"); + m_rendererIO = m_rendererIOPtr.get(); + m_uiIO = m_uiIOPtr.get(); + m_inputIO = m_inputIOPtr.get(); + m_gameIO = m_gameIOPtr.get(); + + // Create and configure BgfxRenderer + m_renderer = std::make_unique(); + { + JsonDataNode config("config"); + config.setDouble("nativeWindowHandle", + static_cast(reinterpret_cast(wmi.info.win.window))); + config.setInt("windowWidth", 1024); + config.setInt("windowHeight", 768); + config.setString("backend", "d3d11"); + config.setBool("vsync", true); + m_renderer->setConfiguration(config, m_rendererIO, nullptr); + } + + // Create and configure UIModule with inline layout + m_uiModule = std::make_unique(); + { + nlohmann::json layoutJson = createUILayout(); + JsonDataNode config("config", layoutJson); + config.setInt("windowWidth", 1024); + config.setInt("windowHeight", 768); + config.setInt("baseLayer", 1000); + + // Add layout as child + auto layoutNode = std::make_unique("layout", layoutJson); + config.setChild("layout", std::move(layoutNode)); + + m_uiModule->setConfiguration(config, m_uiIO, nullptr); + } + + // Subscribe game to UI events + m_gameIO->subscribe("ui:action"); + m_gameIO->subscribe("ui:click"); + m_gameIO->subscribe("ui:value_changed"); + m_gameIO->subscribe("ui:text_changed"); + m_gameIO->subscribe("ui:text_submit"); + m_gameIO->subscribe("ui:hover"); + + m_logger->info("Modules initialized"); + m_logger->info("Controls: Mouse to interact, ESC to exit"); + + return true; + } + + void handleSDLEvent(SDL_Event& e) { + // Convert SDL events to IIO input messages + // IMPORTANT: Publish from m_gameIO (not m_inputIO or m_uiIO) because IIO doesn't deliver to self + if (e.type == SDL_MOUSEMOTION) { + auto msg = std::make_unique("mouse"); + msg->setDouble("x", static_cast(e.motion.x)); + msg->setDouble("y", static_cast(e.motion.y)); + m_gameIO->publish("input:mouse:move", std::move(msg)); + } + else if (e.type == SDL_MOUSEBUTTONDOWN || e.type == SDL_MOUSEBUTTONUP) { + auto msg = std::make_unique("mouse"); + msg->setInt("button", e.button.button - 1); // SDL uses 1-based + msg->setBool("pressed", e.type == SDL_MOUSEBUTTONDOWN); + msg->setDouble("x", static_cast(e.button.x)); + msg->setDouble("y", static_cast(e.button.y)); + m_gameIO->publish("input:mouse:button", std::move(msg)); + } + else if (e.type == SDL_MOUSEWHEEL) { + auto msg = std::make_unique("wheel"); + msg->setDouble("delta", static_cast(e.wheel.y)); + m_gameIO->publish("input:mouse:wheel", std::move(msg)); + } + else if (e.type == SDL_KEYDOWN) { + auto msg = std::make_unique("key"); + msg->setInt("keyCode", e.key.keysym.sym); + msg->setInt("scancode", e.key.keysym.scancode); + msg->setBool("pressed", true); + msg->setInt("char", 0); + m_gameIO->publish("input:keyboard", std::move(msg)); + } + else if (e.type == SDL_TEXTINPUT) { + auto msg = std::make_unique("key"); + msg->setInt("keyCode", 0); + msg->setBool("pressed", true); + msg->setInt("char", static_cast(e.text.text[0])); + m_gameIO->publish("input:keyboard", std::move(msg)); + } + } + + void update(float dt) { + m_time += dt; + m_frameCount++; + + // Process UI events from game perspective + processUIEvents(); + + // Animate progress bars + animateProgressBars(dt); + } + + void render() { + // Set camera for full window + { + auto cam = std::make_unique("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", 1024); + cam->setInt("viewportH", 768); + m_gameIO->publish("render:camera", std::move(cam)); + } + + // Process modules in order: UI first (to handle input and prepare render commands) + JsonDataNode input("input"); + input.setDouble("deltaTime", 0.016); + input.setInt("frameCount", m_frameCount); + + m_uiModule->process(input); + m_renderer->process(input); + } + + void shutdown() { + m_uiModule->shutdown(); + m_renderer->shutdown(); + + IntraIOManager::getInstance().removeInstance("renderer"); + IntraIOManager::getInstance().removeInstance("ui_module"); + IntraIOManager::getInstance().removeInstance("input"); + IntraIOManager::getInstance().removeInstance("game"); + + m_logger->info("UIShowcase shutdown complete"); + } + + int getFrameCount() const { return m_frameCount; } + +private: + void processUIEvents() { + while (m_gameIO->hasMessages() > 0) { + auto msg = m_gameIO->pullMessage(); + + std::string logEntry; + + if (msg.topic == "ui:action") { + std::string action = msg.data->getString("action", ""); + std::string widgetId = msg.data->getString("widgetId", ""); + logEntry = "Action: " + action + " (" + widgetId + ")"; + + // Handle specific actions + if (action == "action_primary") { + m_logger->info("Primary button clicked!"); + } + else if (action == "action_danger") { + m_logger->warn("Danger button clicked!"); + } + } + else if (msg.topic == "ui:click") { + std::string widgetId = msg.data->getString("widgetId", ""); + double x = msg.data->getDouble("x", 0); + double y = msg.data->getDouble("y", 0); + logEntry = "Click: " + widgetId + " at (" + + std::to_string(static_cast(x)) + "," + + std::to_string(static_cast(y)) + ")"; + } + else if (msg.topic == "ui:value_changed") { + std::string widgetId = msg.data->getString("widgetId", ""); + if (widgetId == "volume_slider") { + double value = msg.data->getDouble("value", 0); + logEntry = "Volume: " + std::to_string(static_cast(value)) + "%"; + } + else if (widgetId.find("chk_") == 0) { + bool checked = msg.data->getBool("checked", false); + logEntry = widgetId + " = " + (checked ? "ON" : "OFF"); + } + } + else if (msg.topic == "ui:text_submit") { + std::string widgetId = msg.data->getString("widgetId", ""); + std::string text = msg.data->getString("text", ""); + logEntry = "Submit: " + widgetId + " = \"" + text + "\""; + } + else if (msg.topic == "ui:hover") { + std::string widgetId = msg.data->getString("widgetId", ""); + bool enter = msg.data->getBool("enter", false); + if (enter && !widgetId.empty()) { + logEntry = "Hover: " + widgetId; + } + } + + // Add to log if we have an entry + if (!logEntry.empty()) { + addLogEntry(logEntry); + } + } + } + + void addLogEntry(const std::string& entry) { + // Shift log entries up + for (int i = 7; i > 0; --i) { + m_logEntries[i] = m_logEntries[i - 1]; + } + m_logEntries[0] = entry; + + m_logger->info("UI Event: {}", entry); + } + + void animateProgressBars(float dt) { + // Animate loading progress (loops) + m_loadingProgress += dt * 0.3f; + if (m_loadingProgress > 1.0f) { + m_loadingProgress = 0.0f; + } + + // We would update progress bars here if we had a way to modify widgets + // For now, this is just simulation - the actual values are set in layout + } + + std::shared_ptr m_logger; + std::unique_ptr m_renderer; + std::unique_ptr m_uiModule; + // Keep shared_ptr alive + std::shared_ptr m_rendererIOPtr; + std::shared_ptr m_uiIOPtr; + std::shared_ptr m_inputIOPtr; + std::shared_ptr m_gameIOPtr; + // Abstract IIO* interface + IIO* m_rendererIO = nullptr; + IIO* m_uiIO = nullptr; + IIO* m_inputIO = nullptr; + IIO* m_gameIO = nullptr; + + float m_time = 0.0f; + int m_frameCount = 0; + + // Progress animation + float m_loadingProgress = 0.0f; + + // Event log + std::string m_logEntries[8]; +}; + +int main(int argc, char* argv[]) { + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; + return 1; + } + + // Enable text input for text fields + SDL_StartTextInput(); + + SDL_Window* window = SDL_CreateWindow( + "UIModule Showcase", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + 1024, 768, + SDL_WINDOW_SHOWN + ); + + if (!window) { + std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl; + SDL_Quit(); + return 1; + } + + UIShowcase showcase; + if (!showcase.init(window)) { + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + bool running = true; + Uint64 lastTime = SDL_GetPerformanceCounter(); + + while (running) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT || + (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)) { + running = false; + } + showcase.handleSDLEvent(e); + } + + Uint64 now = SDL_GetPerformanceCounter(); + float dt = static_cast(now - lastTime) / SDL_GetPerformanceFrequency(); + lastTime = now; + + showcase.update(dt); + showcase.render(); + + SDL_Delay(1); + } + + SDL_StopTextInput(); + + int frames = showcase.getFrameCount(); + showcase.shutdown(); + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "Rendered " << frames << " frames" << std::endl; + return 0; +}