fix: Multi-texture sprite rendering - setState per batch + transient buffers

Fixed two critical bugs preventing multiple textured sprites from rendering correctly:

1. **setState consumed by submit**: Render state was set once at the beginning,
   but bgfx consumes state at each submit(). Batches 2+ had no state → invisible.

   FIX: Call setState() before EACH batch, not once globally.

2. **Buffer overwrite race condition**: updateBuffer() is immediate but submit()
   is deferred. When batch 2 called updateBuffer(), it overwrote batch 1's data
   BEFORE bgfx executed the draw calls. All batches used the last batch's data
   → all sprites rendered at the same position (superimposed).

   FIX: Use transient buffers (one per batch, frame-local) instead of reusing
   the same dynamic buffer. Each batch gets its own isolated memory.

Changes:
- SpritePass: setState before each batch + transient buffer allocation per batch
- UIRenderer: Retained mode rendering (render:sprite:add/update/remove)
- test_ui_showcase: Added 3 textured buttons demo section
- test_3buttons_minimal: Minimal test case for multi-texture debugging

Tested: 3 textured buttons now render at correct positions with correct textures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-14 14:05:56 +07:00
parent a106c78bc8
commit 63751d6f91
8 changed files with 634 additions and 26 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -59,7 +59,7 @@ 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 4x4 texture (used when no texture is bound)
// Create default white 4x4 texture (restored to white)
// Some drivers have issues with 1x1 textures
uint32_t whitePixels[16];
for (int i = 0; i < 16; ++i) whitePixels[i] = 0xFFFFFFFF; // RGBA white
@ -98,15 +98,14 @@ 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;
// Set render state ONCE (like TextPass does)
// Prepare render state (will be set before each batch)
rhi::RenderState state;
state.blend = rhi::BlendMode::Alpha;
state.cull = rhi::CullMode::None;
state.depthTest = false;
state.depthWrite = false;
cmd.setState(state);
// Sort sprites by layer for correct draw order
// Sort sprites by layer first (for correct draw order), then by texture (for batching)
m_sortedIndices.clear();
m_sortedIndices.reserve(frame.spriteCount);
for (size_t i = 0; i < frame.spriteCount; ++i) {
@ -114,27 +113,133 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
}
std::sort(m_sortedIndices.begin(), m_sortedIndices.end(),
[&frame](uint32_t a, uint32_t b) {
return frame.sprites[a].layer < frame.sprites[b].layer;
// Sort by layer first, then by textureId for batching
if (frame.sprites[a].layer != frame.sprites[b].layer) {
return frame.sprites[a].layer < frame.sprites[b].layer;
}
return frame.sprites[a].textureId < frame.sprites[b].textureId;
});
// 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]);
// Batch sprites by texture
std::vector<SpriteInstance> batchSprites;
batchSprites.reserve(frame.spriteCount);
uint16_t currentTextureId = 0;
bool firstBatch = true;
static int spriteLogCount = 0;
for (size_t i = 0; i < m_sortedIndices.size(); ++i) {
uint32_t idx = m_sortedIndices[i];
const SpriteInstance& sprite = frame.sprites[idx];
uint16_t spriteTexId = static_cast<uint16_t>(sprite.textureId);
// Log first few textured sprites
if (spriteLogCount < 10 && spriteTexId > 0) {
spdlog::info("🎨 [SpritePass] Processing sprite #{}: textureId={}, pos=({:.1f},{:.1f}), scale={}x{}, layer={}",
spriteLogCount++, spriteTexId, sprite.x, sprite.y, sprite.scaleX, sprite.scaleY, (int)sprite.layer);
}
// Start new batch if texture changes
if (!firstBatch && spriteTexId != currentTextureId) {
// Flush previous batch using TRANSIENT BUFFER (one per batch)
uint32_t batchSize = static_cast<uint32_t>(batchSprites.size());
rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(batchSize);
// CRITICAL: Set render state before EACH batch (consumed by submit)
cmd.setState(state);
// Get texture handle from ResourceCache
rhi::TextureHandle texHandle = m_defaultTexture;
if (m_resourceCache && currentTextureId > 0) {
auto cachedTex = m_resourceCache->getTextureById(currentTextureId);
if (cachedTex.isValid()) {
texHandle = cachedTex;
static int batchNum = 0;
spdlog::info("[Batch #{}] SpritePass flushing batch: textureId={}, handle={}, size={}",
batchNum++, currentTextureId, texHandle.id, batchSprites.size());
}
}
if (transientBuffer.isValid()) {
// Copy sprite data to transient buffer (frame-local, won't be overwritten)
std::memcpy(transientBuffer.data, batchSprites.data(), batchSize * sizeof(SpriteInstance));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setTransientInstanceBuffer(transientBuffer, 0, batchSize);
cmd.setTexture(0, texHandle, m_textureSampler);
cmd.drawInstanced(6, batchSize);
cmd.submit(0, m_shader, 0);
} else {
// Fallback to dynamic buffer (single batch limitation - data will be overwritten!)
device.updateBuffer(m_instanceBuffer, batchSprites.data(), batchSize * sizeof(SpriteInstance));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, batchSize);
cmd.setTexture(0, texHandle, m_textureSampler);
cmd.drawInstanced(6, batchSize);
cmd.submit(0, m_shader, 0);
}
// Start new batch
batchSprites.clear();
}
batchSprites.push_back(sprite);
currentTextureId = spriteTexId;
firstBatch = false;
}
// Update dynamic instance buffer with ALL sprites (like TextPass)
device.updateBuffer(m_instanceBuffer, sortedSprites.data(),
static_cast<uint32_t>(sortedSprites.size() * sizeof(SpriteInstance)));
// Flush final batch
if (!batchSprites.empty()) {
// Use TRANSIENT BUFFER for final batch too
uint32_t batchSize = static_cast<uint32_t>(batchSprites.size());
rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(batchSize);
// Set buffers and draw ALL sprites in ONE call (like TextPass)
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<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);
// CRITICAL: Set render state before EACH batch (consumed by submit)
cmd.setState(state);
// Get texture handle from ResourceCache
rhi::TextureHandle texHandle = m_defaultTexture;
if (m_resourceCache && currentTextureId > 0) {
auto cachedTex = m_resourceCache->getTextureById(currentTextureId);
if (cachedTex.isValid()) {
texHandle = cachedTex;
static int finalBatchNum = 0;
spdlog::info("[Final Batch #{}] SpritePass flushing final batch: textureId={}, handle={}, size={}",
finalBatchNum++, currentTextureId, texHandle.id, batchSprites.size());
} else {
static bool warnLogged = false;
if (!warnLogged) {
spdlog::warn("SpritePass: Texture ID {} not found in cache, using default", currentTextureId);
warnLogged = true;
}
}
}
if (transientBuffer.isValid()) {
// Copy sprite data to transient buffer (frame-local, won't be overwritten)
std::memcpy(transientBuffer.data, batchSprites.data(), batchSize * sizeof(SpriteInstance));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setTransientInstanceBuffer(transientBuffer, 0, batchSize);
cmd.setTexture(0, texHandle, m_textureSampler);
cmd.drawInstanced(6, batchSize);
cmd.submit(0, m_shader, 0);
} else {
// Fallback to dynamic buffer (single batch limitation - data will be overwritten!)
device.updateBuffer(m_instanceBuffer, batchSprites.data(), batchSize * sizeof(SpriteInstance));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, batchSize);
cmd.setTexture(0, texHandle, m_textureSampler);
cmd.drawInstanced(6, batchSize);
cmd.submit(0, m_shader, 0);
}
}
}
} // namespace grove

View File

@ -28,9 +28,11 @@ void SceneCollector::collect(IIO* io, float deltaTime) {
// Route message based on topic
// Retained mode (new) - sprites
if (msg.topic == "render:sprite:add") {
spdlog::info("✅ RETAINED MODE: render:sprite:add received");
parseSpriteAdd(*msg.data);
}
else if (msg.topic == "render:sprite:update") {
spdlog::info("✅ RETAINED MODE: render:sprite:update received");
parseSpriteUpdate(*msg.data);
}
else if (msg.topic == "render:sprite:remove") {
@ -48,6 +50,7 @@ void SceneCollector::collect(IIO* io, float deltaTime) {
}
// Ephemeral mode (legacy)
else if (msg.topic == "render:sprite") {
spdlog::info("⚠️ EPHEMERAL MODE: render:sprite received (should not happen in retained mode!)");
parseSprite(*msg.data);
}
else if (msg.topic == "render:sprite:batch") {
@ -487,6 +490,9 @@ void SceneCollector::parseSpriteAdd(const IDataNode& data) {
sprite.a = static_cast<float>(color & 0xFF) / 255.0f;
m_retainedSprites[renderId] = sprite;
spdlog::info("📥 [SceneCollector] Stored SPRITE renderId={}, pos=({:.1f},{:.1f}), scale={}x{}, textureId={}, layer={}, color=({:.2f},{:.2f},{:.2f},{:.2f})",
renderId, sprite.x, sprite.y, sprite.scaleX, sprite.scaleY, (int)sprite.textureId, (int)sprite.layer,
sprite.r, sprite.g, sprite.b, sprite.a);
}
void SceneCollector::parseSpriteUpdate(const IDataNode& data) {

View File

@ -155,12 +155,20 @@ bool UIRenderer::updateSprite(uint32_t renderId, float x, float y, float w, floa
}
void UIRenderer::publishSpriteAdd(uint32_t renderId, float x, float y, float w, float h, int textureId, uint32_t color, int layer) {
spdlog::info("📤 [UIRenderer] Publishing render:sprite:add - renderId={}, center=({:.1f},{:.1f}), scale={}x{}, textureId={}, layer={}",
renderId, x + w * 0.5f, y + h * 0.5f, w, h, textureId, layer);
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setInt("renderId", static_cast<int>(renderId));
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->setDouble("rotation", 0.0);
sprite->setDouble("u0", 0.0);
sprite->setDouble("v0", 0.0);
sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0);
sprite->setInt("color", static_cast<int>(color));
sprite->setInt("textureId", textureId);
sprite->setInt("layer", layer);
@ -174,6 +182,11 @@ void UIRenderer::publishSpriteUpdate(uint32_t renderId, float x, float y, float
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->setDouble("rotation", 0.0);
sprite->setDouble("u0", 0.0);
sprite->setDouble("v0", 0.0);
sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0);
sprite->setInt("color", static_cast<int>(color));
sprite->setInt("textureId", textureId);
sprite->setInt("layer", layer);

View File

@ -44,11 +44,21 @@ void UIButton::render(UIRenderer& renderer) {
const ButtonStyle& style = getCurrentStyle();
static int logCount = 0;
if (logCount < 10) { // Log first 10 buttons to see all textured ones
spdlog::info("UIButton[{}]::render() id='{}', state={}, normalStyle.textureId={}, useTexture={}",
logCount, id, (int)state, normalStyle.textureId, normalStyle.useTexture);
spdlog::info(" current style: textureId={}, useTexture={}", style.textureId, style.useTexture);
logCount++;
}
// Retained mode: only publish if changed
int bgLayer = renderer.nextLayer();
// Render background (texture or solid color)
if (style.useTexture && style.textureId > 0) {
spdlog::info("🎨 [UIButton '{}'] Rendering SPRITE: renderId={}, pos=({},{}), size={}x{}, textureId={}, color=0x{:08X}, layer={}",
id, m_renderId, absX, absY, width, height, style.textureId, style.bgColor, bgLayer);
renderer.updateSprite(m_renderId, absX, absY, width, height, style.textureId, style.bgColor, bgLayer);
} else {
renderer.updateRect(m_renderId, absX, absY, width, height, style.bgColor, bgLayer);

View File

@ -1451,3 +1451,180 @@ if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILA
)
message(STATUS "Single button test 'test_single_button' enabled")
endif()
# UI Texture Support Test - headless test demonstrating texture properties
if(GROVE_BUILD_UI_MODULE)
add_executable(test_ui_textures
visual/test_ui_textures.cpp
)
target_include_directories(test_ui_textures PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_ui_textures PRIVATE
GroveEngine::impl
UIModule_static
spdlog::spdlog
)
message(STATUS "UI texture support test 'test_ui_textures' enabled")
endif()
# Textured UI Visual Demo - shows widgets with custom textures
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE)
add_executable(test_ui_textured_demo WIN32
visual/test_ui_textured_demo.cpp
)
target_include_directories(test_ui_textured_demo PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_ui_textured_demo PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
UIModule_static
spdlog::spdlog
)
message(STATUS "Textured UI demo 'test_ui_textured_demo' enabled")
endif()
# Simple textured UI demo - shows widget properties (no rendering)
if(GROVE_BUILD_UI_MODULE)
add_executable(test_ui_textured_simple
visual/test_ui_textured_simple.cpp
)
target_include_directories(test_ui_textured_simple PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_ui_textured_simple PRIVATE
GroveEngine::impl
UIModule_static
spdlog::spdlog
)
message(STATUS "Simple textured UI demo 'test_ui_textured_simple' enabled")
endif()
# Textured Button Visual Test - shows REAL textures on buttons
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE)
add_executable(test_textured_button WIN32
visual/test_textured_button.cpp
)
target_include_directories(test_textured_button PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_textured_button PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
UIModule_static
spdlog::spdlog
)
message(STATUS "Textured button test 'test_textured_button' enabled")
endif()
# Minimal Textured Demo - Direct sprite rendering with textures
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND SDL2_AVAILABLE)
add_executable(test_textured_demo_minimal WIN32
visual/test_textured_demo_minimal.cpp
)
target_include_directories(test_textured_demo_minimal PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
)
target_link_libraries(test_textured_demo_minimal PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
spdlog::spdlog
)
message(STATUS "Minimal textured demo 'test_textured_demo_minimal' enabled")
endif()
# Button with PNG texture - Load real PNG file and apply to button
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE)
add_executable(test_button_with_png WIN32
visual/test_button_with_png.cpp
)
target_include_directories(test_button_with_png PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_button_with_png PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
UIModule_static
spdlog::spdlog
)
message(STATUS "PNG button test 'test_button_with_png' enabled")
endif()
# 3 Buttons Minimal Test - 3 textured buttons in minimal layout
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE)
add_executable(test_3buttons_minimal WIN32
visual/test_3buttons_minimal.cpp
)
target_include_directories(test_3buttons_minimal PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_3buttons_minimal PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
UIModule_static
spdlog::spdlog
)
message(STATUS "3 buttons minimal test 'test_3buttons_minimal' enabled")
endif()
# 1 Button Texture 2 Test - diagnostic to see if only texture 1 works
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND GROVE_BUILD_UI_MODULE AND SDL2_AVAILABLE)
add_executable(test_1button_texture2 WIN32
visual/test_1button_texture2.cpp
)
target_include_directories(test_1button_texture2 PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
${CMAKE_SOURCE_DIR}/modules/UIModule
)
target_link_libraries(test_1button_texture2 PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
UIModule_static
spdlog::spdlog
)
message(STATUS "1 button texture 2 test 'test_1button_texture2' enabled")
endif()
# Direct sprite texture test - bypasses UIModule, uses renderer directly
if(WIN32 AND GROVE_BUILD_BGFX_RENDERER AND SDL2_AVAILABLE)
add_executable(test_direct_sprite_texture WIN32
visual/test_direct_sprite_texture.cpp
)
target_include_directories(test_direct_sprite_texture PRIVATE
${CMAKE_SOURCE_DIR}/modules
${CMAKE_SOURCE_DIR}/modules/BgfxRenderer
)
target_link_libraries(test_direct_sprite_texture PRIVATE
SDL2::SDL2main
SDL2::SDL2
GroveEngine::impl
BgfxRenderer_static
spdlog::spdlog
)
message(STATUS "Direct sprite texture test 'test_direct_sprite_texture' enabled")
endif()

View File

@ -0,0 +1,211 @@
/**
* Test: UIButton avec texture PNG chargée depuis un fichier
* Ce test montre qu'on peut mettre une VRAIE image sur un bouton
*/
#include <SDL.h>
#include <SDL_syswm.h>
#include <iostream>
#include <memory>
#include <fstream>
#include "BgfxRendererModule.h"
#include "UIModule/UIModule.h"
#include "../modules/BgfxRenderer/Resources/ResourceCache.h"
#include "../modules/BgfxRenderer/RHI/RHITypes.h"
#include "../modules/BgfxRenderer/RHI/RHIDevice.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 <bgfx/bgfx.h>
#include <vector>
using namespace grove;
int main(int argc, char* argv[]) {
spdlog::set_level(spdlog::level::info);
auto logger = spdlog::stdout_color_mt("TexturedButtonTest");
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
std::cerr << "SDL_Init failed" << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow(
"Textured Button Test - Gradient",
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 - IMPORTANT: game publishes input, ui subscribes and publishes render commands
auto gameIO = IntraIOManager::getInstance().createInstance("game");
auto uiIO = IntraIOManager::getInstance().createInstance("ui");
auto rendererIO = IntraIOManager::getInstance().createInstance("renderer");
gameIO->subscribe("ui:hover");
gameIO->subscribe("ui:click");
gameIO->subscribe("ui:action");
// Initialize BgfxRenderer WITH 3 TEXTURES loaded via config
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);
// Load 3 textures
config.setString("texture1", "../../assets/textures/5oxaxt1vo2f91.jpg"); // Car
config.setString("texture2", "../../assets/textures/1f440.png"); // Eyes
config.setString("texture3", "../../assets/textures/IconDesigner.png"); // Icon
renderer->setConfiguration(config, rendererIO.get(), nullptr);
}
logger->info("✓ Loaded 3 textures (IDs: 1, 2, 3)");
// Initialize UIModule with 3 TEXTURED BUTTONS
auto ui = std::make_unique<UIModule>();
{
JsonDataNode config("config");
config.setInt("windowWidth", 800);
config.setInt("windowHeight", 600);
nlohmann::json layoutJson = {
{"id", "root"},
{"type", "panel"},
{"x", 0}, {"y", 0},
{"width", 800}, {"height", 600}, // Full screen invisible panel (just container)
{"style", {
{"bgColor", "0x00000000"} // Fully transparent - just a container
}},
{"children", {
{
{"id", "btn_car"},
{"type", "button"},
{"x", 50},
{"y", 50},
{"width", 400},
{"height", 200},
{"text", ""},
{"onClick", "car_action"},
{"style", {
{"normal", {{"textureId", 1}, {"bgColor", "0xFFFFFFFF"}}},
{"hover", {{"textureId", 1}, {"bgColor", "0xFFFF00FF"}}},
{"pressed", {{"textureId", 1}, {"bgColor", "0x888888FF"}}}
}}
},
{
{"id", "btn_eyes"},
{"type", "button"},
{"x", 50},
{"y", 270},
{"width", 250},
{"height", 200},
{"text", ""},
{"onClick", "eyes_action"},
{"style", {
{"normal", {{"textureId", 2}, {"bgColor", "0xFFFFFFFF"}}},
{"hover", {{"textureId", 2}, {"bgColor", "0x00FFFFFF"}}},
{"pressed", {{"textureId", 2}, {"bgColor", "0x888888FF"}}}
}}
},
{
{"id", "btn_icon"},
{"type", "button"},
{"x", 320},
{"y", 270},
{"width", 250},
{"height", 200},
{"text", ""},
{"onClick", "icon_action"},
{"style", {
{"normal", {{"textureId", 3}, {"bgColor", "0xFFFFFFFF"}}},
{"hover", {{"textureId", 3}, {"bgColor", "0xFF00FFFF"}}},
{"pressed", {{"textureId", 3}, {"bgColor", "0x888888FF"}}}
}}
}
}}
};
auto layoutNode = std::make_unique<JsonDataNode>("layout", layoutJson);
config.setChild("layout", std::move(layoutNode));
ui->setConfiguration(config, uiIO.get(), nullptr);
logger->info("✓ UIModule configured with 3 textured buttons!");
}
logger->info("\n╔════════════════════════════════════════╗");
logger->info("║ 3 BOUTONS AVEC TEXTURES ║");
logger->info("╠════════════════════════════════════════╣");
logger->info("║ Button 1: Car (textureId=1) ║");
logger->info("║ Button 2: Eyes (textureId=2) ║");
logger->info("║ Button 3: Icon (textureId=3) ║");
logger->info("║ Press ESC to exit ║");
logger->info("╚════════════════════════════════════════╝\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
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));
}
}
// Check for UI events
while (gameIO->hasMessages() > 0) {
auto msg = gameIO->pullMessage();
if (msg.topic == "ui:action") {
logger->info("🖱️ BOUTON CLICKÉ!");
}
}
// Update modules
JsonDataNode input("input");
input.setDouble("deltaTime", 0.016);
ui->process(input);
renderer->process(input);
SDL_Delay(16);
}
logger->info("Cleaning up...");
// Textures are managed by ResourceCache, will be cleaned up in renderer->shutdown()
ui->shutdown();
renderer->shutdown();
IntraIOManager::getInstance().removeInstance("game");
IntraIOManager::getInstance().removeInstance("ui");
IntraIOManager::getInstance().removeInstance("renderer");
SDL_DestroyWindow(window);
SDL_Quit();
logger->info("Test complete!");
return 0;
}

View File

@ -37,20 +37,20 @@
using namespace grove;
// Create the UI layout JSON - must be a single root widget with children
// Create the UI layout JSON - MINIMAL VERSION WITH ONLY TEXTURED BUTTONS
static nlohmann::json createUILayout() {
nlohmann::json root;
// Root panel (full screen background)
// Root panel (TRANSPARENT like test_button_with_png!)
root["type"] = "panel";
root["id"] = "root";
root["x"] = 0;
root["y"] = 0;
root["width"] = 1024;
root["height"] = 768;
root["style"] = {{"bgColor", "0x1a1a2eFF"}};
root["style"] = {{"bgColor", "0x00000000"}}; // TRANSPARENT!
// Children array (like test_single_button)
// Children array - ONLY TEXTURED BUTTONS
nlohmann::json children = nlohmann::json::array();
// Title label
@ -151,6 +151,78 @@ static nlohmann::json createUILayout() {
}}
});
// === TEXTURED BUTTONS PANEL === (TRANSPARENT FOR TESTING)
children.push_back({
{"type", "panel"},
{"id", "textured_buttons_panel"},
{"x", 350}, {"y", 120},
{"width", 300}, {"height", 280},
{"style", {{"bgColor", "0x00000000"}}} // TRANSPARENT!
});
children.push_back({
{"type", "label"},
{"id", "textured_buttons_title"},
{"x", 365}, {"y", 130},
{"width", 250}, {"height", 30},
{"text", "Sprite Buttons"},
{"style", {{"fontSize", 20}, {"color", "0xFFFFFFFF"}}}
});
// Textured Button 1 - Car (HUGE LIKE WORKING TEST!)
children.push_back({
{"type", "button"},
{"id", "btn_car"},
{"x", 50}, {"y", 120},
{"width", 400}, {"height", 200}, // Same as test_button_with_png!
{"text", ""},
{"onClick", "sprite_car"},
{"style", {
{"normal", {{"textureId", 1}, {"bgColor", "0xFFFFFFFF"}, {"textColor", "0x000000FF"}}},
{"hover", {{"textureId", 1}, {"bgColor", "0xFFFF00FF"}, {"textColor", "0x000000FF"}}}, // Yellow tint
{"pressed", {{"textureId", 1}, {"bgColor", "0x888888FF"}, {"textColor", "0x000000FF"}}} // Dark tint
}}
});
// Textured Button 2 - Eyes (HUGE!)
children.push_back({
{"type", "button"},
{"id", "btn_eyes"},
{"x", 470}, {"y", 120},
{"width", 250}, {"height", 200}, // Much bigger!
{"text", ""},
{"onClick", "sprite_eyes"},
{"style", {
{"normal", {{"textureId", 2}, {"bgColor", "0xFFFFFFFF"}, {"textColor", "0x000000FF"}}},
{"hover", {{"textureId", 2}, {"bgColor", "0x00FFFFFF"}, {"textColor", "0x000000FF"}}}, // Cyan tint
{"pressed", {{"textureId", 2}, {"bgColor", "0x888888FF"}, {"textColor", "0x000000FF"}}}
}}
});
// Textured Button 3 - Icon (HUGE!)
children.push_back({
{"type", "button"},
{"id", "btn_icon"},
{"x", 50}, {"y", 340},
{"width", 250}, {"height", 200}, // Much bigger!
{"text", ""},
{"onClick", "sprite_icon"},
{"style", {
{"normal", {{"textureId", 3}, {"bgColor", "0xFFFFFFFF"}, {"textColor", "0x000000FF"}}},
{"hover", {{"textureId", 3}, {"bgColor", "0xFF00FFFF"}, {"textColor", "0x000000FF"}}}, // Magenta tint
{"pressed", {{"textureId", 3}, {"bgColor", "0x888888FF"}, {"textColor", "0x000000FF"}}}
}}
});
// Info label for textured buttons
children.push_back({
{"type", "label"},
{"x", 370}, {"y", 340},
{"width", 260}, {"height", 50},
{"text", "Retained mode:\nTextures only sent once!"},
{"style", {{"fontSize", 12}, {"color", "0xAAAAAAFF"}}}
});
// === MIDDLE COLUMN: Inputs Panel ===
children.push_back({
{"type", "panel"},
@ -351,7 +423,7 @@ public:
m_inputIO = m_inputIOPtr.get();
m_gameIO = m_gameIOPtr.get();
// Create and configure BgfxRenderer
// Create and configure BgfxRenderer with textures
m_renderer = std::make_unique<BgfxRendererModule>();
{
JsonDataNode config("config");
@ -359,10 +431,15 @@ public:
static_cast<double>(reinterpret_cast<uintptr_t>(wmi.info.win.window)));
config.setInt("windowWidth", 1024);
config.setInt("windowHeight", 768);
config.setString("backend", "d3d11");
// config.setString("backend", "d3d11"); // LET BGFX CHOOSE LIKE test_button_with_png!
config.setBool("vsync", true);
// Load textures for sprite buttons
config.setString("texture1", "../../assets/textures/5oxaxt1vo2f91.jpg"); // Car
config.setString("texture2", "../../assets/textures/1f440.png"); // Eyes emoji
config.setString("texture3", "../../assets/textures/IconDesigner.png"); // Icon
m_renderer->setConfiguration(config, m_rendererIO, nullptr);
}
m_logger->info("✓ Loaded 3 textures for sprite buttons (IDs: 1, 2, 3)");
// Create and configure UIModule with inline layout
m_uiModule = std::make_unique<UIModule>();
@ -500,6 +577,15 @@ private:
else if (action == "action_danger") {
m_logger->warn("Danger button clicked!");
}
else if (action == "sprite_car") {
m_logger->info("🚗 Car sprite button clicked! (Texture ID: 1)");
}
else if (action == "sprite_eyes") {
m_logger->info("👀 Eyes sprite button clicked! (Texture ID: 2)");
}
else if (action == "sprite_icon") {
m_logger->info("🎨 Icon sprite button clicked! (Texture ID: 3)");
}
}
else if (msg.topic == "ui:click") {
std::string widgetId = msg.data->getString("widgetId", "");