Skip to content

Resolution Scaling

PixelRoot32 features a powerful Independent Resolution Scaling system. This allows the engine to render internally at a lower resolution (Logical Resolution) and then scale the final image to the display's actual hardware resolution (Physical Resolution).

Why use Resolution Scaling?

On microcontrollers like the ESP32, memory and processing power are limited. Rendering at a full 240x240 resolution consumes significant RAM and CPU cycles for every pixel drawn.

By using a lower logical resolution (e.g., 128x128): 1. Memory Savings: A 128x128 8bpp buffer uses ~16KB, while 240x240 uses ~57KB (72% reduction). 2. Performance Boost: Fewer pixels to process means more complex scenes and higher FPS. 3. Retro Aesthetic: Nearest-neighbor scaling preserves the pixel-art look perfectly.

Logical vs Physical Resolution

  • Logical Resolution: The virtual canvas where your game logic, sprites, and UI are drawn.
  • Physical Resolution: The actual pixel dimensions of your hardware display.
flowchart LR
    subgraph Logical [Logical Resolution 128x128]
        A[Game Logic] --> B[Renderer API]
        B --> C[Internal Framebuffer]
    end

    subgraph Scaling [Hardware Scaling]
        C --> D[Nearest Neighbor Scaler]
    end

    subgraph Physical [Physical Display 240x240]
        D --> E[SPI/DMA Transfer]
        E --> F[LCD Hardware]
    end

Configuration

Using Presets

The easiest way to configure scaling is using the ResolutionPresets helper.

#include <graphics/ResolutionPresets.h>

// Create a config for 128x128 logical resolution scaled to 240x240 physical
auto config = pixelroot32::graphics::ResolutionPresets::create(
    pixelroot32::graphics::RES_128x128,
    pixelroot32::graphics::ST7789
);

Manual Configuration

You can also specify custom dimensions in the DisplayConfig constructor.

pixelroot32::graphics::DisplayConfig config(
    pixelroot32::graphics::ST7789, // Driver
    0,                      // Rotation
    240, 240,               // Physical Width, Physical Height
    160, 160                // Logical Width, Logical Height
);

Performance Impact

The following table shows estimated savings on an ESP32 for a standard 240x240 display:

Logical Resolution Memory (8bpp) RAM Savings FPS Gain (est.)
240x240 (Full) 57.6 KB 0% Baseline
160x160 25.6 KB ~55% +30%
128x128 16.4 KB ~72% +60%
96x96 9.2 KB ~84% +100%

Final FPS Analysis (v1.0.0)

At 240x240 physical pixels, the baseline limit was ~14 FPS due to SPI overhead. However, in v1.0.0, the engine achieves ~43 FPS stable at this resolution via:

  • DMA Pipelining: No CPU stalls while waiting for the bus.
  • Fast-Path Scaling: Direct 32-bit row copying without individual pixel processing.

To exceed 43 FPS, you must either: 1. Use a smaller physical display (128x128 physical = 60+ FPS). 2. Use a faster SPI clock (Experimental 80MHz = 60+ FPS, but may be unstable). 3. Reduce the rendering area using Logical Offsets.

Implementation Details

Nearest Neighbor Scaling

The engine uses a Nearest Neighbor algorithm optimized for ESP32. It avoids floating-point math by using pre-calculated Lookup Tables (LUTs).

On-the-fly Scaling

To save even more RAM, the engine does not maintain a physical-sized buffer. Instead, it scales the image line-by-line during the SPI DMA transfer. This means the only large buffer in memory is the small logical one.

Profiling

You can measure the performance of the scaling system by enabling the Debug Statistics Overlay. This provides real-time data on FPS, CPU load, and RAM usage directly on the screen.

See Engine - Debug Overlay for instructions on how to enable it.

Alternatively, you can enable low-level profiling in EngineConfig.h:

#define PIXELROOT32_ENABLE_PROFILING

This will output the time taken for scaling and transfer to the Serial monitor: [PROFILING] Scaled Transfer: 12450 us (80 FPS max)

Best Practices

  1. Aspect Ratio: Keep the logical aspect ratio the same as the physical one to avoid stretching.
  2. Integer Multiples: For the sharpest results, try to use logical resolutions that are simple fractions of the physical resolution (e.g., 120x120 for a 240x240 screen).
  3. Hardware Recommendation: For high-action games requiring 30+ FPS (like the Metroidvania sample), the engine now supports up to ~43 FPS on 240x240 displays at 40MHz. While 128x128 physical displays can still reach 60+ FPS, the v1.0.0 optimizations (DMA Pipelining) make 240x240 displays perfectly viable for most games.
  4. UI Positioning: Use UIAnchorLayout to ensure your UI elements stay correctly positioned regardless of the logical resolution chosen.