Skip to content

picogame: enable on RP2040 - #11358

Open
lynt-smitka wants to merge 4 commits into
adafruit:mainfrom
MakerClassCZ:pg-pico
Open

picogame: enable on RP2040#11358
lynt-smitka wants to merge 4 commits into
adafruit:mainfrom
MakerClassCZ:pg-pico

Conversation

@lynt-smitka

Copy link
Copy Markdown

Enables picogame on raspberry_pi_pico and raspberry_pi_pico_w.

Neither fits at the port default -O3: pico has 48 KB free of its 1020 KB partition and pico_w 2 KB of its 1536 KB, while the engine needs 50 KB. Both build at -O2, which leaves around 140 KB free and measures within 1% of -O3 across the render kernels on this M0+.

pico_w sits at 99.9% today, so it builds all 17 translations on every pull request; with the flags it skips them like the other boards.

Neither fits at -O3: pico has 48 KB free of its 1020 KB partition and
pico_w 2 KB of its 1536 KB, while the engine needs 50 KB. At -O2 with
the loop passes both keep around 140 KB free, within 1% of -O3 on this
M0+.
Comment thread ports/raspberrypi/boards/raspberry_pi_pico/mpconfigboard.mk Outdated
They were set per board. Every RP2040 build has the same reason to use them, so
they belong next to the other RP2040 defaults, with a note on what each pass
does and why RP2350 keeps -O3.
@dhalbert dhalbert changed the title picogame: enable on raspberry_pi_pico and raspberry_pi_pico_w picogame: enable on RP2040 Sep 13, 2026

@dhalbert dhalbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good; I would just like some comment tweaks.

Comment thread ports/raspberrypi/mpconfigport.mk Outdated
Comment thread ports/raspberrypi/mpconfigport.mk Outdated
@lynt-smitka

Copy link
Copy Markdown
Author

I'll rework it a bit, including adding better comments. I ran the tests for each flag separately and I have to interpret the results (the vectorization need few more tests). I hope it will be done in few hours.

@dhalbert

Copy link
Copy Markdown
Collaborator

I meant to have this enabled for all of RP2040 -- sorry I was talking about optimization flags only.

@lynt-smitka

Copy link
Copy Markdown
Author

The new recommendation after heavy benchmarking individual flags:

OPTIMIZATION_FLAGS ?= -O2 -funswitch-loops -fvect-cost-model=dynamic

AI summarized benchmark results:

1. Each flag on its own

added to -O2 flash blit_flip mode7 fill_rect fill_circle vspans 4 KB copy AES 4 KB interpreter
nothing (-O2) baseline 113 5915 1163 322 3703 3857 17248 154754
-funswitch-loops +16344 87 5396 1162 322 3704 3851 17236 154754
-fpredictive-commoning +40 111 5914 1176 322 3709 3857 17266 154754
-fgcse-after-reload -168 111 5941 1165 322 3702 3857 17248 154754
-ftree-partial-pre +900 111 5913 1163 322 3703 3857 17218 154846
-fsplit-paths +2376 118 5912 1177 321 3708 3881 17254 154754
-fvect-cost-model=dynamic +4328 111 5913 731 245 3975 2441 14599 154754

Two flags do the work, and they do not overlap. -funswitch-loops owns the sprite blit and mode7,
and it is the expensive one at 16 KB. -fvect-cost-model=dynamic owns the fills, the memory copies
and AES, and it costs 4 KB. The other four are between -168 and +2,376 bytes and win no column.

vspans is left unmarked here because every build except the cost model sits between 3,702 and
3,709, a 0.2 % spread. It is the one call the cost model makes slower, which section 4 covers.

The interpreter does not move under any flag. fib(20) and a 20,000-iteration integer loop stay
within 0.1 % everywhere, so anything spending its time in the bytecode dispatch loop sees no
difference at all.

2. Combinations

set flash blit_flip mode7 fill_rect fill_circle vspans 4 KB copy AES 4 KB
-O2 baseline 113 5915 1163 322 3703 3857 17248
cost model alone +4328 111 5913 731 245 3975 2441 14599
predcom + cost model +4368 111 5913 730 245 3983 2441 14459
five passes +18860 87 5396 1162 332 3705 3851 17181
unswitch + cost model +20664 87 5413 732 245 3945 2441 14508
unswitch + predcom + cost model +20704 87 5417 733 246 3945 2447 14514
five passes + cost model +23252 87 5395 729 249 3944 2453 14538
-O3 +151020 87 5406 739 243 3807 2435 13543

Two flags reach -O3 on every kernel for 20,664 bytes instead of 151,020, which is 130,356 bytes
saved on every RP2040 board. The only column -O3 still holds alone is AES, by 7 %. Adding the
other three passes on top costs a further 2,588 bytes and wins nothing.

Dropping -funswitch-loops as well would save another 16,336 bytes, but the sprite blit is then
24 % slower than the -O3 those boards ship today. That is a real regression, not a wash, so the
set keeps it.

3. Outside picogame

-funswitch-loops rewrote 94 functions, so the drawing kernels are not the only place it shows.
These are core CircuitPython calls, same board, and the two -O2 columns are separate flashes so
the repeatability is visible in the table itself.

call -O2 -O2 again unswitch the set -O3
bitmaptools.alphablend, 96x72 13287 13293 12982 12982 12640
ulab sum, 1200 int16 4080 4080 3983 3987 3977
ulab arithmetic on 1200 floats 7904 7916 7763 7769 7792
ulab sum + mean + std 15457 15455 15219 15225 15220
bitmaptools.rotozoom 39154 39093 38830 38775 38800
bitmaptools.fill_region 4646 4646 4653 4638 4650

The gains are small but they are not noise and they are not picogame: 2.3 % on alpha blending,
2.4 % on an integer ulab reduction, 1.8 % on float arithmetic, 1.5 % on the statistics. The set
matches -O3 on all of them except alpha blending, where -O3 keeps a further 2.6 %.
fill_region is a memset wrapper and moves for neither flag.

4. What the two flags do

-funswitch-loops changed 94 functions for +16,236 bytes, the largest being:

common_hal_bitmaptools_alphablend       1762 -> 3394  (+1632)
picogame_blit_bitmap                    1928 -> 3506  (+1578)
numerical_sum_mean_std_ndarray          2188 -> 3064   (+876)
common_hal_audioi2sin_i2sin_fill_buffer 1148 -> 2008   (+860)

It did two different things to the two calls that got faster, and only one of them is unswitching in
the textbook sense.

In mode7 it is textbook. The format == PAL8 test is hoisted out of the whole row nest and the
nest is kept in two copies, one for RGB565 and one for PAL8. The RGB565 copy then carries no format
test and no row-interpolation call at all, because neither can be reached there. That removes three
instructions from the per-pixel loop, 31 cycles per pixel instead of 34. The predicted ratio is
0.912 and the measured one is 5396/5915 = 0.9123.

In the blit it is not. The specialization on the flip and transpose flags already exists at
-O2, in both builds the flags are tested once per row in the loop preheader, and the number of
inner-loop copies is the same. What changed is the unroll factor of the flipped opaque loop, two
pixels per iteration at -O2 and four with the flag, with a remainder prologue in front. That takes
one taken branch per two pixels down to one per four. The predicted saving over the 1,024 pixels of
the benchmark is 27 microseconds and the measured one is 28. That the unswitching is what let the
unroller reach four is the natural reading, but the binaries only show that the unroll factor and
the register allocation changed while the specialization structure did not.

This also explains a column that never moves. The unflipped blit stays at 59 to 62 microseconds in
every build because that case takes a memcpy fast path, which neither flag touches.

-fvect-cost-model=dynamic is narrower. It rewrites exactly three functions: memset (88 to 248
bytes), memcpy (94 to 166) and picogame's own row filler fill565 (96 to 148), each into an
unrolled version with an alignment prologue and epilogue. Every drawing function that calls them is
byte-identical to -O2. That is why it wins the fills, the copies and AES and nothing else.

5. What the cost model costs

It is a trade, not a free win. The one call that gets slower is vspans, the span renderer that
fills the raycaster's wall columns. -O2 sets -fvect-cost-model=very-cheap, and for that loop
very-cheap is the better answer.

build vspans
-O2 (very-cheap) 3703
-O2 + five passes + dynamic 3944 (+7 %)
-O3 (dynamic) 3807 (+3 %)
-O3 with very-cheap forced back on 2548 (-31 %)
five passes + the -O3 inline parameters, no vectorizer change 2545 (-31 %)

So vspans is fastest under very-cheap, and what speeds it up is -O3's inline parameters, not any
of the passes. Those cost 92,744 bytes on their own, which is why the set does not take them.

The reason is the unrolled fill565 from section 4. It pays on long runs, and fill_rect fills 240
pixels per row, so it gains 37 %. vspans issues runs four pixels wide, so every call pays the
alignment prologue and never reaches the fast body.

The same trade shows on big integers: -O3 takes the 60-factorial loop from 971 to 1516
microseconds, a 56 % regression that neither the passes nor the cost model cause on their own.

6. The eight -O3 passes left out

Added on top of the five passes. The last column is what the pass did to the machine code of the
drawing functions.

added on top of the five flash fill_rect fill_circle hot functions
-fipa-cp-clone +11280 1168 325 byte-identical
-fpeel-loops +24052 1168 325 byte-identical
-floop-unroll-and-jam +0 1177 332 byte-identical
-fsplit-loops +3296 1162 322 byte-identical
-ftree-loop-distribution -40 1176 332 byte-identical
-funroll-completely-grow-size does not link
-fversion-loops-for-strides +5904 1162 322 rewrites blit and mode7
-floop-interchange +0 1163 332 byte-identical

Six of the seven that link leave the drawing code untouched. -fversion-loops-for-strides is the
exception: it rewrites 985 instructions in the blit and 214 in mode7 and is still no faster.
Together with the inline parameters these are the 130 KB between the set and -O3.

The five-pass set was 16-37 % behind -O3 on rectangle fills, memory copies
and aesio, because what recovers those is the vectorizer cost model, not the
loop passes. Adding -fvect-cost-model=dynamic closes that gap.

Four of the five passes then turn out to emit the same code as plain -O2 for
the loops that matter here, so drop them. The result matches -O3 on every
benchmark for 130 KB less flash, and is 2.5 KB smaller than the five-pass
set it replaces.
The port now sets the same flags for every RP2040, and this board's copy
assigned with = rather than ?=, so it would pin the old five-pass set and be
the one board that misses the port default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants