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:
StillHammer 2025-11-28 17:15:45 +08:00
parent 5795bbb37e
commit 9618a647a2
12 changed files with 599 additions and 62 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@ -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");
}

View File

@ -52,6 +52,7 @@ add_library(BgfxRenderer SHARED
Passes/TilemapPass.cpp
Passes/SpritePass.cpp
Passes/TextPass.cpp
Passes/ParticlePass.cpp
Passes/DebugPass.cpp
# Text

View 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

View 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

View File

@ -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;
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 {
// Look up texture by ID
currentTexture = m_resourceCache->getTextureById(texId);
if (!currentTexture.isValid()) {
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
}
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;
}
}

View File

@ -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};

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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
// ============================================================================

View File

@ -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
// ========================================