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.
|
||||
|
||||
## É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 ✅
|
||||
|
||||
**Phase 1** - Squelette du module
|
||||
- `libBgfxRenderer.so` compilé et chargeable dynamiquement
|
||||
- `libBgfxRenderer.so/.dll` compilé et chargeable dynamiquement
|
||||
|
||||
**Phase 2** - RHI Layer
|
||||
- `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
|
||||
- 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
|
||||
|
||||
```
|
||||
modules/BgfxRenderer/
|
||||
├── BgfxRendererModule.cpp # Point d'entrée module
|
||||
├── BgfxRendererModule.cpp # Point d'entrée module + ShaderManager
|
||||
├── RHI/
|
||||
│ ├── RHIDevice.h # Interface abstraite
|
||||
│ ├── BgfxDevice.cpp # Implémentation bgfx
|
||||
@ -41,59 +58,56 @@ modules/BgfxRenderer/
|
||||
│ └── RenderGraph.cpp # Tri topologique passes
|
||||
├── Passes/
|
||||
│ ├── ClearPass.cpp # Clear screen
|
||||
│ ├── SpritePass.cpp # Rendu sprites (à compléter)
|
||||
│ ├── SpritePass.cpp # Rendu sprites instancié
|
||||
│ └── DebugPass.cpp # Debug shapes
|
||||
├── Frame/
|
||||
│ ├── FramePacket.h # Données immutables par frame
|
||||
│ └── FrameAllocator.cpp # Allocateur bump
|
||||
└── Scene/
|
||||
└── SceneCollector.cpp # Collecte messages IIO
|
||||
└── SceneCollector.cpp # Collecte messages IIO + matrices view/proj
|
||||
```
|
||||
|
||||
## Prochaine Phase : Phase 4
|
||||
## Prochaine Phase : Phase 5
|
||||
|
||||
### Objectif
|
||||
Intégrer le ShaderManager dans le module principal et rendre le SpritePass fonctionnel.
|
||||
Test visuel complet avec sprites via IIO.
|
||||
|
||||
### Tâches
|
||||
|
||||
1. **Mettre à jour BgfxRendererModule.cpp** :
|
||||
- Ajouter `ShaderManager` comme membre
|
||||
- Initialiser les shaders dans `setConfiguration()`
|
||||
- Passer le program aux passes
|
||||
1. **Créer test_23_bgfx_sprites_visual.cpp** :
|
||||
- Charger le module BgfxRenderer via ModuleLoader
|
||||
- Publier des sprites via IIO depuis un "game module" simulé
|
||||
- Valider le rendu visuel (sprites affichés à l'écran)
|
||||
|
||||
2. **Compléter SpritePass.cpp** :
|
||||
- Utiliser le shader "sprite" du ShaderManager
|
||||
- Implémenter l'update du instance buffer avec les données FramePacket
|
||||
- Soumettre les draw calls instancés
|
||||
2. **Compléter la boucle render** :
|
||||
- Appeler `SceneCollector::collect()` pour récupérer les messages IIO
|
||||
- Passer le `FramePacket` finalisé aux passes
|
||||
- S'assurer que `SpritePass::execute()` dessine les sprites
|
||||
|
||||
3. **Test d'intégration** :
|
||||
- Créer un test qui charge le module via `ModuleLoader`
|
||||
- Envoyer des sprites via IIO
|
||||
- Vérifier le rendu
|
||||
3. **Debug** :
|
||||
- Ajouter les debug shapes (lignes, rectangles) si besoin
|
||||
|
||||
### Build & Test
|
||||
|
||||
```bash
|
||||
# Build avec BgfxRenderer
|
||||
cmake -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx
|
||||
# Windows (MinGW + Ninja)
|
||||
cmake -G Ninja -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx
|
||||
cmake --build build-bgfx -j4
|
||||
|
||||
# Tests RHI
|
||||
./build-bgfx/tests/test_20_bgfx_rhi
|
||||
# IMPORTANT: Sur Windows, ajouter MinGW au PATH pour ctest:
|
||||
PATH="/c/ProgramData/mingw64/mingw64/bin:$PATH" ctest -R Bgfx --output-on-failure
|
||||
|
||||
# Test visuel triangle
|
||||
./build-bgfx/tests/test_21_bgfx_triangle
|
||||
# Tests actuels
|
||||
./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
|
||||
|
||||
- **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)
|
||||
- **Shaders** : Pré-compilés, pas besoin de shaderc à runtime
|
||||
- **Thread Safety** : Voir `docs/coding_guidelines.md` pour les patterns mutex
|
||||
|
||||
## Commit Actuel
|
||||
|
||||
```
|
||||
1443c12 feat(BgfxRenderer): Complete Phase 2-3 with shaders and triangle rendering
|
||||
```
|
||||
- **IIO Routing** : Les messages ne sont pas routés vers l'instance émettrice, utiliser deux instances séparées (pattern game → renderer)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include "IDataNode.h"
|
||||
|
||||
namespace grove {
|
||||
|
||||
@ -3,10 +3,15 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <dlfcn.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include "IModule.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
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.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) {
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
#include <grove/ModuleFactory.h>
|
||||
#include <filesystem>
|
||||
#include <dlfcn.h>
|
||||
#include <algorithm>
|
||||
#include <logger/Logger.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace grove {
|
||||
@ -152,7 +157,11 @@ void ModuleFactory::registerModule(const std::string& modulePath) {
|
||||
if (resolveSymbols(tempInfo)) {
|
||||
// Get the actual type from the module
|
||||
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");
|
||||
#endif
|
||||
if (getTypeFunc) {
|
||||
moduleType = getTypeFunc();
|
||||
}
|
||||
@ -375,6 +384,15 @@ std::shared_ptr<spdlog::logger> ModuleFactory::getFactoryLogger() {
|
||||
bool ModuleFactory::loadSharedLibrary(const std::string& path, ModuleInfo& info) {
|
||||
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
|
||||
dlerror();
|
||||
|
||||
@ -385,6 +403,7 @@ bool ModuleFactory::loadSharedLibrary(const std::string& path, ModuleInfo& info)
|
||||
logger->error("❌ dlopen failed for '{}': {}", path, error ? error : "unknown error");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
logger->trace("✅ Shared library loaded: '{}'", path);
|
||||
return true;
|
||||
@ -394,11 +413,19 @@ void ModuleFactory::unloadSharedLibrary(ModuleInfo& info) {
|
||||
if (info.handle) {
|
||||
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);
|
||||
if (result != 0) {
|
||||
const char* error = dlerror();
|
||||
logger->warn("⚠️ dlclose warning for '{}': {}", info.path, error ? error : "unknown error");
|
||||
}
|
||||
#endif
|
||||
|
||||
info.handle = nullptr;
|
||||
info.createFunc = nullptr;
|
||||
@ -409,41 +436,72 @@ void ModuleFactory::unloadSharedLibrary(ModuleInfo& info) {
|
||||
bool ModuleFactory::resolveSymbols(ModuleInfo& info) {
|
||||
logger->trace("🔍 Resolving symbols for: '{}'", info.path);
|
||||
|
||||
// Clear any existing error
|
||||
dlerror();
|
||||
|
||||
// Resolve create_module function
|
||||
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");
|
||||
const char* error = dlerror();
|
||||
if (error || !createFunc) {
|
||||
logger->error("❌ Failed to resolve 'create_module': {}", error ? error : "symbol not found");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
info.createFunc = createFunc;
|
||||
|
||||
// Resolve destroy_module function
|
||||
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");
|
||||
error = dlerror();
|
||||
if (error || !destroyFunc) {
|
||||
logger->error("❌ Failed to resolve 'destroy_module': {}", error ? error : "symbol not found");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
info.destroyFunc = destroyFunc;
|
||||
|
||||
// Resolve get_module_type function
|
||||
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");
|
||||
error = dlerror();
|
||||
if (error || !getTypeFunc) {
|
||||
logger->error("❌ Failed to resolve 'get_module_type': {}", error ? error : "symbol not found");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
info.type = getTypeFunc();
|
||||
|
||||
// Resolve get_module_version function
|
||||
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");
|
||||
error = dlerror();
|
||||
if (error || !getVersionFunc) {
|
||||
@ -452,6 +510,7 @@ bool ModuleFactory::resolveSymbols(ModuleInfo& info) {
|
||||
} else {
|
||||
info.version = getVersionFunc();
|
||||
}
|
||||
#endif
|
||||
|
||||
logger->trace("✅ All symbols resolved for '{}' (type: '{}', version: '{}')",
|
||||
info.path, info.type, info.version);
|
||||
|
||||
@ -3,11 +3,19 @@
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#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 {
|
||||
|
||||
ModuleLoader::ModuleLoader() {
|
||||
@ -58,7 +66,7 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
||||
if (isReload) {
|
||||
// CRITICAL FIX: Wait for file to be fully written after compilation
|
||||
// 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 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";
|
||||
int tempFd = mkstemps(tempTemplate, 3); // 3 for ".so"
|
||||
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
|
||||
}
|
||||
}
|
||||
#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)
|
||||
libraryHandle = dlopen(actualPath.c_str(), RTLD_NOW);
|
||||
if (!libraryHandle) {
|
||||
@ -141,6 +211,7 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
||||
logLoadError(error);
|
||||
throw std::runtime_error("Failed to load module: " + error);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Store temp path for cleanup later
|
||||
if (usedTempCopy) {
|
||||
@ -149,6 +220,17 @@ std::unique_ptr<IModule> ModuleLoader::load(const std::string& path, const std::
|
||||
}
|
||||
|
||||
// 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"));
|
||||
if (!createFunc) {
|
||||
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);
|
||||
throw std::runtime_error("Module missing createModule function: " + error);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create module instance
|
||||
IModule* modulePtr = createFunc();
|
||||
if (!modulePtr) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary(static_cast<HMODULE>(libraryHandle));
|
||||
#else
|
||||
dlclose(libraryHandle);
|
||||
#endif
|
||||
libraryHandle = nullptr;
|
||||
createFunc = nullptr;
|
||||
logLoadError("createModule returned null");
|
||||
@ -188,20 +275,36 @@ void ModuleLoader::unload() {
|
||||
logUnloadStart();
|
||||
|
||||
// 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);
|
||||
if (result != 0) {
|
||||
std::string error = dlerror();
|
||||
logger->error("❌ dlclose failed: {}", error);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Clean up temp file if it was used
|
||||
if (!tempLibraryPath.empty()) {
|
||||
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) {
|
||||
logger->debug("✅ Temp file deleted");
|
||||
} else {
|
||||
logger->warn("⚠️ Failed to delete temp file: {}", tempLibraryPath);
|
||||
}
|
||||
#endif
|
||||
tempLibraryPath.clear();
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
#include "SystemUtils.h"
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <dirent.h>
|
||||
#include <sstream>
|
||||
#include <glob.h>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#include <glob.h>
|
||||
#endif
|
||||
|
||||
namespace grove {
|
||||
|
||||
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
|
||||
std::ifstream file("/proc/self/status");
|
||||
std::string line;
|
||||
@ -23,9 +37,14 @@ size_t getCurrentMemoryUsage() {
|
||||
}
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int getOpenFileDescriptors() {
|
||||
#ifdef _WIN32
|
||||
// Windows: Not easily available, return 0
|
||||
return 0;
|
||||
#else
|
||||
// Linux: /proc/self/fd
|
||||
int count = 0;
|
||||
DIR* dir = opendir("/proc/self/fd");
|
||||
@ -39,6 +58,7 @@ int getOpenFileDescriptors() {
|
||||
}
|
||||
|
||||
return count - 2; // Exclude . and ..
|
||||
#endif
|
||||
}
|
||||
|
||||
float getCurrentCPUUsage() {
|
||||
@ -49,6 +69,35 @@ float getCurrentCPUUsage() {
|
||||
}
|
||||
|
||||
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;
|
||||
memset(&globResult, 0, sizeof(globResult));
|
||||
|
||||
@ -63,9 +112,22 @@ int countTempFiles(const std::string& pattern) {
|
||||
globfree(&globResult);
|
||||
|
||||
return count;
|
||||
#endif
|
||||
}
|
||||
|
||||
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
|
||||
std::ifstream file("/proc/self/maps");
|
||||
std::string line;
|
||||
@ -90,6 +152,7 @@ int getMappedLibraryCount() {
|
||||
}
|
||||
|
||||
return count;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace grove
|
||||
|
||||
@ -5,6 +5,15 @@
|
||||
#include <sys/stat.h>
|
||||
#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 {
|
||||
|
||||
/**
|
||||
@ -15,22 +24,33 @@ namespace grove {
|
||||
*/
|
||||
class FileWatcher {
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
using TimeSpec = FileTimeInfo;
|
||||
#else
|
||||
using TimeSpec = timespec;
|
||||
#endif
|
||||
|
||||
struct FileInfo {
|
||||
timespec lastModified;
|
||||
TimeSpec lastModified;
|
||||
bool exists;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, FileInfo> watchedFiles;
|
||||
|
||||
timespec getModificationTime(const std::string& path) {
|
||||
TimeSpec getModificationTime(const std::string& path) {
|
||||
struct stat fileStat;
|
||||
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;
|
||||
#endif
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -59,7 +79,7 @@ public:
|
||||
}
|
||||
|
||||
FileInfo& oldInfo = it->second;
|
||||
timespec currentMod = getModificationTime(path);
|
||||
TimeSpec currentMod = getModificationTime(path);
|
||||
bool currentExists = (currentMod.tv_sec != 0 || currentMod.tv_nsec != 0);
|
||||
|
||||
// Check if existence changed
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
#include <iostream>
|
||||
#include <dlfcn.h>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <grove/IModule.h>
|
||||
#include <grove/JsonDataNode.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
@ -35,13 +40,41 @@ public:
|
||||
|
||||
~SimpleModuleLoader() {
|
||||
if (handle) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary(static_cast<HMODULE>(handle));
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool load() {
|
||||
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);
|
||||
if (!handle) {
|
||||
std::cerr << "[Loader] ERROR: Failed to load module: " << dlerror() << std::endl;
|
||||
@ -69,6 +102,7 @@ public:
|
||||
handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::cout << "[Loader] ✅ Module loaded successfully" << std::endl;
|
||||
return true;
|
||||
@ -77,7 +111,11 @@ public:
|
||||
void unload() {
|
||||
if (handle) {
|
||||
std::cout << "[Loader] Unloading module..." << std::endl;
|
||||
#ifdef _WIN32
|
||||
FreeLibrary(static_cast<HMODULE>(handle));
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
handle = nullptr;
|
||||
createFn = nullptr;
|
||||
destroyFn = nullptr;
|
||||
|
||||
@ -91,7 +91,7 @@ void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleS
|
||||
module.reset();
|
||||
|
||||
// Reload the same .so file
|
||||
auto newModule = loader.load(modulePath, "LeakTestModule", true);
|
||||
auto newModule = loader.load(modulePath.string(), "LeakTestModule", true);
|
||||
|
||||
g_reloadCount++;
|
||||
|
||||
@ -225,7 +225,7 @@ int main() {
|
||||
|
||||
// Load initial module
|
||||
try {
|
||||
auto module = loader.load(modulePath, "LeakTestModule", false);
|
||||
auto module = loader.load(modulePath.string(), "LeakTestModule", false);
|
||||
if (!module) {
|
||||
std::cerr << "❌ Failed to load LeakTestModule\n";
|
||||
return 1;
|
||||
|
||||
@ -18,7 +18,11 @@
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
@ -28,6 +32,33 @@
|
||||
#include <spdlog/spdlog.h>
|
||||
#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 json = nlohmann::json;
|
||||
|
||||
@ -69,23 +100,23 @@ public:
|
||||
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) {
|
||||
logger_->error("Failed to load module {}: {}", name, dlerror());
|
||||
logger_->error("Failed to load module {}: {}", name, grove_dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (IModule* (*)())dlsym(dlHandle, "createModule");
|
||||
auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
logger_->error("Failed to find createModule in {}: {}", name, dlerror());
|
||||
dlclose(dlHandle);
|
||||
logger_->error("Failed to find createModule in {}: {}", name, grove_dlerror());
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
IModule* instance = createFunc();
|
||||
if (!instance) {
|
||||
logger_->error("createModule returned nullptr for {}", name);
|
||||
dlclose(dlHandle);
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -189,14 +220,14 @@ public:
|
||||
auto& handle = it->second;
|
||||
handle.instance->shutdown();
|
||||
|
||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
||||
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||
if (destroyFunc) {
|
||||
destroyFunc(handle.instance);
|
||||
} else {
|
||||
delete handle.instance;
|
||||
}
|
||||
|
||||
dlclose(handle.dlHandle);
|
||||
grove_dlclose(handle.dlHandle);
|
||||
|
||||
modules_.erase(it);
|
||||
logger_->info("Unloaded {}", name);
|
||||
@ -280,32 +311,32 @@ private:
|
||||
|
||||
// Destroy old instance
|
||||
handle.instance->shutdown();
|
||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
||||
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||
if (destroyFunc) {
|
||||
destroyFunc(handle.instance);
|
||||
} else {
|
||||
delete handle.instance;
|
||||
}
|
||||
dlclose(handle.dlHandle);
|
||||
grove_dlclose(handle.dlHandle);
|
||||
|
||||
// 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) {
|
||||
logger_->error("Failed to reload {}: {}", name, dlerror());
|
||||
logger_->error("Failed to reload {}: {}", name, grove_dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (IModule* (*)())dlsym(newHandle, "createModule");
|
||||
auto createFunc = (IModule* (*)())grove_dlsym(newHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
logger_->error("Failed to find createModule in reloaded {}", name);
|
||||
dlclose(newHandle);
|
||||
grove_dlclose(newHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
IModule* newInstance = createFunc();
|
||||
if (!newInstance) {
|
||||
logger_->error("createModule returned nullptr for reloaded {}", name);
|
||||
dlclose(newHandle);
|
||||
grove_dlclose(newHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,11 @@
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
@ -29,6 +33,33 @@
|
||||
#include <spdlog/spdlog.h>
|
||||
#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 json = nlohmann::json;
|
||||
|
||||
@ -73,23 +104,23 @@ public:
|
||||
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) {
|
||||
logger_->error("Failed to load {}: {}", key, dlerror());
|
||||
logger_->error("Failed to load {}: {}", key, grove_dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (IModule* (*)())dlsym(dlHandle, "createModule");
|
||||
auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
logger_->error("Failed to find createModule in {}: {}", key, dlerror());
|
||||
dlclose(dlHandle);
|
||||
logger_->error("Failed to find createModule in {}: {}", key, grove_dlerror());
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
IModule* instance = createFunc();
|
||||
if (!instance) {
|
||||
logger_->error("createModule returned nullptr for {}", key);
|
||||
dlclose(dlHandle);
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -122,14 +153,14 @@ public:
|
||||
auto& handle = it->second;
|
||||
if (handle.instance) {
|
||||
handle.instance->shutdown();
|
||||
auto destroyFunc = (void (*)(IModule*))dlsym(handle.dlHandle, "destroyModule");
|
||||
auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule");
|
||||
if (destroyFunc) {
|
||||
destroyFunc(handle.instance);
|
||||
}
|
||||
}
|
||||
|
||||
if (handle.dlHandle) {
|
||||
dlclose(handle.dlHandle);
|
||||
grove_dlclose(handle.dlHandle);
|
||||
}
|
||||
|
||||
versions_.erase(it);
|
||||
|
||||
@ -21,7 +21,11 @@
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
@ -29,6 +33,33 @@
|
||||
#include <vector>
|
||||
#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;
|
||||
|
||||
// Module handle for testing
|
||||
@ -56,23 +87,23 @@ public:
|
||||
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) {
|
||||
std::cerr << "Failed to load module " << name << ": " << dlerror() << "\n";
|
||||
std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (grove::IModule* (*)())dlsym(dlHandle, "createModule");
|
||||
auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
std::cerr << "Failed to find createModule in " << name << ": " << dlerror() << "\n";
|
||||
dlclose(dlHandle);
|
||||
std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n";
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
grove::IModule* instance = createFunc();
|
||||
if (!instance) {
|
||||
std::cerr << "createModule returned nullptr for " << name << "\n";
|
||||
dlclose(dlHandle);
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -108,7 +139,7 @@ public:
|
||||
}
|
||||
|
||||
if (handle.dlHandle) {
|
||||
dlclose(handle.dlHandle);
|
||||
grove_dlclose(handle.dlHandle);
|
||||
handle.dlHandle = nullptr;
|
||||
}
|
||||
|
||||
|
||||
@ -2,15 +2,22 @@
|
||||
* Test: BgfxRenderer Sprite Integration Test (Headless)
|
||||
*
|
||||
* 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/IntraIOManager.h>
|
||||
#include <grove/IntraIO.h>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
TEST_CASE("SpriteInstance data layout", "[bgfx][unit]") {
|
||||
// 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("scaleX") == 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("textureId") == 5);
|
||||
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