Skip to content

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