Testando ECS, SoA e Instancing em C++ com Raylib
ONLINE
FILE_ID: TESTANDO ECS, SOA E INSTANCING EM C++ COM RAYLIB

Testando ECS, SoA e Instancing em C++ com Raylib

Willian Frantz
2 min
PerformanceOptimizationWeb VitalsMonitoring

Quem já tentou renderizar milhares de objetos em tempo real sabe: a performance despenca rápido.
O culpado? Muitas vezes não é a GPU, e sim a quantidade absurda de draw calls que a CPU precisa enviar.

Neste post, vou mostrar como usei ECS + SoA + instancing com Raylib para melhorar drasticamente a performance ao renderizar 125.000 cubos.


O problema: draw calls demais

Renderizar cada cubo com DrawModel parece simples, mas cada chamada gera um overhead na CPU:

  • Configuração de estado
  • Binding de buffers
  • Comandos de driver

125.000 cubos = 125.000 draw calls = CPU engasgada.


Estruturação de dados: AoS vs SoA

A forma como guardamos dados impacta diretamente o desempenho.

AoS (Array of Structs)

struct Entity {
   float x, y, z;
   float rotY;
   bool visible;
};
std::vector<Entity> entities;

Dados ficam misturados, pouco cache-friendly.

SoA (Structure of Arrays)

struct TransformComponent {
    std::vector<float> posX, posY, posZ;
    std::vector<float> rotY;
};

struct RenderComponent {
    std::vector<bool> visible;
};

Dados ficam contíguos na memória → melhor para cache, SIMD e processamento em batch.
Esse layout é essencial para performance em engines modernas.

Na memória, os dados seriam armazenados da seguinte maneira:

posX: [x1, x2, x3, x4...]  
posY: [y1, y2, y3, y4...]  
posZ: [z1, z2, z3, z4...]  
rotY: [r1, r2, r3, r4...]  
visible: [v1, v2, v3, v4...]

ECS na prática

Implementei um mini ECS onde cada entidade é definida por:

  • TransformComponent (posição, rotação)
  • RenderComponent (visibilidade)

Isso permite manipular milhares de entidades de forma eficiente, sem precisar criar structs gigantes por objeto.


Comparando abordagens

1. Naïve (DrawModel por cubo)

  • Loop em todas entidades
  • Translate, Rotate, DrawModel
  • Resultado: draw calls = número de cubos

2. Instancing + SoA (DrawMeshInstanced)

  • Pré-cálculo das matrizes de transformação
  • Um único draw call com todas as instâncias
  • Resultado: draw calls ≈ 1

Medindo performance

Adicionei um HUD e logging em CSV para capturar métricas em tempo real:

  • FPS
  • Frame time (ms)
  • Cubes drawn
  • Draw calls

Exemplo da função de logging:

void LogToCSV(float frameTime, int fps, int cubes, int drawCalls) {
    static std::ofstream log("performance_log.csv", std::ios::app);
    static bool initialized = false;

    if (!initialized) {
        log << "FrameTimeMs,FPS,CubesDrawn,DrawCalls\n";
        initialized = true;
    }

    log << std::fixed << std::setprecision(4)
        << frameTime << "," << fps << "," << cubes << "," << drawCalls << "\n";
}

Esses dados podem ser exportados depois para gerar gráficos de comparação.


Resultados

  • Naïve: 125.000 draw calls, CPU virou gargalo, FPS despencou.
  • Instancing + SoA: 1 draw call, GPU cuidou do trabalho, FPS estável.

Conclusão

  • SoA + ECS → organiza dados de forma cache-friendly e SIMD-ready
  • Instancing → reduz overhead de draw calls, essencial em qualquer engine
  • Mesmo em uma lib simples como Raylib, dá pra experimentar conceitos usados em engines AAA

🚀 Próximos passos: aplicar SSE/AVX para otimizar a atualização das matrizes em batch.

Link para o código: https://github.com/WLSF/raylib_tests


E você?

Já testou instancing ou ECS em seus projetos?
Quais ganhos percebeu?

Me marca no Twitter(@frantz_willian) para trocarmos ideia. 😃

END_OF_FILE • ARTICLE_COMPLETE