fix: UIModule button interaction + JsonDataNode array children support

- Fix JsonDataNode::getChildReadOnly() to handle JSON array access by numeric index
- Fix test_ui_showcase to use JSON array for children (matching test_single_button pattern)
- Add visual test files: test_single_button, test_ui_showcase, test_sprite_debug
- Clean up debug logging from SpritePass, SceneCollector, UIButton, BgfxDevice

The root cause was that UITree couldn't access array children in JSON layouts.
UIButton hover/click now works correctly in both test files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-05 18:23:16 +07:00
parent 2f16ba0362
commit 5cef0e25b0
37 changed files with 4739 additions and 1223 deletions

View File

@ -26,6 +26,17 @@ struct Message {
std::string topic;
std::unique_ptr<IDataNode> 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 {

View File

@ -70,6 +70,15 @@ private:
std::chrono::high_resolution_clock::time_point lastBatch;
std::unordered_map<std::string, Message> batchedMessages; // For replaceable messages
std::vector<Message> 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<Subscription> highFreqSubscriptions;

View File

@ -177,61 +177,45 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
}
void BgfxRendererModule::process(const IDataNode& input) {
// Validate device
if (!m_device) {
m_logger->error("BgfxRenderer::process called but device is not initialized");
m_logger->error("Device not initialized");
return;
}
// Read deltaTime from input (provided by ModuleSystem)
// 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<float>(input.getDouble("deltaTime", 0.016));
// Check for resize in input
int newWidth = input.getInt("windowWidth", 0);
int newHeight = input.getInt("windowHeight", 0);
if (newWidth > 0 && newHeight > 0 &&
(static_cast<uint16_t>(newWidth) != m_width || static_cast<uint16_t>(newHeight) != m_height)) {
m_width = static_cast<uint16_t>(newWidth);
m_height = static_cast<uint16_t>(newHeight);
m_device->reset(m_width, m_height);
m_logger->info("Window resized to {}x{}", m_width, m_height);
}
// 1. Collect IIO messages (pull-based)
if (m_sceneCollector && m_io) {
// 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();
}
// 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<uint32_t>(frame.spriteCount), 1);
m_debugOverlay->render(m_width, m_height);
}
// 6. Present
// Present frame
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);
@ -299,8 +283,11 @@ std::unique_ptr<IDataNode> BgfxRendererModule::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
@ -318,3 +305,5 @@ GROVE_MODULE_EXPORT void destroyModule(grove::IModule* module) {
}
}
#endif // GROVE_MODULE_STATIC

View File

@ -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);
}

View File

@ -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 <stdint.h>
# 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
# ============================================================================

View File

@ -1,8 +1,27 @@
#include "DebugPass.h"
#include "../RHI/RHIDevice.h"
#include "../Frame/FramePacket.h"
#include <vector>
#include <cstring>
#include <spdlog/spdlog.h>
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<float>((color >> 24) & 0xFF) / 255.0f;
g = static_cast<float>((color >> 16) & 0xFF) / 255.0f;
b = static_cast<float>((color >> 8) & 0xFF) / 255.0f;
a = static_cast<float>(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<DebugVertex> 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<uint32_t>(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);
// Draw all lines
cmd.setVertexBuffer(m_lineVB);
cmd.draw(static_cast<uint32_t>(frame.debugLineCount * 2));
cmd.draw(static_cast<uint32_t>(vertices.size()));
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
}
}
} // namespace grove

View File

@ -4,6 +4,7 @@
#include "../Resources/ResourceCache.h"
#include <algorithm>
#include <cstring>
#include <spdlog/spdlog.h>
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<uint32_t>(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<uint16_t>(frame.sprites[m_sortedIndices[batchStart]].textureId);
uint32_t batchEnd = batchStart + 1;
while (batchEnd < frame.spriteCount) {
uint16_t nextTexId = static_cast<uint16_t>(frame.sprites[m_sortedIndices[batchEnd]].textureId);
if (nextTexId != currentTexId) {
break; // Texture changed, flush this batch
}
++batchEnd;
// Copy sorted sprites to temporary buffer (like TextPass does with glyphs)
std::vector<SpriteInstance> sortedSprites;
sortedSprites.reserve(frame.spriteCount);
for (uint32_t idx : m_sortedIndices) {
sortedSprites.push_back(frame.sprites[idx]);
}
uint32_t batchCount = batchEnd - batchStart;
// Update dynamic instance buffer with ALL sprites (like TextPass)
device.updateBuffer(m_instanceBuffer, sortedSprites.data(),
static_cast<uint32_t>(sortedSprites.size() * sizeof(SpriteInstance)));
// 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<SpriteInstance*>(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
// Set buffers and draw ALL sprites in ONE call (like TextPass)
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setTransientInstanceBuffer(transientBuffer, 0, batchCount);
cmd.setTexture(0, batchTexture, m_textureSampler);
cmd.drawInstanced(6, batchCount);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(sortedSprites.size()));
cmd.setTexture(0, m_defaultTexture, m_textureSampler);
cmd.drawInstanced(6, static_cast<uint32_t>(sortedSprites.size()));
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<SpriteInstance> 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<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
cmd.setInstanceBuffer(m_instanceBuffer, 0, batchCount);
flushBatch(device, cmd, batchTexture, batchCount);
}
batchStart = batchEnd;
}
}
} // namespace grove

View File

@ -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

View File

@ -30,9 +30,18 @@ public:
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;
// Let bgfx auto-select the best renderer (D3D11 on Windows)
init.type = bgfx::RendererType::Count;
init.type = bgfx::RendererType::Direct3D11;
init.resolution.width = width;
init.resolution.height = height;
init.resolution.reset = BGFX_RESET_VSYNC;
@ -49,8 +58,8 @@ public:
// 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);
// 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;
@ -327,6 +336,8 @@ public:
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
@ -386,6 +397,20 @@ public:
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;
}

View File

@ -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;
};

View File

@ -3,6 +3,7 @@
#include "grove/IDataNode.h"
#include "../Frame/FrameAllocator.h"
#include <cstring>
#include <spdlog/spdlog.h>
namespace grove {
@ -196,6 +197,7 @@ void SceneCollector::parseSprite(const IDataNode& data) {
sprite.y = static_cast<float>(data.getDouble("y", 0.0));
sprite.scaleX = static_cast<float>(data.getDouble("scaleX", 1.0));
sprite.scaleY = static_cast<float>(data.getDouble("scaleY", 1.0));
// i_data1
sprite.rotation = static_cast<float>(data.getDouble("rotation", 0.0));
sprite.u0 = static_cast<float>(data.getDouble("u0", 0.0));
@ -366,8 +368,13 @@ void SceneCollector::parseDebugRect(const IDataNode& data) {
DebugRect rect;
rect.x = static_cast<float>(data.getDouble("x", 0.0));
rect.y = static_cast<float>(data.getDouble("y", 0.0));
rect.w = static_cast<float>(data.getDouble("w", 0.0));
rect.h = static_cast<float>(data.getDouble("h", 0.0));
// 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<float>(w);
rect.h = static_cast<float>(h);
rect.color = static_cast<uint32_t>(data.getInt("color", 0x00FF00FF));
rect.filled = data.getBool("filled", false);

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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
)
if(SDL2_FOUND)
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_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

View File

@ -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()

View File

@ -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()
}
/**

View File

@ -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<uint32_t>(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<uint32_t>(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<uint32_t>(std::stoul(borderColorStr, nullptr, 16));
}
style.borderWidth = static_cast<float>(styleNode->getDouble("borderWidth", style.borderWidth));
style.borderRadius = static_cast<float>(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<IDataNode&>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(std::stoul(textColorStr, nullptr, 16));
}
parseButtonStyle(disabledStyle, button->disabledStyle);
}
// Font size from style root
button->fontSize = static_cast<float>(style->getDouble("fontSize", 16.0));
}
// Auto-generate hover/pressed styles if not explicitly set
button->generateDefaultStyles();
return button;
});

View File

@ -1,5 +1,6 @@
#include "UIRenderer.h"
#include <grove/JsonDataNode.h>
#include <spdlog/spdlog.h>
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<JsonDataNode>("sprite");
sprite->setDouble("x", static_cast<double>(x));
sprite->setDouble("y", static_cast<double>(y));
sprite->setDouble("width", static_cast<double>(w));
sprite->setDouble("height", static_cast<double>(h));
// Position at center of rect (sprite shader centers quads)
sprite->setDouble("x", static_cast<double>(x + w * 0.5f));
sprite->setDouble("y", static_cast<double>(y + h * 0.5f));
sprite->setDouble("scaleX", static_cast<double>(w));
sprite->setDouble("scaleY", static_cast<double>(h));
sprite->setInt("color", static_cast<int>(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<JsonDataNode>("sprite");
sprite->setDouble("x", static_cast<double>(x));
sprite->setDouble("y", static_cast<double>(y));
sprite->setDouble("width", static_cast<double>(w));
sprite->setDouble("height", static_cast<double>(h));
// Position at center of sprite (sprite shader centers quads)
sprite->setDouble("x", static_cast<double>(x + w * 0.5f));
sprite->setDouble("y", static_cast<double>(y + h * 0.5f));
sprite->setDouble("scaleX", static_cast<double>(w));
sprite->setDouble("scaleY", static_cast<double>(h));
sprite->setInt("color", static_cast<int>(color));
sprite->setInt("textureId", textureId);
sprite->setInt("layer", nextLayer());

View File

@ -424,8 +424,11 @@ std::unique_ptr<IDataNode> 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

View File

@ -2,6 +2,8 @@
#include "../Core/UIContext.h"
#include "../Rendering/UIRenderer.h"
#include <algorithm>
#include <cmath>
#include <spdlog/spdlog.h>
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
// 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<uint8_t>(std::min(255.0f, std::max(0.0f, r * factor)));
g = static_cast<uint8_t>(std::min(255.0f, std::max(0.0f, g * factor)));
b = static_cast<uint8_t>(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;

View File

@ -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

View File

@ -2,6 +2,7 @@
#include "../Core/UIContext.h"
#include "../Core/UILayout.h"
#include "../Rendering/UIRenderer.h"
#include <spdlog/spdlog.h>
namespace grove {

View File

@ -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 {

View File

@ -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,6 +26,21 @@ IntraIOManager::~IntraIOManager() {
if (batchThread.joinable()) {
batchThread.join();
}
// 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
@ -34,6 +49,7 @@ IntraIOManager::~IntraIOManager() {
logger->info(" Total routed messages: {}", stats["total_routed_messages"].get<size_t>());
logger->info(" Total routes: {}", stats["total_routes"].get<size_t>());
logger->info(" Active instances: {}", stats["active_instances"].get<size_t>());
}
{
std::unique_lock lock(managerMutex); // WRITE - exclusive access needed
@ -48,8 +64,10 @@ IntraIOManager::~IntraIOManager() {
batchBuffers.clear();
}
if (loggerValid) {
logger->info("🌐🔗 IntraIOManager destroyed");
}
}
std::shared_ptr<IntraIO> IntraIOManager::createInstance(const std::string& instanceId) {
std::unique_lock lock(managerMutex); // WRITE - exclusive access needed

View File

@ -35,29 +35,92 @@ std::unique_ptr<IDataNode> 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;
}
// Return raw pointer without copying - valid as long as parent exists
if (it != m_children.end()) {
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<JsonDataNode>(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<JsonDataNode>(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<std::string> JsonDataNode::getChildNames() {
std::vector<std::string> 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;
}
// ========================================

View File

@ -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) {

View File

@ -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,17 +701,24 @@ 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
)
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
@ -698,19 +728,42 @@ if(GROVE_BUILD_BGFX_RENDERER)
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
)
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
@ -720,6 +773,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_22_bgfx_sprites' enabled (run manually)")
@ -729,10 +783,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_23_bgfx_sprites_visual.cpp
)
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
@ -740,6 +799,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_23_bgfx_sprites_visual' enabled (run manually)")
@ -750,10 +810,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_24_ui_basic.cpp
)
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
@ -761,6 +826,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_24_ui_basic' enabled (run manually)")
@ -770,10 +836,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_25_ui_layout.cpp
)
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
@ -781,6 +852,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_25_ui_layout' enabled (run manually)")
@ -790,10 +862,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_26_ui_buttons.cpp
)
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
@ -801,6 +878,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_26_ui_buttons' enabled (run manually)")
@ -810,10 +888,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_28_ui_scroll.cpp
)
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
@ -821,6 +904,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_28_ui_scroll' enabled (run manually)")
@ -830,10 +914,15 @@ if(GROVE_BUILD_BGFX_RENDERER)
visual/test_29_ui_advanced.cpp
)
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
@ -841,6 +930,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
dl
X11
)
endif()
# Not added to CTest (requires display)
message(STATUS "Visual test 'test_29_ui_advanced' enabled (run manually)")
@ -853,10 +943,18 @@ if(GROVE_BUILD_BGFX_RENDERER)
)
target_include_directories(test_30_input_module PRIVATE
/usr/include/SDL2
${CMAKE_SOURCE_DIR}/modules
)
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
@ -864,6 +962,7 @@ if(GROVE_BUILD_BGFX_RENDERER)
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,11 +1303,18 @@ 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
)
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
@ -1204,11 +1322,132 @@ if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER)
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()

View File

@ -0,0 +1,90 @@
/**
* Test using BgfxDevice directly via IRHIDevice::create()
* No ShaderManager, no RenderGraph - just init + frame
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <memory>
#include "RHI/RHIDevice.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
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;
}

View File

@ -0,0 +1,91 @@
/**
* Minimal bgfx test for Windows - no DLL, just renders a red screen
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <bgfx/bgfx.h>
#include <bgfx/platform.h>
#include <iostream>
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;
}

View File

@ -0,0 +1,188 @@
/**
* Minimal test using BgfxRenderer_static - no DLLs loaded
* Tests if BgfxRendererModule works in isolation
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <cmath>
#include <memory>
#include "BgfxRendererModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
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<BgfxRendererModule>();
JsonDataNode config("config");
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(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<JsonDataNode>("camera");
cam->setDouble("x", 0.0);
cam->setDouble("y", 0.0);
cam->setDouble("zoom", 1.0);
cam->setInt("viewportX", 0);
cam->setInt("viewportY", 0);
cam->setInt("viewportW", 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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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;
}

View File

@ -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 <grove/ModuleLoader.h>
@ -34,6 +38,11 @@
#include <dlfcn.h>
#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<IModule> renderer;
#ifdef USE_STATIC_BGFX
// Static linking: instantiate directly (required on Windows)
renderer = std::make_unique<BgfxRendererModule>();
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++;
}

View File

@ -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 <grove/ModuleLoader.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <grove/JsonDataNode.h>
#include <SDL.h>
#include <SDL_syswm.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <iostream>
#include <vector>
#include <random>
#include <thread>
#include <chrono>
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#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<float>(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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<float> posX(100.0f, 1820.0f);
std::uniform_real_distribution<float> posY(100.0f, 980.0f);
std::uniform_real_distribution<float> vel(-1.0f, 1.0f);
std::uniform_int_distribution<uint32_t> 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<spdlog::logger> m_logger;
std::vector<Sprite> 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<spdlog::sinks::stdout_color_sink_mt>();
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("full_stack_demo.log", true);
std::vector<spdlog::sink_ptr> sinks {console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("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<IModule> renderer;
#ifdef USE_STATIC_BGFX
// Static linking: instantiate directly (required on Windows)
renderer = std::make_unique<BgfxRendererModule>();
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<IModule> 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<JsonDataNode>("layout");
auto widgets = std::make_unique<JsonDataNode>("widgets");
// Panel background
auto panel = std::make_unique<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<IModule> 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;
}

View File

@ -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 <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <cmath>
#include <memory>
#include <vector>
#include <random>
#include "BgfxRendererModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
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<BgfxRendererModule>();
JsonDataNode config("config");
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(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<JsonDataNode>("clear");
clear->setInt("color", clearColors[m_clearColorIndex]);
m_gameIO->publish("render:clear", std::move(clear));
}
void sendCamera() {
auto cam = std::make_unique<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<uint8_t>(128 + pulse * 127);
uint32_t color = 0xFF4444FF | (alpha);
auto rect = std::make_unique<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("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<float> angleDist(0.0f, 2.0f * 3.14159f);
std::uniform_real_distribution<float> speedDist(50.0f, 200.0f);
std::uniform_real_distribution<float> sizeDist(5.0f, 20.0f);
std::uniform_real_distribution<float> lifeDist(0.5f, 2.0f);
uint32_t colors[] = {
0xFF4444FF, 0xFF8844FF, 0xFFCC44FF, 0xFFFF44FF,
0xFF6644FF, 0xFFAA00FF
};
std::uniform_int_distribution<int> 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<spdlog::logger> m_logger;
std::unique_ptr<BgfxRendererModule> m_renderer;
std::shared_ptr<IntraIO> m_rendererIOPtr; // Keep shared_ptr alive
std::shared_ptr<IntraIO> 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<Particle> 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<float>(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;
}

View File

@ -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 <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <memory>
#include "BgfxRendererModule.h"
#include "UIModule/UIModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
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<BgfxRendererModule>();
{
JsonDataNode config("config");
config.setDouble("nativeWindowHandle",
static_cast<double>(reinterpret_cast<uintptr_t>(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<UIModule>();
{
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<JsonDataNode>("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<JsonDataNode>("mouse");
mouseMsg->setDouble("x", static_cast<double>(e.motion.x));
mouseMsg->setDouble("y", static_cast<double>(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<JsonDataNode>("mouse");
mouseMsg->setInt("button", e.button.button);
mouseMsg->setBool("pressed", e.type == SDL_MOUSEBUTTONDOWN);
mouseMsg->setDouble("x", static_cast<double>(e.button.x));
mouseMsg->setDouble("y", static_cast<double>(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;
}

View File

@ -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 le pipeline échoue
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <memory>
#include "BgfxRendererModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
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<BgfxRendererModule>();
JsonDataNode config("config");
config.setDouble("nativeWindowHandle",
static_cast<double>(reinterpret_cast<uintptr_t>(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<JsonDataNode>("camera");
cam->setDouble("x", 0.0);
cam->setDouble("y", 0.0);
cam->setDouble("zoom", 1.0);
cam->setInt("viewportX", 0);
cam->setInt("viewportY", 0);
cam->setInt("viewportW", 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<JsonDataNode>("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<JsonDataNode>("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<JsonDataNode>("sprite");
// UIRenderer centers the sprite
sprite->setDouble("x", static_cast<double>(btnX + btnW * 0.5f));
sprite->setDouble("y", static_cast<double>(btnY + btnH * 0.5f));
sprite->setDouble("scaleX", static_cast<double>(btnW));
sprite->setDouble("scaleY", static_cast<double>(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<spdlog::logger> m_logger;
std::unique_ptr<BgfxRendererModule> m_renderer;
std::shared_ptr<IntraIO> m_rendererIO;
std::shared_ptr<IntraIO> 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;
}

View File

@ -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 <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <cmath>
#include <memory>
#include <string>
#include "BgfxRendererModule.h"
#include "UIModule.h"
#include <grove/JsonDataNode.h>
#include <grove/IntraIOManager.h>
#include <grove/IntraIO.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <nlohmann/json.hpp>
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<BgfxRendererModule>();
{
JsonDataNode config("config");
config.setDouble("nativeWindowHandle",
static_cast<double>(reinterpret_cast<uintptr_t>(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<UIModule>();
{
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<JsonDataNode>("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<JsonDataNode>("mouse");
msg->setDouble("x", static_cast<double>(e.motion.x));
msg->setDouble("y", static_cast<double>(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<JsonDataNode>("mouse");
msg->setInt("button", e.button.button - 1); // SDL uses 1-based
msg->setBool("pressed", e.type == SDL_MOUSEBUTTONDOWN);
msg->setDouble("x", static_cast<double>(e.button.x));
msg->setDouble("y", static_cast<double>(e.button.y));
m_gameIO->publish("input:mouse:button", std::move(msg));
}
else if (e.type == SDL_MOUSEWHEEL) {
auto msg = std::make_unique<JsonDataNode>("wheel");
msg->setDouble("delta", static_cast<double>(e.wheel.y));
m_gameIO->publish("input:mouse:wheel", std::move(msg));
}
else if (e.type == SDL_KEYDOWN) {
auto msg = std::make_unique<JsonDataNode>("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<JsonDataNode>("key");
msg->setInt("keyCode", 0);
msg->setBool("pressed", true);
msg->setInt("char", static_cast<int>(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<JsonDataNode>("camera");
cam->setDouble("x", 0.0);
cam->setDouble("y", 0.0);
cam->setDouble("zoom", 1.0);
cam->setInt("viewportX", 0);
cam->setInt("viewportY", 0);
cam->setInt("viewportW", 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<int>(x)) + "," +
std::to_string(static_cast<int>(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<int>(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<spdlog::logger> m_logger;
std::unique_ptr<BgfxRendererModule> m_renderer;
std::unique_ptr<UIModule> m_uiModule;
// Keep shared_ptr alive
std::shared_ptr<IntraIO> m_rendererIOPtr;
std::shared_ptr<IntraIO> m_uiIOPtr;
std::shared_ptr<IntraIO> m_inputIOPtr;
std::shared_ptr<IntraIO> 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<float>(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;
}