ESP32 Performance Guide - PixelRoot32 β
π₯ Hot Path Rules β
Hot paths = update(), draw(), collision detection loops, audio callbacks.
Prohibited in hot paths:
| Feature | Why | Alternative |
|---|---|---|
std::optional | Extra branching/code size | Raw pointers or sentinel values |
| Virtual calls | Vtable indirection | Templates or function pointers |
new / malloc | Heap fragmentation | Pre-allocated pools |
std::vector::push_back | Potential reallocation | Fixed-size arrays with flags |
Logging / log() | String formatting overhead | Debug-only counters or periodic logging |
std::rand() | Slow, uses division | math::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::Circleand callphysics::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.
- Represent the projectile as a small
Dirty Region Selective Clear β
Reduces framebuffer clearing overhead by tracking which 8Γ8 pixel cells were actually drawn to in the previous frame, utilizing a double dirty grid pipeline.
- Benefit: Replaces full-screen
memsetwith targeted selective row-run 8bpp clearing. It skips untouched rows entirely and uses__builtin_popcountoptimizations to quickly identify blocks of dirty cells. - RAM cost: 64β226 bytes (depends on resolution and cell size).
- When it pays off: Games with mostly static backgrounds and small moving sprites.
- Profiling flag:
PIXELROOT32_ENABLE_DIRTY_REGION_PROFILING=1 - Metric:
dirty_ratioβ fraction of cells marked dirty. Good values are <0.5; >0.8 suggests full clear is cheaper.
; Enable in platformio.ini
build_flags =
-DPIXELROOT32_ENABLE_DIRTY_REGIONS=1
-DPIXELROOT32_ENABLE_DIRTY_REGION_PROFILING=1Tip: If
dirty_ratio> 0.8, disable dirty regions and use full clearβit avoids the tracking overhead.
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 to128samples, and usingtaskYIELD()for cooperative multitasking. - Float Operations: Soft-float emulation on the ESP32-C3 is extremely slow. The engine provides fixed-point Q15 implementations for performance-critical inner loops (like
tickEnvelopeQ15, LFO generation for vibrato/tremolo, HPF filtering, 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 (likeaddEntity). - Use
std::moveonly when transferring ownership explicitly. - β οΈ Do not use in hot paths:
unique_ptris 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_ptris for init-time ownership semantics. - Pattern: Use fixed-size arrays (e.g.,
Particle particles[50]) and flags (isActive) instead ofstd::vectorwithpush_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
nullptrand 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-exceptionsflag - 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.
π Recommended Build Profiles β
Choose a profile based on your game type to optimize memory usage:
| Game Type | Profile | Enabled | Disabled |
|---|---|---|---|
| Arcade shooters/platformers | arcade | Audio, Physics, Particles | UI System |
| Puzzle/casual games | puzzle | Audio, UI System | Physics, Particles |
| Retro/minimal | retro | None | All |
| Educational/tools | puzzle or custom | Audio, UI System | Physics, Particles |
Example platformio.ini configuration:
[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.
π Related Documentation β
| Document | Description |
|---|---|
| Memory Management Guide | Complete C++17 memory guide with smart pointers |
| Platform Compatibility | Hardware matrix and feature support |
| Architecture Index | Layer architecture and subsystem navigation |
| Rendering Guide | Core rendering pipeline |
