Memory Management¶
ESP32 has limited memory, so efficient memory management is crucial for PixelRoot32 games. This guide covers memory constraints, object pooling, C++17 smart pointers, and best practices.
ESP32 Memory Constraints¶
Available Memory by Platform¶
| Platform | Total RAM | Usable Heap | Notes |
|---|---|---|---|
| ESP32 Classic | 520KB | ~300KB | Dual-core, FPU support |
| ESP32-S3 | 512KB | ~350KB | PSRAM option available |
| ESP32-C3 | 400KB | ~250KB | Single-core, Fixed16 math |
| ESP32-S2 | 320KB | ~200KB | USB OTG, lowest memory |
| ESP32-C6 | 512KB | ~350KB | WiFi 6, RISC-V architecture |
Driver-Specific Memory Impact¶
Choosing the right display driver significantly affects memory usage:
- TFT_eSPI (240x240, 16bpp): ~115KB framebuffer
- TFT_eSPI (128x128, 16bpp): ~32KB framebuffer
- U8G2 (128x64, 1bpp): ~1KB framebuffer
- SDL2 (Native): Uses system memory (unlimited)
Tip: For memory-constrained ESP32-C3/S2, consider U8G2 with OLED displays.
C++17 Smart Pointers¶
PixelRoot32 migrated to C++17 with comprehensive smart pointer support for safer memory management.
Basic Smart Pointer Usage¶
#include <memory>
#include <vector>
class GameScene : public Scene {
private:
// Modern ownership with unique_ptr
std::unique_ptr<PlayerActor> player;
std::vector<std::unique_ptr<EnemyActor>> enemies;
std::unique_ptr<MusicPlayer> musicPlayer;
public:
void init() override {
// Create with make_unique (recommended)
player = std::make_unique<PlayerActor>(100, 100, 32, 32);
// Add to scene (non-owning raw pointer)
addEntity(player.get());
// Create enemies
for (int i = 0; i < 5; i++) {
auto enemy = std::make_unique<EnemyActor>(
rand() % 200, rand() % 100, 16, 16
);
enemies.push_back(std::move(enemy));
addEntity(enemies.back().get());
}
}
// No manual destructor needed!
// All unique_ptr objects are automatically destroyed
};
Ownership Transfer Patterns¶
// Transfer ownership to engine
auto customRenderer = std::make_unique<CustomRenderer>(config);
engine.setRenderer(std::move(customRenderer));
// Custom display driver ownership transfer
auto display = std::make_unique<CustomDisplay>(240, 240);
DisplayConfig config = PIXELROOT32_CUSTOM_DISPLAY(
display.release(), 240, 240 // Transfer ownership
);
Object Pooling with Smart Pointers¶
class BulletPool {
private:
static constexpr size_t MAX_BULLETS = 50;
std::array<std::unique_ptr<BulletActor>, MAX_BULLETS> pool;
std::bitset<MAX_BULLETS> activeFlags;
public:
void init() {
// Pre-allocate all bullets
for (size_t i = 0; i < MAX_BULLETS; ++i) {
pool[i] = std::make_unique<BulletActor>(0, 0, 4, 4);
pool[i]->setEnabled(false);
}
}
BulletActor* spawn(Vector2 position, Vector2 velocity) {
for (size_t i = 0; i < MAX_BULLETS; ++i) {
if (!activeFlags[i]) {
activeFlags[i] = true;
pool[i]->reset(position, velocity); // Custom reset method
pool[i]->setEnabled(true);
return pool[i].get(); // Return non-owning pointer
}
}
return nullptr; // Pool exhausted
}
void despawn(BulletActor* bullet) {
for (size_t i = 0; i < MAX_BULLETS; ++i) {
if (activeFlags[i] && pool[i].get() == bullet) {
activeFlags[i] = false;
pool[i]->setEnabled(false);
break;
}
}
}
};
Memory Safety Best Practices¶
✅ Do: Use make_unique for Creation¶
// Good
auto player = std::make_unique<PlayerActor>(x, y, w, h);
// Bad - potential exception safety issues
auto player = std::unique_ptr<PlayerActor>(new PlayerActor(x, y, w, h));
✅ Do: Use .get() for Non-Owning Access¶
// Good - scene doesn't own the entity
scene.addEntity(player.get());
// Bad - creates shared ownership confusion
scene.addEntity(player.release()); // Don't do this!
✅ Do: Check After Potential Move¶
auto resource = std::make_unique<Resource>();
if (condition) {
engine.setResource(std::move(resource));
}
if (resource) { // Safe check after potential move
resource->cleanup();
}
❌ Don't: Mix Raw Pointers and Smart Pointers¶
// Bad - potential double delete
Actor* rawPtr = new Actor();
std::unique_ptr<Actor> smartPtr(rawPtr);
delete rawPtr; // Undefined behavior!
Object Pooling¶
Traditional Fixed-Size Pool¶
class ParticleSystem {
private:
static constexpr int MAX_PARTICLES = 100;
Particle particles[MAX_PARTICLES];
bool active[MAX_PARTICLES] = {false};
public:
Particle* spawn(Vector2 position, Vector2 velocity) {
for (int i = 0; i < MAX_PARTICLES; i++) {
if (!active[i]) {
active[i] = true;
particles[i].reset(position, velocity);
return &particles[i];
}
}
return nullptr; // Pool full
}
void update(unsigned long deltaTime) {
for (int i = 0; i < MAX_PARTICLES; i++) {
if (active[i]) {
particles[i].update(deltaTime);
if (particles[i].isDead()) {
active[i] = false;
}
}
}
}
};
Memory Pool with Placement New¶
#include <new>
template<typename T, size_t Size>
class MemoryPool {
private:
alignas(T) char storage[Size * sizeof(T)];
bool occupied[Size] = {false};
public:
T* acquire() {
for (size_t i = 0; i < Size; i++) {
if (!occupied[i]) {
occupied[i] = true;
return reinterpret_cast<T*>(&storage[i * sizeof(T)]);
}
}
return nullptr;
}
void release(T* ptr) {
size_t index = (reinterpret_cast<char*>(ptr) - storage) / sizeof(T);
if (index < Size) {
occupied[index] = false;
ptr->~T(); // Call destructor
}
}
};
Real-World Memory Optimization¶
Scene Memory Management¶
class GameScene : public Scene {
private:
// Pre-allocated pools
MemoryPool<BulletActor, 50> bulletPool;
MemoryPool<EnemyActor, 20> enemyPool;
MemoryPool<Particle, 100> particlePool;
// Smart pointers for major components
std::unique_ptr<PlayerActor> player;
std::unique_ptr<Background> background;
std::unique_ptr<MusicPlayer> musicPlayer;
public:
void init() override {
// Initialize pools
bulletPool = MemoryPool<BulletActor, 50>();
enemyPool = MemoryPool<EnemyActor, 20>();
particlePool = MemoryPool<Particle, 100>();
// Create major components
player = std::make_unique<PlayerActor>(120, 200, 32, 32);
background = std::make_unique<Background>();
musicPlayer = std::make_unique<MusicPlayer>(engine.getAudioEngine());
// Add to scene
addEntity(player.get());
addEntity(background.get());
}
void spawnBullet(Vector2 position, Vector2 velocity) {
BulletActor* bullet = bulletPool.acquire();
if (bullet) {
new (bullet) BulletActor(position.x, position.y, 4, 4); // Placement new
bullet->setVelocity(velocity);
addEntity(bullet);
}
}
};
Memory Monitoring¶
void logMemoryUsage(const char* context) {
#ifdef ESP32
Serial.print("[");
Serial.print(context);
Serial.print("] Free heap: ");
Serial.print(ESP.getFreeHeap());
Serial.print(" bytes, Largest block: ");
Serial.print(ESP.getMaxAllocHeap());
Serial.println(" bytes");
#endif
}
// Usage in critical sections
void GameScene::init() {
logMemoryUsage("Scene Init Start");
// Initialization code...
logMemoryUsage("Scene Init End");
}
Platform-Specific Considerations¶
ESP32 Memory Constraints¶
#ifdef ESP32
// Aggressive memory optimization for ESP32
constexpr size_t MAX_SPRITES = 32;
constexpr size_t MAX_ENTITIES = 24;
// Use flash storage for large data
static const uint8_t levelData[] PROGMEM = {
// Level data stored in flash
};
#else
// More generous limits for native platform
constexpr size_t MAX_SPRITES = 128;
constexpr size_t MAX_ENTITIES = 100;
#endif
Fixed16 Math Memory Benefits¶
// On non-FPU platforms (ESP32-C3, S2, C6)
// Fixed16 uses 2 bytes vs 4 bytes for float
// Also avoids expensive software float emulation
struct PhysicsComponent {
#ifdef SOC_CPU_HAS_FPU
float velocity; // 4 bytes on FPU platforms
float acceleration;
#else
Scalar velocity; // 2 bytes on non-FPU platforms (Fixed16)
Scalar acceleration;
#endif
};
Debugging Memory Issues¶
Memory Leak Detection¶
class MemoryTracker {
private:
static size_t baselineHeap;
public:
static void markBaseline() {
#ifdef ESP32
baselineHeap = ESP.getFreeHeap();
#endif
}
static void checkLeak(const char* context) {
#ifdef ESP32
size_t currentHeap = ESP.getFreeHeap();
int32_t leak = baselineHeap - currentHeap;
if (leak > 100) { // More than 100 bytes leaked
Serial.print("⚠️ Memory leak in ");
Serial.print(context);
Serial.print(": ");
Serial.print(leak);
Serial.println(" bytes");
}
#endif
}
};
// Usage
void testFunction() {
MemoryTracker::markBaseline();
// Code that might leak
auto obj = std::make_unique<TestObject>();
// ... use object ...
// Object automatically destroyed when unique_ptr goes out of scope
MemoryTracker::checkLeak("testFunction");
}
Heap Fragmentation Analysis¶
void analyzeHeapFragmentation() {
#ifdef ESP32
size_t freeHeap = ESP.getFreeHeap();
size_t largestBlock = ESP.getMaxAllocHeap();
float fragmentation = 1.0f - (float)largestBlock / (float)freeHeap;
Serial.print("Heap fragmentation: ");
Serial.print(fragmentation * 100.0f);
Serial.println("%");
if (fragmentation > 0.5f) {
Serial.println("⚠️ High fragmentation detected!");
Serial.println("Consider restarting or using memory pools");
}
#endif
}
References¶
- ESP32 Memory Guide: See ESP32 Memory Layout
- C++ Smart Pointers: https://en.cppreference.com/book/intro/smart_pointers
- Object Pool Pattern: https://gameprogrammingpatterns.com/object-pool.html
- PlatformIO Memory Analysis: https://docs.platformio.org/en/latest/plus/debugging.html