GroveEngine/modules/BgfxRenderer/Passes/SpritePass.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

246 lines
10 KiB
C++

#include "SpritePass.h"
#include "../RHI/RHIDevice.h"
#include "../Frame/FramePacket.h"
#include "../Resources/ResourceCache.h"
#include <algorithm>
#include <cstring>
#include <spdlog/spdlog.h>
namespace grove {
SpritePass::SpritePass(rhi::ShaderHandle shader)
: m_shader(shader)
{
m_sortedIndices.reserve(MAX_SPRITES_PER_BATCH);
}
void SpritePass::setup(rhi::IRHIDevice& device) {
// Create quad vertex buffer (unit quad, instanced)
// Layout must match shader: a_position (vec3) + a_color0 (vec4)
// Note: Color is white (1,1,1,1) - actual color comes from instance data
float quadVertices[] = {
// pos.x, pos.y, pos.z, r, g, b, a
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-left
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-right
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-right
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-left
};
rhi::BufferDesc vbDesc;
vbDesc.type = rhi::BufferDesc::Vertex;
vbDesc.size = sizeof(quadVertices);
vbDesc.data = quadVertices;
vbDesc.dynamic = false;
vbDesc.layout = rhi::BufferDesc::PosColor; // Match shader: a_position + a_color0
m_quadVB = device.createBuffer(vbDesc);
// Create index buffer
uint16_t quadIndices[] = {
0, 1, 2, // first triangle
0, 2, 3 // second triangle
};
rhi::BufferDesc ibDesc;
ibDesc.type = rhi::BufferDesc::Index;
ibDesc.size = sizeof(quadIndices);
ibDesc.data = quadIndices;
ibDesc.dynamic = false;
m_quadIB = device.createBuffer(ibDesc);
// Note: We no longer create a persistent instance buffer since we use transient buffers
// But keep it for fallback if transient allocation fails
rhi::BufferDesc instDesc;
instDesc.type = rhi::BufferDesc::Instance;
instDesc.size = MAX_SPRITES_PER_BATCH * sizeof(SpriteInstance);
instDesc.data = nullptr;
instDesc.dynamic = true;
m_instanceBuffer = device.createBuffer(instDesc);
// Create texture sampler uniform (must match shader: s_texColor)
m_textureSampler = device.createUniform("s_texColor", 1);
// 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
rhi::TextureDesc texDesc;
texDesc.width = 4;
texDesc.height = 4;
texDesc.format = rhi::TextureDesc::RGBA8;
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) {
device.destroy(m_quadVB);
device.destroy(m_quadIB);
device.destroy(m_instanceBuffer);
device.destroy(m_textureSampler);
device.destroy(m_defaultTexture);
// Note: m_shader is owned by ShaderManager, not destroyed here
}
void SpritePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
rhi::TextureHandle texture, uint32_t count) {
if (count == 0) return;
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
// Note: Instance buffer should be set before calling this
cmd.setTexture(0, texture, m_textureSampler);
cmd.drawInstanced(6, count);
cmd.submit(0, m_shader, 0);
}
void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) {
if (frame.spriteCount == 0) return;
// 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;
// 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) {
m_sortedIndices.push_back(static_cast<uint32_t>(i));
}
std::sort(m_sortedIndices.begin(), m_sortedIndices.end(),
[&frame](uint32_t a, uint32_t b) {
// 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;
});
// 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;
}
// 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);
// 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