GroveEngine/modules/UIModule/Rendering/UIRenderer.cpp
StillHammer 63751d6f91 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>
2026-01-14 14:05:56 +07:00

290 lines
11 KiB
C++

#include "UIRenderer.h"
#include <grove/JsonDataNode.h>
#include <spdlog/spdlog.h>
#include <cmath>
namespace grove {
UIRenderer::UIRenderer(IIO* io)
: m_io(io) {
}
// ============================================================================
// Retained Mode Implementation
// ============================================================================
uint32_t UIRenderer::registerEntry() {
return m_nextRenderId++;
}
void UIRenderer::unregisterEntry(uint32_t renderId) {
auto it = m_entries.find(renderId);
if (it != m_entries.end()) {
// Send remove message based on type
if (it->second.type == RenderEntryType::Text) {
publishTextRemove(renderId);
} else {
publishSpriteRemove(renderId);
}
m_entries.erase(it);
}
}
static bool floatEqual(float a, float b, float epsilon = 0.001f) {
return std::fabs(a - b) < epsilon;
}
bool UIRenderer::updateRect(uint32_t renderId, float x, float y, float w, float h, uint32_t color, int layer) {
if (!m_io) return false;
auto it = m_entries.find(renderId);
if (it == m_entries.end()) {
// New entry - add it
RenderEntry entry;
entry.type = RenderEntryType::Rect;
entry.x = x;
entry.y = y;
entry.w = w;
entry.h = h;
entry.color = color;
entry.textureId = 0;
entry.layer = layer; // Store initial layer (stable)
m_entries[renderId] = entry;
publishSpriteAdd(renderId, x, y, w, h, 0, color, layer);
return true;
}
// Check if changed (ignore layer - it's set once at registration)
RenderEntry& entry = it->second;
bool changed = !floatEqual(entry.x, x) || !floatEqual(entry.y, y) ||
!floatEqual(entry.w, w) || !floatEqual(entry.h, h) ||
entry.color != color;
if (changed) {
entry.x = x;
entry.y = y;
entry.w = w;
entry.h = h;
entry.color = color;
// Keep original layer (don't update it)
publishSpriteUpdate(renderId, x, y, w, h, 0, color, entry.layer);
return true;
}
return false; // No change, no publish
}
bool UIRenderer::updateText(uint32_t renderId, float x, float y, const std::string& text, float fontSize, uint32_t color, int layer) {
if (!m_io) return false;
auto it = m_entries.find(renderId);
if (it == m_entries.end()) {
// New entry - add it
RenderEntry entry;
entry.type = RenderEntryType::Text;
entry.x = x;
entry.y = y;
entry.text = text;
entry.fontSize = fontSize;
entry.color = color;
entry.layer = layer; // Store initial layer (stable)
m_entries[renderId] = entry;
publishTextAdd(renderId, x, y, text, fontSize, color, layer);
return true;
}
// Check if changed (ignore layer - it's set once at registration)
RenderEntry& entry = it->second;
bool changed = !floatEqual(entry.x, x) || !floatEqual(entry.y, y) ||
entry.text != text || !floatEqual(entry.fontSize, fontSize) ||
entry.color != color;
if (changed) {
entry.x = x;
entry.y = y;
entry.text = text;
entry.fontSize = fontSize;
entry.color = color;
// Keep original layer (don't update it)
publishTextUpdate(renderId, x, y, text, fontSize, color, entry.layer);
return true;
}
return false;
}
bool UIRenderer::updateSprite(uint32_t renderId, float x, float y, float w, float h, int textureId, uint32_t color, int layer) {
if (!m_io) return false;
auto it = m_entries.find(renderId);
if (it == m_entries.end()) {
// New entry - add it
RenderEntry entry;
entry.type = RenderEntryType::Sprite;
entry.x = x;
entry.y = y;
entry.w = w;
entry.h = h;
entry.textureId = textureId;
entry.color = color;
entry.layer = layer; // Store initial layer (stable)
m_entries[renderId] = entry;
publishSpriteAdd(renderId, x, y, w, h, textureId, color, layer);
return true;
}
// Check if changed (ignore layer - it's set once at registration)
RenderEntry& entry = it->second;
bool changed = !floatEqual(entry.x, x) || !floatEqual(entry.y, y) ||
!floatEqual(entry.w, w) || !floatEqual(entry.h, h) ||
entry.textureId != textureId || entry.color != color;
if (changed) {
entry.x = x;
entry.y = y;
entry.w = w;
entry.h = h;
entry.textureId = textureId;
entry.color = color;
// Keep original layer (don't update it)
publishSpriteUpdate(renderId, x, y, w, h, textureId, color, entry.layer);
return true;
}
return false;
}
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);
m_io->publish("render:sprite:add", std::move(sprite));
}
void UIRenderer::publishSpriteUpdate(uint32_t renderId, float x, float y, float w, float h, int textureId, uint32_t color, int 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);
m_io->publish("render:sprite:update", std::move(sprite));
}
void UIRenderer::publishSpriteRemove(uint32_t renderId) {
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setInt("renderId", static_cast<int>(renderId));
m_io->publish("render:sprite:remove", std::move(sprite));
}
void UIRenderer::publishTextAdd(uint32_t renderId, float x, float y, const std::string& text, float fontSize, uint32_t color, int layer) {
auto textNode = std::make_unique<JsonDataNode>("text");
textNode->setInt("renderId", static_cast<int>(renderId));
textNode->setDouble("x", static_cast<double>(x));
textNode->setDouble("y", static_cast<double>(y));
textNode->setString("text", text);
textNode->setDouble("fontSize", static_cast<double>(fontSize));
textNode->setInt("color", static_cast<int>(color));
textNode->setInt("layer", layer);
m_io->publish("render:text:add", std::move(textNode));
}
void UIRenderer::publishTextUpdate(uint32_t renderId, float x, float y, const std::string& text, float fontSize, uint32_t color, int layer) {
auto textNode = std::make_unique<JsonDataNode>("text");
textNode->setInt("renderId", static_cast<int>(renderId));
textNode->setDouble("x", static_cast<double>(x));
textNode->setDouble("y", static_cast<double>(y));
textNode->setString("text", text);
textNode->setDouble("fontSize", static_cast<double>(fontSize));
textNode->setInt("color", static_cast<int>(color));
textNode->setInt("layer", layer);
m_io->publish("render:text:update", std::move(textNode));
}
void UIRenderer::publishTextRemove(uint32_t renderId) {
auto textNode = std::make_unique<JsonDataNode>("text");
textNode->setInt("renderId", static_cast<int>(renderId));
m_io->publish("render:text:remove", std::move(textNode));
}
// ============================================================================
// Immediate Mode (Legacy)
// ============================================================================
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");
// 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());
m_io->publish("render:sprite", std::move(sprite));
}
void UIRenderer::drawText(float x, float y, const std::string& text, float fontSize, uint32_t color) {
if (!m_io) return;
auto textNode = std::make_unique<JsonDataNode>("text");
textNode->setDouble("x", static_cast<double>(x));
textNode->setDouble("y", static_cast<double>(y));
textNode->setString("text", text);
textNode->setDouble("fontSize", static_cast<double>(fontSize));
textNode->setInt("color", static_cast<int>(color));
textNode->setInt("layer", nextLayer());
m_io->publish("render:text", std::move(textNode));
}
void UIRenderer::drawSprite(float x, float y, float w, float h, int textureId, uint32_t color) {
if (!m_io) return;
auto sprite = std::make_unique<JsonDataNode>("sprite");
// 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());
m_io->publish("render:sprite", std::move(sprite));
}
} // namespace grove