diff --git a/assets/textures/5oxaxt1vo2f91.jpg b/assets/textures/5oxaxt1vo2f91.jpg new file mode 100644 index 0000000..8e745eb Binary files /dev/null and b/assets/textures/5oxaxt1vo2f91.jpg differ diff --git a/modules/BgfxRenderer/BgfxRendererModule.cpp b/modules/BgfxRenderer/BgfxRendererModule.cpp index 95427fe..ccd4eac 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.cpp +++ b/modules/BgfxRenderer/BgfxRendererModule.cpp @@ -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 @@ -106,6 +107,12 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas m_renderGraph->addPass(std::make_unique(spriteShader)); m_logger->info("Added TextPass"); + // Create ParticlePass (uses sprite shader, renders after sprites with additive blending) + auto particlePass = std::make_unique(spriteShader); + particlePass->setResourceCache(m_resourceCache.get()); + m_renderGraph->addPass(std::move(particlePass)); + m_logger->info("Added ParticlePass"); + m_renderGraph->addPass(std::make_unique(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"); } diff --git a/modules/BgfxRenderer/CMakeLists.txt b/modules/BgfxRenderer/CMakeLists.txt index 0512d53..b33ae4a 100644 --- a/modules/BgfxRenderer/CMakeLists.txt +++ b/modules/BgfxRenderer/CMakeLists.txt @@ -52,6 +52,7 @@ add_library(BgfxRenderer SHARED Passes/TilemapPass.cpp Passes/SpritePass.cpp Passes/TextPass.cpp + Passes/ParticlePass.cpp Passes/DebugPass.cpp # Text diff --git a/modules/BgfxRenderer/Passes/ParticlePass.cpp b/modules/BgfxRenderer/Passes/ParticlePass.cpp new file mode 100644 index 0000000..7dd79b5 --- /dev/null +++ b/modules/BgfxRenderer/Passes/ParticlePass.cpp @@ -0,0 +1,208 @@ +#include "ParticlePass.h" +#include "../RHI/RHIDevice.h" +#include "../Resources/ResourceCache.h" +#include + +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(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(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((color >> 24) & 0xFF) / 255.0f; + inst.g = static_cast((color >> 16) & 0xFF) / 255.0f; + inst.b = static_cast((color >> 8) & 0xFF) / 255.0f; + // Alpha fades with life + float baseAlpha = static_cast(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(m_particleInstances.size())); + m_particleInstances.clear(); + } + } + + // Flush remaining particles + if (!m_particleInstances.empty()) { + flushBatch(device, cmd, currentTexture, static_cast(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 diff --git a/modules/BgfxRenderer/Passes/ParticlePass.h b/modules/BgfxRenderer/Passes/ParticlePass.h new file mode 100644 index 0000000..05c5e89 --- /dev/null +++ b/modules/BgfxRenderer/Passes/ParticlePass.h @@ -0,0 +1,63 @@ +#pragma once + +#include "../RenderGraph/RenderPass.h" +#include "../RHI/RHITypes.h" +#include "../Frame/FramePacket.h" +#include + +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 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 m_particleInstances; + + static constexpr uint32_t MAX_PARTICLES_PER_BATCH = 10000; +}; + +} // namespace grove diff --git a/modules/BgfxRenderer/Passes/SpritePass.cpp b/modules/BgfxRenderer/Passes/SpritePass.cpp index c58bbaf..65e2740 100644 --- a/modules/BgfxRenderer/Passes/SpritePass.cpp +++ b/modules/BgfxRenderer/Passes/SpritePass.cpp @@ -3,6 +3,7 @@ #include "../Frame/FramePacket.h" #include "../Resources/ResourceCache.h" #include +#include 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 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(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(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(batchData.size() * sizeof(SpriteInstance))); - flushBatch(device, cmd, currentTexture, static_cast(batchData.size())); - batchData.clear(); + while (batchEnd < frame.spriteCount) { + uint16_t nextTexId = static_cast(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(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 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(batchData.size() * sizeof(SpriteInstance))); - flushBatch(device, cmd, currentTexture, static_cast(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(batchData.size() * sizeof(SpriteInstance))); - flushBatch(device, cmd, currentTexture, static_cast(batchData.size())); + batchStart = batchEnd; } } diff --git a/modules/BgfxRenderer/RHI/BgfxDevice.cpp b/modules/BgfxRenderer/RHI/BgfxDevice.cpp index c34f034..a9b4e0c 100644 --- a/modules/BgfxRenderer/RHI/BgfxDevice.cpp +++ b/modules/BgfxRenderer/RHI/BgfxDevice.cpp @@ -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}; diff --git a/modules/BgfxRenderer/RHI/RHICommandBuffer.cpp b/modules/BgfxRenderer/RHI/RHICommandBuffer.cpp index 853197e..42b20e1 100644 --- a/modules/BgfxRenderer/RHI/RHICommandBuffer.cpp +++ b/modules/BgfxRenderer/RHI/RHICommandBuffer.cpp @@ -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; diff --git a/modules/BgfxRenderer/RHI/RHICommandBuffer.h b/modules/BgfxRenderer/RHI/RHICommandBuffer.h index 8c9e5cf..a594af3 100644 --- a/modules/BgfxRenderer/RHI/RHICommandBuffer.h +++ b/modules/BgfxRenderer/RHI/RHICommandBuffer.h @@ -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); diff --git a/modules/BgfxRenderer/RHI/RHIDevice.h b/modules/BgfxRenderer/RHI/RHIDevice.h index ad57adc..3b55120 100644 --- a/modules/BgfxRenderer/RHI/RHIDevice.h +++ b/modules/BgfxRenderer/RHI/RHIDevice.h @@ -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; diff --git a/modules/BgfxRenderer/RHI/RHITypes.h b/modules/BgfxRenderer/RHI/RHITypes.h index 37913ee..af621dc 100644 --- a/modules/BgfxRenderer/RHI/RHITypes.h +++ b/modules/BgfxRenderer/RHI/RHITypes.h @@ -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 // ============================================================================ diff --git a/tests/visual/test_23_bgfx_sprites_visual.cpp b/tests/visual/test_23_bgfx_sprites_visual.cpp index 8d7b426..8d0e3be 100644 --- a/tests/visual/test_23_bgfx_sprites_visual.cpp +++ b/tests/visual/test_23_bgfx_sprites_visual.cpp @@ -120,8 +120,9 @@ int main(int argc, char* argv[]) { config.setDouble("nativeWindowHandle", static_cast(reinterpret_cast(nativeWindowHandle))); config.setDouble("nativeDisplayHandle", static_cast(reinterpret_cast(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("sprite"); @@ -252,29 +253,157 @@ int main(int argc, char* argv[]) { uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF}; sprite->setInt("color", static_cast(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 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("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 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("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(200 * (life - 0.7f) / 0.3f); + } else if (life > 0.3f) { + // Orange + r = 255; g = static_cast(100 + 155 * (life - 0.3f) / 0.4f); b = 0; + } else { + // Red fading out + r = static_cast(200 + 55 * life / 0.3f); g = static_cast(50 * life / 0.3f); b = 0; + } + uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF; + particle->setInt("color", static_cast(color)); + particle->setInt("textureId", 0); + + std::unique_ptr 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("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(80 + 80 * (1.0f - life)); // Gets lighter + uint8_t alpha = static_cast(200 * life); // Fades out + uint32_t smokeColor = (gray << 24) | (gray << 16) | (gray << 8) | alpha; + smoke->setInt("color", static_cast(smokeColor)); + smoke->setInt("textureId", 0); + smoke->setInt("blendMode", 0); // Alpha blend (not additive) + + std::unique_ptr 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("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(255 * (1.0f - hue * 3.0f)); + g = 255; + b = static_cast(255 * hue * 3.0f); + } else if (hue < 0.66f) { + r = static_cast(255 * (hue - 0.33f) * 3.0f); + g = static_cast(255 * (1.0f - (hue - 0.33f) * 3.0f)); + b = 255; + } else { + r = 255; + g = static_cast(255 * (hue - 0.66f) * 3.0f); + b = static_cast(255 * (1.0f - (hue - 0.66f) * 3.0f)); + } + uint32_t sparkleColor = (r << 24) | (g << 16) | (b << 8) | 0xFF; + sparkle->setInt("color", static_cast(sparkleColor)); + sparkle->setInt("textureId", 0); + + std::unique_ptr sparkleData = std::move(sparkle); + gameIO->publish("render:particle", std::move(sparkleData)); + } + // ======================================== // Test text rendering // ========================================