feat(BgfxRenderer): Fix multi-texture batching and add particle effects
- Fix texture state management in BgfxDevice: defer setTexture until submit() - Add transient instance buffer support for multi-batch rendering - Add ParticlePass with fire, smoke and sparkle particle systems - Load multiple textures from config (texture1..texture10) - Visual test now demonstrates multi-texture sprites and multi-particle effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5795bbb37e
commit
9618a647a2
BIN
assets/textures/5oxaxt1vo2f91.jpg
Normal file
BIN
assets/textures/5oxaxt1vo2f91.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@ -11,6 +11,7 @@
|
||||
#include "Passes/TilemapPass.h"
|
||||
#include "Passes/SpritePass.h"
|
||||
#include "Passes/TextPass.h"
|
||||
#include "Passes/ParticlePass.h"
|
||||
#include "Passes/DebugPass.h"
|
||||
|
||||
#include <grove/JsonDataNode.h>
|
||||
@ -106,6 +107,12 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
||||
m_renderGraph->addPass(std::make_unique<TextPass>(spriteShader));
|
||||
m_logger->info("Added TextPass");
|
||||
|
||||
// Create ParticlePass (uses sprite shader, renders after sprites with additive blending)
|
||||
auto particlePass = std::make_unique<ParticlePass>(spriteShader);
|
||||
particlePass->setResourceCache(m_resourceCache.get());
|
||||
m_renderGraph->addPass(std::move(particlePass));
|
||||
m_logger->info("Added ParticlePass");
|
||||
|
||||
m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
|
||||
m_logger->info("Added DebugPass");
|
||||
m_renderGraph->setup(*m_device);
|
||||
@ -129,15 +136,30 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
||||
// Load default texture if specified in config
|
||||
std::string defaultTexturePath = config.getString("defaultTexture", "");
|
||||
if (!defaultTexturePath.empty()) {
|
||||
rhi::TextureHandle tex = m_resourceCache->loadTexture(*m_device, defaultTexturePath);
|
||||
if (tex.isValid()) {
|
||||
uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, defaultTexturePath);
|
||||
if (texId > 0) {
|
||||
rhi::TextureHandle tex = m_resourceCache->getTextureById(texId);
|
||||
m_spritePass->setTexture(tex);
|
||||
m_logger->info("Loaded default texture: {}", defaultTexturePath);
|
||||
m_logger->info("Loaded default texture: {} (id={})", defaultTexturePath, texId);
|
||||
} else {
|
||||
m_logger->warn("Failed to load default texture: {}", defaultTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load additional textures (texture1, texture2, etc.)
|
||||
for (int i = 1; i <= 10; ++i) {
|
||||
std::string key = "texture" + std::to_string(i);
|
||||
std::string path = config.getString(key, "");
|
||||
if (!path.empty()) {
|
||||
uint16_t texId = m_resourceCache->loadTextureWithId(*m_device, path);
|
||||
if (texId > 0) {
|
||||
m_logger->info("Loaded texture: {} (id={})", path, texId);
|
||||
} else {
|
||||
m_logger->warn("Failed to load texture: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_logger->info("BgfxRenderer initialized successfully");
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,7 @@ add_library(BgfxRenderer SHARED
|
||||
Passes/TilemapPass.cpp
|
||||
Passes/SpritePass.cpp
|
||||
Passes/TextPass.cpp
|
||||
Passes/ParticlePass.cpp
|
||||
Passes/DebugPass.cpp
|
||||
|
||||
# Text
|
||||
|
||||
208
modules/BgfxRenderer/Passes/ParticlePass.cpp
Normal file
208
modules/BgfxRenderer/Passes/ParticlePass.cpp
Normal file
@ -0,0 +1,208 @@
|
||||
#include "ParticlePass.h"
|
||||
#include "../RHI/RHIDevice.h"
|
||||
#include "../Resources/ResourceCache.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace grove {
|
||||
|
||||
ParticlePass::ParticlePass(rhi::ShaderHandle shader)
|
||||
: m_shader(shader)
|
||||
{
|
||||
m_particleInstances.reserve(MAX_PARTICLES_PER_BATCH);
|
||||
}
|
||||
|
||||
void ParticlePass::setup(rhi::IRHIDevice& device) {
|
||||
// Create quad vertex buffer (unit quad centered at origin for particles)
|
||||
float quadVertices[] = {
|
||||
// pos.x, pos.y, pos.z, r, g, b, a
|
||||
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-left
|
||||
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-right
|
||||
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-right
|
||||
-0.5f, 0.5f, 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;
|
||||
m_quadVB = device.createBuffer(vbDesc);
|
||||
|
||||
// Create index buffer
|
||||
uint16_t quadIndices[] = {
|
||||
0, 1, 2,
|
||||
0, 2, 3
|
||||
};
|
||||
|
||||
rhi::BufferDesc ibDesc;
|
||||
ibDesc.type = rhi::BufferDesc::Index;
|
||||
ibDesc.size = sizeof(quadIndices);
|
||||
ibDesc.data = quadIndices;
|
||||
ibDesc.dynamic = false;
|
||||
m_quadIB = device.createBuffer(ibDesc);
|
||||
|
||||
// Fallback dynamic instance buffer (only used if transient allocation fails)
|
||||
rhi::BufferDesc instDesc;
|
||||
instDesc.type = rhi::BufferDesc::Instance;
|
||||
instDesc.size = MAX_PARTICLES_PER_BATCH * sizeof(SpriteInstance);
|
||||
instDesc.data = nullptr;
|
||||
instDesc.dynamic = true;
|
||||
m_instanceBuffer = device.createBuffer(instDesc);
|
||||
|
||||
// Create texture sampler uniform
|
||||
m_textureSampler = device.createUniform("s_texColor", 1);
|
||||
|
||||
// Create default white texture for untextured particles
|
||||
uint32_t whitePixel = 0xFFFFFFFF;
|
||||
rhi::TextureDesc texDesc;
|
||||
texDesc.width = 1;
|
||||
texDesc.height = 1;
|
||||
texDesc.format = rhi::TextureDesc::RGBA8;
|
||||
texDesc.data = &whitePixel;
|
||||
texDesc.dataSize = sizeof(whitePixel);
|
||||
m_defaultTexture = device.createTexture(texDesc);
|
||||
}
|
||||
|
||||
void ParticlePass::shutdown(rhi::IRHIDevice& device) {
|
||||
device.destroy(m_quadVB);
|
||||
device.destroy(m_quadIB);
|
||||
device.destroy(m_instanceBuffer);
|
||||
device.destroy(m_textureSampler);
|
||||
device.destroy(m_defaultTexture);
|
||||
}
|
||||
|
||||
void ParticlePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) {
|
||||
if (frame.particleCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set render state for particles
|
||||
rhi::RenderState state;
|
||||
state.blend = m_additiveBlending ? rhi::BlendMode::Additive : rhi::BlendMode::Alpha;
|
||||
state.cull = rhi::CullMode::None;
|
||||
state.depthTest = false;
|
||||
state.depthWrite = false;
|
||||
cmd.setState(state);
|
||||
|
||||
m_particleInstances.clear();
|
||||
|
||||
// Current texture for batching
|
||||
uint16_t currentTextureId = UINT16_MAX;
|
||||
rhi::TextureHandle currentTexture = m_defaultTexture;
|
||||
|
||||
for (size_t i = 0; i < frame.particleCount; ++i) {
|
||||
const ParticleInstance& particle = frame.particles[i];
|
||||
|
||||
// Skip dead particles
|
||||
if (particle.life <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if texture changed - flush batch
|
||||
if (particle.textureId != currentTextureId && !m_particleInstances.empty()) {
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(m_particleInstances.size()));
|
||||
m_particleInstances.clear();
|
||||
}
|
||||
|
||||
// Update current texture if needed
|
||||
if (particle.textureId != currentTextureId) {
|
||||
currentTextureId = particle.textureId;
|
||||
if (particle.textureId > 0 && m_resourceCache) {
|
||||
currentTexture = m_resourceCache->getTextureById(particle.textureId);
|
||||
}
|
||||
if (!currentTexture.isValid()) {
|
||||
currentTexture = m_defaultTexture;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert ParticleInstance to GPU-aligned SpriteInstance
|
||||
SpriteInstance inst;
|
||||
|
||||
// Position (particle position is center)
|
||||
inst.x = particle.x;
|
||||
inst.y = particle.y;
|
||||
|
||||
// Scale by particle size
|
||||
inst.scaleX = particle.size;
|
||||
inst.scaleY = particle.size;
|
||||
|
||||
// No rotation (could add spin later)
|
||||
inst.rotation = 0.0f;
|
||||
|
||||
// Full UV (use entire texture)
|
||||
inst.u0 = 0.0f;
|
||||
inst.v0 = 0.0f;
|
||||
inst.u1 = 1.0f;
|
||||
inst.v1 = 1.0f;
|
||||
|
||||
// Texture ID
|
||||
inst.textureId = static_cast<float>(particle.textureId);
|
||||
|
||||
// Layer (particles render at high layer by default)
|
||||
inst.layer = 200.0f;
|
||||
|
||||
// Padding/reserved
|
||||
inst.padding0 = 0.0f;
|
||||
inst.reserved[0] = 0.0f;
|
||||
inst.reserved[1] = 0.0f;
|
||||
inst.reserved[2] = 0.0f;
|
||||
inst.reserved[3] = 0.0f;
|
||||
|
||||
// Color with life-based alpha fade
|
||||
uint32_t color = particle.color;
|
||||
inst.r = static_cast<float>((color >> 24) & 0xFF) / 255.0f;
|
||||
inst.g = static_cast<float>((color >> 16) & 0xFF) / 255.0f;
|
||||
inst.b = static_cast<float>((color >> 8) & 0xFF) / 255.0f;
|
||||
// Alpha fades with life
|
||||
float baseAlpha = static_cast<float>(color & 0xFF) / 255.0f;
|
||||
inst.a = baseAlpha * particle.life;
|
||||
|
||||
m_particleInstances.push_back(inst);
|
||||
|
||||
// Flush if batch is full
|
||||
if (m_particleInstances.size() >= MAX_PARTICLES_PER_BATCH) {
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(m_particleInstances.size()));
|
||||
m_particleInstances.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining particles
|
||||
if (!m_particleInstances.empty()) {
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(m_particleInstances.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void ParticlePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
||||
rhi::TextureHandle texture, uint32_t count) {
|
||||
if (count == 0) return;
|
||||
|
||||
// Try to use transient buffer for multi-batch support
|
||||
rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(count);
|
||||
|
||||
if (transientBuffer.isValid()) {
|
||||
// Copy particle data to transient buffer
|
||||
std::memcpy(transientBuffer.data, m_particleInstances.data(), count * sizeof(SpriteInstance));
|
||||
|
||||
// Set buffers
|
||||
cmd.setVertexBuffer(m_quadVB);
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setTransientInstanceBuffer(transientBuffer, 0, count);
|
||||
cmd.setTexture(0, texture, m_textureSampler);
|
||||
cmd.drawInstanced(6, count);
|
||||
cmd.submit(0, m_shader, 0);
|
||||
} else {
|
||||
// Fallback to dynamic buffer (single batch per frame limitation)
|
||||
device.updateBuffer(m_instanceBuffer, m_particleInstances.data(),
|
||||
count * sizeof(SpriteInstance));
|
||||
|
||||
cmd.setVertexBuffer(m_quadVB);
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, count);
|
||||
cmd.setTexture(0, texture, m_textureSampler);
|
||||
cmd.drawInstanced(6, count);
|
||||
cmd.submit(0, m_shader, 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace grove
|
||||
63
modules/BgfxRenderer/Passes/ParticlePass.h
Normal file
63
modules/BgfxRenderer/Passes/ParticlePass.h
Normal file
@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "../RenderGraph/RenderPass.h"
|
||||
#include "../RHI/RHITypes.h"
|
||||
#include "../Frame/FramePacket.h"
|
||||
#include <vector>
|
||||
|
||||
namespace grove {
|
||||
|
||||
class ResourceCache;
|
||||
|
||||
// ============================================================================
|
||||
// Particle Pass - Renders 2D particles with additive blending
|
||||
// ============================================================================
|
||||
|
||||
class ParticlePass : public RenderPass {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct ParticlePass with required shader
|
||||
* @param shader The shader program to use for particle rendering
|
||||
*/
|
||||
explicit ParticlePass(rhi::ShaderHandle shader);
|
||||
|
||||
const char* getName() const override { return "Particles"; }
|
||||
uint32_t getSortOrder() const override { return 150; } // After sprites (100)
|
||||
std::vector<const char*> getDependencies() const override { return {"Sprites"}; }
|
||||
|
||||
void setup(rhi::IRHIDevice& device) override;
|
||||
void shutdown(rhi::IRHIDevice& device) override;
|
||||
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
|
||||
|
||||
/**
|
||||
* @brief Set resource cache for texture lookup by ID
|
||||
*/
|
||||
void setResourceCache(ResourceCache* cache) { m_resourceCache = cache; }
|
||||
|
||||
/**
|
||||
* @brief Set blend mode for particles
|
||||
* @param additive true for additive blending (fire, sparks), false for alpha (smoke)
|
||||
*/
|
||||
void setAdditiveBlending(bool additive) { m_additiveBlending = additive; }
|
||||
|
||||
private:
|
||||
void flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
||||
rhi::TextureHandle texture, uint32_t count);
|
||||
|
||||
rhi::ShaderHandle m_shader;
|
||||
rhi::BufferHandle m_quadVB;
|
||||
rhi::BufferHandle m_quadIB;
|
||||
rhi::BufferHandle m_instanceBuffer; // Fallback for when transient allocation fails
|
||||
rhi::UniformHandle m_textureSampler;
|
||||
rhi::TextureHandle m_defaultTexture; // White 1x1 texture for untextured particles
|
||||
|
||||
ResourceCache* m_resourceCache = nullptr;
|
||||
bool m_additiveBlending = true; // Default to additive for fire/spark effects
|
||||
|
||||
// GPU-aligned particle instances for batching
|
||||
std::vector<SpriteInstance> m_particleInstances;
|
||||
|
||||
static constexpr uint32_t MAX_PARTICLES_PER_BATCH = 10000;
|
||||
};
|
||||
|
||||
} // namespace grove
|
||||
@ -3,6 +3,7 @@
|
||||
#include "../Frame/FramePacket.h"
|
||||
#include "../Resources/ResourceCache.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace grove {
|
||||
|
||||
@ -45,7 +46,8 @@ void SpritePass::setup(rhi::IRHIDevice& device) {
|
||||
ibDesc.dynamic = false;
|
||||
m_quadIB = device.createBuffer(ibDesc);
|
||||
|
||||
// Create dynamic instance buffer
|
||||
// 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);
|
||||
@ -82,7 +84,7 @@ void SpritePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
||||
|
||||
cmd.setVertexBuffer(m_quadVB);
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, count);
|
||||
// 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);
|
||||
@ -120,61 +122,70 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
|
||||
return sa.textureId < sb.textureId;
|
||||
});
|
||||
|
||||
// Process sprites in batches by layer and texture
|
||||
// Flush batch when layer OR texture changes to maintain correct draw order
|
||||
std::vector<SpriteInstance> batchData;
|
||||
batchData.reserve(MAX_SPRITES_PER_BATCH);
|
||||
// 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;
|
||||
|
||||
uint16_t currentTextureId = UINT16_MAX;
|
||||
float currentLayer = -1e9f; // Use a value that won't match any real layer
|
||||
rhi::TextureHandle currentTexture;
|
||||
|
||||
for (uint32_t idx : m_sortedIndices) {
|
||||
const SpriteInstance& sprite = frame.sprites[idx];
|
||||
uint16_t texId = static_cast<uint16_t>(sprite.textureId);
|
||||
|
||||
// Check if texture OR layer changed (must flush to maintain layer order)
|
||||
if (texId != currentTextureId || sprite.layer != currentLayer) {
|
||||
// Flush previous batch
|
||||
if (!batchData.empty()) {
|
||||
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
||||
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
||||
batchData.clear();
|
||||
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;
|
||||
}
|
||||
|
||||
// Update current state
|
||||
currentTextureId = texId;
|
||||
currentLayer = sprite.layer;
|
||||
if (texId == 0 || !m_resourceCache) {
|
||||
// Use default/active texture for textureId=0
|
||||
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||
} else {
|
||||
// Look up texture by ID
|
||||
currentTexture = m_resourceCache->getTextureById(texId);
|
||||
if (!currentTexture.isValid()) {
|
||||
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||
}
|
||||
uint32_t batchCount = batchEnd - batchStart;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Add sprite to batch
|
||||
batchData.push_back(sprite);
|
||||
// Allocate transient instance buffer for this batch
|
||||
rhi::TransientInstanceBuffer transientBuffer = device.allocTransientInstanceBuffer(batchCount);
|
||||
|
||||
// Flush if batch is full
|
||||
if (batchData.size() >= MAX_SPRITES_PER_BATCH) {
|
||||
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
|
||||
cmd.setVertexBuffer(m_quadVB);
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setTransientInstanceBuffer(transientBuffer, 0, batchCount);
|
||||
cmd.setTexture(0, batchTexture, m_textureSampler);
|
||||
cmd.drawInstanced(6, batchCount);
|
||||
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)));
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
||||
batchData.clear();
|
||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, batchCount);
|
||||
flushBatch(device, cmd, batchTexture, batchCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining sprites
|
||||
if (!batchData.empty()) {
|
||||
device.updateBuffer(m_instanceBuffer, batchData.data(),
|
||||
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
|
||||
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
|
||||
batchStart = batchEnd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -260,6 +260,38 @@ public:
|
||||
bgfx::updateTexture2D(h, 0, 0, 0, 0, m_width, m_height, bgfx::copy(data, size));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Transient Instance Buffers
|
||||
// ========================================
|
||||
|
||||
TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) override {
|
||||
TransientInstanceBuffer result;
|
||||
|
||||
constexpr uint16_t INSTANCE_STRIDE = 80; // 5 x vec4
|
||||
|
||||
// Check if we have space in the pool
|
||||
if (m_transientPoolCount >= MAX_TRANSIENT_BUFFERS) {
|
||||
return result; // Pool full, return invalid
|
||||
}
|
||||
|
||||
// Check if bgfx has enough transient memory
|
||||
if (bgfx::getAvailInstanceDataBuffer(count, INSTANCE_STRIDE) < count) {
|
||||
return result; // Not enough memory
|
||||
}
|
||||
|
||||
// Allocate from bgfx
|
||||
uint16_t poolIndex = m_transientPoolCount++;
|
||||
bgfx::allocInstanceDataBuffer(&m_transientPool[poolIndex], count, INSTANCE_STRIDE);
|
||||
|
||||
result.data = m_transientPool[poolIndex].data;
|
||||
result.size = count * INSTANCE_STRIDE;
|
||||
result.count = count;
|
||||
result.stride = INSTANCE_STRIDE;
|
||||
result.poolIndex = poolIndex;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// View Setup
|
||||
// ========================================
|
||||
@ -282,6 +314,8 @@ public:
|
||||
|
||||
void frame() override {
|
||||
bgfx::frame();
|
||||
// Reset transient pool for next frame
|
||||
m_transientPoolCount = 0;
|
||||
}
|
||||
|
||||
void executeCommandBuffer(const RHICommandBuffer& cmdBuffer) override {
|
||||
@ -293,6 +327,12 @@ public:
|
||||
uint32_t instStart = 0;
|
||||
uint32_t instCount = 0;
|
||||
|
||||
// Store texture state to apply at draw time (not immediately)
|
||||
TextureHandle pendingTexture;
|
||||
UniformHandle pendingSampler;
|
||||
uint8_t pendingTextureSlot = 0;
|
||||
bool hasTexture = false;
|
||||
|
||||
for (const Command& cmd : cmdBuffer.getCommands()) {
|
||||
switch (cmd.type) {
|
||||
case CommandType::SetState: {
|
||||
@ -339,9 +379,12 @@ public:
|
||||
}
|
||||
|
||||
case CommandType::SetTexture: {
|
||||
bgfx::TextureHandle tex = { cmd.setTexture.texture.id };
|
||||
bgfx::UniformHandle sampler = { cmd.setTexture.sampler.id };
|
||||
bgfx::setTexture(cmd.setTexture.slot, sampler, tex);
|
||||
// Store texture state - apply at draw time, not immediately
|
||||
// This ensures texture is set after all other state is configured
|
||||
pendingTexture = cmd.setTexture.texture;
|
||||
pendingSampler = cmd.setTexture.sampler;
|
||||
pendingTextureSlot = cmd.setTexture.slot;
|
||||
hasTexture = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -365,6 +408,15 @@ public:
|
||||
currentInstBuffer = cmd.setInstanceBuffer.buffer;
|
||||
instStart = cmd.setInstanceBuffer.start;
|
||||
instCount = cmd.setInstanceBuffer.count;
|
||||
m_useTransientInstance = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::SetTransientInstanceBuffer: {
|
||||
m_currentTransientIndex = cmd.setTransientInstanceBuffer.poolIndex;
|
||||
instStart = cmd.setTransientInstanceBuffer.start;
|
||||
instCount = cmd.setTransientInstanceBuffer.count;
|
||||
m_useTransientInstance = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -441,7 +493,11 @@ public:
|
||||
bgfx::setIndexBuffer(h, 0, cmd.drawInstanced.indexCount);
|
||||
}
|
||||
}
|
||||
if (currentInstBuffer.isValid()) {
|
||||
// Set instance buffer (either dynamic or transient)
|
||||
if (m_useTransientInstance && m_currentTransientIndex < m_transientPoolCount) {
|
||||
// Transient instance buffer from pool
|
||||
bgfx::setInstanceDataBuffer(&m_transientPool[m_currentTransientIndex], instStart, instCount);
|
||||
} else if (currentInstBuffer.isValid()) {
|
||||
bool isDynamic = (currentInstBuffer.id & 0x8000) != 0;
|
||||
uint16_t idx = currentInstBuffer.id & 0x7FFF;
|
||||
if (isDynamic) {
|
||||
@ -453,8 +509,16 @@ public:
|
||||
}
|
||||
|
||||
case CommandType::Submit: {
|
||||
// Apply pending texture right before submit
|
||||
if (hasTexture) {
|
||||
bgfx::TextureHandle tex = { pendingTexture.id };
|
||||
bgfx::UniformHandle sampler = { pendingSampler.id };
|
||||
bgfx::setTexture(pendingTextureSlot, sampler, tex);
|
||||
}
|
||||
bgfx::ProgramHandle program = { cmd.submit.shader.id };
|
||||
bgfx::submit(cmd.submit.view, program, cmd.submit.depth);
|
||||
// Reset texture state after submit (consumed)
|
||||
hasTexture = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -466,6 +530,15 @@ private:
|
||||
uint16_t m_height = 0;
|
||||
bool m_initialized = false;
|
||||
|
||||
// Transient instance buffer pool (reset each frame)
|
||||
static constexpr uint16_t MAX_TRANSIENT_BUFFERS = 256;
|
||||
bgfx::InstanceDataBuffer m_transientPool[MAX_TRANSIENT_BUFFERS];
|
||||
uint16_t m_transientPoolCount = 0;
|
||||
|
||||
// Transient buffer state for command execution
|
||||
bool m_useTransientInstance = false;
|
||||
uint16_t m_currentTransientIndex = UINT16_MAX;
|
||||
|
||||
// Empty buffer for null data fallback in buffer creation
|
||||
inline static const uint8_t s_emptyBuffer[1] = {0};
|
||||
|
||||
|
||||
@ -53,6 +53,15 @@ void RHICommandBuffer::setInstanceBuffer(BufferHandle buffer, uint32_t start, ui
|
||||
m_commands.push_back(cmd);
|
||||
}
|
||||
|
||||
void RHICommandBuffer::setTransientInstanceBuffer(const TransientInstanceBuffer& buffer, uint32_t start, uint32_t count) {
|
||||
Command cmd;
|
||||
cmd.type = CommandType::SetTransientInstanceBuffer;
|
||||
cmd.setTransientInstanceBuffer.poolIndex = buffer.poolIndex;
|
||||
cmd.setTransientInstanceBuffer.start = start;
|
||||
cmd.setTransientInstanceBuffer.count = count;
|
||||
m_commands.push_back(cmd);
|
||||
}
|
||||
|
||||
void RHICommandBuffer::setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
Command cmd;
|
||||
cmd.type = CommandType::SetScissor;
|
||||
|
||||
@ -17,6 +17,7 @@ enum class CommandType : uint8_t {
|
||||
SetVertexBuffer,
|
||||
SetIndexBuffer,
|
||||
SetInstanceBuffer,
|
||||
SetTransientInstanceBuffer, // For frame-local multi-batch rendering
|
||||
SetScissor,
|
||||
Draw,
|
||||
DrawIndexed,
|
||||
@ -33,6 +34,7 @@ struct Command {
|
||||
struct { BufferHandle buffer; uint32_t offset; } setVertexBuffer;
|
||||
struct { BufferHandle buffer; uint32_t offset; bool is32Bit; } setIndexBuffer;
|
||||
struct { BufferHandle buffer; uint32_t start; uint32_t count; } setInstanceBuffer;
|
||||
struct { uint16_t poolIndex; uint32_t start; uint32_t count; } setTransientInstanceBuffer;
|
||||
struct { uint16_t x, y, w, h; } setScissor;
|
||||
struct { uint32_t vertexCount; uint32_t startVertex; } draw;
|
||||
struct { uint32_t indexCount; uint32_t startIndex; } drawIndexed;
|
||||
@ -68,6 +70,7 @@ public:
|
||||
void setVertexBuffer(BufferHandle buffer, uint32_t offset = 0);
|
||||
void setIndexBuffer(BufferHandle buffer, uint32_t offset = 0, bool is32Bit = false);
|
||||
void setInstanceBuffer(BufferHandle buffer, uint32_t start, uint32_t count);
|
||||
void setTransientInstanceBuffer(const TransientInstanceBuffer& buffer, uint32_t start, uint32_t count);
|
||||
void setScissor(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||
void draw(uint32_t vertexCount, uint32_t startVertex = 0);
|
||||
void drawIndexed(uint32_t indexCount, uint32_t startIndex = 0);
|
||||
|
||||
@ -54,6 +54,11 @@ public:
|
||||
virtual void updateBuffer(BufferHandle handle, const void* data, uint32_t size) = 0;
|
||||
virtual void updateTexture(TextureHandle handle, const void* data, uint32_t size) = 0;
|
||||
|
||||
// Transient instance buffers (frame-local, for multi-batch rendering)
|
||||
// These are automatically freed at end of frame - no manual cleanup needed
|
||||
// Returns buffer with data pointer for CPU-side writing
|
||||
virtual TransientInstanceBuffer allocTransientInstanceBuffer(uint32_t count) = 0;
|
||||
|
||||
// View setup
|
||||
virtual void setViewClear(ViewId id, uint32_t rgba, float depth) = 0;
|
||||
virtual void setViewRect(ViewId id, uint16_t x, uint16_t y, uint16_t w, uint16_t h) = 0;
|
||||
|
||||
@ -35,6 +35,19 @@ struct FramebufferHandle {
|
||||
|
||||
using ViewId = uint16_t;
|
||||
|
||||
// ============================================================================
|
||||
// Transient Instance Buffer - Frame-local allocation for multi-batch rendering
|
||||
// ============================================================================
|
||||
|
||||
struct TransientInstanceBuffer {
|
||||
void* data = nullptr; // CPU-side pointer for writing data
|
||||
uint32_t size = 0; // Size in bytes
|
||||
uint32_t count = 0; // Number of instances
|
||||
uint16_t stride = 0; // Bytes per instance
|
||||
uint16_t poolIndex = UINT16_MAX; // Index in device's transient pool
|
||||
bool isValid() const { return data != nullptr && poolIndex != UINT16_MAX; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Render States
|
||||
// ============================================================================
|
||||
|
||||
@ -120,8 +120,9 @@ int main(int argc, char* argv[]) {
|
||||
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeWindowHandle)));
|
||||
config.setDouble("nativeDisplayHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeDisplayHandle)));
|
||||
|
||||
// Load texture from assets folder
|
||||
// Load textures from assets folder
|
||||
config.setString("defaultTexture", "../../assets/textures/1f440.png");
|
||||
config.setString("texture1", "../../assets/textures/5oxaxt1vo2f91.jpg"); // Second texture (Multipla)
|
||||
|
||||
// Enable debug overlay
|
||||
config.setBool("debugOverlay", true);
|
||||
@ -228,7 +229,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
// Send animated sprites in a circle
|
||||
// Using different textureIds to test batching (all will use default texture fallback)
|
||||
// Using different textureIds to test multi-texture batching
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
||||
|
||||
@ -252,29 +253,157 @@ int main(int argc, char* argv[]) {
|
||||
uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF};
|
||||
sprite->setInt("color", static_cast<int>(colors[i]));
|
||||
|
||||
// Use textureId=0 for all (default texture)
|
||||
sprite->setInt("textureId", 0);
|
||||
// Alternate between textures: even = eye (id=1), odd = multipla (id=2)
|
||||
sprite->setInt("textureId", (i % 2 == 0) ? 1 : 2);
|
||||
sprite->setInt("layer", i);
|
||||
|
||||
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
||||
gameIO->publish("render:sprite", std::move(data));
|
||||
}
|
||||
|
||||
// Add a center sprite
|
||||
// Add a center sprite (Multipla!)
|
||||
{
|
||||
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
||||
sprite->setDouble("x", width / 2.0f);
|
||||
sprite->setDouble("y", height / 2.0f);
|
||||
sprite->setDouble("scaleX", 80.0);
|
||||
sprite->setDouble("scaleY", 80.0);
|
||||
sprite->setDouble("rotation", -time);
|
||||
sprite->setInt("color", 0xFFFFFFFF); // White
|
||||
sprite->setDouble("scaleX", 120.0);
|
||||
sprite->setDouble("scaleY", 80.0); // Wider than tall (car aspect ratio)
|
||||
sprite->setDouble("rotation", -time * 0.5f);
|
||||
sprite->setInt("color", 0xFFFFFFFF); // White (no tint)
|
||||
sprite->setInt("textureId", 2); // Multipla texture
|
||||
sprite->setInt("layer", 10);
|
||||
|
||||
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
||||
gameIO->publish("render:sprite", std::move(data));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Test particle rendering (fire-like effect)
|
||||
// ========================================
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
auto particle = std::make_unique<grove::JsonDataNode>("particle");
|
||||
|
||||
// Spawn particles rising from bottom center
|
||||
float spawnX = width / 2.0f + (std::sin(time * 5.0f + i * 0.5f) * 50.0f);
|
||||
float spawnY = height - 50.0f;
|
||||
|
||||
// Particles rise up with some horizontal drift
|
||||
float particleAge = std::fmod(time * 2.0f + i * 0.2f, 2.0f); // 0-2 second cycle
|
||||
float life = 1.0f - (particleAge / 2.0f); // 1.0 -> 0.0
|
||||
|
||||
float px = spawnX + std::sin(particleAge * 3.0f + i) * 30.0f;
|
||||
float py = spawnY - particleAge * 100.0f; // Rise up
|
||||
|
||||
particle->setDouble("x", px);
|
||||
particle->setDouble("y", py);
|
||||
particle->setDouble("vx", 0.0);
|
||||
particle->setDouble("vy", -50.0);
|
||||
particle->setDouble("size", 15.0 + life * 20.0); // Shrink as they age
|
||||
particle->setDouble("life", life);
|
||||
|
||||
// Fire colors: white -> yellow -> orange -> red based on life
|
||||
uint8_t r, g, b;
|
||||
if (life > 0.7f) {
|
||||
// White/yellow core
|
||||
r = 255; g = 255; b = static_cast<uint8_t>(200 * (life - 0.7f) / 0.3f);
|
||||
} else if (life > 0.3f) {
|
||||
// Orange
|
||||
r = 255; g = static_cast<uint8_t>(100 + 155 * (life - 0.3f) / 0.4f); b = 0;
|
||||
} else {
|
||||
// Red fading out
|
||||
r = static_cast<uint8_t>(200 + 55 * life / 0.3f); g = static_cast<uint8_t>(50 * life / 0.3f); b = 0;
|
||||
}
|
||||
uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
||||
particle->setInt("color", static_cast<int>(color));
|
||||
particle->setInt("textureId", 0);
|
||||
|
||||
std::unique_ptr<grove::IDataNode> data = std::move(particle);
|
||||
gameIO->publish("render:particle", std::move(data));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Smoke particles (second particle system - alpha blending, gray colors)
|
||||
// ========================================
|
||||
float smokeSpawnX = width * 0.7f; // Right side
|
||||
float smokeSpawnY = height * 0.8f;
|
||||
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
auto smoke = std::make_unique<grove::JsonDataNode>("particle");
|
||||
|
||||
// Smoke rises slower, drifts more
|
||||
float smokeAge = std::fmod(time * 1.0f + i * 0.15f, 3.0f); // 3 second cycle
|
||||
float life = 1.0f - (smokeAge / 3.0f);
|
||||
|
||||
float sx = smokeSpawnX + std::sin(smokeAge * 2.0f + i * 0.5f) * 50.0f;
|
||||
float sy = smokeSpawnY - smokeAge * 60.0f; // Rise slower
|
||||
|
||||
smoke->setDouble("x", sx);
|
||||
smoke->setDouble("y", sy);
|
||||
smoke->setDouble("vx", std::sin(time + i) * 20.0f);
|
||||
smoke->setDouble("vy", -30.0);
|
||||
smoke->setDouble("size", 20.0 + (1.0f - life) * 40.0); // Grow as they age
|
||||
smoke->setDouble("life", life);
|
||||
|
||||
// Smoke colors: dark gray -> light gray, fading alpha
|
||||
uint8_t gray = static_cast<uint8_t>(80 + 80 * (1.0f - life)); // Gets lighter
|
||||
uint8_t alpha = static_cast<uint8_t>(200 * life); // Fades out
|
||||
uint32_t smokeColor = (gray << 24) | (gray << 16) | (gray << 8) | alpha;
|
||||
smoke->setInt("color", static_cast<int>(smokeColor));
|
||||
smoke->setInt("textureId", 0);
|
||||
smoke->setInt("blendMode", 0); // Alpha blend (not additive)
|
||||
|
||||
std::unique_ptr<grove::IDataNode> smokeData = std::move(smoke);
|
||||
gameIO->publish("render:particle", std::move(smokeData));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Sparkle particles (third particle system - small, fast, bright)
|
||||
// ========================================
|
||||
float sparkleX = width * 0.5f;
|
||||
float sparkleY = height * 0.3f;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
auto sparkle = std::make_unique<grove::JsonDataNode>("particle");
|
||||
|
||||
float angle = (time * 3.0f + i * (3.14159f * 2.0f / 8.0f));
|
||||
float radius = 50.0f + std::sin(time * 5.0f + i) * 20.0f;
|
||||
float sparkleAge = std::fmod(time * 4.0f + i * 0.1f, 1.0f);
|
||||
float life = 1.0f - sparkleAge;
|
||||
|
||||
float spx = sparkleX + std::cos(angle) * radius;
|
||||
float spy = sparkleY + std::sin(angle) * radius * 0.5f;
|
||||
|
||||
sparkle->setDouble("x", spx);
|
||||
sparkle->setDouble("y", spy);
|
||||
sparkle->setDouble("vx", 0.0);
|
||||
sparkle->setDouble("vy", 0.0);
|
||||
sparkle->setDouble("size", 5.0 + life * 10.0);
|
||||
sparkle->setDouble("life", life);
|
||||
|
||||
// Sparkle colors: cyan -> white -> magenta cycle
|
||||
float hue = std::fmod(time * 0.5f + i * 0.1f, 1.0f);
|
||||
uint8_t r, g, b;
|
||||
if (hue < 0.33f) {
|
||||
r = static_cast<uint8_t>(255 * (1.0f - hue * 3.0f));
|
||||
g = 255;
|
||||
b = static_cast<uint8_t>(255 * hue * 3.0f);
|
||||
} else if (hue < 0.66f) {
|
||||
r = static_cast<uint8_t>(255 * (hue - 0.33f) * 3.0f);
|
||||
g = static_cast<uint8_t>(255 * (1.0f - (hue - 0.33f) * 3.0f));
|
||||
b = 255;
|
||||
} else {
|
||||
r = 255;
|
||||
g = static_cast<uint8_t>(255 * (hue - 0.66f) * 3.0f);
|
||||
b = static_cast<uint8_t>(255 * (1.0f - (hue - 0.66f) * 3.0f));
|
||||
}
|
||||
uint32_t sparkleColor = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
||||
sparkle->setInt("color", static_cast<int>(sparkleColor));
|
||||
sparkle->setInt("textureId", 0);
|
||||
|
||||
std::unique_ptr<grove::IDataNode> sparkleData = std::move(sparkle);
|
||||
gameIO->publish("render:particle", std::move(sparkleData));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Test text rendering
|
||||
// ========================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user