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:
parent
a106c78bc8
commit
63751d6f91
BIN
assets/textures/IconDesigner.png
Normal file
BIN
assets/textures/IconDesigner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
211
tests/visual/test_3buttons_minimal.cpp
Normal file
211
tests/visual/test_3buttons_minimal.cpp
Normal 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;
|
||||
}
|
||||
@ -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", "");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user