Skip to content

ESP32 Performance Guide - PixelRoot32

🔥 Hot Path Rules

Hot paths = update(), draw(), collision detection loops, audio callbacks.

Prohibited in hot paths:

FeatureWhyAlternative
std::optionalExtra branching/code sizeRaw pointers or sentinel values
Virtual callsVtable indirectionTemplates or function pointers
new / mallocHeap fragmentationPre-allocated pools
std::vector::push_backPotential reallocationFixed-size arrays with flags
Logging / log()String formatting overheadDebug-only counters or periodic logging
std::rand()Slow, uses divisionmath::randomScalar() (Xorshift)

⚡ Performance (ESP32 Focus)

Inlining

  • Define trivial accessors (e.g., getHitBox, getX) in the header (.h) to allow compiler inlining.
  • Keep heavy implementation logic in .cpp.

Fast Randomness

std::rand() is slow and uses division. Use math::randomScalar() or math::randomRange() (which use optimized Xorshift algorithms compatible with Fixed16) for visual effects.

Collision Detection

  • Use simple AABB (Axis-Aligned Bounding Box) checks first. Use Collision Layers (GameLayers.h) to avoid checking unnecessary pairs.
  • For very fast projectiles (bullets, lasers), prefer lightweight sweep tests:
    • Represent the projectile as a small physics::Circle and call physics::sweepCircleVsRect(startCircle, endCircle, targetRect, tHit) against potential targets.
    • Use sweep tests only for the few entities that need them; keep everything else on basic AABB to avoid unnecessary CPU cost.

Single-Core Resource Contention (ESP32-C3)

Single-core architectures (like the ESP32-C3) run the game logic, display transfers, and audio synthesis on a single core.

  • Priority Inversion: Heavy display transfers (like full-screen U8G2 refreshes) can block the audio task, causing buffer underruns and audio glitches. The engine dynamically detects single-core platforms and elevates the audio task priority (e.g., to 18) to protect audio streams.
  • Context Thrashing: An audio priority that is too high (e.g., 24) will preempt the display transfer constantly to synthesize audio, fragmenting the hardware SPI transaction and ballooning draw times (up to 4x). The engine mitigates this by balancing priority, reducing audio buffer block sizes to 128 samples, and using taskYIELD() for cooperative multitasking.
  • Float Operations: Soft-float emulation on the ESP32-C3 is extremely slow. The engine provides integer Q15 implementations for performance-critical inner loops (like tickEnvelopeQ15 and audio mixer LUTs). Avoid introducing new float-based calculations inside per-sample audio loops or per-pixel drawing loops.

💾 Memory & Resources

📖 For comprehensive C++17 memory management guide, see Memory Management Guide

Smart Pointers (C++17)

Use std::unique_ptr for init-time ownership (Scenes, Actors, UI elements) to automate memory management and document ownership.

  • Use std::make_unique<T>(...) to create objects during initialization only.
  • Pass raw pointers (via .get()) to functions that do not take ownership (like addEntity).
  • Use std::move only when transferring ownership explicitly.
  • ⚠️ Do not use in hot paths: unique_ptr is for init-time, not runtime game loop.

Object Pooling

Pre-allocate all game objects (obstacles, particles, enemies) during init().

  • Pools are for runtime zero-allocation recycling; unique_ptr is for init-time ownership semantics.
  • Pattern: Use fixed-size arrays (e.g., Particle particles[50]) and flags (isActive) instead of std::vector with push_back/erase.
  • Trade-off: Eliminates runtime allocations and fragmentation at the cost of a slightly higher fixed RAM footprint; dimension pools to realistic worst-case usage.

Zero Runtime Allocation

Never use new or malloc inside the game loop (update or draw).

String Handling

Avoid std::string copies. Use std::string_view for passing strings. For formatting, use snprintf with stack-allocated char buffers.

Scene Arenas (PIXELROOT32_ENABLE_SCENE_ARENA)

Use a single pre-allocated buffer per scene for temporary entities or scratch data when you need strict zero-allocation guarantees.

  • Trade-off: Very cache-friendly and fragmentation-proof, but the buffer cannot grow at runtime; oversizing wastes RAM, undersizing returns nullptr and requires graceful fallback logic.

🏗️ Build Profiles

PixelRoot32 supports two build profiles for different use cases:

Embedded Profile (Default)

For ESP32 and resource-constrained hardware:

  • Zero allocation at runtime
  • No exceptions, -fno-exceptions flag
  • Deterministic behavior prioritized
  • Modular compilation to reduce binary size
  • All Hot Path Rules enforced

Native Profile (Optional)

For PC simulation and development:

  • Relaxed constraints for faster iteration
  • Exceptions permitted if needed for tooling
  • Debug-friendly features enabled
  • All subsystems can be compiled in

Use PLATFORM_NATIVE flag to switch profiles.


Choose a profile based on your game type to optimize memory usage:

Game TypeProfileEnabledDisabled
Arcade shooters/platformersarcadeAudio, Physics, ParticlesUI System
Puzzle/casual gamespuzzleAudio, UI SystemPhysics, Particles
Retro/minimalretroNoneAll
Educational/toolspuzzle or customAudio, UI SystemPhysics, Particles

Example platformio.ini configuration:

ini
[env:esp32_arcade]
extends = base_esp32, profile_arcade
build_flags =
    ${base_esp32.build_flags}
    ${profile_arcade.build_flags}

[env:esp32_puzzle]
extends = base_esp32, profile_puzzle
build_flags =
    ${base_esp32.build_flags}
    ${profile_puzzle.build_flags}

[env:native_retro]
build_flags =
    -DPLATFORM_NATIVE=1
    -DPIXELROOT32_ENABLE_AUDIO=0
    -DPIXELROOT32_ENABLE_PHYSICS=0
    -DPIXELROOT32_ENABLE_PARTICLES=0
    -DPIXELROOT32_ENABLE_UI_SYSTEM=0

📐 Resolution Scaling

PixelRoot32 separates logical resolution (what your game draws at) from physical resolution (the actual display), so you can target low pixel counts for performance while filling modern panels.

When to Use

  • Ship gameplay at 128×128 or 160×144 but drive a 240×240 TFT
  • Keep UI and physics in logical space; only the final blit scales up

Configure DisplayConfig

Set logicalWidth / logicalHeight for the render buffer and physicalWidth / physicalHeight for the panel. The renderer and input pipeline map between the two.

See DisplayConfig / Engine and the architecture deep dive Resolution Scaling for implementation details, ESP32 considerations, and coordinate mapping.


DocumentDescription
Memory Management GuideComplete C++17 memory guide with smart pointers
Platform CompatibilityHardware matrix and feature support
Architecture IndexLayer architecture and subsystem navigation
Rendering GuideCore rendering pipeline

Released under the MIT License.