- Fix texture state management in BgfxDevice: defer setTexture until submit() - Add transient instance buffer support for multi-batch rendering - Add ParticlePass with fire, smoke and sparkle particle systems - Load multiple textures from config (texture1..texture10) - Visual test now demonstrates multi-texture sprites and multi-particle effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
522 lines
21 KiB
C++
522 lines
21 KiB
C++
/**
|
|
* Test: BgfxRenderer Visual Sprites Test
|
|
*
|
|
* Tests the full BgfxRendererModule with sprites via IIO.
|
|
* This validates Phase 5: sprites rendered via the module pipeline.
|
|
*/
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <SDL2/SDL_syswm.h>
|
|
|
|
#include <grove/ModuleLoader.h>
|
|
#include <grove/IntraIOManager.h>
|
|
#include <grove/IntraIO.h>
|
|
#include <grove/JsonDataNode.h>
|
|
|
|
#include <iostream>
|
|
#include <cstdint>
|
|
#include <cmath>
|
|
|
|
int main(int argc, char* argv[]) {
|
|
std::cout << "========================================\n";
|
|
std::cout << "BgfxRenderer Visual Sprites Test\n";
|
|
std::cout << "========================================\n\n";
|
|
|
|
// Initialize SDL
|
|
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
|
std::cerr << "SDL_Init failed: " << SDL_GetError() << "\n";
|
|
return 1;
|
|
}
|
|
|
|
// Create window
|
|
int width = 800;
|
|
int height = 600;
|
|
|
|
SDL_Window* window = SDL_CreateWindow(
|
|
"BgfxRenderer Sprites Test - Press ESC to exit",
|
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
width, height,
|
|
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE
|
|
);
|
|
|
|
if (!window) {
|
|
std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << "\n";
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
|
|
// Get native window handle
|
|
SDL_SysWMinfo wmi;
|
|
SDL_VERSION(&wmi.version);
|
|
if (!SDL_GetWindowWMInfo(window, &wmi)) {
|
|
std::cerr << "SDL_GetWindowWMInfo failed: " << SDL_GetError() << "\n";
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
void* nativeWindowHandle = wmi.info.win.window;
|
|
void* nativeDisplayHandle = nullptr;
|
|
#else
|
|
void* nativeWindowHandle = (void*)(uintptr_t)wmi.info.x11.window;
|
|
void* nativeDisplayHandle = wmi.info.x11.display;
|
|
#endif
|
|
|
|
std::cout << "Window created: " << width << "x" << height << "\n";
|
|
|
|
std::cout << "Native handles ready\n";
|
|
|
|
// ========================================
|
|
// Setup GroveEngine systems
|
|
// ========================================
|
|
|
|
// Use singleton IntraIOManager for proper routing
|
|
auto& ioManager = grove::IntraIOManager::getInstance();
|
|
|
|
// Create two IO instances: one for "game" publishing, one for renderer
|
|
auto gameIO = ioManager.createInstance("game_module");
|
|
auto rendererIO = ioManager.createInstance("bgfx_renderer");
|
|
|
|
std::cout << "IIO Manager setup complete\n";
|
|
|
|
// Load BgfxRenderer module
|
|
grove::ModuleLoader loader;
|
|
|
|
// Find the module library (in modules/ subdirectory relative to build)
|
|
std::string modulePath = "../modules/libBgfxRenderer.so";
|
|
#ifdef _WIN32
|
|
modulePath = "../modules/BgfxRenderer.dll";
|
|
#endif
|
|
|
|
std::unique_ptr<grove::IModule> module;
|
|
try {
|
|
module = loader.load(modulePath, "bgfx_renderer");
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Failed to load BgfxRenderer module: " << e.what() << "\n";
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
|
|
if (!module) {
|
|
std::cerr << "Failed to load BgfxRenderer module from: " << modulePath << "\n";
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
return 1;
|
|
}
|
|
|
|
std::cout << "BgfxRenderer module loaded\n";
|
|
|
|
// Configure the module with native window handles
|
|
grove::JsonDataNode config("config");
|
|
config.setInt("windowWidth", width);
|
|
config.setInt("windowHeight", height);
|
|
config.setString("backend", "auto");
|
|
config.setBool("vsync", true);
|
|
config.setInt("maxSpritesPerBatch", 10000);
|
|
config.setInt("frameAllocatorSizeMB", 16);
|
|
// Pass native handles as double (can store 64-bit integers up to 2^53 without loss)
|
|
config.setDouble("nativeWindowHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeWindowHandle)));
|
|
config.setDouble("nativeDisplayHandle", static_cast<double>(reinterpret_cast<uintptr_t>(nativeDisplayHandle)));
|
|
|
|
// Load textures from assets folder
|
|
config.setString("defaultTexture", "../../assets/textures/1f440.png");
|
|
config.setString("texture1", "../../assets/textures/5oxaxt1vo2f91.jpg"); // Second texture (Multipla)
|
|
|
|
// Enable debug overlay
|
|
config.setBool("debugOverlay", true);
|
|
|
|
module->setConfiguration(config, rendererIO.get(), nullptr);
|
|
|
|
std::cout << "Module configured\n";
|
|
|
|
// ========================================
|
|
// Main loop
|
|
// ========================================
|
|
|
|
std::cout << "\n*** Rendering sprites via IIO ***\n";
|
|
std::cout << "Press ESC to exit or wait 10 seconds\n\n";
|
|
|
|
bool running = true;
|
|
uint32_t frameCount = 0;
|
|
Uint32 startTime = SDL_GetTicks();
|
|
const Uint32 testDuration = 10000; // 10 seconds
|
|
|
|
while (running) {
|
|
// Process SDL events
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
if (event.type == SDL_QUIT) {
|
|
running = false;
|
|
}
|
|
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {
|
|
running = false;
|
|
}
|
|
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
width = event.window.data1;
|
|
height = event.window.data2;
|
|
}
|
|
}
|
|
|
|
// Check timeout
|
|
Uint32 elapsed = SDL_GetTicks() - startTime;
|
|
if (elapsed > testDuration) {
|
|
running = false;
|
|
}
|
|
|
|
float time = elapsed / 1000.0f;
|
|
|
|
// ========================================
|
|
// Game module publishes render commands via IIO
|
|
// ========================================
|
|
|
|
// Send camera
|
|
{
|
|
auto camera = std::make_unique<grove::JsonDataNode>("camera");
|
|
camera->setDouble("x", 0.0);
|
|
camera->setDouble("y", 0.0);
|
|
camera->setDouble("zoom", 1.0);
|
|
camera->setInt("viewportX", 0);
|
|
camera->setInt("viewportY", 0);
|
|
camera->setInt("viewportW", width);
|
|
camera->setInt("viewportH", height);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(camera);
|
|
gameIO->publish("render:camera", std::move(data));
|
|
}
|
|
|
|
// Send clear color (changes over time)
|
|
{
|
|
auto clear = std::make_unique<grove::JsonDataNode>("clear");
|
|
uint8_t r = static_cast<uint8_t>(48 + 20 * std::sin(time * 0.5));
|
|
uint8_t g = static_cast<uint8_t>(48 + 20 * std::sin(time * 0.5 + 1.0f));
|
|
uint8_t b = static_cast<uint8_t>(80 + 30 * std::sin(time * 0.5 + 2.0f));
|
|
uint32_t clearColor = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
|
clear->setInt("color", static_cast<int>(clearColor));
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(clear);
|
|
gameIO->publish("render:clear", std::move(data));
|
|
}
|
|
|
|
// ========================================
|
|
// Test tilemap rendering (simple checkerboard)
|
|
// Must be sent every frame (like sprites)
|
|
// ========================================
|
|
{
|
|
auto tilemap = std::make_unique<grove::JsonDataNode>("tilemap");
|
|
tilemap->setDouble("x", 50.0);
|
|
tilemap->setDouble("y", 450.0);
|
|
tilemap->setInt("width", 20);
|
|
tilemap->setInt("height", 3);
|
|
tilemap->setInt("tileW", 32);
|
|
tilemap->setInt("tileH", 32);
|
|
tilemap->setInt("textureId", 0); // Uses default texture
|
|
|
|
// Checkerboard pattern: 1 = tile, 0 = empty
|
|
std::string tileData;
|
|
for (int row = 0; row < 3; ++row) {
|
|
for (int col = 0; col < 20; ++col) {
|
|
int tileIndex = ((row + col) % 2 == 0) ? 1 : 0;
|
|
if (!tileData.empty()) tileData += ",";
|
|
tileData += std::to_string(tileIndex);
|
|
}
|
|
}
|
|
tilemap->setString("tileData", tileData);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(tilemap);
|
|
gameIO->publish("render:tilemap", std::move(data));
|
|
}
|
|
|
|
// Send animated sprites in a circle
|
|
// Using different textureIds to test multi-texture batching
|
|
for (int i = 0; i < 5; ++i) {
|
|
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
|
|
|
// Animate position in a circle
|
|
float angle = time * 2.0f + i * (3.14159f * 2.0f / 5.0f);
|
|
float radius = 150.0f;
|
|
float centerX = width / 2.0f;
|
|
float centerY = height / 2.0f;
|
|
|
|
sprite->setDouble("x", centerX + std::cos(angle) * radius);
|
|
sprite->setDouble("y", centerY + std::sin(angle) * radius);
|
|
sprite->setDouble("scaleX", 50.0); // 50x50 pixel sprite
|
|
sprite->setDouble("scaleY", 50.0);
|
|
sprite->setDouble("rotation", angle);
|
|
sprite->setDouble("u0", 0.0);
|
|
sprite->setDouble("v0", 0.0);
|
|
sprite->setDouble("u1", 1.0);
|
|
sprite->setDouble("v1", 1.0);
|
|
|
|
// Different colors per sprite to verify rendering
|
|
uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF};
|
|
sprite->setInt("color", static_cast<int>(colors[i]));
|
|
|
|
// Alternate between textures: even = eye (id=1), odd = multipla (id=2)
|
|
sprite->setInt("textureId", (i % 2 == 0) ? 1 : 2);
|
|
sprite->setInt("layer", i);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
|
gameIO->publish("render:sprite", std::move(data));
|
|
}
|
|
|
|
// Add a center sprite (Multipla!)
|
|
{
|
|
auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
|
|
sprite->setDouble("x", width / 2.0f);
|
|
sprite->setDouble("y", height / 2.0f);
|
|
sprite->setDouble("scaleX", 120.0);
|
|
sprite->setDouble("scaleY", 80.0); // Wider than tall (car aspect ratio)
|
|
sprite->setDouble("rotation", -time * 0.5f);
|
|
sprite->setInt("color", 0xFFFFFFFF); // White (no tint)
|
|
sprite->setInt("textureId", 2); // Multipla texture
|
|
sprite->setInt("layer", 10);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(sprite);
|
|
gameIO->publish("render:sprite", std::move(data));
|
|
}
|
|
|
|
// ========================================
|
|
// Test particle rendering (fire-like effect)
|
|
// ========================================
|
|
for (int i = 0; i < 20; ++i) {
|
|
auto particle = std::make_unique<grove::JsonDataNode>("particle");
|
|
|
|
// Spawn particles rising from bottom center
|
|
float spawnX = width / 2.0f + (std::sin(time * 5.0f + i * 0.5f) * 50.0f);
|
|
float spawnY = height - 50.0f;
|
|
|
|
// Particles rise up with some horizontal drift
|
|
float particleAge = std::fmod(time * 2.0f + i * 0.2f, 2.0f); // 0-2 second cycle
|
|
float life = 1.0f - (particleAge / 2.0f); // 1.0 -> 0.0
|
|
|
|
float px = spawnX + std::sin(particleAge * 3.0f + i) * 30.0f;
|
|
float py = spawnY - particleAge * 100.0f; // Rise up
|
|
|
|
particle->setDouble("x", px);
|
|
particle->setDouble("y", py);
|
|
particle->setDouble("vx", 0.0);
|
|
particle->setDouble("vy", -50.0);
|
|
particle->setDouble("size", 15.0 + life * 20.0); // Shrink as they age
|
|
particle->setDouble("life", life);
|
|
|
|
// Fire colors: white -> yellow -> orange -> red based on life
|
|
uint8_t r, g, b;
|
|
if (life > 0.7f) {
|
|
// White/yellow core
|
|
r = 255; g = 255; b = static_cast<uint8_t>(200 * (life - 0.7f) / 0.3f);
|
|
} else if (life > 0.3f) {
|
|
// Orange
|
|
r = 255; g = static_cast<uint8_t>(100 + 155 * (life - 0.3f) / 0.4f); b = 0;
|
|
} else {
|
|
// Red fading out
|
|
r = static_cast<uint8_t>(200 + 55 * life / 0.3f); g = static_cast<uint8_t>(50 * life / 0.3f); b = 0;
|
|
}
|
|
uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
|
particle->setInt("color", static_cast<int>(color));
|
|
particle->setInt("textureId", 0);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(particle);
|
|
gameIO->publish("render:particle", std::move(data));
|
|
}
|
|
|
|
// ========================================
|
|
// Smoke particles (second particle system - alpha blending, gray colors)
|
|
// ========================================
|
|
float smokeSpawnX = width * 0.7f; // Right side
|
|
float smokeSpawnY = height * 0.8f;
|
|
|
|
for (int i = 0; i < 15; ++i) {
|
|
auto smoke = std::make_unique<grove::JsonDataNode>("particle");
|
|
|
|
// Smoke rises slower, drifts more
|
|
float smokeAge = std::fmod(time * 1.0f + i * 0.15f, 3.0f); // 3 second cycle
|
|
float life = 1.0f - (smokeAge / 3.0f);
|
|
|
|
float sx = smokeSpawnX + std::sin(smokeAge * 2.0f + i * 0.5f) * 50.0f;
|
|
float sy = smokeSpawnY - smokeAge * 60.0f; // Rise slower
|
|
|
|
smoke->setDouble("x", sx);
|
|
smoke->setDouble("y", sy);
|
|
smoke->setDouble("vx", std::sin(time + i) * 20.0f);
|
|
smoke->setDouble("vy", -30.0);
|
|
smoke->setDouble("size", 20.0 + (1.0f - life) * 40.0); // Grow as they age
|
|
smoke->setDouble("life", life);
|
|
|
|
// Smoke colors: dark gray -> light gray, fading alpha
|
|
uint8_t gray = static_cast<uint8_t>(80 + 80 * (1.0f - life)); // Gets lighter
|
|
uint8_t alpha = static_cast<uint8_t>(200 * life); // Fades out
|
|
uint32_t smokeColor = (gray << 24) | (gray << 16) | (gray << 8) | alpha;
|
|
smoke->setInt("color", static_cast<int>(smokeColor));
|
|
smoke->setInt("textureId", 0);
|
|
smoke->setInt("blendMode", 0); // Alpha blend (not additive)
|
|
|
|
std::unique_ptr<grove::IDataNode> smokeData = std::move(smoke);
|
|
gameIO->publish("render:particle", std::move(smokeData));
|
|
}
|
|
|
|
// ========================================
|
|
// Sparkle particles (third particle system - small, fast, bright)
|
|
// ========================================
|
|
float sparkleX = width * 0.5f;
|
|
float sparkleY = height * 0.3f;
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
auto sparkle = std::make_unique<grove::JsonDataNode>("particle");
|
|
|
|
float angle = (time * 3.0f + i * (3.14159f * 2.0f / 8.0f));
|
|
float radius = 50.0f + std::sin(time * 5.0f + i) * 20.0f;
|
|
float sparkleAge = std::fmod(time * 4.0f + i * 0.1f, 1.0f);
|
|
float life = 1.0f - sparkleAge;
|
|
|
|
float spx = sparkleX + std::cos(angle) * radius;
|
|
float spy = sparkleY + std::sin(angle) * radius * 0.5f;
|
|
|
|
sparkle->setDouble("x", spx);
|
|
sparkle->setDouble("y", spy);
|
|
sparkle->setDouble("vx", 0.0);
|
|
sparkle->setDouble("vy", 0.0);
|
|
sparkle->setDouble("size", 5.0 + life * 10.0);
|
|
sparkle->setDouble("life", life);
|
|
|
|
// Sparkle colors: cyan -> white -> magenta cycle
|
|
float hue = std::fmod(time * 0.5f + i * 0.1f, 1.0f);
|
|
uint8_t r, g, b;
|
|
if (hue < 0.33f) {
|
|
r = static_cast<uint8_t>(255 * (1.0f - hue * 3.0f));
|
|
g = 255;
|
|
b = static_cast<uint8_t>(255 * hue * 3.0f);
|
|
} else if (hue < 0.66f) {
|
|
r = static_cast<uint8_t>(255 * (hue - 0.33f) * 3.0f);
|
|
g = static_cast<uint8_t>(255 * (1.0f - (hue - 0.33f) * 3.0f));
|
|
b = 255;
|
|
} else {
|
|
r = 255;
|
|
g = static_cast<uint8_t>(255 * (hue - 0.66f) * 3.0f);
|
|
b = static_cast<uint8_t>(255 * (1.0f - (hue - 0.66f) * 3.0f));
|
|
}
|
|
uint32_t sparkleColor = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
|
sparkle->setInt("color", static_cast<int>(sparkleColor));
|
|
sparkle->setInt("textureId", 0);
|
|
|
|
std::unique_ptr<grove::IDataNode> sparkleData = std::move(sparkle);
|
|
gameIO->publish("render:particle", std::move(sparkleData));
|
|
}
|
|
|
|
// ========================================
|
|
// Test text rendering
|
|
// ========================================
|
|
|
|
// Title text (large, white)
|
|
{
|
|
auto text = std::make_unique<grove::JsonDataNode>("text");
|
|
text->setDouble("x", 10.0);
|
|
text->setDouble("y", 10.0);
|
|
text->setString("text", "GroveEngine - Text Rendering Test");
|
|
text->setInt("fontSize", 16);
|
|
text->setInt("color", static_cast<int>(0xFFFFFFFF));
|
|
text->setInt("layer", 100);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(text);
|
|
gameIO->publish("render:text", std::move(data));
|
|
}
|
|
|
|
// Frame counter (yellow)
|
|
{
|
|
auto text = std::make_unique<grove::JsonDataNode>("text");
|
|
text->setDouble("x", 10.0);
|
|
text->setDouble("y", 30.0);
|
|
text->setString("text", "Frame: " + std::to_string(frameCount));
|
|
text->setInt("fontSize", 8);
|
|
text->setInt("color", static_cast<int>(0xFFFF00FF)); // Yellow
|
|
text->setInt("layer", 100);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(text);
|
|
gameIO->publish("render:text", std::move(data));
|
|
}
|
|
|
|
// Instructions (green)
|
|
{
|
|
auto text = std::make_unique<grove::JsonDataNode>("text");
|
|
text->setDouble("x", 10.0);
|
|
text->setDouble("y", height - 20.0);
|
|
text->setString("text", "Press ESC to exit");
|
|
text->setInt("fontSize", 8);
|
|
text->setInt("color", static_cast<int>(0x00FF00FF)); // Green
|
|
text->setInt("layer", 100);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(text);
|
|
gameIO->publish("render:text", std::move(data));
|
|
}
|
|
|
|
// Center animated text
|
|
{
|
|
auto text = std::make_unique<grove::JsonDataNode>("text");
|
|
float textX = width / 2.0f - 100.0f + std::sin(time * 2.0f) * 50.0f;
|
|
float textY = height / 2.0f + 80.0f;
|
|
text->setDouble("x", textX);
|
|
text->setDouble("y", textY);
|
|
text->setString("text", "Hello, World!");
|
|
text->setInt("fontSize", 24);
|
|
|
|
// Cycle through colors
|
|
uint8_t r = static_cast<uint8_t>(128 + 127 * std::sin(time * 3.0f));
|
|
uint8_t g = static_cast<uint8_t>(128 + 127 * std::sin(time * 3.0f + 2.0f));
|
|
uint8_t b = static_cast<uint8_t>(128 + 127 * std::sin(time * 3.0f + 4.0f));
|
|
uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
|
|
text->setInt("color", static_cast<int>(color));
|
|
text->setInt("layer", 100);
|
|
|
|
std::unique_ptr<grove::IDataNode> data = std::move(text);
|
|
gameIO->publish("render:text", std::move(data));
|
|
}
|
|
|
|
// ========================================
|
|
// Process frame (renderer pulls IIO messages)
|
|
// ========================================
|
|
|
|
grove::JsonDataNode input("input");
|
|
input.setDouble("deltaTime", 1.0 / 60.0);
|
|
input.setInt("frameNumber", static_cast<int>(frameCount));
|
|
input.setInt("windowWidth", width);
|
|
input.setInt("windowHeight", height);
|
|
|
|
module->process(input);
|
|
frameCount++;
|
|
|
|
// Log progress every second
|
|
if (frameCount % 60 == 0) {
|
|
std::cout << "Frame " << frameCount << " - " << (elapsed / 1000.0f) << "s\n";
|
|
}
|
|
}
|
|
|
|
float elapsedSec = (SDL_GetTicks() - startTime) / 1000.0f;
|
|
float fps = frameCount / elapsedSec;
|
|
|
|
std::cout << "\nTest completed!\n";
|
|
std::cout << " Frames: " << frameCount << "\n";
|
|
std::cout << " Time: " << elapsedSec << "s\n";
|
|
std::cout << " FPS: " << fps << "\n";
|
|
|
|
// ========================================
|
|
// Cleanup
|
|
// ========================================
|
|
|
|
module->shutdown();
|
|
loader.unload();
|
|
|
|
// Remove IIO instances
|
|
ioManager.removeInstance("game_module");
|
|
ioManager.removeInstance("bgfx_renderer");
|
|
|
|
SDL_DestroyWindow(window);
|
|
SDL_Quit();
|
|
|
|
std::cout << "\n========================================\n";
|
|
std::cout << "PASS: Sprites rendered via IIO!\n";
|
|
std::cout << "========================================\n";
|
|
|
|
return 0;
|
|
}
|