feat(BgfxRenderer): Phase 7-8 - Text, Tilemap, Multi-texture, Resize

Phase 7 - Text Rendering:
- Add BitmapFont with embedded 8x8 CP437 font (ASCII 32-126)
- Add TextPass for instanced glyph rendering
- Fix SceneCollector::parseText() to copy strings to FrameAllocator

Phase 8A - Multi-texture Support:
- Add numeric texture ID system in ResourceCache
- SpritePass sorts by textureId and batches per texture
- Flush batch on texture change for efficient rendering

Phase 8B - Tilemap Rendering:
- Add TilemapPass for grid-based tile rendering
- Support tileData as comma-separated string
- Tiles rendered as instanced quads

Window Resize:
- Handle window resize via process() input
- Call bgfx::reset() on size change

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-27 22:09:48 +08:00
parent 4932017244
commit 613283d75c
16 changed files with 1446 additions and 120 deletions

View File

@ -1,102 +1,112 @@
# GroveEngine - Session Successor Prompt # GroveEngine - Session Successor Prompt
## Contexte Rapide ## Context
GroveEngine is a C++17 hot-reload game engine with a 2D bgfx-based renderer module.
GroveEngine est un moteur de jeu C++17 avec hot-reload de modules. On développe le module **BgfxRenderer** pour le rendu 2D. ## Current State - BgfxRenderer (27 Nov 2025)
## État Actuel (27 Nov 2025) ### Completed Phases
### Phases Complétées ✅ | Phase | Feature | Status |
|-------|---------|--------|
**Phase 1-4** - Squelette, RHI, RenderGraph, ShaderManager | 5 | IIO Pipeline (messages → SceneCollector → RenderGraph) | Done |
- Tout fonctionne, voir commits précédents | 5.5 | Sprite shader with GPU instancing (80-byte SpriteInstance) | Done |
| 6 | Texture loading via stb_image | Done |
**Phase 5** - Pipeline IIO → Rendu ✅ (PARTIEL) | 6.5 | Debug overlay (FPS/stats via bgfx debug text) | Done |
- `test_23_bgfx_sprites_visual.cpp` : test complet SDL2 + IIO + Module | 7 | Text rendering with embedded 8x8 bitmap font | Done |
- Pipeline vérifié fonctionnel : | 8A | Multi-texture support (sorted batching by textureId) | Done |
- Module charge avec Vulkan (~500 FPS) | 8B | Tilemap rendering (TilemapPass with instanced tiles) | Done |
- IIO route les messages (sprites, camera, clear)
- SceneCollector collecte et crée FramePacket
- RenderGraph exécute les passes
- **MAIS** : Les sprites ne s'affichent PAS visuellement
### Problème à Résoudre
Le shader actuel (`vs_color`/`fs_color` de bgfx drawstress) est un shader position+couleur simple. Il ne supporte pas l'instancing nécessaire pour les sprites.
**Ce qui manque :**
1. Un shader sprite avec instancing qui lit les données d'instance (position, scale, rotation, color, UV)
2. Le vertex layout correct pour le quad (pos.xy, uv.xy)
3. L'instance layout correct pour SpriteInstance
### Fichiers Clés
### Key Files
``` ```
modules/BgfxRenderer/ modules/BgfxRenderer/
├── BgfxRendererModule.cpp # Main module entry
├── Shaders/ ├── Shaders/
│ ├── ShaderManager.cpp # Charge les shaders embedded │ ├── vs_sprite.sc, fs_sprite.sc # Instanced sprite shader
│ ├── vs_color.bin.h # Shader actuel (PAS d'instancing) │ ├── varying.def.sc # Shader inputs/outputs
│ └── fs_color.bin.h │ └── *.bin.h # Compiled shader bytecode
├── Passes/ ├── Passes/
│ └── SpritePass.cpp # Execute avec instance buffer │ ├── ClearPass.cpp # Clear framebuffer
├── Frame/ │ ├── TilemapPass.cpp # Tilemap grid rendering
│ └── FramePacket.h # SpriteInstance struct │ ├── SpritePass.cpp # Instanced sprite rendering (sorted by texture)
└── RHI/ │ ├── TextPass.cpp # Text rendering (glyph quads)
└── BgfxDevice.cpp # createBuffer avec VertexLayout │ └── DebugPass.cpp # Debug lines/shapes
├── Text/
│ └── BitmapFont.h/.cpp # Embedded 8x8 CP437-style font
├── Resources/
│ ├── TextureLoader.cpp # stb_image PNG/JPG loading
│ └── ResourceCache.cpp # Texture cache with numeric IDs
├── Scene/
│ └── SceneCollector.cpp # IIO message parsing (render:*)
├── Debug/
│ └── DebugOverlay.cpp # FPS/stats display
└── Frame/
└── FramePacket.h # SpriteInstance, TextCommand, TilemapChunk
``` ```
### Structure SpriteInstance (à matcher dans le shader) ### ResourceCache - Texture ID System
```cpp ```cpp
struct SpriteInstance { // Load texture and get numeric ID
float x, y; // Position uint16_t texId = resourceCache->loadTextureWithId(device, "path/to/image.png");
float scaleX, scaleY; // Scale
float rotation; // Rotation en radians // Get texture by ID (for sprite rendering)
float u0, v0, u1, v1; // UV coords rhi::TextureHandle tex = resourceCache->getTextureById(texId);
uint32_t color; // ABGR
uint16_t textureId; // ID texture
uint16_t layer; // Layer de tri
};
``` ```
## Prochaine Étape : Shader Sprite Instancing ### IIO Message Formats
### Option A : Shader BGFX natif (.sc) **Sprite** (`render:sprite`)
```cpp
{ "x": 100.0, "y": 50.0, "scaleX": 32.0, "scaleY": 32.0,
"rotation": 0.0, "u0": 0.0, "v0": 0.0, "u1": 1.0, "v1": 1.0,
"textureId": 1, "color": 0xFFFFFFFF, "layer": 0 }
```
Créer `vs_sprite.sc` et `fs_sprite.sc` avec : **Text** (`render:text`)
- Vertex input : position (vec2), uv (vec2) ```cpp
- Instance input : transform, color, uvRect { "x": 10.0, "y": 10.0, "text": "Hello",
- Compiler avec shaderc pour toutes les plateformes "fontSize": 16, "color": 0xFFFFFFFF, "layer": 100 }
```
### Option B : Simplifier temporairement **Tilemap** (`render:tilemap`)
```cpp
{ "x": 0.0, "y": 0.0, "width": 10, "height": 10,
"tileW": 16, "tileH": 16, "textureId": 0,
"tileData": "1,0,1,0,1,0,..." } // comma-separated tile indices
```
Dessiner chaque sprite comme un quad individuel sans instancing : ## Next Task: Phase 9 - Choose One
- Plus lent mais fonctionne avec le shader actuel
- Modifier SpritePass pour soumettre un draw par sprite ### Option A: Layer Sorting
- Currently sprites are sorted by textureId only
- Add proper layer sorting (render back-to-front)
- Sort key: `(layer << 16) | textureId` for efficient batching
### Option B: Dynamic Texture Loading via IIO
- `render:texture:load` message to load textures at runtime
- Returns textureId that can be used in sprites
- Useful for dynamically loaded assets
### Option C: Particle System
- ParticlePass for particle effects
- GPU instanced particles with lifetime/velocity
- FramePacket already has ParticleInstance struct
### Option D: Camera Features
- Zoom and pan support (already in ViewInfo)
- Screen shake effects
- Smooth camera following
### Build & Test ### Build & Test
```bash ```bash
# Build cd build-bgfx
cmake -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx cmake --build . -j4
cmake --build build-bgfx -j4 cd tests && ./test_23_bgfx_sprites_visual
# Tests
./build-bgfx/tests/test_21_bgfx_triangle # Triangle coloré ✅
./build-bgfx/tests/test_23_bgfx_sprites_visual # Pipeline OK, sprites invisibles
# Tous les tests
cd build-bgfx && ctest --output-on-failure
``` ```
## Notes Importantes ## Notes
- Shaders are pre-compiled (embedded in .bin.h)
- **Cross-Platform** : Linux + Windows (MinGW) - shaderc at: `build-bgfx/_deps/bgfx-build/cmake/bgfx/shaderc`
- **WSL2** : Vulkan fonctionne, pas OpenGL - All passes reuse sprite shader (same instancing layout)
- **Shaders embedded** : Pré-compilés dans .bin.h, pas de shaderc runtime - TilemapPass: tile index 0 = empty, 1+ = actual tiles
- **100% tests passent** : 20/20 - SpritePass: stable_sort by textureId preserves layer order within same texture
## Questions pour la prochaine session
1. Option A ou B pour les sprites ?
2. Priorité : voir quelque chose à l'écran vs architecture propre ?

View File

@ -8,7 +8,9 @@
#include "Resources/ResourceCache.h" #include "Resources/ResourceCache.h"
#include "Debug/DebugOverlay.h" #include "Debug/DebugOverlay.h"
#include "Passes/ClearPass.h" #include "Passes/ClearPass.h"
#include "Passes/TilemapPass.h"
#include "Passes/SpritePass.h" #include "Passes/SpritePass.h"
#include "Passes/TextPass.h"
#include "Passes/DebugPass.h" #include "Passes/DebugPass.h"
#include <grove/JsonDataNode.h> #include <grove/JsonDataNode.h>
@ -84,12 +86,26 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
m_renderGraph->addPass(std::make_unique<ClearPass>()); m_renderGraph->addPass(std::make_unique<ClearPass>());
m_logger->info("Added ClearPass"); m_logger->info("Added ClearPass");
// Setup resource cache first (needed by passes)
m_resourceCache = std::make_unique<ResourceCache>();
// Create TilemapPass (renders before sprites)
auto tilemapPass = std::make_unique<TilemapPass>(spriteShader);
tilemapPass->setResourceCache(m_resourceCache.get());
m_renderGraph->addPass(std::move(tilemapPass));
m_logger->info("Added TilemapPass");
// Create SpritePass and keep reference for texture binding // Create SpritePass and keep reference for texture binding
auto spritePass = std::make_unique<SpritePass>(spriteShader); auto spritePass = std::make_unique<SpritePass>(spriteShader);
m_spritePass = spritePass.get(); // Non-owning reference m_spritePass = spritePass.get(); // Non-owning reference
m_spritePass->setResourceCache(m_resourceCache.get());
m_renderGraph->addPass(std::move(spritePass)); m_renderGraph->addPass(std::move(spritePass));
m_logger->info("Added SpritePass"); m_logger->info("Added SpritePass");
// Create TextPass (uses sprite shader for glyph quads)
m_renderGraph->addPass(std::make_unique<TextPass>(spriteShader));
m_logger->info("Added TextPass");
m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader)); m_renderGraph->addPass(std::make_unique<DebugPass>(debugShader));
m_logger->info("Added DebugPass"); m_logger->info("Added DebugPass");
m_renderGraph->setup(*m_device); m_renderGraph->setup(*m_device);
@ -102,9 +118,6 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
m_sceneCollector->setup(io); m_sceneCollector->setup(io);
m_logger->info("SceneCollector setup complete"); m_logger->info("SceneCollector setup complete");
// Setup resource cache
m_resourceCache = std::make_unique<ResourceCache>();
// Setup debug overlay // Setup debug overlay
m_debugOverlay = std::make_unique<DebugOverlay>(); m_debugOverlay = std::make_unique<DebugOverlay>();
bool debugEnabled = config.getBool("debugOverlay", false); bool debugEnabled = config.getBool("debugOverlay", false);
@ -132,6 +145,17 @@ void BgfxRendererModule::process(const IDataNode& input) {
// Read deltaTime from input (provided by ModuleSystem) // Read deltaTime from input (provided by ModuleSystem)
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016)); float deltaTime = static_cast<float>(input.getDouble("deltaTime", 0.016));
// Check for resize in input
int newWidth = input.getInt("windowWidth", 0);
int newHeight = input.getInt("windowHeight", 0);
if (newWidth > 0 && newHeight > 0 &&
(static_cast<uint16_t>(newWidth) != m_width || static_cast<uint16_t>(newHeight) != m_height)) {
m_width = static_cast<uint16_t>(newWidth);
m_height = static_cast<uint16_t>(newHeight);
m_device->reset(m_width, m_height);
m_logger->info("Window resized to {}x{}", m_width, m_height);
}
// 1. Collect IIO messages (pull-based) // 1. Collect IIO messages (pull-based)
m_sceneCollector->collect(m_io, deltaTime); m_sceneCollector->collect(m_io, deltaTime);

View File

@ -49,9 +49,14 @@ add_library(BgfxRenderer SHARED
# Passes # Passes
Passes/ClearPass.cpp Passes/ClearPass.cpp
Passes/TilemapPass.cpp
Passes/SpritePass.cpp Passes/SpritePass.cpp
Passes/TextPass.cpp
Passes/DebugPass.cpp Passes/DebugPass.cpp
# Text
Text/BitmapFont.cpp
# Scene # Scene
Scene/SceneCollector.cpp Scene/SceneCollector.cpp

View File

@ -1,12 +1,15 @@
#include "SpritePass.h" #include "SpritePass.h"
#include "../RHI/RHIDevice.h" #include "../RHI/RHIDevice.h"
#include "../Frame/FramePacket.h" #include "../Frame/FramePacket.h"
#include "../Resources/ResourceCache.h"
#include <algorithm>
namespace grove { namespace grove {
SpritePass::SpritePass(rhi::ShaderHandle shader) SpritePass::SpritePass(rhi::ShaderHandle shader)
: m_shader(shader) : m_shader(shader)
{ {
m_sortedIndices.reserve(MAX_SPRITES_PER_BATCH);
} }
void SpritePass::setup(rhi::IRHIDevice& device) { void SpritePass::setup(rhi::IRHIDevice& device) {
@ -73,6 +76,18 @@ void SpritePass::shutdown(rhi::IRHIDevice& device) {
// Note: m_shader is owned by ShaderManager, not destroyed here // Note: m_shader is owned by ShaderManager, not destroyed here
} }
void SpritePass::flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
rhi::TextureHandle texture, uint32_t count) {
if (count == 0) return;
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, count);
cmd.setTexture(0, texture, m_textureSampler);
cmd.drawInstanced(6, count);
cmd.submit(0, m_shader, 0);
}
void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) { void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) {
if (frame.spriteCount == 0) { if (frame.spriteCount == 0) {
return; return;
@ -86,34 +101,71 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
state.depthWrite = false; state.depthWrite = false;
cmd.setState(state); cmd.setState(state);
// Process sprites in batches // Build sorted indices by textureId for batching
size_t remaining = frame.spriteCount; m_sortedIndices.clear();
size_t offset = 0; m_sortedIndices.reserve(frame.spriteCount);
for (size_t i = 0; i < frame.spriteCount; ++i) {
m_sortedIndices.push_back(static_cast<uint32_t>(i));
}
while (remaining > 0) { // Sort by textureId (stable sort to preserve layer order within same texture)
size_t batchSize = (remaining > MAX_SPRITES_PER_BATCH) std::stable_sort(m_sortedIndices.begin(), m_sortedIndices.end(),
? MAX_SPRITES_PER_BATCH : remaining; [&frame](uint32_t a, uint32_t b) {
return frame.sprites[a].textureId < frame.sprites[b].textureId;
});
// Update instance buffer with sprite data // Process sprites in batches by texture
// The SpriteInstance struct matches what we send to GPU std::vector<SpriteInstance> batchData;
const SpriteInstance* batchData = frame.sprites + offset; batchData.reserve(MAX_SPRITES_PER_BATCH);
device.updateBuffer(m_instanceBuffer, batchData,
static_cast<uint32_t>(batchSize * sizeof(SpriteInstance)));
cmd.setVertexBuffer(m_quadVB); uint16_t currentTextureId = UINT16_MAX;
cmd.setIndexBuffer(m_quadIB); rhi::TextureHandle currentTexture;
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(batchSize));
// Bind texture (use active texture if set, otherwise default white) for (uint32_t idx : m_sortedIndices) {
rhi::TextureHandle texToUse = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture; const SpriteInstance& sprite = frame.sprites[idx];
cmd.setTexture(0, texToUse, m_textureSampler); uint16_t texId = static_cast<uint16_t>(sprite.textureId);
// Submit draw call // Check if texture changed
cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad if (texId != currentTextureId) {
cmd.submit(0, m_shader, 0); // Flush previous batch
if (!batchData.empty()) {
device.updateBuffer(m_instanceBuffer, batchData.data(),
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
batchData.clear();
}
offset += batchSize; // Update current texture
remaining -= batchSize; currentTextureId = texId;
if (texId == 0 || !m_resourceCache) {
// Use default/active texture for textureId=0
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
} else {
// Look up texture by ID
currentTexture = m_resourceCache->getTextureById(texId);
if (!currentTexture.isValid()) {
currentTexture = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
}
}
}
// Add sprite to batch
batchData.push_back(sprite);
// Flush if batch is full
if (batchData.size() >= MAX_SPRITES_PER_BATCH) {
device.updateBuffer(m_instanceBuffer, batchData.data(),
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
batchData.clear();
}
}
// Flush remaining sprites
if (!batchData.empty()) {
device.updateBuffer(m_instanceBuffer, batchData.data(),
static_cast<uint32_t>(batchData.size() * sizeof(SpriteInstance)));
flushBatch(device, cmd, currentTexture, static_cast<uint32_t>(batchData.size()));
} }
} }

View File

@ -2,11 +2,14 @@
#include "../RenderGraph/RenderPass.h" #include "../RenderGraph/RenderPass.h"
#include "../RHI/RHITypes.h" #include "../RHI/RHITypes.h"
#include <vector>
namespace grove { namespace grove {
class ResourceCache;
// ============================================================================ // ============================================================================
// Sprite Pass - Renders 2D sprites with batching // Sprite Pass - Renders 2D sprites with batching by texture
// ============================================================================ // ============================================================================
class SpritePass : public RenderPass { class SpritePass : public RenderPass {
@ -26,21 +29,37 @@ public:
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override; void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
/** /**
* @brief Set a texture to use for all sprites (temporary API) * @brief Set resource cache for texture lookup by ID
* @param texture The texture handle to use (must be valid) */
* void setResourceCache(ResourceCache* cache) { m_resourceCache = cache; }
* TODO: Replace with proper texture array / per-sprite texture support
/**
* @brief Set fallback texture when textureId=0 or texture not found
*/
void setDefaultTexture(rhi::TextureHandle texture) { m_activeTexture = texture; }
/**
* @brief Legacy: Set a single texture for all sprites (backward compat)
* @deprecated Use setResourceCache for multi-texture support
*/ */
void setTexture(rhi::TextureHandle texture) { m_activeTexture = texture; } void setTexture(rhi::TextureHandle texture) { m_activeTexture = texture; }
private: private:
void flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
rhi::TextureHandle texture, uint32_t count);
rhi::ShaderHandle m_shader; rhi::ShaderHandle m_shader;
rhi::BufferHandle m_quadVB; rhi::BufferHandle m_quadVB;
rhi::BufferHandle m_quadIB; rhi::BufferHandle m_quadIB;
rhi::BufferHandle m_instanceBuffer; rhi::BufferHandle m_instanceBuffer;
rhi::UniformHandle m_textureSampler; rhi::UniformHandle m_textureSampler;
rhi::TextureHandle m_defaultTexture; // White 1x1 texture fallback rhi::TextureHandle m_defaultTexture; // White 1x1 texture fallback
rhi::TextureHandle m_activeTexture; // Currently active texture (if set) rhi::TextureHandle m_activeTexture; // Default texture for textureId=0
ResourceCache* m_resourceCache = nullptr;
// Sorted sprite indices for batching
std::vector<uint32_t> m_sortedIndices;
static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000; static constexpr uint32_t MAX_SPRITES_PER_BATCH = 10000;
}; };

View File

@ -0,0 +1,193 @@
#include "TextPass.h"
#include "../RHI/RHIDevice.h"
#include "../Frame/FramePacket.h"
namespace grove {
TextPass::TextPass(rhi::ShaderHandle shader)
: m_shader(shader)
{
m_glyphInstances.reserve(MAX_GLYPHS_PER_BATCH);
}
void TextPass::setup(rhi::IRHIDevice& device) {
// Initialize default bitmap font
if (!m_font.initDefault(device)) {
// Font init failed - text rendering will be disabled
return;
}
// Create quad vertex buffer (unit quad, instanced) - same as SpritePass
float quadVertices[] = {
// pos.x, pos.y, pos.z, r, g, b, a
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-left
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // bottom-right
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-right
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, // top-left
};
rhi::BufferDesc vbDesc;
vbDesc.type = rhi::BufferDesc::Vertex;
vbDesc.size = sizeof(quadVertices);
vbDesc.data = quadVertices;
vbDesc.dynamic = false;
vbDesc.layout = rhi::BufferDesc::PosColor;
m_quadVB = device.createBuffer(vbDesc);
// Create index buffer
uint16_t quadIndices[] = {
0, 1, 2,
0, 2, 3
};
rhi::BufferDesc ibDesc;
ibDesc.type = rhi::BufferDesc::Index;
ibDesc.size = sizeof(quadIndices);
ibDesc.data = quadIndices;
ibDesc.dynamic = false;
m_quadIB = device.createBuffer(ibDesc);
// Create dynamic instance buffer for glyphs
rhi::BufferDesc instDesc;
instDesc.type = rhi::BufferDesc::Instance;
instDesc.size = MAX_GLYPHS_PER_BATCH * sizeof(SpriteInstance);
instDesc.data = nullptr;
instDesc.dynamic = true;
m_instanceBuffer = device.createBuffer(instDesc);
// Create texture sampler uniform
m_textureSampler = device.createUniform("s_texColor", 1);
}
void TextPass::shutdown(rhi::IRHIDevice& device) {
device.destroy(m_quadVB);
device.destroy(m_quadIB);
device.destroy(m_instanceBuffer);
device.destroy(m_textureSampler);
m_font.shutdown(device);
}
void TextPass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) {
if (frame.textCount == 0 || !m_font.isValid()) {
return;
}
// Set render state for text (alpha blending, no depth)
rhi::RenderState state;
state.blend = rhi::BlendMode::Alpha;
state.cull = rhi::CullMode::None;
state.depthTest = false;
state.depthWrite = false;
cmd.setState(state);
m_glyphInstances.clear();
// Convert each TextCommand into glyph instances
for (size_t i = 0; i < frame.textCount; ++i) {
const TextCommand& textCmd = frame.texts[i];
if (!textCmd.text) continue;
// Calculate scale factor based on font size
float scale = static_cast<float>(textCmd.fontSize) / m_font.getBaseSize();
// Extract color components
uint32_t color = textCmd.color;
float r = static_cast<float>((color >> 24) & 0xFF) / 255.0f;
float g = static_cast<float>((color >> 16) & 0xFF) / 255.0f;
float b = static_cast<float>((color >> 8) & 0xFF) / 255.0f;
float a = static_cast<float>(color & 0xFF) / 255.0f;
float cursorX = textCmd.x;
float cursorY = textCmd.y;
const char* ptr = textCmd.text;
while (*ptr) {
char ch = *ptr++;
// Handle newline
if (ch == '\n') {
cursorX = textCmd.x;
cursorY += m_font.getLineHeight() * scale;
continue;
}
const GlyphInfo& glyph = m_font.getGlyph(static_cast<uint8_t>(ch));
// Create sprite instance for this glyph
SpriteInstance inst;
// Position (top-left of glyph)
inst.x = cursorX + glyph.offsetX * scale;
inst.y = cursorY + glyph.offsetY * scale;
// Scale to glyph size
inst.scaleX = glyph.width * scale;
inst.scaleY = glyph.height * scale;
// No rotation
inst.rotation = 0.0f;
// UVs from font atlas
inst.u0 = glyph.u0;
inst.v0 = glyph.v0;
inst.u1 = glyph.u1;
inst.v1 = glyph.v1;
// Texture ID (font atlas = 0)
inst.textureId = 0.0f;
// Layer
inst.layer = static_cast<float>(textCmd.layer);
// Padding/reserved
inst.padding0 = 0.0f;
inst.reserved[0] = 0.0f;
inst.reserved[1] = 0.0f;
inst.reserved[2] = 0.0f;
inst.reserved[3] = 0.0f;
// Color
inst.r = r;
inst.g = g;
inst.b = b;
inst.a = a;
m_glyphInstances.push_back(inst);
// Advance cursor
cursorX += glyph.advance * scale;
// Check batch limit
if (m_glyphInstances.size() >= MAX_GLYPHS_PER_BATCH) {
// Flush current batch
device.updateBuffer(m_instanceBuffer, m_glyphInstances.data(),
static_cast<uint32_t>(m_glyphInstances.size() * sizeof(SpriteInstance)));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(m_glyphInstances.size()));
cmd.setTexture(0, m_font.getTexture(), m_textureSampler);
cmd.drawInstanced(6, static_cast<uint32_t>(m_glyphInstances.size()));
cmd.submit(0, m_shader, 0);
m_glyphInstances.clear();
}
}
}
// Submit remaining glyphs
if (!m_glyphInstances.empty()) {
device.updateBuffer(m_instanceBuffer, m_glyphInstances.data(),
static_cast<uint32_t>(m_glyphInstances.size() * sizeof(SpriteInstance)));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(m_glyphInstances.size()));
cmd.setTexture(0, m_font.getTexture(), m_textureSampler);
cmd.drawInstanced(6, static_cast<uint32_t>(m_glyphInstances.size()));
cmd.submit(0, m_shader, 0);
}
}
} // namespace grove

View File

@ -0,0 +1,50 @@
#pragma once
#include "../RenderGraph/RenderPass.h"
#include "../RHI/RHITypes.h"
#include "../Text/BitmapFont.h"
namespace grove {
// ============================================================================
// Text Pass - Renders 2D text with instanced quads
// ============================================================================
class TextPass : public RenderPass {
public:
/**
* @brief Construct TextPass with required shader
* @param shader The shader program to use (sprite shader works)
*/
explicit TextPass(rhi::ShaderHandle shader);
const char* getName() const override { return "Text"; }
uint32_t getSortOrder() const override { return 150; } // After sprites, before debug
std::vector<const char*> getDependencies() const override { return {"Sprites"}; }
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
/**
* @brief Get the bitmap font (for external texture loading)
*/
BitmapFont& getFont() { return m_font; }
const BitmapFont& getFont() const { return m_font; }
private:
rhi::ShaderHandle m_shader;
rhi::BufferHandle m_quadVB;
rhi::BufferHandle m_quadIB;
rhi::BufferHandle m_instanceBuffer;
rhi::UniformHandle m_textureSampler;
BitmapFont m_font;
// Reusable buffer for glyph instances
std::vector<SpriteInstance> m_glyphInstances;
static constexpr uint32_t MAX_GLYPHS_PER_BATCH = 4096;
};
} // namespace grove

View File

@ -0,0 +1,197 @@
#include "TilemapPass.h"
#include "../RHI/RHIDevice.h"
#include "../Frame/FramePacket.h"
#include "../Resources/ResourceCache.h"
namespace grove {
TilemapPass::TilemapPass(rhi::ShaderHandle shader)
: m_shader(shader)
{
m_tileInstances.reserve(MAX_TILES_PER_BATCH);
}
void TilemapPass::setup(rhi::IRHIDevice& device) {
// Create quad vertex buffer (unit quad, instanced) - same as SpritePass
float quadVertices[] = {
// pos.x, pos.y, pos.z, r, g, b, a
0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f,
};
rhi::BufferDesc vbDesc;
vbDesc.type = rhi::BufferDesc::Vertex;
vbDesc.size = sizeof(quadVertices);
vbDesc.data = quadVertices;
vbDesc.dynamic = false;
vbDesc.layout = rhi::BufferDesc::PosColor;
m_quadVB = device.createBuffer(vbDesc);
// Create index buffer
uint16_t quadIndices[] = {
0, 1, 2,
0, 2, 3
};
rhi::BufferDesc ibDesc;
ibDesc.type = rhi::BufferDesc::Index;
ibDesc.size = sizeof(quadIndices);
ibDesc.data = quadIndices;
ibDesc.dynamic = false;
m_quadIB = device.createBuffer(ibDesc);
// Create dynamic instance buffer
rhi::BufferDesc instDesc;
instDesc.type = rhi::BufferDesc::Instance;
instDesc.size = MAX_TILES_PER_BATCH * sizeof(SpriteInstance);
instDesc.data = nullptr;
instDesc.dynamic = true;
m_instanceBuffer = device.createBuffer(instDesc);
// Create texture sampler uniform
m_textureSampler = device.createUniform("s_texColor", 1);
// Create default white texture
uint32_t whitePixel = 0xFFFFFFFF;
rhi::TextureDesc texDesc;
texDesc.width = 1;
texDesc.height = 1;
texDesc.format = rhi::TextureDesc::RGBA8;
texDesc.data = &whitePixel;
texDesc.dataSize = sizeof(whitePixel);
m_defaultTexture = device.createTexture(texDesc);
}
void TilemapPass::shutdown(rhi::IRHIDevice& device) {
device.destroy(m_quadVB);
device.destroy(m_quadIB);
device.destroy(m_instanceBuffer);
device.destroy(m_textureSampler);
device.destroy(m_defaultTexture);
}
void TilemapPass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) {
if (frame.tilemapCount == 0) {
return;
}
// Set render state for tilemaps (alpha blending, no depth)
rhi::RenderState state;
state.blend = rhi::BlendMode::Alpha;
state.cull = rhi::CullMode::None;
state.depthTest = false;
state.depthWrite = false;
cmd.setState(state);
// Process each tilemap chunk
for (size_t i = 0; i < frame.tilemapCount; ++i) {
const TilemapChunk& chunk = frame.tilemaps[i];
if (!chunk.tiles || chunk.tileCount == 0) {
continue;
}
// Get tileset texture
rhi::TextureHandle tileset;
if (chunk.textureId > 0 && m_resourceCache) {
tileset = m_resourceCache->getTextureById(chunk.textureId);
}
if (!tileset.isValid()) {
tileset = m_defaultTileset.isValid() ? m_defaultTileset : m_defaultTexture;
}
// Calculate UV size per tile in tileset
float tileU = 1.0f / m_tilesPerRow;
float tileV = 1.0f / m_tilesPerCol;
m_tileInstances.clear();
// Generate sprite instances for each tile
for (size_t t = 0; t < chunk.tileCount; ++t) {
uint16_t tileIndex = chunk.tiles[t];
// Skip empty tiles (index 0 is typically empty)
if (tileIndex == 0) {
continue;
}
// Calculate tile position in grid
size_t tileX = t % chunk.width;
size_t tileY = t / chunk.width;
// Calculate world position
float worldX = chunk.x + tileX * chunk.tileWidth;
float worldY = chunk.y + tileY * chunk.tileHeight;
// Calculate UV coords from tile index
// tileIndex-1 because 0 is empty, actual tiles start at 1
uint16_t actualIndex = tileIndex - 1;
uint16_t tileCol = actualIndex % m_tilesPerRow;
uint16_t tileRow = actualIndex / m_tilesPerRow;
float u0 = tileCol * tileU;
float v0 = tileRow * tileV;
float u1 = u0 + tileU;
float v1 = v0 + tileV;
// Create sprite instance for this tile
SpriteInstance inst;
inst.x = worldX;
inst.y = worldY;
inst.scaleX = static_cast<float>(chunk.tileWidth);
inst.scaleY = static_cast<float>(chunk.tileHeight);
inst.rotation = 0.0f;
inst.u0 = u0;
inst.v0 = v0;
inst.u1 = u1;
inst.v1 = v1;
inst.textureId = 0.0f; // Using tileset bound directly
inst.layer = -100.0f; // Tilemaps render behind sprites
inst.padding0 = 0.0f;
inst.reserved[0] = 0.0f;
inst.reserved[1] = 0.0f;
inst.reserved[2] = 0.0f;
inst.reserved[3] = 0.0f;
inst.r = 1.0f;
inst.g = 1.0f;
inst.b = 1.0f;
inst.a = 1.0f;
m_tileInstances.push_back(inst);
// Flush batch if full
if (m_tileInstances.size() >= MAX_TILES_PER_BATCH) {
device.updateBuffer(m_instanceBuffer, m_tileInstances.data(),
static_cast<uint32_t>(m_tileInstances.size() * sizeof(SpriteInstance)));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(m_tileInstances.size()));
cmd.setTexture(0, tileset, m_textureSampler);
cmd.drawInstanced(6, static_cast<uint32_t>(m_tileInstances.size()));
cmd.submit(0, m_shader, 0);
m_tileInstances.clear();
}
}
// Flush remaining tiles for this chunk
if (!m_tileInstances.empty()) {
device.updateBuffer(m_instanceBuffer, m_tileInstances.data(),
static_cast<uint32_t>(m_tileInstances.size() * sizeof(SpriteInstance)));
cmd.setVertexBuffer(m_quadVB);
cmd.setIndexBuffer(m_quadIB);
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(m_tileInstances.size()));
cmd.setTexture(0, tileset, m_textureSampler);
cmd.drawInstanced(6, static_cast<uint32_t>(m_tileInstances.size()));
cmd.submit(0, m_shader, 0);
m_tileInstances.clear();
}
}
}
} // namespace grove

View File

@ -0,0 +1,70 @@
#pragma once
#include "../RenderGraph/RenderPass.h"
#include "../RHI/RHITypes.h"
#include <vector>
namespace grove {
class ResourceCache;
// ============================================================================
// Tilemap Pass - Renders 2D tilemaps efficiently
// ============================================================================
class TilemapPass : public RenderPass {
public:
/**
* @brief Construct TilemapPass with required shader
* @param shader The shader program to use (sprite shader)
*/
explicit TilemapPass(rhi::ShaderHandle shader);
const char* getName() const override { return "Tilemaps"; }
uint32_t getSortOrder() const override { return 50; } // Before sprites
std::vector<const char*> getDependencies() const override { return {"Clear"}; }
void setup(rhi::IRHIDevice& device) override;
void shutdown(rhi::IRHIDevice& device) override;
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
/**
* @brief Set resource cache for texture lookup
*/
void setResourceCache(ResourceCache* cache) { m_resourceCache = cache; }
/**
* @brief Set default tileset texture
*/
void setDefaultTileset(rhi::TextureHandle texture) { m_defaultTileset = texture; }
/**
* @brief Set tileset dimensions (tiles per row/column in atlas)
*/
void setTilesetLayout(uint16_t tilesPerRow, uint16_t tilesPerCol) {
m_tilesPerRow = tilesPerRow;
m_tilesPerCol = tilesPerCol;
}
private:
rhi::ShaderHandle m_shader;
rhi::BufferHandle m_quadVB;
rhi::BufferHandle m_quadIB;
rhi::BufferHandle m_instanceBuffer;
rhi::UniformHandle m_textureSampler;
rhi::TextureHandle m_defaultTexture;
rhi::TextureHandle m_defaultTileset;
ResourceCache* m_resourceCache = nullptr;
// Tileset layout (for UV calculation)
uint16_t m_tilesPerRow = 16;
uint16_t m_tilesPerCol = 16;
// Reusable buffer for tile instances
std::vector<SpriteInstance> m_tileInstances;
static constexpr uint32_t MAX_TILES_PER_BATCH = 16384;
};
} // namespace grove

View File

@ -24,6 +24,68 @@ rhi::ShaderHandle ResourceCache::getShader(const std::string& name) const {
return rhi::ShaderHandle{}; // Invalid handle return rhi::ShaderHandle{}; // Invalid handle
} }
rhi::TextureHandle ResourceCache::getTextureById(uint16_t id) const {
std::shared_lock lock(m_mutex);
if (id < m_textureById.size()) {
return m_textureById[id];
}
return rhi::TextureHandle{}; // Invalid handle
}
uint16_t ResourceCache::getTextureId(const std::string& path) const {
std::shared_lock lock(m_mutex);
auto it = m_pathToTextureId.find(path);
if (it != m_pathToTextureId.end()) {
return it->second;
}
return 0; // Invalid ID
}
uint16_t ResourceCache::loadTextureWithId(rhi::IRHIDevice& device, const std::string& path) {
// Check if already loaded
{
std::shared_lock lock(m_mutex);
auto it = m_pathToTextureId.find(path);
if (it != m_pathToTextureId.end()) {
return it->second;
}
}
// Load texture from file using TextureLoader (stb_image)
auto result = TextureLoader::loadFromFile(device, path);
if (!result.success) {
return 0; // Invalid ID
}
// Store in cache with new ID
{
std::unique_lock lock(m_mutex);
// Double-check after acquiring exclusive lock
auto it = m_pathToTextureId.find(path);
if (it != m_pathToTextureId.end()) {
// Another thread loaded it, destroy our copy
device.destroy(result.handle);
return it->second;
}
// Assign new ID (1-based, 0 = invalid)
uint16_t newId = static_cast<uint16_t>(m_textureById.size());
if (newId == 0) {
// Reserve index 0 as invalid/default
m_textureById.push_back(rhi::TextureHandle{});
newId = 1;
}
m_textureById.push_back(result.handle);
m_pathToTextureId[path] = newId;
m_textures[path] = result.handle;
return newId;
}
}
rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std::string& path) { rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std::string& path) {
// Check if already loaded // Check if already loaded
{ {
@ -42,10 +104,27 @@ rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std
return rhi::TextureHandle{}; return rhi::TextureHandle{};
} }
// Store in cache // Store in cache (also register with ID system)
{ {
std::unique_lock lock(m_mutex); std::unique_lock lock(m_mutex);
// Double check
auto it = m_textures.find(path);
if (it != m_textures.end()) {
device.destroy(result.handle);
return it->second;
}
m_textures[path] = result.handle; m_textures[path] = result.handle;
// Also add to ID system
uint16_t newId = static_cast<uint16_t>(m_textureById.size());
if (newId == 0) {
m_textureById.push_back(rhi::TextureHandle{});
newId = 1;
}
m_textureById.push_back(result.handle);
m_pathToTextureId[path] = newId;
} }
return result.handle; return result.handle;
@ -97,6 +176,8 @@ void ResourceCache::clear(rhi::IRHIDevice& device) {
device.destroy(handle); device.destroy(handle);
} }
m_textures.clear(); m_textures.clear();
m_textureById.clear();
m_pathToTextureId.clear();
for (auto& [name, handle] : m_shaders) { for (auto& [name, handle] : m_shaders) {
device.destroy(handle); device.destroy(handle);

View File

@ -2,6 +2,7 @@
#include "../RHI/RHITypes.h" #include "../RHI/RHITypes.h"
#include <unordered_map> #include <unordered_map>
#include <vector>
#include <string> #include <string>
#include <shared_mutex> #include <shared_mutex>
@ -10,7 +11,7 @@ namespace grove {
namespace rhi { class IRHIDevice; } namespace rhi { class IRHIDevice; }
// ============================================================================ // ============================================================================
// Resource Cache - Thread-safe texture and shader cache // Resource Cache - Thread-safe texture and shader cache with numeric IDs
// ============================================================================ // ============================================================================
class ResourceCache { class ResourceCache {
@ -21,7 +22,16 @@ public:
rhi::TextureHandle getTexture(const std::string& path) const; rhi::TextureHandle getTexture(const std::string& path) const;
rhi::ShaderHandle getShader(const std::string& name) const; rhi::ShaderHandle getShader(const std::string& name) const;
// Loading (called from main thread) // Get texture by numeric ID (for sprite rendering)
rhi::TextureHandle getTextureById(uint16_t id) const;
// Get texture ID from path (returns 0 if not found)
uint16_t getTextureId(const std::string& path) const;
// Loading (called from main thread) - returns texture ID
uint16_t loadTextureWithId(rhi::IRHIDevice& device, const std::string& path);
// Legacy loading (returns handle directly)
rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path); rhi::TextureHandle loadTexture(rhi::IRHIDevice& device, const std::string& path);
rhi::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name, rhi::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name,
const void* vsData, uint32_t vsSize, const void* vsData, uint32_t vsSize,
@ -39,8 +49,14 @@ public:
size_t getShaderCount() const; size_t getShaderCount() const;
private: private:
// Path-based lookup
std::unordered_map<std::string, rhi::TextureHandle> m_textures; std::unordered_map<std::string, rhi::TextureHandle> m_textures;
std::unordered_map<std::string, rhi::ShaderHandle> m_shaders; std::unordered_map<std::string, rhi::ShaderHandle> m_shaders;
// ID-based lookup for textures (index = textureId, 0 = invalid/default)
std::vector<rhi::TextureHandle> m_textureById;
std::unordered_map<std::string, uint16_t> m_pathToTextureId;
mutable std::shared_mutex m_mutex; mutable std::shared_mutex m_mutex;
}; };

View File

@ -77,11 +77,26 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
packet.spriteCount = 0; packet.spriteCount = 0;
} }
// Copy tilemaps // Copy tilemaps (with tile data)
if (!m_tilemaps.empty()) { if (!m_tilemaps.empty()) {
TilemapChunk* tilemaps = allocator.allocateArray<TilemapChunk>(m_tilemaps.size()); TilemapChunk* tilemaps = allocator.allocateArray<TilemapChunk>(m_tilemaps.size());
if (tilemaps) { if (tilemaps) {
std::memcpy(tilemaps, m_tilemaps.data(), m_tilemaps.size() * sizeof(TilemapChunk)); std::memcpy(tilemaps, m_tilemaps.data(), m_tilemaps.size() * sizeof(TilemapChunk));
// Copy tile data to frame allocator and fix up pointers
for (size_t i = 0; i < m_tilemaps.size() && i < m_tilemapTiles.size(); ++i) {
const std::vector<uint16_t>& tiles = m_tilemapTiles[i];
if (!tiles.empty()) {
uint16_t* tilesCopy = static_cast<uint16_t*>(
allocator.allocate(tiles.size() * sizeof(uint16_t), alignof(uint16_t)));
if (tilesCopy) {
std::memcpy(tilesCopy, tiles.data(), tiles.size() * sizeof(uint16_t));
tilemaps[i].tiles = tilesCopy;
tilemaps[i].tileCount = tiles.size();
}
}
}
packet.tilemaps = tilemaps; packet.tilemaps = tilemaps;
packet.tilemapCount = m_tilemaps.size(); packet.tilemapCount = m_tilemaps.size();
} }
@ -90,11 +105,25 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
packet.tilemapCount = 0; packet.tilemapCount = 0;
} }
// Copy texts // Copy texts (with string data)
if (!m_texts.empty()) { if (!m_texts.empty()) {
TextCommand* texts = allocator.allocateArray<TextCommand>(m_texts.size()); TextCommand* texts = allocator.allocateArray<TextCommand>(m_texts.size());
if (texts) { if (texts) {
std::memcpy(texts, m_texts.data(), m_texts.size() * sizeof(TextCommand)); std::memcpy(texts, m_texts.data(), m_texts.size() * sizeof(TextCommand));
// Copy string data to frame allocator and fix up pointers
for (size_t i = 0; i < m_texts.size() && i < m_textStrings.size(); ++i) {
const std::string& str = m_textStrings[i];
if (!str.empty()) {
// Allocate string + null terminator
char* textCopy = static_cast<char*>(allocator.allocate(str.size() + 1, 1));
if (textCopy) {
std::memcpy(textCopy, str.c_str(), str.size() + 1);
texts[i].text = textCopy;
}
}
}
packet.texts = texts; packet.texts = texts;
packet.textCount = m_texts.size(); packet.textCount = m_texts.size();
} }
@ -148,7 +177,9 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
void SceneCollector::clear() { void SceneCollector::clear() {
m_sprites.clear(); m_sprites.clear();
m_tilemaps.clear(); m_tilemaps.clear();
m_tilemapTiles.clear();
m_texts.clear(); m_texts.clear();
m_textStrings.clear();
m_particles.clear(); m_particles.clear();
m_debugLines.clear(); m_debugLines.clear();
m_debugRects.clear(); m_debugRects.clear();
@ -214,7 +245,41 @@ void SceneCollector::parseTilemap(const IDataNode& data) {
chunk.tileWidth = static_cast<uint16_t>(data.getInt("tileW", 16)); chunk.tileWidth = static_cast<uint16_t>(data.getInt("tileW", 16));
chunk.tileHeight = static_cast<uint16_t>(data.getInt("tileH", 16)); chunk.tileHeight = static_cast<uint16_t>(data.getInt("tileH", 16));
chunk.textureId = static_cast<uint16_t>(data.getInt("textureId", 0)); chunk.textureId = static_cast<uint16_t>(data.getInt("textureId", 0));
chunk.tiles = nullptr; // TODO: Parse tile array
// Parse tile array from "tiles" child node
std::vector<uint16_t> tiles;
IDataNode* tilesNode = const_cast<IDataNode&>(data).getChildReadOnly("tiles");
if (tilesNode) {
// Each child is a tile index
for (const auto& name : tilesNode->getChildNames()) {
IDataNode* tileNode = tilesNode->getChildReadOnly(name);
if (tileNode) {
// Try to get as int (direct value)
tiles.push_back(static_cast<uint16_t>(tileNode->getInt("v", 0)));
}
}
}
// Alternative: parse from comma-separated string "tileData"
if (tiles.empty()) {
std::string tileData = data.getString("tileData", "");
if (!tileData.empty()) {
size_t pos = 0;
while (pos < tileData.size()) {
size_t end = tileData.find(',', pos);
if (end == std::string::npos) end = tileData.size();
std::string numStr = tileData.substr(pos, end - pos);
if (!numStr.empty()) {
tiles.push_back(static_cast<uint16_t>(std::stoi(numStr)));
}
pos = end + 1;
}
}
}
// Store tiles - pointer will be fixed in finalize
m_tilemapTiles.push_back(std::move(tiles));
chunk.tiles = nullptr;
chunk.tileCount = 0; chunk.tileCount = 0;
m_tilemaps.push_back(chunk); m_tilemaps.push_back(chunk);
@ -224,12 +289,16 @@ void SceneCollector::parseText(const IDataNode& data) {
TextCommand text; TextCommand text;
text.x = static_cast<float>(data.getDouble("x", 0.0)); text.x = static_cast<float>(data.getDouble("x", 0.0));
text.y = static_cast<float>(data.getDouble("y", 0.0)); text.y = static_cast<float>(data.getDouble("y", 0.0));
text.text = nullptr; // TODO: Copy string to frame allocator
text.fontId = static_cast<uint16_t>(data.getInt("fontId", 0)); text.fontId = static_cast<uint16_t>(data.getInt("fontId", 0));
text.fontSize = static_cast<uint16_t>(data.getInt("fontSize", 16)); text.fontSize = static_cast<uint16_t>(data.getInt("fontSize", 16));
text.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF)); text.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF));
text.layer = static_cast<uint16_t>(data.getInt("layer", 0)); text.layer = static_cast<uint16_t>(data.getInt("layer", 0));
// Store text string - pointer will be fixed up in finalize()
std::string textStr = data.getString("text", "");
m_textStrings.push_back(std::move(textStr));
text.text = nullptr; // Will be set in finalize()
m_texts.push_back(text); m_texts.push_back(text);
} }

View File

@ -35,7 +35,9 @@ private:
// Staging buffers (filled during collect, copied to FramePacket in finalize) // Staging buffers (filled during collect, copied to FramePacket in finalize)
std::vector<SpriteInstance> m_sprites; std::vector<SpriteInstance> m_sprites;
std::vector<TilemapChunk> m_tilemaps; std::vector<TilemapChunk> m_tilemaps;
std::vector<std::vector<uint16_t>> m_tilemapTiles; // Owns tile data until finalize
std::vector<TextCommand> m_texts; std::vector<TextCommand> m_texts;
std::vector<std::string> m_textStrings; // Owns text data until finalize
std::vector<ParticleInstance> m_particles; std::vector<ParticleInstance> m_particles;
std::vector<DebugLine> m_debugLines; std::vector<DebugLine> m_debugLines;
std::vector<DebugRect> m_debugRects; std::vector<DebugRect> m_debugRects;

View File

@ -0,0 +1,330 @@
#include "BitmapFont.h"
#include "../RHI/RHIDevice.h"
#include <cstring>
#include <vector>
namespace grove {
// ============================================================================
// Embedded 8x8 Monospace Font Data
// Classic CP437 style bitmap font covering ASCII 32-126
// Each character is 8x8 pixels, stored as 8 bytes (1 bit per pixel)
// ============================================================================
// clang-format off
static const uint8_t g_fontData8x8[] = {
// 32 SPACE
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 33 !
0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00,
// 34 "
0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 35 #
0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00,
// 36 $
0x18, 0x7E, 0xC0, 0x7C, 0x06, 0xFC, 0x18, 0x00,
// 37 %
0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00,
// 38 &
0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00,
// 39 '
0x30, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
// 40 (
0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00,
// 41 )
0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00,
// 42 *
0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00,
// 43 +
0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00,
// 44 ,
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30,
// 45 -
0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00,
// 46 .
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00,
// 47 /
0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00,
// 48 0
0x7C, 0xCE, 0xDE, 0xF6, 0xE6, 0xC6, 0x7C, 0x00,
// 49 1
0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00,
// 50 2
0x7C, 0xC6, 0x06, 0x7C, 0xC0, 0xC0, 0xFE, 0x00,
// 51 3
0xFC, 0x06, 0x06, 0x3C, 0x06, 0x06, 0xFC, 0x00,
// 52 4
0x0C, 0xCC, 0xCC, 0xCC, 0xFE, 0x0C, 0x0C, 0x00,
// 53 5
0xFE, 0xC0, 0xFC, 0x06, 0x06, 0xC6, 0x7C, 0x00,
// 54 6
0x7C, 0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0x7C, 0x00,
// 55 7
0xFE, 0x06, 0x06, 0x0C, 0x18, 0x18, 0x18, 0x00,
// 56 8
0x7C, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0x7C, 0x00,
// 57 9
0x7C, 0xC6, 0xC6, 0x7E, 0x06, 0x06, 0x7C, 0x00,
// 58 :
0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00,
// 59 ;
0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30,
// 60 <
0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00,
// 61 =
0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00,
// 62 >
0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00,
// 63 ?
0x7C, 0xC6, 0x0C, 0x18, 0x18, 0x00, 0x18, 0x00,
// 64 @
0x7C, 0xC6, 0xDE, 0xDE, 0xDE, 0xC0, 0x7C, 0x00,
// 65 A
0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x00,
// 66 B
0xFC, 0xC6, 0xC6, 0xFC, 0xC6, 0xC6, 0xFC, 0x00,
// 67 C
0x7C, 0xC6, 0xC0, 0xC0, 0xC0, 0xC6, 0x7C, 0x00,
// 68 D
0xF8, 0xCC, 0xC6, 0xC6, 0xC6, 0xCC, 0xF8, 0x00,
// 69 E
0xFE, 0xC0, 0xC0, 0xF8, 0xC0, 0xC0, 0xFE, 0x00,
// 70 F
0xFE, 0xC0, 0xC0, 0xF8, 0xC0, 0xC0, 0xC0, 0x00,
// 71 G
0x7C, 0xC6, 0xC0, 0xCE, 0xC6, 0xC6, 0x7E, 0x00,
// 72 H
0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0x00,
// 73 I
0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00,
// 74 J
0x06, 0x06, 0x06, 0x06, 0xC6, 0xC6, 0x7C, 0x00,
// 75 K
0xC6, 0xCC, 0xD8, 0xF0, 0xD8, 0xCC, 0xC6, 0x00,
// 76 L
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFE, 0x00,
// 77 M
0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00,
// 78 N
0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00,
// 79 O
0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00,
// 80 P
0xFC, 0xC6, 0xC6, 0xFC, 0xC0, 0xC0, 0xC0, 0x00,
// 81 Q
0x7C, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x06,
// 82 R
0xFC, 0xC6, 0xC6, 0xFC, 0xD8, 0xCC, 0xC6, 0x00,
// 83 S
0x7C, 0xC6, 0xC0, 0x7C, 0x06, 0xC6, 0x7C, 0x00,
// 84 T
0xFE, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
// 85 U
0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00,
// 86 V
0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00,
// 87 W
0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00,
// 88 X
0xC6, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0xC6, 0x00,
// 89 Y
0xC6, 0xC6, 0x6C, 0x38, 0x18, 0x18, 0x18, 0x00,
// 90 Z
0xFE, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xFE, 0x00,
// 91 [
0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00,
// 92 backslash
0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00,
// 93 ]
0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00,
// 94 ^
0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00,
// 95 _
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE,
// 96 `
0x18, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00,
// 97 a
0x00, 0x00, 0x7C, 0x06, 0x7E, 0xC6, 0x7E, 0x00,
// 98 b
0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0xC6, 0xFC, 0x00,
// 99 c
0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC6, 0x7C, 0x00,
// 100 d
0x06, 0x06, 0x7E, 0xC6, 0xC6, 0xC6, 0x7E, 0x00,
// 101 e
0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0x7C, 0x00,
// 102 f
0x1C, 0x36, 0x30, 0x78, 0x30, 0x30, 0x30, 0x00,
// 103 g
0x00, 0x00, 0x7E, 0xC6, 0xC6, 0x7E, 0x06, 0x7C,
// 104 h
0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0xC6, 0xC6, 0x00,
// 105 i
0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00,
// 106 j
0x0C, 0x00, 0x0C, 0x0C, 0x0C, 0x0C, 0xCC, 0x78,
// 107 k
0xC0, 0xC0, 0xCC, 0xD8, 0xF0, 0xD8, 0xCC, 0x00,
// 108 l
0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00,
// 109 m
0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xC6, 0xC6, 0x00,
// 110 n
0x00, 0x00, 0xFC, 0xC6, 0xC6, 0xC6, 0xC6, 0x00,
// 111 o
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0x00,
// 112 p
0x00, 0x00, 0xFC, 0xC6, 0xC6, 0xFC, 0xC0, 0xC0,
// 113 q
0x00, 0x00, 0x7E, 0xC6, 0xC6, 0x7E, 0x06, 0x06,
// 114 r
0x00, 0x00, 0xDC, 0xE6, 0xC0, 0xC0, 0xC0, 0x00,
// 115 s
0x00, 0x00, 0x7E, 0xC0, 0x7C, 0x06, 0xFC, 0x00,
// 116 t
0x30, 0x30, 0x7C, 0x30, 0x30, 0x36, 0x1C, 0x00,
// 117 u
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0x7E, 0x00,
// 118 v
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00,
// 119 w
0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xFE, 0x6C, 0x00,
// 120 x
0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00,
// 121 y
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x7C,
// 122 z
0x00, 0x00, 0xFE, 0x0C, 0x38, 0x60, 0xFE, 0x00,
// 123 {
0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00,
// 124 |
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00,
// 125 }
0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00,
// 126 ~
0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// clang-format on
static constexpr int FONT_FIRST_CHAR = 32; // Space
static constexpr int FONT_CHAR_COUNT = 95; // 32-126
static constexpr int FONT_GLYPH_SIZE = 8; // 8x8 pixels
bool BitmapFont::initDefault(rhi::IRHIDevice& device) {
// Create 128x64 atlas (16 chars per row, 8 rows = 128 chars max)
// We only use 95 chars (ASCII 32-126)
m_atlasWidth = 128; // 16 chars * 8 pixels
m_atlasHeight = 64; // 8 rows * 8 pixels
// Create RGBA texture data
const int atlasPixels = m_atlasWidth * m_atlasHeight;
std::vector<uint32_t> atlasData(atlasPixels, 0);
// Render each character into the atlas
for (int charIdx = 0; charIdx < FONT_CHAR_COUNT; ++charIdx) {
int col = charIdx % 16;
int row = charIdx / 16;
int baseX = col * FONT_GLYPH_SIZE;
int baseY = row * FONT_GLYPH_SIZE;
const uint8_t* glyphData = &g_fontData8x8[charIdx * 8];
for (int y = 0; y < 8; ++y) {
uint8_t rowBits = glyphData[y];
for (int x = 0; x < 8; ++x) {
// MSB first: bit 7 is leftmost pixel
bool pixel = (rowBits & (0x80 >> x)) != 0;
int atlasX = baseX + x;
int atlasY = baseY + y;
int idx = atlasY * m_atlasWidth + atlasX;
// White pixel with alpha = 255 if set, 0 if not
atlasData[idx] = pixel ? 0xFFFFFFFF : 0x00000000;
}
}
}
// Create GPU texture
rhi::TextureDesc texDesc;
texDesc.width = m_atlasWidth;
texDesc.height = m_atlasHeight;
texDesc.format = rhi::TextureDesc::RGBA8;
texDesc.data = atlasData.data();
texDesc.dataSize = atlasPixels * sizeof(uint32_t);
m_texture = device.createTexture(texDesc);
if (!m_texture.isValid()) {
return false;
}
// Generate glyph info
generateDefaultGlyphs();
m_baseSize = 8.0f;
m_lineHeight = 8.0f;
return true;
}
void BitmapFont::generateDefaultGlyphs() {
float invW = 1.0f / m_atlasWidth;
float invH = 1.0f / m_atlasHeight;
for (int charIdx = 0; charIdx < FONT_CHAR_COUNT; ++charIdx) {
uint32_t codepoint = FONT_FIRST_CHAR + charIdx;
int col = charIdx % 16;
int row = charIdx / 16;
GlyphInfo glyph;
glyph.u0 = col * FONT_GLYPH_SIZE * invW;
glyph.v0 = row * FONT_GLYPH_SIZE * invH;
glyph.u1 = (col + 1) * FONT_GLYPH_SIZE * invW;
glyph.v1 = (row + 1) * FONT_GLYPH_SIZE * invH;
glyph.width = FONT_GLYPH_SIZE;
glyph.height = FONT_GLYPH_SIZE;
glyph.offsetX = 0.0f;
glyph.offsetY = 0.0f;
glyph.advance = FONT_GLYPH_SIZE; // Monospace: fixed advance
m_glyphs[codepoint] = glyph;
}
// Default glyph for unknown characters (use '?' which is ASCII 63)
m_defaultGlyph = m_glyphs['?'];
}
bool BitmapFont::loadBMFont(rhi::IRHIDevice& device, const std::string& fntPath, const std::string& pngPath) {
// TODO: Implement BMFont loader if needed
// For now, fall back to default font
return initDefault(device);
}
void BitmapFont::shutdown(rhi::IRHIDevice& device) {
if (m_texture.isValid()) {
device.destroy(m_texture);
m_texture = rhi::TextureHandle();
}
m_glyphs.clear();
}
const GlyphInfo& BitmapFont::getGlyph(uint32_t codepoint) const {
auto it = m_glyphs.find(codepoint);
if (it != m_glyphs.end()) {
return it->second;
}
return m_defaultGlyph;
}
float BitmapFont::measureWidth(const char* text) const {
if (!text) return 0.0f;
float width = 0.0f;
while (*text) {
const GlyphInfo& glyph = getGlyph(static_cast<uint8_t>(*text));
width += glyph.advance;
++text;
}
return width;
}
} // namespace grove

View File

@ -0,0 +1,101 @@
#pragma once
#include "../RHI/RHITypes.h"
#include <cstdint>
#include <string>
#include <unordered_map>
namespace grove {
namespace rhi { class IRHIDevice; }
// ============================================================================
// Glyph Info - Metrics for a single character
// ============================================================================
struct GlyphInfo {
float u0, v0, u1, v1; // UV coordinates in atlas
float width, height; // Glyph size in pixels
float offsetX, offsetY; // Offset from cursor position
float advance; // Cursor advance after this glyph
};
// ============================================================================
// BitmapFont - Simple bitmap font for text rendering
// ============================================================================
class BitmapFont {
public:
BitmapFont() = default;
~BitmapFont() = default;
// Non-copyable
BitmapFont(const BitmapFont&) = delete;
BitmapFont& operator=(const BitmapFont&) = delete;
/**
* @brief Initialize with embedded 8x8 monospace font
* @param device RHI device for texture creation
* @return true on success
*/
bool initDefault(rhi::IRHIDevice& device);
/**
* @brief Load font from BMFont format (.fnt + .png)
* @param device RHI device for texture creation
* @param fntPath Path to .fnt file
* @param pngPath Path to .png atlas
* @return true on success
*/
bool loadBMFont(rhi::IRHIDevice& device, const std::string& fntPath, const std::string& pngPath);
/**
* @brief Cleanup GPU resources
*/
void shutdown(rhi::IRHIDevice& device);
/**
* @brief Get glyph info for a character
* @param codepoint Unicode codepoint (ASCII for now)
* @return Glyph info, or default space if not found
*/
const GlyphInfo& getGlyph(uint32_t codepoint) const;
/**
* @brief Get font atlas texture
*/
rhi::TextureHandle getTexture() const { return m_texture; }
/**
* @brief Get line height (for multi-line text)
*/
float getLineHeight() const { return m_lineHeight; }
/**
* @brief Get base font size (pixels)
*/
float getBaseSize() const { return m_baseSize; }
/**
* @brief Calculate text width in pixels
*/
float measureWidth(const char* text) const;
/**
* @brief Check if font is loaded
*/
bool isValid() const { return m_texture.isValid(); }
private:
void generateDefaultGlyphs();
rhi::TextureHandle m_texture;
std::unordered_map<uint32_t, GlyphInfo> m_glyphs;
GlyphInfo m_defaultGlyph;
float m_lineHeight = 8.0f;
float m_baseSize = 8.0f;
uint16_t m_atlasWidth = 128;
uint16_t m_atlasHeight = 64;
};
} // namespace grove

View File

@ -29,8 +29,8 @@ int main(int argc, char* argv[]) {
} }
// Create window // Create window
const int width = 800; int width = 800;
const int height = 600; int height = 600;
SDL_Window* window = SDL_CreateWindow( SDL_Window* window = SDL_CreateWindow(
"BgfxRenderer Sprites Test - Press ESC to exit", "BgfxRenderer Sprites Test - Press ESC to exit",
@ -152,6 +152,10 @@ int main(int argc, char* argv[]) {
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) { if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) {
running = false; running = false;
} }
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
width = event.window.data1;
height = event.window.data2;
}
} }
// Check timeout // Check timeout
@ -194,7 +198,37 @@ int main(int argc, char* argv[]) {
gameIO->publish("render:clear", std::move(data)); 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 // Send animated sprites in a circle
// Using different textureIds to test batching (all will use default texture fallback)
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
auto sprite = std::make_unique<grove::JsonDataNode>("sprite"); auto sprite = std::make_unique<grove::JsonDataNode>("sprite");
@ -214,8 +248,11 @@ int main(int argc, char* argv[]) {
sprite->setDouble("u1", 1.0); sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0); sprite->setDouble("v1", 1.0);
// All sprites white to show texture without tint // Different colors per sprite to verify rendering
sprite->setInt("color", static_cast<int>(0xFFFFFFFF)); uint32_t colors[] = {0xFF0000FF, 0x00FF00FF, 0x0000FFFF, 0xFFFF00FF, 0xFF00FFFF};
sprite->setInt("color", static_cast<int>(colors[i]));
// Use textureId=0 for all (default texture)
sprite->setInt("textureId", 0); sprite->setInt("textureId", 0);
sprite->setInt("layer", i); sprite->setInt("layer", i);
@ -238,6 +275,74 @@ int main(int argc, char* argv[]) {
gameIO->publish("render:sprite", std::move(data)); gameIO->publish("render:sprite", std::move(data));
} }
// ========================================
// 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) // Process frame (renderer pulls IIO messages)
// ======================================== // ========================================
@ -245,6 +350,8 @@ int main(int argc, char* argv[]) {
grove::JsonDataNode input("input"); grove::JsonDataNode input("input");
input.setDouble("deltaTime", 1.0 / 60.0); input.setDouble("deltaTime", 1.0 / 60.0);
input.setInt("frameNumber", static_cast<int>(frameCount)); input.setInt("frameNumber", static_cast<int>(frameCount));
input.setInt("windowWidth", width);
input.setInt("windowHeight", height);
module->process(input); module->process(input);
frameCount++; frameCount++;