RP2350 vs ESP32-P4: choosing the Saucelet's 3D chip

· 13 min read

By Phil · building Legitsauce

The Saucelet is a game machine small enough for your keychain, and 3D is where it gets ambitious. Before I commit to a chip for that, I want measurements, not spec sheets. So I'm running the same demos on two candidates and comparing them frame by frame.

This post covers how the test works, what it has shown so far, and what's still open. There's no final decision yet. For the demos themselves, see Saucelet progress.

The two chips

RP2350 (Seeed XIAO RP2350) ESP32-P4 (M5Stack Stamp-P4)
Cores 2 x Arm Cortex-M33 at 150 MHz 2 x RISC-V at 360 MHz (chip revision v1.3)
On-chip RAM 520 KB 768 KB, part of it used as cache
Extra memory none 32 MB PSRAM in the package
Special hardware interpolators, DSP SIMD, PIO, hardware divide PIE: 128-bit integer SIMD
Board price (hobbyist module) $4.90 $13

How the test works

  • The same code on both chips. The RP2350 demos were compiled for the P4 unchanged. Where they use RP2350-only hardware, the P4 build falls back to plain C versions of those instructions.
  • Plus a "best" version for each chip. Where the P4 has a better way to do something, such as its wide 128-bit stores for clearing memory, I wrote that version too, so each chip is judged at its best as well as on equal code.
  • Both cores on both chips, with the same split of work between them.
  • Every frame is checked. Both chips' frames are compared against reference renders from a PC model of the same code, using the image metric from my Godot exporter: the average brightness error over 8 by 8 pixel blocks ("block-luma MAE") and the average colour error per channel ("RGB MAE"), both on a 0 to 255 scale. The P4's fixed-point frames matched the PC model pixel for pixel on every demo whose state doesn't depend on timing.
  • The XIAO runs on its screen. The P4 runs headless, with no screen attached, and its screen push is modelled from the bus speed: 11.52 milliseconds a frame at 80 MHz, overlapped with drawing, as on the XIAO.
  • On-chip RAM only on the P4. Its PSRAM measured slow (more on that below), so the P4 builds won't even compile with it switched on. Every demo fit on chip.

The RP2350 punches above its clock: it can interpolate

On paper the P4 should win everything. It runs at 2.4 times the clock speed. But the RP2350 has a few small pieces of hardware that do a lot of work per cycle.

Interpolators. Each RP2350 core has two. You give one a starting point and a step, and every read hands back the next texture address, wrap-around included. That's the core of texture mapping, and the hardware does it for free.

The mode 7 demo, a kart on a turning, tilted floor, shows the effect best:

  • On the XIAO, with the interpolators, a frame takes 1.8 milliseconds on one core and 1.45 on the other. The same picture in plain C on one core takes 35 milliseconds.
  • On the P4, the same code with the interpolators modelled in C runs at 0.32x the XIAO's speed. It spends 71 cycles a pixel where the RP2350 spends 9.
  • Rewritten natively for the P4, it's 1.24x faster than the XIAO. Even then, the P4 needs 1.93 of its cycles for every RP2350 cycle.
Two square frames of the mode 7 demo, a yellow kart on a grey track curving right: a blurry camera photo of the XIAO's screen on the left and a clean frame on the right
Mode 7 on both chips. Left: the XIAO's screen, photographed. The light blobs are reflections on the screen's cover, not the image. Right: the P4's frame 150, which matched the PC model pixel for pixel, so the PC model's frame is shown.

In 3D, the interpolators step colour and texture coordinates along each row. My cycle model puts a textured pixel at about 3.25 cycles with them, against 12 in plain C.

DSP SIMD. The M33 cores can work on two 16-bit pixels in one instruction. A short run of these instructions checks two pixels at once for the transparent colour and picks the right one, with no branching. The P4 has no equivalent, so sprite storms with transparent pixels run only 1.14 to 1.31x faster on it, despite the much higher clock.

PIO. The RP2350's programmable I/O blocks, together with DMA, turn an 8-bit palette frame into screen colours while it's being sent. That freed 2.2 milliseconds of CPU time a frame on the XIAO. The P4 has to do that work on a core, which makes it 0.78x the XIAO's speed on that demo.

Where the P4 wins: about twice the 3D throughput

For 3D meshes the P4 is 1.9 to 2.3x faster on the same source code. The clearest case is the small 3D scene, about 1,500 triangles a frame with Ka'zhurn from Ka'zhurn: The Unbinding:

  • On the XIAO, a frame takes 17 to 19 milliseconds on one core and 16 to 18 on the other. That's over the 16.6 milliseconds of a 60 Hz refresh, so it runs in the 45 Hz mode.
  • On the P4, it takes 8.41 and 8.07 milliseconds, with the worst frame at 11.3. It holds 60 FPS with 5 milliseconds to spare.
  • In the triangle storm, the P4 holds 351 shapes at 60 FPS where the XIAO holds about 155.

Both chips ran the same version of the engine for these numbers. The engine has been sped up since, and the XIAO now reaches 55.9 to 58.9 FPS on the 3D scene when run at 60 Hz. The P4 hasn't been re-run on the new version yet.

Two frames of the 3D scene with Ka'zhurn, brick pillars and crates on a tiled floor: on the left a frame read back from the XIAO, on the right a frame from the ESP32-P4
The 3D scene on both chips. Left: a frame read back from the XIAO over USB. Right: the P4's frame 80, which matched the PC model pixel for pixel, so the PC model's frame is shown. Ka'zhurn is from Ka'zhurn: The Unbinding.

Why about 2x and not more:

  • Clock. 360 MHz against 150 is 2.4x.
  • Cycles per pixel. Drawing a pixel of a mesh takes the P4 1.04 to 1.19 of its cycles for every RP2350 cycle, so the 2.4x clock arrives as about 2.0 to 2.2x.
  • Cache. The P4 has a 128 KB cache in front of its flash. When a demo streams data from flash, like a compressed background, the whole stream fits in that cache after the first frame. The XIAO's flash cache is 16 KB and misses on every pass. That's where the P4's biggest wins are: 2.8 to 11.7x on the compressed backgrounds, depending on the format.

The P4 gains less on the two game demos: 1.2 to 1.7x. Those numbers are preliminary, because they come from an older version of the engine than the one the XIAO now runs. Their data is bigger than the P4's 64 KB first-level cache, and the older engine does more math per row. Which of the two costs more isn't measured yet.

Per dollar

At hobbyist module prices, the Stamp-P4 costs $13 and the XIAO RP2350 $4.90, so 2.65 times as much. To win per dollar, the P4 has to be more than 2.65x faster, and it is only on the flash-streamed backgrounds. It ties on see-through sprites. Everywhere else, the XIAO gives more frame time per dollar. Here's the price times the frame time of the slower core (lower is better):

Demo XIAO P4 (best version)
Tile map 14.7 19.2
Ka'zhurn, 1,000 triangles 54 64
The 3D scene 88 109
Triangle storm, shapes at 60 FPS per dollar (higher is better) 32 27

The bare chips differ by about 3.5x: around $1.10 for an RP2350A and around $4 for an ESP32-P4. A finished board costs much more than either chip, so treat these as a guide to the chips, not a product price.

What the P4 buys is headroom: the 3D scene at 60 FPS instead of 45, more than twice the triangles in the storm, and a large cache for streamed assets.

Fixed-point math: fast on both chips, and hard to spot

Every 3D demo draws each frame two ways. The float path uses floating-point math everywhere. The fixed path uses whole numbers with an agreed scale, which chips handle quickly. The chips show the fixed frame, and the float frame is the reference it's measured against.

What the fixed path does, in plain words:

  • 28.4 coordinates. Screen positions are whole numbers counted in sixteenths of a pixel: 28 bits for the whole part and 4 for the fraction. That's precise enough that triangle edges land where they should.
  • 16-bit depth. Each pixel remembers how far away it is in 16 bits, enough to sort near from far at this size.
  • Perspective-correct planes in fixed point. Textures have to shrink toward the horizon correctly or they warp. Both chips do that with whole-number math: the P4 with a quick reciprocal per pixel, the RP2350 with one exact divide every 16 pixels or so and the interpolators in between, keeping the error under a set fraction of a texel (1/32 in the textured triangle demo).
  • No anti-aliasing in either path, so the comparison is like for like.

On the P4's own 3D demos, measured on the chip:

Demo Float ms Fixed ms Speedup Block-luma MAE, fixed vs float
RGB triangle 5.76 1.39 4.15x 0.054
Textured triangle 6.86 1.19 5.78x 0.063
Ka'zhurn, 37.5 K triangles 79.5 57.2 1.39x 0.137
Ka'zhurn, 4.5 K triangles 34.8 16.1 2.16x 0.138

The smallest gain is on the full-detail Ka'zhurn, where much of the time goes into reading the model from flash and setting up thousands of triangles smaller than a pixel, not into the math the fixed path speeds up. Against Godot's own render of the same scenes, the fixed path lands about as close as the float path: block-luma 0.089, 0.088, 0.329 and 0.487 for fixed, against 0.093, 0.055, 0.409 and 0.534 for float.

On the RP2350, the fixed path is 17.4x faster than the float path for the RGB triangle and 12.9x for the textured one, as projected by my cycle model. Measured on the chip, the fixed frames' block-luma error against float is 0.038 for the RGB triangle, 0.086 for the textured triangle, 0.052 for Ka'zhurn and 0.621 for the 3D scene, on average. All of that is well under one step on a 0 to 255 scale.

Three nearly identical frames of the 3D scene with Ka'zhurn among brick pillars and crates: Godot's own render, the float path and the fixed-point path
Frame 80 of the 3D scene: Godot's own render, the float path and the fixed-point path. The fixed frame differs from the float frame by a block-luma MAE of 0.43 on a 0 to 255 scale. Ka'zhurn is from Ka'zhurn: The Unbinding.

A few more technical details

Bands, because a frame doesn't fit twice

A full 240 by 240 frame is 115,200 bytes of colour, plus 115,200 more for depth. Two of each, the usual way to draw one frame while showing another, would be about 460 KB, nearly all of the RP2350's RAM before any game is loaded. So both chips draw the screen in 16-row bands. Each band is drawn, sent to the screen and its memory reused, while the two cores share out the bands between them.

Overlapping the screen push

Sending a full frame to the screen over SPI takes 12.3 milliseconds on the XIAO, about three quarters of a 60 Hz refresh. If drawing waited for that, there would be almost no time left to draw. Instead, each finished band goes out by DMA while the next ones are drawn, so a frame takes as long as the slower of drawing and sending, not both added together. On the P4 the same push is modelled at 11.52 milliseconds.

The tear model

The screen has no pin that says when it's safe to write. The XIAO asks the screen which line it's drawing, then keeps a model of the sweep: 344 lines at 48.2 microseconds each, 60.3 times a second. Each frame starts only inside a window where the write stays behind the sweep from top to bottom. In the 2D demos, the first five 3D demos and Mini Warlord, no frame was at risk of tearing. The P4 runs headless, so it has no tear measurement yet.

Why PSRAM is out, for now

The P4 carries 32 MB of PSRAM in its package, and it measured slow for this kind of work. Reading it in wide chunks reached 0.13 to 0.22 GB/s, against about 1.6 GB/s for on-chip RAM. A single read that misses the cache waits about 190 cycles.

To put 190 cycles in perspective: at 360 MHz, one core gets about 6 million cycles per 60 FPS frame. If each of the screen's 57,600 pixels missed the cache just once, that would be about 11 million cycles of waiting, almost two whole frames on one core. So the P4 demos keep everything on chip, and PSRAM stays off unless I decide otherwise.

Cluster LOD

Models are cut into small clusters of triangles and stored in a level of detail that suits a 240-pixel screen. The full-detail Ka'zhurn has 37,500 triangles, 3 to 4 times more than the screen can show. A 4,500-triangle version brings the P4's fixed frame from 57.2 to 16.1 milliseconds, and the change against Godot's render is 0.16 on the block-luma scale. The RP2350 draws a 1,000-triangle Ka'zhurn at 60 FPS.

The 45 Hz fallback

60 FPS is the default for every Saucelet game. A heavy scene may drop to 45 FPS, and when it does, the screen's own refresh drops to 45 Hz so frames stay locked to the sweep and motion stays even. 30 FPS is never allowed. Right now the 3D scene and Mini TPS run in it on the RP2350.

What's next

The next step is a fully optimised Mini Warlord, the small battlefield from Saucelet progress, on both chips, each using all of its hardware. The XIAO version now runs at 60.3 FPS, dipping to 53-56 in the biggest fights; the P4 version is next. Then comes the chip decision. I'll share both here.

If you've built 3D on small chips like these, I'd like to hear what worked for you.