feat: Windows portage + Phase 4 SceneCollector integration
- Port to Windows (MinGW/Ninja): - ModuleFactory/ModuleLoader: LoadLibrary/GetProcAddress - SystemUtils: Windows process memory APIs - FileWatcher: st_mtime instead of st_mtim - IIO.h: add missing #include <cstdint> - Tests (09, 10, 11): grove_dlopen/dlsym wrappers - Phase 4 - SceneCollector & IIO: - Implement view/proj matrix calculation in parseCamera() - Add IIO routing test with game→renderer pattern - test_22_bgfx_sprites_headless: 5 tests, 23 assertions pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4a30b1f149
commit
e004bc015b
@ -4,12 +4,21 @@
|
|||||||
|
|
||||||
GroveEngine est un moteur de jeu C++17 avec hot-reload de modules. On développe actuellement le module **BgfxRenderer** pour le rendu 2D.
|
GroveEngine est un moteur de jeu C++17 avec hot-reload de modules. On développe actuellement le module **BgfxRenderer** pour le rendu 2D.
|
||||||
|
|
||||||
## État Actuel (26 Nov 2025)
|
## État Actuel (27 Nov 2025)
|
||||||
|
|
||||||
|
### Portage Windows ✅
|
||||||
|
|
||||||
|
Le projet compile maintenant sur **Windows** (MinGW/Ninja) en plus de Linux :
|
||||||
|
- `ModuleFactory.cpp` et `ModuleLoader.cpp` : LoadLibrary/GetProcAddress
|
||||||
|
- `SystemUtils.cpp` : Windows process memory APIs
|
||||||
|
- `FileWatcher.h` : st_mtime au lieu de st_mtim
|
||||||
|
- `IIO.h` : ajout `#include <cstdint>`
|
||||||
|
- Tests integration (test_09, test_10, test_11) : wrappers `grove_dlopen/grove_dlsym/grove_dlclose/grove_dlerror`
|
||||||
|
|
||||||
### Phases Complétées ✅
|
### Phases Complétées ✅
|
||||||
|
|
||||||
**Phase 1** - Squelette du module
|
**Phase 1** - Squelette du module
|
||||||
- `libBgfxRenderer.so` compilé et chargeable dynamiquement
|
- `libBgfxRenderer.so/.dll` compilé et chargeable dynamiquement
|
||||||
|
|
||||||
**Phase 2** - RHI Layer
|
**Phase 2** - RHI Layer
|
||||||
- `BgfxDevice` : init/shutdown/frame, création textures/buffers/shaders
|
- `BgfxDevice` : init/shutdown/frame, création textures/buffers/shaders
|
||||||
@ -23,11 +32,19 @@ GroveEngine est un moteur de jeu C++17 avec hot-reload de modules. On développe
|
|||||||
- Shaders pré-compilés : OpenGL, Vulkan, DX11, Metal
|
- Shaders pré-compilés : OpenGL, Vulkan, DX11, Metal
|
||||||
- Test visuel : `test_21_bgfx_triangle` - triangle RGB coloré (~567 FPS Vulkan)
|
- Test visuel : `test_21_bgfx_triangle` - triangle RGB coloré (~567 FPS Vulkan)
|
||||||
|
|
||||||
|
**Phase 4** - SceneCollector & IIO Integration ✅ (NOUVEAU)
|
||||||
|
- `SceneCollector` : collecte des messages IIO pour `render:sprite`, `render:camera`, etc.
|
||||||
|
- Calcul des matrices view/proj avec support zoom dans `parseCamera()`
|
||||||
|
- Test `test_22_bgfx_sprites_headless` : 23 assertions, 5 test cases passent
|
||||||
|
- Validation structure sprite data
|
||||||
|
- Routing IIO inter-modules (game → renderer pattern)
|
||||||
|
- Structure camera/clear/debug messages
|
||||||
|
|
||||||
### Fichiers Clés
|
### Fichiers Clés
|
||||||
|
|
||||||
```
|
```
|
||||||
modules/BgfxRenderer/
|
modules/BgfxRenderer/
|
||||||
├── BgfxRendererModule.cpp # Point d'entrée module
|
├── BgfxRendererModule.cpp # Point d'entrée module + ShaderManager
|
||||||
├── RHI/
|
├── RHI/
|
||||||
│ ├── RHIDevice.h # Interface abstraite
|
│ ├── RHIDevice.h # Interface abstraite
|
||||||
│ ├── BgfxDevice.cpp # Implémentation bgfx
|
│ ├── BgfxDevice.cpp # Implémentation bgfx
|
||||||
@ -41,59 +58,56 @@ modules/BgfxRenderer/
|
|||||||
│ └── RenderGraph.cpp # Tri topologique passes
|
│ └── RenderGraph.cpp # Tri topologique passes
|
||||||
├── Passes/
|
├── Passes/
|
||||||
│ ├── ClearPass.cpp # Clear screen
|
│ ├── ClearPass.cpp # Clear screen
|
||||||
│ ├── SpritePass.cpp # Rendu sprites (à compléter)
|
│ ├── SpritePass.cpp # Rendu sprites instancié
|
||||||
│ └── DebugPass.cpp # Debug shapes
|
│ └── DebugPass.cpp # Debug shapes
|
||||||
├── Frame/
|
├── Frame/
|
||||||
│ ├── FramePacket.h # Données immutables par frame
|
│ ├── FramePacket.h # Données immutables par frame
|
||||||
│ └── FrameAllocator.cpp # Allocateur bump
|
│ └── FrameAllocator.cpp # Allocateur bump
|
||||||
└── Scene/
|
└── Scene/
|
||||||
└── SceneCollector.cpp # Collecte messages IIO
|
└── SceneCollector.cpp # Collecte messages IIO + matrices view/proj
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prochaine Phase : Phase 4
|
## Prochaine Phase : Phase 5
|
||||||
|
|
||||||
### Objectif
|
### Objectif
|
||||||
Intégrer le ShaderManager dans le module principal et rendre le SpritePass fonctionnel.
|
Test visuel complet avec sprites via IIO.
|
||||||
|
|
||||||
### Tâches
|
### Tâches
|
||||||
|
|
||||||
1. **Mettre à jour BgfxRendererModule.cpp** :
|
1. **Créer test_23_bgfx_sprites_visual.cpp** :
|
||||||
- Ajouter `ShaderManager` comme membre
|
- Charger le module BgfxRenderer via ModuleLoader
|
||||||
- Initialiser les shaders dans `setConfiguration()`
|
- Publier des sprites via IIO depuis un "game module" simulé
|
||||||
- Passer le program aux passes
|
- Valider le rendu visuel (sprites affichés à l'écran)
|
||||||
|
|
||||||
2. **Compléter SpritePass.cpp** :
|
2. **Compléter la boucle render** :
|
||||||
- Utiliser le shader "sprite" du ShaderManager
|
- Appeler `SceneCollector::collect()` pour récupérer les messages IIO
|
||||||
- Implémenter l'update du instance buffer avec les données FramePacket
|
- Passer le `FramePacket` finalisé aux passes
|
||||||
- Soumettre les draw calls instancés
|
- S'assurer que `SpritePass::execute()` dessine les sprites
|
||||||
|
|
||||||
3. **Test d'intégration** :
|
3. **Debug** :
|
||||||
- Créer un test qui charge le module via `ModuleLoader`
|
- Ajouter les debug shapes (lignes, rectangles) si besoin
|
||||||
- Envoyer des sprites via IIO
|
|
||||||
- Vérifier le rendu
|
|
||||||
|
|
||||||
### Build & Test
|
### Build & Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build avec BgfxRenderer
|
# Windows (MinGW + Ninja)
|
||||||
cmake -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx
|
cmake -G Ninja -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx
|
||||||
cmake --build build-bgfx -j4
|
cmake --build build-bgfx -j4
|
||||||
|
|
||||||
# Tests RHI
|
# IMPORTANT: Sur Windows, ajouter MinGW au PATH pour ctest:
|
||||||
./build-bgfx/tests/test_20_bgfx_rhi
|
PATH="/c/ProgramData/mingw64/mingw64/bin:$PATH" ctest -R Bgfx --output-on-failure
|
||||||
|
|
||||||
# Test visuel triangle
|
# Tests actuels
|
||||||
./build-bgfx/tests/test_21_bgfx_triangle
|
./build-bgfx/tests/test_20_bgfx_rhi # 23 tests RHI
|
||||||
|
./build-bgfx/tests/test_21_bgfx_triangle # Test visuel triangle
|
||||||
|
./build-bgfx/tests/test_22_bgfx_sprites_headless # 5 tests IIO/structure
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes Importantes
|
## Notes Importantes
|
||||||
|
|
||||||
|
- **Cross-Platform** : Le projet compile sur Linux ET Windows
|
||||||
|
- **Windows PATH** : Les DLLs MinGW doivent être dans le PATH pour exécuter les tests via ctest
|
||||||
- **WSL2** : Le rendu fonctionne via Vulkan (pas OpenGL)
|
- **WSL2** : Le rendu fonctionne via Vulkan (pas OpenGL)
|
||||||
- **Shaders** : Pré-compilés, pas besoin de shaderc à runtime
|
- **Shaders** : Pré-compilés, pas besoin de shaderc à runtime
|
||||||
- **Thread Safety** : Voir `docs/coding_guidelines.md` pour les patterns mutex
|
- **Thread Safety** : Voir `docs/coding_guidelines.md` pour les patterns mutex
|
||||||
|
- **IIO Routing** : Les messages ne sont pas routés vers l'instance émettrice, utiliser deux instances séparées (pattern game → renderer)
|
||||||
## Commit Actuel
|
|
||||||
|
|
||||||
```
|
|
||||||
1443c12 feat(BgfxRenderer): Complete Phase 2-3 with shaders and triangle rendering
|
|
||||||
```
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <cstdint>
|
||||||
#include "IDataNode.h"
|
#include "IDataNode.h"
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|||||||
@ -3,10 +3,15 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include "IModule.h"
|
#include "IModule.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -242,7 +242,26 @@ void SceneCollector::parseCamera(const IDataNode& data) {
|
|||||||
m_mainView.viewportW = static_cast<uint16_t>(data.getInt("viewportW", 1280));
|
m_mainView.viewportW = static_cast<uint16_t>(data.getInt("viewportW", 1280));
|
||||||
m_mainView.viewportH = static_cast<uint16_t>(data.getInt("viewportH", 720));
|
m_mainView.viewportH = static_cast<uint16_t>(data.getInt("viewportH", 720));
|
||||||
|
|
||||||
// TODO: Compute view and projection matrices from camera params
|
// Compute view matrix (translation by -camera position)
|
||||||
|
std::memset(m_mainView.viewMatrix, 0, sizeof(m_mainView.viewMatrix));
|
||||||
|
m_mainView.viewMatrix[0] = 1.0f;
|
||||||
|
m_mainView.viewMatrix[5] = 1.0f;
|
||||||
|
m_mainView.viewMatrix[10] = 1.0f;
|
||||||
|
m_mainView.viewMatrix[12] = -m_mainView.positionX;
|
||||||
|
m_mainView.viewMatrix[13] = -m_mainView.positionY;
|
||||||
|
m_mainView.viewMatrix[15] = 1.0f;
|
||||||
|
|
||||||
|
// Compute orthographic projection matrix with zoom
|
||||||
|
float width = static_cast<float>(m_mainView.viewportW) / m_mainView.zoom;
|
||||||
|
float height = static_cast<float>(m_mainView.viewportH) / m_mainView.zoom;
|
||||||
|
|
||||||
|
std::memset(m_mainView.projMatrix, 0, sizeof(m_mainView.projMatrix));
|
||||||
|
m_mainView.projMatrix[0] = 2.0f / width;
|
||||||
|
m_mainView.projMatrix[5] = -2.0f / height; // Y-flip for top-left origin
|
||||||
|
m_mainView.projMatrix[10] = 1.0f;
|
||||||
|
m_mainView.projMatrix[12] = -1.0f;
|
||||||
|
m_mainView.projMatrix[13] = 1.0f;
|
||||||
|
m_mainView.projMatrix[15] = 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneCollector::parseClear(const IDataNode& data) {
|
void SceneCollector::parseClear(const IDataNode& data) {
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
#include <grove/ModuleFactory.h>
|
#include <grove/ModuleFactory.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <logger/Logger.h>
|
#include <logger/Logger.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
@ -152,7 +157,11 @@ void ModuleFactory::registerModule(const std::string& modulePath) {
|
|||||||
if (resolveSymbols(tempInfo)) {
|
if (resolveSymbols(tempInfo)) {
|
||||||
// Get the actual type from the module
|
// Get the actual type from the module
|
||||||
typedef const char* (*GetTypeFunc)();
|
typedef const char* (*GetTypeFunc)();
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto getTypeFunc = (GetTypeFunc)GetProcAddress(static_cast<HMODULE>(tempInfo.handle), "get_module_type");
|
||||||
|
#else
|
||||||
auto getTypeFunc = (GetTypeFunc)dlsym(tempInfo.handle, "get_module_type");
|
auto getTypeFunc = (GetTypeFunc)dlsym(tempInfo.handle, "get_module_type");
|
||||||
|
#endif
|
||||||
if (getTypeFunc) {
|
if (getTypeFunc) {
|
||||||
moduleType = getTypeFunc();
|
moduleType = getTypeFunc();
|
||||||
}
|
}
|
||||||
@ -375,6 +384,15 @@ std::shared_ptr<spdlog::logger> ModuleFactory::getFactoryLogger() {
|
|||||||
bool ModuleFactory::loadSharedLibrary(const std::string& path, ModuleInfo& info) {
|
bool ModuleFactory::loadSharedLibrary(const std::string& path, ModuleInfo& info) {
|
||||||
logger->trace("📚 Loading shared library: '{}'", path);
|
logger->trace("📚 Loading shared library: '{}'", path);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Load the shared library on Windows
|
||||||
|
info.handle = LoadLibraryA(path.c_str());
|
||||||
|
if (!info.handle) {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
logger->error("❌ LoadLibrary failed for '{}': error code {}", path, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
// Clear any existing error
|
// Clear any existing error
|
||||||
dlerror();
|
dlerror();
|
||||||
|
|
||||||
@ -385,6 +403,7 @@ bool ModuleFactory::loadSharedLibrary(const std::string& path, ModuleInfo& info)
|
|||||||
logger->error("❌ dlopen failed for '{}': {}", path, error ? error : "unknown error");
|
logger->error("❌ dlopen failed for '{}': {}", path, error ? error : "unknown error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
logger->trace("✅ Shared library loaded: '{}'", path);
|
logger->trace("✅ Shared library loaded: '{}'", path);
|
||||||
return true;
|
return true;
|
||||||
@ -394,11 +413,19 @@ void ModuleFactory::unloadSharedLibrary(ModuleInfo& info) {
|
|||||||
if (info.handle) {
|
if (info.handle) {
|
||||||
logger->trace("🗑️ Unloading shared library: '{}'", info.path);
|
logger->trace("🗑️ Unloading shared library: '{}'", info.path);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
BOOL result = FreeLibrary(static_cast<HMODULE>(info.handle));
|
||||||
|
if (!result) {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
logger->warn("⚠️ FreeLibrary warning for '{}': error code {}", info.path, error);
|
||||||
|
}
|
||||||
|
#else
|
||||||
int result = dlclose(info.handle);
|
int result = dlclose(info.handle);
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
const char* error = dlerror();
|
const char* error = dlerror();
|
||||||
logger->warn("⚠️ dlclose warning for '{}': {}", info.path, error ? error : "unknown error");
|
logger->warn("⚠️ dlclose warning for '{}': {}", info.path, error ? error : "unknown error");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
info.handle = nullptr;
|
info.handle = nullptr;
|
||||||
info.createFunc = nullptr;
|
info.createFunc = nullptr;
|
||||||
@ -409,41 +436,72 @@ void ModuleFactory::unloadSharedLibrary(ModuleInfo& info) {
|
|||||||
bool ModuleFactory::resolveSymbols(ModuleInfo& info) {
|
bool ModuleFactory::resolveSymbols(ModuleInfo& info) {
|
||||||
logger->trace("🔍 Resolving symbols for: '{}'", info.path);
|
logger->trace("🔍 Resolving symbols for: '{}'", info.path);
|
||||||
|
|
||||||
// Clear any existing error
|
|
||||||
dlerror();
|
|
||||||
|
|
||||||
// Resolve create_module function
|
// Resolve create_module function
|
||||||
typedef IModule* (*CreateFunc)();
|
typedef IModule* (*CreateFunc)();
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto createFunc = (CreateFunc)GetProcAddress(static_cast<HMODULE>(info.handle), "create_module");
|
||||||
|
if (!createFunc) {
|
||||||
|
logger->error("❌ Failed to resolve 'create_module': error code {}", GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
dlerror(); // Clear any existing error
|
||||||
auto createFunc = (CreateFunc)dlsym(info.handle, "create_module");
|
auto createFunc = (CreateFunc)dlsym(info.handle, "create_module");
|
||||||
const char* error = dlerror();
|
const char* error = dlerror();
|
||||||
if (error || !createFunc) {
|
if (error || !createFunc) {
|
||||||
logger->error("❌ Failed to resolve 'create_module': {}", error ? error : "symbol not found");
|
logger->error("❌ Failed to resolve 'create_module': {}", error ? error : "symbol not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
info.createFunc = createFunc;
|
info.createFunc = createFunc;
|
||||||
|
|
||||||
// Resolve destroy_module function
|
// Resolve destroy_module function
|
||||||
typedef void (*DestroyFunc)(IModule*);
|
typedef void (*DestroyFunc)(IModule*);
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto destroyFunc = (DestroyFunc)GetProcAddress(static_cast<HMODULE>(info.handle), "destroy_module");
|
||||||
|
if (!destroyFunc) {
|
||||||
|
logger->error("❌ Failed to resolve 'destroy_module': error code {}", GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
auto destroyFunc = (DestroyFunc)dlsym(info.handle, "destroy_module");
|
auto destroyFunc = (DestroyFunc)dlsym(info.handle, "destroy_module");
|
||||||
error = dlerror();
|
error = dlerror();
|
||||||
if (error || !destroyFunc) {
|
if (error || !destroyFunc) {
|
||||||
logger->error("❌ Failed to resolve 'destroy_module': {}", error ? error : "symbol not found");
|
logger->error("❌ Failed to resolve 'destroy_module': {}", error ? error : "symbol not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
info.destroyFunc = destroyFunc;
|
info.destroyFunc = destroyFunc;
|
||||||
|
|
||||||
// Resolve get_module_type function
|
// Resolve get_module_type function
|
||||||
typedef const char* (*GetTypeFunc)();
|
typedef const char* (*GetTypeFunc)();
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto getTypeFunc = (GetTypeFunc)GetProcAddress(static_cast<HMODULE>(info.handle), "get_module_type");
|
||||||
|
if (!getTypeFunc) {
|
||||||
|
logger->error("❌ Failed to resolve 'get_module_type': error code {}", GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
auto getTypeFunc = (GetTypeFunc)dlsym(info.handle, "get_module_type");
|
auto getTypeFunc = (GetTypeFunc)dlsym(info.handle, "get_module_type");
|
||||||
error = dlerror();
|
error = dlerror();
|
||||||
if (error || !getTypeFunc) {
|
if (error || !getTypeFunc) {
|
||||||
logger->error("❌ Failed to resolve 'get_module_type': {}", error ? error : "symbol not found");
|
logger->error("❌ Failed to resolve 'get_module_type': {}", error ? error : "symbol not found");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
info.type = getTypeFunc();
|
info.type = getTypeFunc();
|
||||||
|
|
||||||
// Resolve get_module_version function
|
// Resolve get_module_version function
|
||||||
typedef const char* (*GetVersionFunc)();
|
typedef const char* (*GetVersionFunc)();
|
||||||
|
#ifdef _WIN32
|
||||||
|
auto getVersionFunc = (GetVersionFunc)GetProcAddress(static_cast<HMODULE>(info.handle), "get_module_version");
|
||||||
|
if (!getVersionFunc) {
|
||||||
|
logger->warn("⚠️ Failed to resolve 'get_module_version': error code {}", GetLastError());
|
||||||
|
info.version = "unknown";
|
||||||
|
} else {
|
||||||
|
info.version = getVersionFunc();
|
||||||
|
}
|
||||||
|
#else
|
||||||
auto getVersionFunc = (GetVersionFunc)dlsym(info.handle, "get_module_version");
|
auto getVersionFunc = (GetVersionFunc)dlsym(info.handle, "get_module_version");
|
||||||
error = dlerror();
|
error = dlerror();
|
||||||
if (error || !getVersionFunc) {
|
if (error || !getVersionFunc) {
|
||||||
@ -452,6 +510,7 @@ bool ModuleFactory::resolveSymbols(ModuleInfo& info) {
|
|||||||
} else {
|
} else {
|
||||||
info.version = getVersionFunc();
|
info.version = getVersionFunc();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
logger->trace("✅ All symbols resolved for '{}' (type: '{}', version: '{}')",
|
logger->trace("✅ All symbols resolved for '{}' (type: '{}', version: '{}')",
|
||||||
info.path, info.type, info.version);
|
info.path, info.type, info.version);
|
||||||
|
|||||||
@ -3,11 +3,19 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <unistd.h>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <logger/Logger.h>
|
#include <logger/Logger.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#define PATH_SEPARATOR '\\'
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#define PATH_SEPARATOR '/'
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
ModuleLoader::ModuleLoader() {
|
ModuleLoader::ModuleLoader() {
|
||||||
@ -58,7 +66,7 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
if (isReload) {
|
if (isReload) {
|
||||||
// CRITICAL FIX: Wait for file to be fully written after compilation
|
// CRITICAL FIX: Wait for file to be fully written after compilation
|
||||||
// The FileWatcher may detect the change while the compiler is still writing
|
// The FileWatcher may detect the change while the compiler is still writing
|
||||||
logger->debug("⏳ Waiting for .so file to be fully written...");
|
logger->debug("⏳ Waiting for library file to be fully written...");
|
||||||
|
|
||||||
size_t lastSize = 0;
|
size_t lastSize = 0;
|
||||||
size_t stableCount = 0;
|
size_t stableCount = 0;
|
||||||
@ -88,7 +96,52 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unique temp filename
|
#ifdef _WIN32
|
||||||
|
// Windows: Create unique temp filename in temp directory
|
||||||
|
char tempDir[MAX_PATH];
|
||||||
|
if (GetTempPathA(MAX_PATH, tempDir) == 0) {
|
||||||
|
logger->warn("⚠️ Failed to get temp directory, loading directly");
|
||||||
|
} else {
|
||||||
|
char tempFile[MAX_PATH];
|
||||||
|
if (GetTempFileNameA(tempDir, "grv", 0, tempFile) == 0) {
|
||||||
|
logger->warn("⚠️ Failed to create temp file, loading directly");
|
||||||
|
} else {
|
||||||
|
// GetTempFileName creates the file, so we rename it to .dll
|
||||||
|
tempPath = std::string(tempFile) + ".dll";
|
||||||
|
DeleteFileA(tempFile); // Remove the original temp file
|
||||||
|
|
||||||
|
// Copy original .dll to temp location using std::filesystem
|
||||||
|
try {
|
||||||
|
std::filesystem::copy_file(path, tempPath,
|
||||||
|
std::filesystem::copy_options::overwrite_existing);
|
||||||
|
|
||||||
|
// CRITICAL FIX: Verify the copy succeeded completely
|
||||||
|
auto origSize = std::filesystem::file_size(path);
|
||||||
|
auto copiedSize = std::filesystem::file_size(tempPath);
|
||||||
|
|
||||||
|
if (copiedSize != origSize) {
|
||||||
|
logger->error("❌ Incomplete copy: orig={} bytes, copied={} bytes", origSize, copiedSize);
|
||||||
|
DeleteFileA(tempPath.c_str());
|
||||||
|
throw std::runtime_error("Incomplete file copy detected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (origSize == 0) {
|
||||||
|
logger->error("❌ Source file is empty!");
|
||||||
|
DeleteFileA(tempPath.c_str());
|
||||||
|
throw std::runtime_error("Source library file is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
actualPath = tempPath;
|
||||||
|
usedTempCopy = true;
|
||||||
|
logger->debug("🔄 Using temp copy for hot-reload: {} ({} bytes)", tempPath, copiedSize);
|
||||||
|
} catch (const std::filesystem::filesystem_error& e) {
|
||||||
|
logger->warn("⚠️ Failed to copy library ({}), loading directly", e.what());
|
||||||
|
DeleteFileA(tempPath.c_str()); // Clean up failed temp file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Linux/Unix: Create unique temp filename
|
||||||
char tempTemplate[] = "/tmp/grove_module_XXXXXX.so";
|
char tempTemplate[] = "/tmp/grove_module_XXXXXX.so";
|
||||||
int tempFd = mkstemps(tempTemplate, 3); // 3 for ".so"
|
int tempFd = mkstemps(tempTemplate, 3); // 3 for ".so"
|
||||||
if (tempFd == -1) {
|
if (tempFd == -1) {
|
||||||
@ -126,8 +179,25 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
unlink(tempPath.c_str()); // Clean up failed temp file
|
unlink(tempPath.c_str()); // Clean up failed temp file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open library
|
||||||
|
#ifdef _WIN32
|
||||||
|
libraryHandle = LoadLibraryA(actualPath.c_str());
|
||||||
|
if (!libraryHandle) {
|
||||||
|
DWORD errorCode = GetLastError();
|
||||||
|
std::string error = "LoadLibrary failed with error code " + std::to_string(errorCode);
|
||||||
|
|
||||||
|
// Clean up temp file if it was created
|
||||||
|
if (usedTempCopy) {
|
||||||
|
DeleteFileA(tempPath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
logLoadError(error);
|
||||||
|
throw std::runtime_error("Failed to load module: " + error);
|
||||||
|
}
|
||||||
|
#else
|
||||||
// Open library with RTLD_NOW (resolve all symbols immediately)
|
// Open library with RTLD_NOW (resolve all symbols immediately)
|
||||||
libraryHandle = dlopen(actualPath.c_str(), RTLD_NOW);
|
libraryHandle = dlopen(actualPath.c_str(), RTLD_NOW);
|
||||||
if (!libraryHandle) {
|
if (!libraryHandle) {
|
||||||
@ -141,6 +211,7 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
logLoadError(error);
|
logLoadError(error);
|
||||||
throw std::runtime_error("Failed to load module: " + error);
|
throw std::runtime_error("Failed to load module: " + error);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Store temp path for cleanup later
|
// Store temp path for cleanup later
|
||||||
if (usedTempCopy) {
|
if (usedTempCopy) {
|
||||||
@ -149,6 +220,17 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find createModule factory function
|
// Find createModule factory function
|
||||||
|
#ifdef _WIN32
|
||||||
|
createFunc = reinterpret_cast<CreateModuleFunc>(GetProcAddress(static_cast<HMODULE>(libraryHandle), "createModule"));
|
||||||
|
if (!createFunc) {
|
||||||
|
DWORD errorCode = GetLastError();
|
||||||
|
std::string error = "GetProcAddress failed with error code " + std::to_string(errorCode);
|
||||||
|
FreeLibrary(static_cast<HMODULE>(libraryHandle));
|
||||||
|
libraryHandle = nullptr;
|
||||||
|
logLoadError("createModule symbol not found: " + error);
|
||||||
|
throw std::runtime_error("Module missing createModule function: " + error);
|
||||||
|
}
|
||||||
|
#else
|
||||||
createFunc = reinterpret_cast<CreateModuleFunc>(dlsym(libraryHandle, "createModule"));
|
createFunc = reinterpret_cast<CreateModuleFunc>(dlsym(libraryHandle, "createModule"));
|
||||||
if (!createFunc) {
|
if (!createFunc) {
|
||||||
std::string error = dlerror();
|
std::string error = dlerror();
|
||||||
@ -157,11 +239,16 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
|||||||
logLoadError("createModule symbol not found: " + error);
|
logLoadError("createModule symbol not found: " + error);
|
||||||
throw std::runtime_error("Module missing createModule function: " + error);
|
throw std::runtime_error("Module missing createModule function: " + error);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Create module instance
|
// Create module instance
|
||||||
IModule* modulePtr = createFunc();
|
IModule* modulePtr = createFunc();
|
||||||
if (!modulePtr) {
|
if (!modulePtr) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
FreeLibrary(static_cast<HMODULE>(libraryHandle));
|
||||||
|
#else
|
||||||
dlclose(libraryHandle);
|
dlclose(libraryHandle);
|
||||||
|
#endif
|
||||||
libraryHandle = nullptr;
|
libraryHandle = nullptr;
|
||||||
createFunc = nullptr;
|
createFunc = nullptr;
|
||||||
logLoadError("createModule returned null");
|
logLoadError("createModule returned null");
|
||||||
@ -188,20 +275,36 @@ void ModuleLoader::unload() {
|
|||||||
logUnloadStart();
|
logUnloadStart();
|
||||||
|
|
||||||
// Close library
|
// Close library
|
||||||
|
#ifdef _WIN32
|
||||||
|
BOOL result = FreeLibrary(static_cast<HMODULE>(libraryHandle));
|
||||||
|
if (!result) {
|
||||||
|
DWORD errorCode = GetLastError();
|
||||||
|
logger->error("❌ FreeLibrary failed with error code: {}", errorCode);
|
||||||
|
}
|
||||||
|
#else
|
||||||
int result = dlclose(libraryHandle);
|
int result = dlclose(libraryHandle);
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
std::string error = dlerror();
|
std::string error = dlerror();
|
||||||
logger->error("❌ dlclose failed: {}", error);
|
logger->error("❌ dlclose failed: {}", error);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Clean up temp file if it was used
|
// Clean up temp file if it was used
|
||||||
if (!tempLibraryPath.empty()) {
|
if (!tempLibraryPath.empty()) {
|
||||||
logger->debug("🧹 Cleaning up temp file: {}", tempLibraryPath);
|
logger->debug("🧹 Cleaning up temp file: {}", tempLibraryPath);
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (DeleteFileA(tempLibraryPath.c_str())) {
|
||||||
|
logger->debug("✅ Temp file deleted");
|
||||||
|
} else {
|
||||||
|
logger->warn("⚠️ Failed to delete temp file: {}", tempLibraryPath);
|
||||||
|
}
|
||||||
|
#else
|
||||||
if (unlink(tempLibraryPath.c_str()) == 0) {
|
if (unlink(tempLibraryPath.c_str()) == 0) {
|
||||||
logger->debug("✅ Temp file deleted");
|
logger->debug("✅ Temp file deleted");
|
||||||
} else {
|
} else {
|
||||||
logger->warn("⚠️ Failed to delete temp file: {}", tempLibraryPath);
|
logger->warn("⚠️ Failed to delete temp file: {}", tempLibraryPath);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
tempLibraryPath.clear();
|
tempLibraryPath.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,28 @@
|
|||||||
#include "SystemUtils.h"
|
#include "SystemUtils.h"
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <dirent.h>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <glob.h>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <psapi.h>
|
||||||
|
#else
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <glob.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
size_t getCurrentMemoryUsage() {
|
size_t getCurrentMemoryUsage() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
PROCESS_MEMORY_COUNTERS pmc;
|
||||||
|
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
|
||||||
|
return pmc.WorkingSetSize;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
// Linux: /proc/self/status -> VmRSS
|
// Linux: /proc/self/status -> VmRSS
|
||||||
std::ifstream file("/proc/self/status");
|
std::ifstream file("/proc/self/status");
|
||||||
std::string line;
|
std::string line;
|
||||||
@ -23,9 +37,14 @@ size_t getCurrentMemoryUsage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
int getOpenFileDescriptors() {
|
int getOpenFileDescriptors() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows: Not easily available, return 0
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
// Linux: /proc/self/fd
|
// Linux: /proc/self/fd
|
||||||
int count = 0;
|
int count = 0;
|
||||||
DIR* dir = opendir("/proc/self/fd");
|
DIR* dir = opendir("/proc/self/fd");
|
||||||
@ -39,6 +58,7 @@ int getOpenFileDescriptors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return count - 2; // Exclude . and ..
|
return count - 2; // Exclude . and ..
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
float getCurrentCPUUsage() {
|
float getCurrentCPUUsage() {
|
||||||
@ -49,6 +69,35 @@ float getCurrentCPUUsage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int countTempFiles(const std::string& pattern) {
|
int countTempFiles(const std::string& pattern) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows: Use std::filesystem to count matching files
|
||||||
|
int count = 0;
|
||||||
|
try {
|
||||||
|
std::filesystem::path dirPath = std::filesystem::path(pattern).parent_path();
|
||||||
|
std::string filename = std::filesystem::path(pattern).filename().string();
|
||||||
|
|
||||||
|
// Simple pattern matching - just count files starting with the prefix
|
||||||
|
// This is a simplification; for full glob support, use a library
|
||||||
|
if (std::filesystem::exists(dirPath)) {
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(dirPath)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
std::string name = entry.path().filename().string();
|
||||||
|
// Check if name starts with the pattern prefix (before *)
|
||||||
|
size_t starPos = filename.find('*');
|
||||||
|
if (starPos != std::string::npos) {
|
||||||
|
std::string prefix = filename.substr(0, starPos);
|
||||||
|
if (name.find(prefix) == 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
#else
|
||||||
glob_t globResult;
|
glob_t globResult;
|
||||||
memset(&globResult, 0, sizeof(globResult));
|
memset(&globResult, 0, sizeof(globResult));
|
||||||
|
|
||||||
@ -63,9 +112,22 @@ int countTempFiles(const std::string& pattern) {
|
|||||||
globfree(&globResult);
|
globfree(&globResult);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
int getMappedLibraryCount() {
|
int getMappedLibraryCount() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows: Count loaded modules
|
||||||
|
HMODULE hMods[1024];
|
||||||
|
DWORD cbNeeded;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
if (EnumProcessModules(GetCurrentProcess(), hMods, sizeof(hMods), &cbNeeded)) {
|
||||||
|
count = cbNeeded / sizeof(HMODULE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
#else
|
||||||
// Count unique .so libraries in /proc/self/maps
|
// Count unique .so libraries in /proc/self/maps
|
||||||
std::ifstream file("/proc/self/maps");
|
std::ifstream file("/proc/self/maps");
|
||||||
std::string line;
|
std::string line;
|
||||||
@ -90,6 +152,7 @@ int getMappedLibraryCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace grove
|
} // namespace grove
|
||||||
|
|||||||
@ -5,6 +5,15 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <ctime>
|
||||||
|
// Windows doesn't have timespec in all cases, and uses st_mtime instead of st_mtim
|
||||||
|
struct FileTimeInfo {
|
||||||
|
time_t tv_sec;
|
||||||
|
long tv_nsec;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,22 +24,33 @@ namespace grove {
|
|||||||
*/
|
*/
|
||||||
class FileWatcher {
|
class FileWatcher {
|
||||||
private:
|
private:
|
||||||
|
#ifdef _WIN32
|
||||||
|
using TimeSpec = FileTimeInfo;
|
||||||
|
#else
|
||||||
|
using TimeSpec = timespec;
|
||||||
|
#endif
|
||||||
|
|
||||||
struct FileInfo {
|
struct FileInfo {
|
||||||
timespec lastModified;
|
TimeSpec lastModified;
|
||||||
bool exists;
|
bool exists;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unordered_map<std::string, FileInfo> watchedFiles;
|
std::unordered_map<std::string, FileInfo> watchedFiles;
|
||||||
|
|
||||||
timespec getModificationTime(const std::string& path) {
|
TimeSpec getModificationTime(const std::string& path) {
|
||||||
struct stat fileStat;
|
struct stat fileStat;
|
||||||
if (stat(path.c_str(), &fileStat) == 0) {
|
if (stat(path.c_str(), &fileStat) == 0) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows uses st_mtime (seconds only)
|
||||||
|
return {fileStat.st_mtime, 0};
|
||||||
|
#else
|
||||||
return fileStat.st_mtim;
|
return fileStat.st_mtim;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
return {0, 0};
|
return {0, 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool timesEqual(const timespec& a, const timespec& b) {
|
bool timesEqual(const TimeSpec& a, const TimeSpec& b) {
|
||||||
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
|
return a.tv_sec == b.tv_sec && a.tv_nsec == b.tv_nsec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +79,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileInfo& oldInfo = it->second;
|
FileInfo& oldInfo = it->second;
|
||||||
timespec currentMod = getModificationTime(path);
|
TimeSpec currentMod = getModificationTime(path);
|
||||||
bool currentExists = (currentMod.tv_sec != 0 || currentMod.tv_nsec != 0);
|
bool currentExists = (currentMod.tv_sec != 0 || currentMod.tv_nsec != 0);
|
||||||
|
|
||||||
// Check if existence changed
|
// Check if existence changed
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <grove/IModule.h>
|
#include <grove/IModule.h>
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace grove;
|
using namespace grove;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,13 +40,41 @@ public:
|
|||||||
|
|
||||||
~SimpleModuleLoader() {
|
~SimpleModuleLoader() {
|
||||||
if (handle) {
|
if (handle) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
FreeLibrary(static_cast<HMODULE>(handle));
|
||||||
|
#else
|
||||||
dlclose(handle);
|
dlclose(handle);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool load() {
|
bool load() {
|
||||||
std::cout << "\n[Loader] Loading module: " << modulePath << std::endl;
|
std::cout << "\n[Loader] Loading module: " << modulePath << std::endl;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
handle = LoadLibraryA(modulePath.c_str());
|
||||||
|
if (!handle) {
|
||||||
|
std::cerr << "[Loader] ERROR: Failed to load module: error code " << GetLastError() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load factory functions
|
||||||
|
createFn = (CreateModuleFn)GetProcAddress(static_cast<HMODULE>(handle), "createModule");
|
||||||
|
if (!createFn) {
|
||||||
|
std::cerr << "[Loader] ERROR: Cannot load createModule: error code " << GetLastError() << std::endl;
|
||||||
|
FreeLibrary(static_cast<HMODULE>(handle));
|
||||||
|
handle = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyFn = (DestroyModuleFn)GetProcAddress(static_cast<HMODULE>(handle), "destroyModule");
|
||||||
|
if (!destroyFn) {
|
||||||
|
std::cerr << "[Loader] ERROR: Cannot load destroyModule: error code " << GetLastError() << std::endl;
|
||||||
|
FreeLibrary(static_cast<HMODULE>(handle));
|
||||||
|
handle = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
handle = dlopen(modulePath.c_str(), RTLD_NOW | RTLD_LOCAL);
|
handle = dlopen(modulePath.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
std::cerr << "[Loader] ERROR: Failed to load module: " << dlerror() << std::endl;
|
std::cerr << "[Loader] ERROR: Failed to load module: " << dlerror() << std::endl;
|
||||||
@ -69,6 +102,7 @@ public:
|
|||||||
handle = nullptr;
|
handle = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
std::cout << "[Loader] ✅ Module loaded successfully" << std::endl;
|
std::cout << "[Loader] ✅ Module loaded successfully" << std::endl;
|
||||||
return true;
|
return true;
|
||||||
@ -77,7 +111,11 @@ public:
|
|||||||
void unload() {
|
void unload() {
|
||||||
if (handle) {
|
if (handle) {
|
||||||
std::cout << "[Loader] Unloading module..." << std::endl;
|
std::cout << "[Loader] Unloading module..." << std::endl;
|
||||||
|
#ifdef _WIN32
|
||||||
|
FreeLibrary(static_cast<HMODULE>(handle));
|
||||||
|
#else
|
||||||
dlclose(handle);
|
dlclose(handle);
|
||||||
|
#endif
|
||||||
handle = nullptr;
|
handle = nullptr;
|
||||||
createFn = nullptr;
|
createFn = nullptr;
|
||||||
destroyFn = nullptr;
|
destroyFn = nullptr;
|
||||||
|
|||||||
@ -91,7 +91,7 @@ void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleS
|
|||||||
module.reset();
|
module.reset();
|
||||||
|
|
||||||
// Reload the same .so file
|
// Reload the same .so file
|
||||||
auto newModule = loader.load(modulePath, "LeakTestModule", true);
|
auto newModule = loader.load(modulePath.string(), "LeakTestModule", true);
|
||||||
|
|
||||||
g_reloadCount++;
|
g_reloadCount++;
|
||||||
|
|
||||||
@ -225,7 +225,7 @@ int main() {
|
|||||||
|
|
||||||
// Load initial module
|
// Load initial module
|
||||||
try {
|
try {
|
||||||
auto module = loader.load(modulePath, "LeakTestModule", false);
|
auto module = loader.load(modulePath.string(), "LeakTestModule", false);
|
||||||
if (!module) {
|
if (!module) {
|
||||||
std::cerr << "❌ Failed to load LeakTestModule\n";
|
std::cerr << "❌ Failed to load LeakTestModule\n";
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@ -18,7 +18,11 @@
|
|||||||
#include "../helpers/TestAssertions.h"
|
#include "../helpers/TestAssertions.h"
|
||||||
#include "../helpers/TestReporter.h"
|
#include "../helpers/TestReporter.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
@ -28,6 +32,33 @@
|
|||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
// Cross-platform dlopen wrappers
|
||||||
|
#ifdef _WIN32
|
||||||
|
inline void* grove_dlopen(const char* path, int flags) {
|
||||||
|
(void)flags;
|
||||||
|
return LoadLibraryA(path);
|
||||||
|
}
|
||||||
|
inline void* grove_dlsym(void* handle, const char* symbol) {
|
||||||
|
return (void*)GetProcAddress((HMODULE)handle, symbol);
|
||||||
|
}
|
||||||
|
inline int grove_dlclose(void* handle) {
|
||||||
|
return FreeLibrary((HMODULE)handle) ? 0 : -1;
|
||||||
|
}
|
||||||
|
inline const char* grove_dlerror() {
|
||||||
|
static thread_local char buf[256];
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
#define RTLD_NOW 0
|
||||||
|
#define RTLD_LOCAL 0
|
||||||
|
#else
|
||||||
|
#define grove_dlopen dlopen
|
||||||
|
#define grove_dlsym dlsym
|
||||||
|
#define grove_dlclose dlclose
|
||||||
|
#define grove_dlerror dlerror
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace grove;
|
using namespace grove;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@ -69,23 +100,23 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* dlHandle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (!dlHandle) {
|
if (!dlHandle) {
|
||||||
logger_->error("Failed to load module {}: {}", name, dlerror());
|
logger_->error("Failed to load module {}: {}", name, grove_dlerror());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto createFunc = (IModule* (*)())dlsym(dlHandle, "createModule");
|
auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||||
if (!createFunc) {
|
if (!createFunc) {
|
||||||
logger_->error("Failed to find createModule in {}: {}", name, dlerror());
|
logger_->error("Failed to find createModule in {}: {}", name, grove_dlerror());
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IModule* instance = createFunc();
|
IModule* instance = createFunc();
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
logger_->error("createModule returned nullptr for {}", name);
|
logger_->error("createModule returned nullptr for {}", name);
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,14 +220,14 @@ public:
|
|||||||
auto& handle = it->second;
|
auto& handle = it->second;
|
||||||
handle.instance->shutdown();
|
handle.instance->shutdown();
|
||||||
|
|
||||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||||
if (destroyFunc) {
|
if (destroyFunc) {
|
||||||
destroyFunc(handle.instance);
|
destroyFunc(handle.instance);
|
||||||
} else {
|
} else {
|
||||||
delete handle.instance;
|
delete handle.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
dlclose(handle.dlHandle);
|
grove_dlclose(handle.dlHandle);
|
||||||
|
|
||||||
modules_.erase(it);
|
modules_.erase(it);
|
||||||
logger_->info("Unloaded {}", name);
|
logger_->info("Unloaded {}", name);
|
||||||
@ -280,32 +311,32 @@ private:
|
|||||||
|
|
||||||
// Destroy old instance
|
// Destroy old instance
|
||||||
handle.instance->shutdown();
|
handle.instance->shutdown();
|
||||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||||
if (destroyFunc) {
|
if (destroyFunc) {
|
||||||
destroyFunc(handle.instance);
|
destroyFunc(handle.instance);
|
||||||
} else {
|
} else {
|
||||||
delete handle.instance;
|
delete handle.instance;
|
||||||
}
|
}
|
||||||
dlclose(handle.dlHandle);
|
grove_dlclose(handle.dlHandle);
|
||||||
|
|
||||||
// Reload shared library
|
// Reload shared library
|
||||||
void* newHandle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
void* newHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (!newHandle) {
|
if (!newHandle) {
|
||||||
logger_->error("Failed to reload {}: {}", name, dlerror());
|
logger_->error("Failed to reload {}: {}", name, grove_dlerror());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto createFunc = (IModule* (*)())dlsym(newHandle, "createModule");
|
auto createFunc = (IModule* (*)())grove_dlsym(newHandle, "createModule");
|
||||||
if (!createFunc) {
|
if (!createFunc) {
|
||||||
logger_->error("Failed to find createModule in reloaded {}", name);
|
logger_->error("Failed to find createModule in reloaded {}", name);
|
||||||
dlclose(newHandle);
|
grove_dlclose(newHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IModule* newInstance = createFunc();
|
IModule* newInstance = createFunc();
|
||||||
if (!newInstance) {
|
if (!newInstance) {
|
||||||
logger_->error("createModule returned nullptr for reloaded {}", name);
|
logger_->error("createModule returned nullptr for reloaded {}", name);
|
||||||
dlclose(newHandle);
|
grove_dlclose(newHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,11 @@
|
|||||||
#include "../helpers/TestAssertions.h"
|
#include "../helpers/TestAssertions.h"
|
||||||
#include "../helpers/TestReporter.h"
|
#include "../helpers/TestReporter.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -29,6 +33,33 @@
|
|||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
// Cross-platform dlopen wrappers
|
||||||
|
#ifdef _WIN32
|
||||||
|
inline void* grove_dlopen(const char* path, int flags) {
|
||||||
|
(void)flags;
|
||||||
|
return LoadLibraryA(path);
|
||||||
|
}
|
||||||
|
inline void* grove_dlsym(void* handle, const char* symbol) {
|
||||||
|
return (void*)GetProcAddress((HMODULE)handle, symbol);
|
||||||
|
}
|
||||||
|
inline int grove_dlclose(void* handle) {
|
||||||
|
return FreeLibrary((HMODULE)handle) ? 0 : -1;
|
||||||
|
}
|
||||||
|
inline const char* grove_dlerror() {
|
||||||
|
static thread_local char buf[256];
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
#define RTLD_NOW 0
|
||||||
|
#define RTLD_LOCAL 0
|
||||||
|
#else
|
||||||
|
#define grove_dlopen dlopen
|
||||||
|
#define grove_dlsym dlsym
|
||||||
|
#define grove_dlclose dlclose
|
||||||
|
#define grove_dlerror dlerror
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace grove;
|
using namespace grove;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@ -73,23 +104,23 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* dlHandle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (!dlHandle) {
|
if (!dlHandle) {
|
||||||
logger_->error("Failed to load {}: {}", key, dlerror());
|
logger_->error("Failed to load {}: {}", key, grove_dlerror());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto createFunc = (IModule* (*)())dlsym(dlHandle, "createModule");
|
auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||||
if (!createFunc) {
|
if (!createFunc) {
|
||||||
logger_->error("Failed to find createModule in {}: {}", key, dlerror());
|
logger_->error("Failed to find createModule in {}: {}", key, grove_dlerror());
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IModule* instance = createFunc();
|
IModule* instance = createFunc();
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
logger_->error("createModule returned nullptr for {}", key);
|
logger_->error("createModule returned nullptr for {}", key);
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,14 +153,14 @@ public:
|
|||||||
auto& handle = it->second;
|
auto& handle = it->second;
|
||||||
if (handle.instance) {
|
if (handle.instance) {
|
||||||
handle.instance->shutdown();
|
handle.instance->shutdown();
|
||||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||||
if (destroyFunc) {
|
if (destroyFunc) {
|
||||||
destroyFunc(handle.instance);
|
destroyFunc(handle.instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handle.dlHandle) {
|
if (handle.dlHandle) {
|
||||||
dlclose(handle.dlHandle);
|
grove_dlclose(handle.dlHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
versions_.erase(it);
|
versions_.erase(it);
|
||||||
|
|||||||
@ -21,7 +21,11 @@
|
|||||||
#include "../helpers/TestAssertions.h"
|
#include "../helpers/TestAssertions.h"
|
||||||
#include "../helpers/TestReporter.h"
|
#include "../helpers/TestReporter.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
|
#endif
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@ -29,6 +33,33 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
|
// Cross-platform dlopen wrappers
|
||||||
|
#ifdef _WIN32
|
||||||
|
inline void* grove_dlopen(const char* path, int flags) {
|
||||||
|
(void)flags;
|
||||||
|
return LoadLibraryA(path);
|
||||||
|
}
|
||||||
|
inline void* grove_dlsym(void* handle, const char* symbol) {
|
||||||
|
return (void*)GetProcAddress((HMODULE)handle, symbol);
|
||||||
|
}
|
||||||
|
inline int grove_dlclose(void* handle) {
|
||||||
|
return FreeLibrary((HMODULE)handle) ? 0 : -1;
|
||||||
|
}
|
||||||
|
inline const char* grove_dlerror() {
|
||||||
|
static thread_local char buf[256];
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
#define RTLD_NOW 0
|
||||||
|
#define RTLD_LOCAL 0
|
||||||
|
#else
|
||||||
|
#define grove_dlopen dlopen
|
||||||
|
#define grove_dlsym dlsym
|
||||||
|
#define grove_dlclose dlclose
|
||||||
|
#define grove_dlerror dlerror
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace grove;
|
using namespace grove;
|
||||||
|
|
||||||
// Module handle for testing
|
// Module handle for testing
|
||||||
@ -56,23 +87,23 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* dlHandle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||||
if (!dlHandle) {
|
if (!dlHandle) {
|
||||||
std::cerr << "Failed to load module " << name << ": " << dlerror() << "\n";
|
std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto createFunc = (grove::IModule* (*)())dlsym(dlHandle, "createModule");
|
auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||||
if (!createFunc) {
|
if (!createFunc) {
|
||||||
std::cerr << "Failed to find createModule in " << name << ": " << dlerror() << "\n";
|
std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n";
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
grove::IModule* instance = createFunc();
|
grove::IModule* instance = createFunc();
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
std::cerr << "createModule returned nullptr for " << name << "\n";
|
std::cerr << "createModule returned nullptr for " << name << "\n";
|
||||||
dlclose(dlHandle);
|
grove_dlclose(dlHandle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +139,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (handle.dlHandle) {
|
if (handle.dlHandle) {
|
||||||
dlclose(handle.dlHandle);
|
grove_dlclose(handle.dlHandle);
|
||||||
handle.dlHandle = nullptr;
|
handle.dlHandle = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,22 @@
|
|||||||
* Test: BgfxRenderer Sprite Integration Test (Headless)
|
* Test: BgfxRenderer Sprite Integration Test (Headless)
|
||||||
*
|
*
|
||||||
* Tests the BgfxRendererModule data structures without actual rendering.
|
* Tests the BgfxRendererModule data structures without actual rendering.
|
||||||
* Validates: JsonDataNode for sprite data, FramePacket structures.
|
* Validates: JsonDataNode for sprite data, IIO message publishing.
|
||||||
|
*
|
||||||
|
* Note: SceneCollector/FramePacket tests are in the visual test that links with BgfxRenderer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <grove/JsonDataNode.h>
|
#include <grove/JsonDataNode.h>
|
||||||
|
#include <grove/IntraIOManager.h>
|
||||||
|
#include <grove/IntraIO.h>
|
||||||
|
|
||||||
#include <catch2/catch_test_macros.hpp>
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
using Catch::Matchers::WithinAbs;
|
||||||
|
|
||||||
TEST_CASE("SpriteInstance data layout", "[bgfx][unit]") {
|
TEST_CASE("SpriteInstance data layout", "[bgfx][unit]") {
|
||||||
// Test that SpriteInstance struct can be constructed from IIO message data
|
// Test that SpriteInstance struct can be constructed from IIO message data
|
||||||
|
|
||||||
@ -33,8 +40,87 @@ TEST_CASE("SpriteInstance data layout", "[bgfx][unit]") {
|
|||||||
REQUIRE(sprite->getDouble("y") == 200.0);
|
REQUIRE(sprite->getDouble("y") == 200.0);
|
||||||
REQUIRE(sprite->getDouble("scaleX") == 32.0);
|
REQUIRE(sprite->getDouble("scaleX") == 32.0);
|
||||||
REQUIRE(sprite->getDouble("scaleY") == 32.0);
|
REQUIRE(sprite->getDouble("scaleY") == 32.0);
|
||||||
REQUIRE(std::abs(sprite->getDouble("rotation") - 1.57f) < 0.01);
|
REQUIRE_THAT(sprite->getDouble("rotation"), WithinAbs(1.57, 0.01));
|
||||||
REQUIRE(sprite->getInt("color") == 0xFF0000FF);
|
REQUIRE(sprite->getInt("color") == 0xFF0000FF);
|
||||||
REQUIRE(sprite->getInt("textureId") == 5);
|
REQUIRE(sprite->getInt("textureId") == 5);
|
||||||
REQUIRE(sprite->getInt("layer") == 10);
|
REQUIRE(sprite->getInt("layer") == 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("IIO sprite message routing between modules", "[bgfx][integration]") {
|
||||||
|
// Use singleton IntraIOManager (same as IntraIO::publish uses)
|
||||||
|
auto& ioManager = grove::IntraIOManager::getInstance();
|
||||||
|
|
||||||
|
// Create two IO instances to simulate module communication
|
||||||
|
auto gameIO = ioManager.createInstance("test_game_module");
|
||||||
|
auto rendererIO = ioManager.createInstance("test_renderer_module");
|
||||||
|
|
||||||
|
// Renderer subscribes to render topics
|
||||||
|
rendererIO->subscribe("render:*");
|
||||||
|
|
||||||
|
// Game module publishes sprites via IIO
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
||||||
|
sprite->setDouble("x", 100.0 + i * 50);
|
||||||
|
sprite->setDouble("y", 200.0 + i * 30);
|
||||||
|
sprite->setDouble("scaleX", 32.0);
|
||||||
|
sprite->setDouble("scaleY", 32.0);
|
||||||
|
sprite->setInt("color", 0xFFFFFFFF);
|
||||||
|
sprite->setInt("layer", i);
|
||||||
|
|
||||||
|
std::unique_ptr<grove::IDataNode> spriteData = std::move(sprite);
|
||||||
|
gameIO->publish("render:sprite", std::move(spriteData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages should be routed to renderer
|
||||||
|
REQUIRE(rendererIO->hasMessages() == 3);
|
||||||
|
|
||||||
|
// Pull and verify first message
|
||||||
|
auto msg1 = rendererIO->pullMessage();
|
||||||
|
REQUIRE(msg1.topic == "render:sprite");
|
||||||
|
REQUIRE(msg1.data != nullptr);
|
||||||
|
REQUIRE_THAT(msg1.data->getDouble("x"), WithinAbs(100.0, 0.01));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
rendererIO->clearAllMessages();
|
||||||
|
ioManager.removeInstance("test_game_module");
|
||||||
|
ioManager.removeInstance("test_renderer_module");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Camera message structure", "[bgfx][unit]") {
|
||||||
|
auto camera = std::make_unique<grove::JsonDataNode>("camera");
|
||||||
|
camera->setDouble("x", 100.0);
|
||||||
|
camera->setDouble("y", 50.0);
|
||||||
|
camera->setDouble("zoom", 2.0);
|
||||||
|
camera->setInt("viewportX", 0);
|
||||||
|
camera->setInt("viewportY", 0);
|
||||||
|
camera->setInt("viewportW", 800);
|
||||||
|
camera->setInt("viewportH", 600);
|
||||||
|
|
||||||
|
REQUIRE(camera->getDouble("x") == 100.0);
|
||||||
|
REQUIRE(camera->getDouble("y") == 50.0);
|
||||||
|
REQUIRE(camera->getDouble("zoom") == 2.0);
|
||||||
|
REQUIRE(camera->getInt("viewportW") == 800);
|
||||||
|
REQUIRE(camera->getInt("viewportH") == 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Clear color message structure", "[bgfx][unit]") {
|
||||||
|
auto clear = std::make_unique<grove::JsonDataNode>("clear");
|
||||||
|
clear->setInt("color", 0x112233FF);
|
||||||
|
|
||||||
|
REQUIRE(clear->getInt("color") == 0x112233FF);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Debug line message structure", "[bgfx][unit]") {
|
||||||
|
auto line = std::make_unique<grove::JsonDataNode>("line");
|
||||||
|
line->setDouble("x1", 10.0);
|
||||||
|
line->setDouble("y1", 20.0);
|
||||||
|
line->setDouble("x2", 100.0);
|
||||||
|
line->setDouble("y2", 200.0);
|
||||||
|
line->setInt("color", 0xFF0000FF);
|
||||||
|
|
||||||
|
REQUIRE(line->getDouble("x1") == 10.0);
|
||||||
|
REQUIRE(line->getDouble("y1") == 20.0);
|
||||||
|
REQUIRE(line->getDouble("x2") == 100.0);
|
||||||
|
REQUIRE(line->getDouble("y2") == 200.0);
|
||||||
|
REQUIRE(line->getInt("color") == 0xFF0000FF);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user