diff --git a/modules/BgfxRenderer/BgfxRendererModule.cpp b/modules/BgfxRenderer/BgfxRendererModule.cpp index 9420a22..6a03e48 100644 --- a/modules/BgfxRenderer/BgfxRendererModule.cpp +++ b/modules/BgfxRenderer/BgfxRendererModule.cpp @@ -38,8 +38,13 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas size_t allocatorSize = static_cast(config.getInt("frameAllocatorSizeMB", 16)) * 1024 * 1024; // Window handle (passed via config or 0 if separate WindowModule) + // Use double to preserve 64-bit pointer values void* windowHandle = reinterpret_cast( - static_cast(config.getInt("nativeWindowHandle", 0)) + static_cast(config.getDouble("nativeWindowHandle", 0.0)) + ); + // Display handle (X11 Display* on Linux, 0/nullptr on Windows) + void* displayHandle = reinterpret_cast( + static_cast(config.getDouble("nativeDisplayHandle", 0.0)) ); m_logger->info("Initializing BgfxRenderer: {}x{} backend={}", m_width, m_height, m_backend); @@ -48,7 +53,7 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas m_frameAllocator = std::make_unique(allocatorSize); m_device = rhi::IRHIDevice::create(); - if (!m_device->init(windowHandle, m_width, m_height)) { + if (!m_device->init(windowHandle, displayHandle, m_width, m_height)) { m_logger->error("Failed to initialize RHI device"); return; } @@ -73,16 +78,23 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas } // Setup render graph with passes (inject shaders via constructors) + m_logger->info("Creating RenderGraph..."); m_renderGraph = std::make_unique(); m_renderGraph->addPass(std::make_unique()); + m_logger->info("Added ClearPass"); m_renderGraph->addPass(std::make_unique(spriteShader)); + m_logger->info("Added SpritePass"); m_renderGraph->addPass(std::make_unique(debugShader)); + m_logger->info("Added DebugPass"); m_renderGraph->setup(*m_device); + m_logger->info("RenderGraph setup complete"); m_renderGraph->compile(); + m_logger->info("RenderGraph compiled"); // Setup scene collector with IIO subscriptions m_sceneCollector = std::make_unique(); m_sceneCollector->setup(io); + m_logger->info("SceneCollector setup complete"); // Setup resource cache m_resourceCache = std::make_unique(); diff --git a/modules/BgfxRenderer/RHI/BgfxDevice.cpp b/modules/BgfxRenderer/RHI/BgfxDevice.cpp index 37255a2..049d24e 100644 --- a/modules/BgfxRenderer/RHI/BgfxDevice.cpp +++ b/modules/BgfxRenderer/RHI/BgfxDevice.cpp @@ -19,7 +19,7 @@ public: BgfxDevice() = default; ~BgfxDevice() override = default; - bool init(void* nativeWindowHandle, uint16_t width, uint16_t height) override { + bool init(void* nativeWindowHandle, void* nativeDisplayHandle, uint16_t width, uint16_t height) override { m_width = width; m_height = height; @@ -29,14 +29,9 @@ public: init.resolution.height = height; init.resolution.reset = BGFX_RESET_VSYNC; - // If nativeWindowHandle is provided, use it directly - // Otherwise, bgfx will use the platform data set via bgfx::setPlatformData() - // Note: bgfx::setPlatformData() must be called BEFORE this init() call - if (nativeWindowHandle != nullptr) { - init.platformData.nwh = nativeWindowHandle; - } - // When nativeWindowHandle is nullptr, we leave init.platformData empty - // and rely on the global platform data from bgfx::setPlatformData() + // Set platform data + init.platformData.nwh = nativeWindowHandle; + init.platformData.ndt = nativeDisplayHandle; // X11 Display* on Linux, nullptr on Windows if (!bgfx::init(init)) { return false; @@ -110,19 +105,25 @@ public: BufferHandle result; if (desc.type == BufferDesc::Vertex) { - bgfx::VertexBufferHandle vb; if (desc.dynamic) { + // Dynamic vertex buffer - create with initial size (will resize on update) + // Use 1-byte vertex layout to treat as raw bytes + bgfx::VertexLayout layout; + layout.begin().add(bgfx::Attrib::Position, 1, bgfx::AttribType::Uint8).end(); bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( - desc.size, - bgfx::VertexLayout(), // Will be set at draw time + desc.size, // number of 1-byte "vertices" = size in bytes + layout, BGFX_BUFFER_ALLOW_RESIZE ); // Store as dynamic (high bit set) result.id = dvb.idx | 0x8000; } else { - vb = bgfx::createVertexBuffer( - desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(nullptr, desc.size), - bgfx::VertexLayout() + // Static vertex buffer with data + bgfx::VertexLayout layout; + layout.begin().add(bgfx::Attrib::Position, 1, bgfx::AttribType::Uint8).end(); + bgfx::VertexBufferHandle vb = bgfx::createVertexBuffer( + desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(s_emptyBuffer, 1), + layout ); result.id = vb.idx; } @@ -135,14 +136,17 @@ public: result.id = dib.idx | 0x8000; } else { bgfx::IndexBufferHandle ib = bgfx::createIndexBuffer( - desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(nullptr, desc.size) + desc.data ? bgfx::copy(desc.data, desc.size) : bgfx::makeRef(s_emptyBuffer, 1) ); result.id = ib.idx; } - } else { // Instance buffer - treated as vertex buffer + } else { // Instance buffer - treated as dynamic vertex buffer + // For instance buffers, use same approach as dynamic vertex + bgfx::VertexLayout layout; + layout.begin().add(bgfx::Attrib::Position, 1, bgfx::AttribType::Uint8).end(); bgfx::DynamicVertexBufferHandle dvb = bgfx::createDynamicVertexBuffer( desc.size, - bgfx::VertexLayout(), + layout, BGFX_BUFFER_ALLOW_RESIZE ); result.id = dvb.idx | 0x8000; @@ -447,6 +451,9 @@ private: uint16_t m_height = 0; bool m_initialized = false; + // Empty buffer for null data fallback in buffer creation + inline static const uint8_t s_emptyBuffer[1] = {0}; + static bgfx::TextureFormat::Enum toBgfxFormat(TextureDesc::Format format) { switch (format) { case TextureDesc::RGBA8: return bgfx::TextureFormat::RGBA8; diff --git a/modules/BgfxRenderer/RHI/RHIDevice.h b/modules/BgfxRenderer/RHI/RHIDevice.h index 3821ff9..ad57adc 100644 --- a/modules/BgfxRenderer/RHI/RHIDevice.h +++ b/modules/BgfxRenderer/RHI/RHIDevice.h @@ -29,7 +29,9 @@ public: virtual ~IRHIDevice() = default; // Lifecycle - virtual bool init(void* nativeWindowHandle, uint16_t width, uint16_t height) = 0; + // nativeWindowHandle: Window handle (HWND on Windows, X11 Window on Linux) + // nativeDisplayHandle: Display handle (nullptr on Windows, X11 Display* on Linux) + virtual bool init(void* nativeWindowHandle, void* nativeDisplayHandle, uint16_t width, uint16_t height) = 0; virtual void shutdown() = 0; virtual void reset(uint16_t width, uint16_t height) = 0; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b8713dc..f46b1e2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -695,6 +695,26 @@ if(GROVE_BUILD_BGFX_RENDERER) # Not added to CTest (requires display) message(STATUS "Visual test 'test_22_bgfx_sprites' enabled (run manually)") + + # Test 23: Visual Sprites Test via Module + IIO + add_executable(test_23_bgfx_sprites_visual + visual/test_23_bgfx_sprites_visual.cpp + ) + + target_include_directories(test_23_bgfx_sprites_visual PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_23_bgfx_sprites_visual PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_23_bgfx_sprites_visual' enabled (run manually)") else() message(STATUS "SDL2 not found - visual tests disabled") endif() diff --git a/tests/visual/test_23_bgfx_sprites_visual.cpp b/tests/visual/test_23_bgfx_sprites_visual.cpp new file mode 100644 index 0000000..43bf215 --- /dev/null +++ b/tests/visual/test_23_bgfx_sprites_visual.cpp @@ -0,0 +1,286 @@ +/** + * Test: BgfxRenderer Visual Sprites Test + * + * Tests the full BgfxRendererModule with sprites via IIO. + * This validates Phase 5: sprites rendered via the module pipeline. + */ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +int main(int argc, char* argv[]) { + std::cout << "========================================\n"; + std::cout << "BgfxRenderer Visual Sprites Test\n"; + std::cout << "========================================\n\n"; + + // Initialize SDL + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + std::cerr << "SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + // Create window + const int width = 800; + const int height = 600; + + SDL_Window* window = SDL_CreateWindow( + "BgfxRenderer Sprites Test - Press ESC to exit", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + width, height, + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE + ); + + if (!window) { + std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << "\n"; + SDL_Quit(); + return 1; + } + + // Get native window handle + SDL_SysWMinfo wmi; + SDL_VERSION(&wmi.version); + if (!SDL_GetWindowWMInfo(window, &wmi)) { + std::cerr << "SDL_GetWindowWMInfo failed: " << SDL_GetError() << "\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + +#ifdef _WIN32 + void* nativeWindowHandle = wmi.info.win.window; + void* nativeDisplayHandle = nullptr; +#else + void* nativeWindowHandle = (void*)(uintptr_t)wmi.info.x11.window; + void* nativeDisplayHandle = wmi.info.x11.display; +#endif + + std::cout << "Window created: " << width << "x" << height << "\n"; + + std::cout << "Native handles ready\n"; + + // ======================================== + // Setup GroveEngine systems + // ======================================== + + // Use singleton IntraIOManager for proper routing + auto& ioManager = grove::IntraIOManager::getInstance(); + + // Create two IO instances: one for "game" publishing, one for renderer + auto gameIO = ioManager.createInstance("game_module"); + auto rendererIO = ioManager.createInstance("bgfx_renderer"); + + std::cout << "IIO Manager setup complete\n"; + + // Load BgfxRenderer module + grove::ModuleLoader loader; + + // Find the module library (in modules/ subdirectory relative to build) + std::string modulePath = "../modules/libBgfxRenderer.so"; +#ifdef _WIN32 + modulePath = "../modules/BgfxRenderer.dll"; +#endif + + std::unique_ptr module; + try { + module = loader.load(modulePath, "bgfx_renderer"); + } catch (const std::exception& e) { + std::cerr << "Failed to load BgfxRenderer module: " << e.what() << "\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + if (!module) { + std::cerr << "Failed to load BgfxRenderer module from: " << modulePath << "\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + std::cout << "BgfxRenderer module loaded\n"; + + // Configure the module with native window handles + grove::JsonDataNode config("config"); + config.setInt("windowWidth", width); + config.setInt("windowHeight", height); + config.setString("backend", "auto"); + config.setBool("vsync", true); + config.setInt("maxSpritesPerBatch", 10000); + config.setInt("frameAllocatorSizeMB", 16); + // Pass native handles as double (can store 64-bit integers up to 2^53 without loss) + config.setDouble("nativeWindowHandle", static_cast(reinterpret_cast(nativeWindowHandle))); + config.setDouble("nativeDisplayHandle", static_cast(reinterpret_cast(nativeDisplayHandle))); + + module->setConfiguration(config, rendererIO.get(), nullptr); + + std::cout << "Module configured\n"; + + // ======================================== + // Main loop + // ======================================== + + std::cout << "\n*** Rendering sprites via IIO ***\n"; + std::cout << "Press ESC to exit or wait 10 seconds\n\n"; + + bool running = true; + uint32_t frameCount = 0; + Uint32 startTime = SDL_GetTicks(); + const Uint32 testDuration = 10000; // 10 seconds + + while (running) { + // Process SDL events + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + running = false; + } + if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) { + running = false; + } + } + + // Check timeout + Uint32 elapsed = SDL_GetTicks() - startTime; + if (elapsed > testDuration) { + running = false; + } + + float time = elapsed / 1000.0f; + + // ======================================== + // Game module publishes render commands via IIO + // ======================================== + + // Send camera + { + auto camera = std::make_unique("camera"); + camera->setDouble("x", 0.0); + camera->setDouble("y", 0.0); + camera->setDouble("zoom", 1.0); + camera->setInt("viewportX", 0); + camera->setInt("viewportY", 0); + camera->setInt("viewportW", width); + camera->setInt("viewportH", height); + + std::unique_ptr data = std::move(camera); + gameIO->publish("render:camera", std::move(data)); + } + + // Send clear color (changes over time) + { + auto clear = std::make_unique("clear"); + uint8_t r = static_cast(48 + 20 * std::sin(time * 0.5)); + uint8_t g = static_cast(48 + 20 * std::sin(time * 0.5 + 1.0f)); + uint8_t b = static_cast(80 + 30 * std::sin(time * 0.5 + 2.0f)); + uint32_t clearColor = (r << 24) | (g << 16) | (b << 8) | 0xFF; + clear->setInt("color", static_cast(clearColor)); + + std::unique_ptr data = std::move(clear); + gameIO->publish("render:clear", std::move(data)); + } + + // Send animated sprites in a circle + for (int i = 0; i < 5; ++i) { + auto sprite = std::make_unique("sprite"); + + // Animate position in a circle + float angle = time * 2.0f + i * (3.14159f * 2.0f / 5.0f); + float radius = 150.0f; + float centerX = width / 2.0f; + float centerY = height / 2.0f; + + sprite->setDouble("x", centerX + std::cos(angle) * radius); + sprite->setDouble("y", centerY + std::sin(angle) * radius); + sprite->setDouble("scaleX", 50.0); // 50x50 pixel sprite + sprite->setDouble("scaleY", 50.0); + sprite->setDouble("rotation", angle); + sprite->setDouble("u0", 0.0); + sprite->setDouble("v0", 0.0); + 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])); + sprite->setInt("textureId", 0); + sprite->setInt("layer", i); + + std::unique_ptr data = std::move(sprite); + gameIO->publish("render:sprite", std::move(data)); + } + + // Add a center sprite + { + 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->setInt("layer", 10); + + std::unique_ptr data = std::move(sprite); + gameIO->publish("render:sprite", std::move(data)); + } + + // ======================================== + // Process frame (renderer pulls IIO messages) + // ======================================== + + grove::JsonDataNode input("input"); + input.setDouble("deltaTime", 1.0 / 60.0); + input.setInt("frameNumber", static_cast(frameCount)); + + module->process(input); + frameCount++; + + // Log progress every second + if (frameCount % 60 == 0) { + std::cout << "Frame " << frameCount << " - " << (elapsed / 1000.0f) << "s\n"; + } + } + + float elapsedSec = (SDL_GetTicks() - startTime) / 1000.0f; + float fps = frameCount / elapsedSec; + + std::cout << "\nTest completed!\n"; + std::cout << " Frames: " << frameCount << "\n"; + std::cout << " Time: " << elapsedSec << "s\n"; + std::cout << " FPS: " << fps << "\n"; + + // ======================================== + // Cleanup + // ======================================== + + module->shutdown(); + loader.unload(); + + // Remove IIO instances + ioManager.removeInstance("game_module"); + ioManager.removeInstance("bgfx_renderer"); + + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "\n========================================\n"; + std::cout << "PASS: Sprites rendered via IIO!\n"; + std::cout << "========================================\n"; + + return 0; +}