diff --git a/assets/textures/1f440.png b/assets/textures/1f440.png new file mode 100644 index 0000000..624fca8 Binary files /dev/null and b/assets/textures/1f440.png differ diff --git a/modules/BgfxRenderer/BgfxRendererModule.cpp b/modules/BgfxRenderer/BgfxRendererModule.cpp index 6a03e48..d209007 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.cpp +++ b/modules/BgfxRenderer/BgfxRendererModule.cpp @@ -82,8 +82,13 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas m_renderGraph = std::make_unique(); m_renderGraph->addPass(std::make_unique()); m_logger->info("Added ClearPass"); - m_renderGraph->addPass(std::make_unique(spriteShader)); + + // Create SpritePass and keep reference for texture binding + auto spritePass = std::make_unique(spriteShader); + m_spritePass = spritePass.get(); // Non-owning reference + m_renderGraph->addPass(std::move(spritePass)); m_logger->info("Added SpritePass"); + m_renderGraph->addPass(std::make_unique(debugShader)); m_logger->info("Added DebugPass"); m_renderGraph->setup(*m_device); @@ -99,6 +104,18 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas // Setup resource cache m_resourceCache = std::make_unique(); + // 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()) { + m_spritePass->setTexture(tex); + m_logger->info("Loaded default texture: {}", defaultTexturePath); + } else { + m_logger->warn("Failed to load default texture: {}", defaultTexturePath); + } + } + m_logger->info("BgfxRenderer initialized successfully"); } diff --git a/modules/BgfxRenderer/BgfxRendererModule.h b/modules/BgfxRenderer/BgfxRendererModule.h index 95ce1b0..6feb140 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.h +++ b/modules/BgfxRenderer/BgfxRendererModule.h @@ -16,6 +16,7 @@ class RenderGraph; class SceneCollector; class ResourceCache; class ShaderManager; +class SpritePass; // ============================================================================ // BgfxRenderer Module - 2D rendering via bgfx @@ -55,6 +56,9 @@ private: std::unique_ptr m_sceneCollector; std::unique_ptr m_resourceCache; + // Pass references (non-owning, owned by RenderGraph) + SpritePass* m_spritePass = nullptr; + // IIO (non-owning) IIO* m_io = nullptr; diff --git a/modules/BgfxRenderer/CMakeLists.txt b/modules/BgfxRenderer/CMakeLists.txt index 06cead2..814e797 100644 --- a/modules/BgfxRenderer/CMakeLists.txt +++ b/modules/BgfxRenderer/CMakeLists.txt @@ -57,11 +57,13 @@ add_library(BgfxRenderer SHARED # Resources Resources/ResourceCache.cpp + Resources/TextureLoader.cpp ) target_include_directories(BgfxRenderer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../../include + ${bgfx_SOURCE_DIR}/bimg/3rdparty # stb_image ) target_link_libraries(BgfxRenderer PRIVATE diff --git a/modules/BgfxRenderer/Passes/SpritePass.cpp b/modules/BgfxRenderer/Passes/SpritePass.cpp index 77c8f45..73d1cfc 100644 --- a/modules/BgfxRenderer/Passes/SpritePass.cpp +++ b/modules/BgfxRenderer/Passes/SpritePass.cpp @@ -104,8 +104,9 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi: cmd.setIndexBuffer(m_quadIB); cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast(batchSize)); - // Bind default texture (TODO: support per-sprite textures via texture array) - cmd.setTexture(0, m_defaultTexture, m_textureSampler); + // Bind texture (use active texture if set, otherwise default white) + rhi::TextureHandle texToUse = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture; + cmd.setTexture(0, texToUse, m_textureSampler); // Submit draw call cmd.drawInstanced(6, static_cast(batchSize)); // 6 indices per quad diff --git a/modules/BgfxRenderer/Passes/SpritePass.h b/modules/BgfxRenderer/Passes/SpritePass.h index ac40cee..adc88c9 100644 --- a/modules/BgfxRenderer/Passes/SpritePass.h +++ b/modules/BgfxRenderer/Passes/SpritePass.h @@ -25,6 +25,14 @@ public: void shutdown(rhi::IRHIDevice& device) override; void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override; + /** + * @brief Set a texture to use for all sprites (temporary API) + * @param texture The texture handle to use (must be valid) + * + * TODO: Replace with proper texture array / per-sprite texture support + */ + void setTexture(rhi::TextureHandle texture) { m_activeTexture = texture; } + private: rhi::ShaderHandle m_shader; rhi::BufferHandle m_quadVB; @@ -32,6 +40,7 @@ private: rhi::BufferHandle m_instanceBuffer; rhi::UniformHandle m_textureSampler; rhi::TextureHandle m_defaultTexture; // White 1x1 texture fallback + rhi::TextureHandle m_activeTexture; // Currently active texture (if set) static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000; }; diff --git a/modules/BgfxRenderer/Resources/ResourceCache.cpp b/modules/BgfxRenderer/Resources/ResourceCache.cpp index 5df72df..c4c93b1 100644 --- a/modules/BgfxRenderer/Resources/ResourceCache.cpp +++ b/modules/BgfxRenderer/Resources/ResourceCache.cpp @@ -1,4 +1,5 @@ #include "ResourceCache.h" +#include "TextureLoader.h" #include "../RHI/RHIDevice.h" #include #include @@ -33,29 +34,21 @@ rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std } } - // Load texture data from file - // TODO: Use stb_image or similar to load actual texture files - // For now, create a placeholder 1x1 white texture + // Load texture from file using TextureLoader (stb_image) + auto result = TextureLoader::loadFromFile(device, path); - uint32_t whitePixel = 0xFFFFFFFF; - - rhi::TextureDesc desc; - desc.width = 1; - desc.height = 1; - desc.mipLevels = 1; - desc.format = rhi::TextureDesc::RGBA8; - desc.data = &whitePixel; - desc.dataSize = sizeof(whitePixel); - - rhi::TextureHandle handle = device.createTexture(desc); + if (!result.success) { + // Return invalid handle on failure + return rhi::TextureHandle{}; + } // Store in cache { std::unique_lock lock(m_mutex); - m_textures[path] = handle; + m_textures[path] = result.handle; } - return handle; + return result.handle; } rhi::ShaderHandle ResourceCache::loadShader(rhi::IRHIDevice& device, const std::string& name, diff --git a/modules/BgfxRenderer/Resources/TextureLoader.cpp b/modules/BgfxRenderer/Resources/TextureLoader.cpp new file mode 100644 index 0000000..5d4fa02 --- /dev/null +++ b/modules/BgfxRenderer/Resources/TextureLoader.cpp @@ -0,0 +1,70 @@ +#include "TextureLoader.h" +#include "../RHI/RHIDevice.h" + +#define STB_IMAGE_IMPLEMENTATION +#include + +#include + +namespace grove { + +TextureLoader::LoadResult TextureLoader::loadFromFile(rhi::IRHIDevice& device, const std::string& path) { + LoadResult result; + + // Read file into memory + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file.is_open()) { + result.error = "Failed to open file: " + path; + return result; + } + + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(static_cast(size)); + if (!file.read(reinterpret_cast(buffer.data()), size)) { + result.error = "Failed to read file: " + path; + return result; + } + + return loadFromMemory(device, buffer.data(), buffer.size()); +} + +TextureLoader::LoadResult TextureLoader::loadFromMemory(rhi::IRHIDevice& device, const uint8_t* data, size_t size) { + LoadResult result; + + // Decode image with stb_image + int width, height, channels; + // Force RGBA output (4 channels) + stbi_uc* pixels = stbi_load_from_memory(data, static_cast(size), &width, &height, &channels, 4); + + if (!pixels) { + result.error = "stb_image failed: "; + result.error += stbi_failure_reason(); + return result; + } + + // Create texture via RHI + rhi::TextureDesc desc; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.format = rhi::TextureDesc::RGBA8; + desc.data = pixels; + desc.dataSize = static_cast(width * height * 4); + + result.handle = device.createTexture(desc); + result.width = desc.width; + result.height = desc.height; + result.success = result.handle.isValid(); + + if (!result.success) { + result.error = "Failed to create GPU texture"; + } + + // Free stb_image memory + stbi_image_free(pixels); + + return result; +} + +} // namespace grove diff --git a/modules/BgfxRenderer/Resources/TextureLoader.h b/modules/BgfxRenderer/Resources/TextureLoader.h new file mode 100644 index 0000000..e91b614 --- /dev/null +++ b/modules/BgfxRenderer/Resources/TextureLoader.h @@ -0,0 +1,45 @@ +#pragma once + +#include "../RHI/RHITypes.h" +#include +#include +#include + +namespace grove { + +namespace rhi { class IRHIDevice; } + +/** + * @brief Loads textures from files (PNG, JPG, etc.) + * + * Uses stb_image for decoding. All textures are loaded as RGBA8. + */ +class TextureLoader { +public: + struct LoadResult { + bool success = false; + rhi::TextureHandle handle; + uint16_t width = 0; + uint16_t height = 0; + std::string error; + }; + + /** + * @brief Load texture from file + * @param device RHI device for texture creation + * @param path Path to image file (PNG, JPG, BMP, TGA, etc.) + * @return LoadResult with handle and dimensions on success + */ + static LoadResult loadFromFile(rhi::IRHIDevice& device, const std::string& path); + + /** + * @brief Load texture from memory + * @param device RHI device for texture creation + * @param data Raw file data (PNG/JPG bytes, not decoded pixels) + * @param size Size of data in bytes + * @return LoadResult with handle and dimensions on success + */ + static LoadResult loadFromMemory(rhi::IRHIDevice& device, const uint8_t* data, size_t size); +}; + +} // namespace grove diff --git a/tests/visual/test_23_bgfx_sprites_visual.cpp b/tests/visual/test_23_bgfx_sprites_visual.cpp index 43bf215..1846fa7 100644 --- a/tests/visual/test_23_bgfx_sprites_visual.cpp +++ b/tests/visual/test_23_bgfx_sprites_visual.cpp @@ -120,6 +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 + config.setString("defaultTexture", "../../assets/textures/1f440.png"); + module->setConfiguration(config, rendererIO.get(), nullptr); std::cout << "Module configured\n"; @@ -208,15 +211,8 @@ int main(int argc, char* argv[]) { sprite->setDouble("u1", 1.0); sprite->setDouble("v1", 1.0); - // Different colors for each sprite (ABGR format for bgfx) - uint32_t colors[] = { - 0xFF0000FF, // Red - 0xFF00FF00, // Green - 0xFFFF0000, // Blue - 0xFF00FFFF, // Yellow - 0xFFFF00FF // Magenta - }; - sprite->setInt("color", static_cast(colors[i])); + // All sprites white to show texture without tint + sprite->setInt("color", static_cast(0xFFFFFFFF)); sprite->setInt("textureId", 0); sprite->setInt("layer", i);