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:
parent
4932017244
commit
613283d75c
@ -1,102 +1,112 @@
|
||||
# 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 1-4** - Squelette, RHI, RenderGraph, ShaderManager
|
||||
- Tout fonctionne, voir commits précédents
|
||||
|
||||
**Phase 5** - Pipeline IIO → Rendu ✅ (PARTIEL)
|
||||
- `test_23_bgfx_sprites_visual.cpp` : test complet SDL2 + IIO + Module
|
||||
- Pipeline vérifié fonctionnel :
|
||||
- Module charge avec Vulkan (~500 FPS)
|
||||
- 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
|
||||
| Phase | Feature | Status |
|
||||
|-------|---------|--------|
|
||||
| 5 | IIO Pipeline (messages → SceneCollector → RenderGraph) | Done |
|
||||
| 5.5 | Sprite shader with GPU instancing (80-byte SpriteInstance) | Done |
|
||||
| 6 | Texture loading via stb_image | Done |
|
||||
| 6.5 | Debug overlay (FPS/stats via bgfx debug text) | Done |
|
||||
| 7 | Text rendering with embedded 8x8 bitmap font | Done |
|
||||
| 8A | Multi-texture support (sorted batching by textureId) | Done |
|
||||
| 8B | Tilemap rendering (TilemapPass with instanced tiles) | Done |
|
||||
|
||||
### Key Files
|
||||
```
|
||||
modules/BgfxRenderer/
|
||||
├── BgfxRendererModule.cpp # Main module entry
|
||||
├── Shaders/
|
||||
│ ├── ShaderManager.cpp # Charge les shaders embedded
|
||||
│ ├── vs_color.bin.h # Shader actuel (PAS d'instancing)
|
||||
│ └── fs_color.bin.h
|
||||
│ ├── vs_sprite.sc, fs_sprite.sc # Instanced sprite shader
|
||||
│ ├── varying.def.sc # Shader inputs/outputs
|
||||
│ └── *.bin.h # Compiled shader bytecode
|
||||
├── Passes/
|
||||
│ └── SpritePass.cpp # Execute avec instance buffer
|
||||
├── Frame/
|
||||
│ └── FramePacket.h # SpriteInstance struct
|
||||
└── RHI/
|
||||
└── BgfxDevice.cpp # createBuffer avec VertexLayout
|
||||
│ ├── ClearPass.cpp # Clear framebuffer
|
||||
│ ├── TilemapPass.cpp # Tilemap grid rendering
|
||||
│ ├── SpritePass.cpp # Instanced sprite rendering (sorted by texture)
|
||||
│ ├── TextPass.cpp # Text rendering (glyph quads)
|
||||
│ └── 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
|
||||
struct SpriteInstance {
|
||||
float x, y; // Position
|
||||
float scaleX, scaleY; // Scale
|
||||
float rotation; // Rotation en radians
|
||||
float u0, v0, u1, v1; // UV coords
|
||||
uint32_t color; // ABGR
|
||||
uint16_t textureId; // ID texture
|
||||
uint16_t layer; // Layer de tri
|
||||
};
|
||||
// Load texture and get numeric ID
|
||||
uint16_t texId = resourceCache->loadTextureWithId(device, "path/to/image.png");
|
||||
|
||||
// Get texture by ID (for sprite rendering)
|
||||
rhi::TextureHandle tex = resourceCache->getTextureById(texId);
|
||||
```
|
||||
|
||||
## 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 :
|
||||
- Vertex input : position (vec2), uv (vec2)
|
||||
- Instance input : transform, color, uvRect
|
||||
- Compiler avec shaderc pour toutes les plateformes
|
||||
**Text** (`render:text`)
|
||||
```cpp
|
||||
{ "x": 10.0, "y": 10.0, "text": "Hello",
|
||||
"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 :
|
||||
- Plus lent mais fonctionne avec le shader actuel
|
||||
- Modifier SpritePass pour soumettre un draw par sprite
|
||||
## Next Task: Phase 9 - Choose One
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cmake -DGROVE_BUILD_BGFX_RENDERER=ON -B build-bgfx
|
||||
cmake --build build-bgfx -j4
|
||||
|
||||
# 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
|
||||
cd build-bgfx
|
||||
cmake --build . -j4
|
||||
cd tests && ./test_23_bgfx_sprites_visual
|
||||
```
|
||||
|
||||
## Notes Importantes
|
||||
|
||||
- **Cross-Platform** : Linux + Windows (MinGW)
|
||||
- **WSL2** : Vulkan fonctionne, pas OpenGL
|
||||
- **Shaders embedded** : Pré-compilés dans .bin.h, pas de shaderc runtime
|
||||
- **100% tests passent** : 20/20
|
||||
|
||||
## Questions pour la prochaine session
|
||||
|
||||
1. Option A ou B pour les sprites ?
|
||||
2. Priorité : voir quelque chose à l'écran vs architecture propre ?
|
||||
## Notes
|
||||
- Shaders are pre-compiled (embedded in .bin.h)
|
||||
- shaderc at: `build-bgfx/_deps/bgfx-build/cmake/bgfx/shaderc`
|
||||
- All passes reuse sprite shader (same instancing layout)
|
||||
- TilemapPass: tile index 0 = empty, 1+ = actual tiles
|
||||
- SpritePass: stable_sort by textureId preserves layer order within same texture
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
#include "Resources/ResourceCache.h"
|
||||
#include "Debug/DebugOverlay.h"
|
||||
#include "Passes/ClearPass.h"
|
||||
#include "Passes/TilemapPass.h"
|
||||
#include "Passes/SpritePass.h"
|
||||
#include "Passes/TextPass.h"
|
||||
#include "Passes/DebugPass.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_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
|
||||
auto spritePass = std::make_unique<SpritePass>(spriteShader);
|
||||
m_spritePass = spritePass.get(); // Non-owning reference
|
||||
m_spritePass->setResourceCache(m_resourceCache.get());
|
||||
m_renderGraph->addPass(std::move(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_logger->info("Added DebugPass");
|
||||
m_renderGraph->setup(*m_device);
|
||||
@ -102,9 +118,6 @@ void BgfxRendererModule::setConfiguration(const IDataNode& config, IIO* io, ITas
|
||||
m_sceneCollector->setup(io);
|
||||
m_logger->info("SceneCollector setup complete");
|
||||
|
||||
// Setup resource cache
|
||||
m_resourceCache = std::make_unique<ResourceCache>();
|
||||
|
||||
// Setup debug overlay
|
||||
m_debugOverlay = std::make_unique<DebugOverlay>();
|
||||
bool debugEnabled = config.getBool("debugOverlay", false);
|
||||
@ -132,6 +145,17 @@ void BgfxRendererModule::process(const IDataNode& input) {
|
||||
// Read deltaTime from input (provided by ModuleSystem)
|
||||
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)
|
||||
m_sceneCollector->collect(m_io, deltaTime);
|
||||
|
||||
|
||||
@ -49,9 +49,14 @@ add_library(BgfxRenderer SHARED
|
||||
|
||||
# Passes
|
||||
Passes/ClearPass.cpp
|
||||
Passes/TilemapPass.cpp
|
||||
Passes/SpritePass.cpp
|
||||
Passes/TextPass.cpp
|
||||
Passes/DebugPass.cpp
|
||||
|
||||
# Text
|
||||
Text/BitmapFont.cpp
|
||||
|
||||
# Scene
|
||||
Scene/SceneCollector.cpp
|
||||
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
#include "SpritePass.h"
|
||||
#include "../RHI/RHIDevice.h"
|
||||
#include "../Frame/FramePacket.h"
|
||||
#include "../Resources/ResourceCache.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace grove {
|
||||
|
||||
SpritePass::SpritePass(rhi::ShaderHandle shader)
|
||||
: m_shader(shader)
|
||||
{
|
||||
m_sortedIndices.reserve(MAX_SPRITES_PER_BATCH);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if (frame.spriteCount == 0) {
|
||||
return;
|
||||
@ -86,34 +101,71 @@ void SpritePass::execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi:
|
||||
state.depthWrite = false;
|
||||
cmd.setState(state);
|
||||
|
||||
// Process sprites in batches
|
||||
size_t remaining = frame.spriteCount;
|
||||
size_t offset = 0;
|
||||
// Build sorted indices by textureId for batching
|
||||
m_sortedIndices.clear();
|
||||
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) {
|
||||
size_t batchSize = (remaining > MAX_SPRITES_PER_BATCH)
|
||||
? MAX_SPRITES_PER_BATCH : remaining;
|
||||
// Sort by textureId (stable sort to preserve layer order within same texture)
|
||||
std::stable_sort(m_sortedIndices.begin(), m_sortedIndices.end(),
|
||||
[&frame](uint32_t a, uint32_t b) {
|
||||
return frame.sprites[a].textureId < frame.sprites[b].textureId;
|
||||
});
|
||||
|
||||
// Update instance buffer with sprite data
|
||||
// The SpriteInstance struct matches what we send to GPU
|
||||
const SpriteInstance* batchData = frame.sprites + offset;
|
||||
device.updateBuffer(m_instanceBuffer, batchData,
|
||||
static_cast<uint32_t>(batchSize * sizeof(SpriteInstance)));
|
||||
// Process sprites in batches by texture
|
||||
std::vector<SpriteInstance> batchData;
|
||||
batchData.reserve(MAX_SPRITES_PER_BATCH);
|
||||
|
||||
cmd.setVertexBuffer(m_quadVB);
|
||||
cmd.setIndexBuffer(m_quadIB);
|
||||
cmd.setInstanceBuffer(m_instanceBuffer, 0, static_cast<uint32_t>(batchSize));
|
||||
uint16_t currentTextureId = UINT16_MAX;
|
||||
rhi::TextureHandle currentTexture;
|
||||
|
||||
// Bind texture (use active texture if set, otherwise default white)
|
||||
rhi::TextureHandle texToUse = m_activeTexture.isValid() ? m_activeTexture : m_defaultTexture;
|
||||
cmd.setTexture(0, texToUse, m_textureSampler);
|
||||
for (uint32_t idx : m_sortedIndices) {
|
||||
const SpriteInstance& sprite = frame.sprites[idx];
|
||||
uint16_t texId = static_cast<uint16_t>(sprite.textureId);
|
||||
|
||||
// Submit draw call
|
||||
cmd.drawInstanced(6, static_cast<uint32_t>(batchSize)); // 6 indices per quad
|
||||
cmd.submit(0, m_shader, 0);
|
||||
// Check if texture changed
|
||||
if (texId != currentTextureId) {
|
||||
// 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;
|
||||
remaining -= batchSize;
|
||||
// Update current texture
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
|
||||
#include "../RenderGraph/RenderPass.h"
|
||||
#include "../RHI/RHITypes.h"
|
||||
#include <vector>
|
||||
|
||||
namespace grove {
|
||||
|
||||
class ResourceCache;
|
||||
|
||||
// ============================================================================
|
||||
// Sprite Pass - Renders 2D sprites with batching
|
||||
// Sprite Pass - Renders 2D sprites with batching by texture
|
||||
// ============================================================================
|
||||
|
||||
class SpritePass : public RenderPass {
|
||||
@ -26,21 +29,37 @@ public:
|
||||
void execute(const FramePacket& frame, rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd) override;
|
||||
|
||||
/**
|
||||
* @brief Set a texture to use for all sprites (temporary API)
|
||||
* @param texture The texture handle to use (must be valid)
|
||||
*
|
||||
* TODO: Replace with proper texture array / per-sprite texture support
|
||||
* @brief Set resource cache for texture lookup by ID
|
||||
*/
|
||||
void setResourceCache(ResourceCache* cache) { m_resourceCache = cache; }
|
||||
|
||||
/**
|
||||
* @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; }
|
||||
|
||||
private:
|
||||
void flushBatch(rhi::IRHIDevice& device, rhi::RHICommandBuffer& cmd,
|
||||
rhi::TextureHandle texture, uint32_t count);
|
||||
|
||||
rhi::ShaderHandle m_shader;
|
||||
rhi::BufferHandle m_quadVB;
|
||||
rhi::BufferHandle m_quadIB;
|
||||
rhi::BufferHandle m_instanceBuffer;
|
||||
rhi::UniformHandle m_textureSampler;
|
||||
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;
|
||||
};
|
||||
|
||||
193
modules/BgfxRenderer/Passes/TextPass.cpp
Normal file
193
modules/BgfxRenderer/Passes/TextPass.cpp
Normal 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
|
||||
50
modules/BgfxRenderer/Passes/TextPass.h
Normal file
50
modules/BgfxRenderer/Passes/TextPass.h
Normal 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
|
||||
197
modules/BgfxRenderer/Passes/TilemapPass.cpp
Normal file
197
modules/BgfxRenderer/Passes/TilemapPass.cpp
Normal 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
|
||||
70
modules/BgfxRenderer/Passes/TilemapPass.h
Normal file
70
modules/BgfxRenderer/Passes/TilemapPass.h
Normal 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
|
||||
@ -24,6 +24,68 @@ rhi::ShaderHandle ResourceCache::getShader(const std::string& name) const {
|
||||
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) {
|
||||
// Check if already loaded
|
||||
{
|
||||
@ -42,10 +104,27 @@ rhi::TextureHandle ResourceCache::loadTexture(rhi::IRHIDevice& device, const std
|
||||
return rhi::TextureHandle{};
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
// Store in cache (also register with ID system)
|
||||
{
|
||||
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;
|
||||
|
||||
// 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;
|
||||
@ -97,6 +176,8 @@ void ResourceCache::clear(rhi::IRHIDevice& device) {
|
||||
device.destroy(handle);
|
||||
}
|
||||
m_textures.clear();
|
||||
m_textureById.clear();
|
||||
m_pathToTextureId.clear();
|
||||
|
||||
for (auto& [name, handle] : m_shaders) {
|
||||
device.destroy(handle);
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "../RHI/RHITypes.h"
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <shared_mutex>
|
||||
|
||||
@ -10,7 +11,7 @@ namespace grove {
|
||||
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 {
|
||||
@ -21,7 +22,16 @@ public:
|
||||
rhi::TextureHandle getTexture(const std::string& path) 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::ShaderHandle loadShader(rhi::IRHIDevice& device, const std::string& name,
|
||||
const void* vsData, uint32_t vsSize,
|
||||
@ -39,8 +49,14 @@ public:
|
||||
size_t getShaderCount() const;
|
||||
|
||||
private:
|
||||
// Path-based lookup
|
||||
std::unordered_map<std::string, rhi::TextureHandle> m_textures;
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -77,11 +77,26 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
|
||||
packet.spriteCount = 0;
|
||||
}
|
||||
|
||||
// Copy tilemaps
|
||||
// Copy tilemaps (with tile data)
|
||||
if (!m_tilemaps.empty()) {
|
||||
TilemapChunk* tilemaps = allocator.allocateArray<TilemapChunk>(m_tilemaps.size());
|
||||
if (tilemaps) {
|
||||
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.tilemapCount = m_tilemaps.size();
|
||||
}
|
||||
@ -90,11 +105,25 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
|
||||
packet.tilemapCount = 0;
|
||||
}
|
||||
|
||||
// Copy texts
|
||||
// Copy texts (with string data)
|
||||
if (!m_texts.empty()) {
|
||||
TextCommand* texts = allocator.allocateArray<TextCommand>(m_texts.size());
|
||||
if (texts) {
|
||||
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.textCount = m_texts.size();
|
||||
}
|
||||
@ -148,7 +177,9 @@ FramePacket SceneCollector::finalize(FrameAllocator& allocator) {
|
||||
void SceneCollector::clear() {
|
||||
m_sprites.clear();
|
||||
m_tilemaps.clear();
|
||||
m_tilemapTiles.clear();
|
||||
m_texts.clear();
|
||||
m_textStrings.clear();
|
||||
m_particles.clear();
|
||||
m_debugLines.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.tileHeight = static_cast<uint16_t>(data.getInt("tileH", 16));
|
||||
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;
|
||||
|
||||
m_tilemaps.push_back(chunk);
|
||||
@ -224,12 +289,16 @@ void SceneCollector::parseText(const IDataNode& data) {
|
||||
TextCommand text;
|
||||
text.x = static_cast<float>(data.getDouble("x", 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.fontSize = static_cast<uint16_t>(data.getInt("fontSize", 16));
|
||||
text.color = static_cast<uint32_t>(data.getInt("color", 0xFFFFFFFF));
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,9 @@ private:
|
||||
// Staging buffers (filled during collect, copied to FramePacket in finalize)
|
||||
std::vector<SpriteInstance> m_sprites;
|
||||
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<std::string> m_textStrings; // Owns text data until finalize
|
||||
std::vector<ParticleInstance> m_particles;
|
||||
std::vector<DebugLine> m_debugLines;
|
||||
std::vector<DebugRect> m_debugRects;
|
||||
|
||||
330
modules/BgfxRenderer/Text/BitmapFont.cpp
Normal file
330
modules/BgfxRenderer/Text/BitmapFont.cpp
Normal 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
|
||||
101
modules/BgfxRenderer/Text/BitmapFont.h
Normal file
101
modules/BgfxRenderer/Text/BitmapFont.h
Normal 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
|
||||
@ -29,8 +29,8 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
// Create window
|
||||
const int width = 800;
|
||||
const int height = 600;
|
||||
int width = 800;
|
||||
int height = 600;
|
||||
|
||||
SDL_Window* window = SDL_CreateWindow(
|
||||
"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) {
|
||||
running = false;
|
||||
}
|
||||
if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
width = event.window.data1;
|
||||
height = event.window.data2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check timeout
|
||||
@ -194,7 +198,37 @@ int main(int argc, char* argv[]) {
|
||||
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 batching (all will use default texture fallback)
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
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("v1", 1.0);
|
||||
|
||||
// All sprites white to show texture without tint
|
||||
sprite->setInt("color", static_cast<int>(0xFFFFFFFF));
|
||||
// Different colors per sprite to verify rendering
|
||||
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("layer", i);
|
||||
|
||||
@ -238,6 +275,74 @@ int main(int argc, char* argv[]) {
|
||||
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)
|
||||
// ========================================
|
||||
@ -245,6 +350,8 @@ int main(int argc, char* argv[]) {
|
||||
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++;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user