What the library functions cost per frame in real scenes, measured on luna (cycle-accurate) on 2026-10-09 with the SDK of that day (0.49.0 in preparation; first measured on 2026-09-26, and the differences are noted). PHILOSOPHY.md asks every API with a cost to say it; this page is the measured side of that promise, and docs/BENCHMARK.md the compiler side.
Unit: master cycles (mclk) per frame. An NTSC frame is about 357,370 mclk (21.477 MHz / 60.1 Hz); the VBlank, where VRAM may be written, about 49,000 of them (48,988: 37 lines of 1324 available master clocks, snesdev-wiki "Timing"). 1 % of a frame is about 3,570 mclk.
| Scene | What runs | Idle (in WaitForVBlank) |
|---|---|---|
| examples/games/likemario | walking right: map scroll, dynamic sprite, animation, SNESMOD | 88 % of the frame |
| examples/games/rpg | walking: map, sprites, HUD | 91 % of the frame |
| examples/sprites/sprite_swarm | many sprites moved every frame (direct OAM writes) | 69 % of the frame |
| examples/games/tetris | playing: board redraw, HUD, SNESMOD | 81 % of the frame |
| examples/audio/snesmod_music | music playing, nothing else | 98 % of the frame |
Idle time is what the game still has. On 2026-09-26 sprite_swarm had 17 % of its frame left and was close to dropping frames; on 2026-10-09 it has 69 %.
Mean mclk per frame over 300 frames, and in brackets an upper bound of the worst frame. A function appears only in the scenes that call it, and only above 300 mclk per frame.
| Function | likemario | rpg | sprite_swarm | tetris | snesmod_music |
|---|---|---|---|---|---|
| NmiHandler | 8,162 (8,170) | 8,163 (8,170) | 7,670 (7,716) | 5,676 (5,676) | 5,650 (5,650) |
| oamSet | 3,426 (4,068) | ||||
| animTick | 3,387 (5,306) | ||||
| animPlay | 2,125 (3,214) | ||||
| dmaCopyVram | 1,747 (2,248) | ||||
| oamSetSize | 1,736 (1,970) | ||||
| oamDynamicDraw | 1,351 (1,626) | ||||
| oamHide | 624 (4,290) | ||||
| bgSetScroll | 561 (600) | 589 (640) | 560 (560) |
The per-function figures are what luna counts at the function's own labels. Asm routines a function calls under their own names (the map engine's map_prepare_column, SNESMOD's SPC transfer routines) are not added to it, and code the compiler inlined into the caller is counted in the caller.
Until 2026-10-03 a handful of small functions were inline in their headers (fixSin, fixCos, textSetPos, setScreenOn, setScreenOff, getBrightness, colorMathInit, colorMathSetLayers, colorMathDisable, mosaicInit, hdmaWaveSetSpeed, scopeCalibrate, scopeSetHoldDelay and the two ease functions). An inline body needs its variables in the public header, and those were names a game would collide with (sine_table, cursor_x, force_blanked). They are ordinary lib functions now.
The one that runs per frame is fixSin / fixCos. Measured on examples/backgrounds/mode2, which calls fixSin 32 times a frame (luna profile, frames 60 to 300):
| busy master cycles per frame | share of the frame | |
|---|---|---|
| inlined (before) | 62,896 | 17.60 % |
| compiled C function | 77,415 | 21.66 % |
| assembly function (what ships) | 65,959 | 18.46 % |
So a call costs about 96 master cycles more than the inlined lookup did, and the first, compiled version cost about 450: fixSin and fixCos are written in assembly for that reason. The other functions run at setup or once per frame, where a call is not measurable.
oamDrawMetasprite() (2026-10-03) takes the base tile, palette and size from a MetaspriteStyle instead of three arguments. Measured on examples/sprites/metasprite, two metasprites a frame (luna profile, frames 60 to 300):
| master cycles per frame in the draw | share of the frame | |
|---|---|---|
| the former public oamDrawMeta (before; removed from the API 2026-10-05) | 51,992 | 14.55 % |
| one function, flip tested per piece | 58,190 | 16.28 % |
| one function, two loops | 61,252 | 17.14 % |
| reads the style, then runs the old loop (what ships) | 56,057 | 15.69 % |
About 2,000 master cycles per call, 0.6 % of a frame per metasprite drawn: three reads through the style pointer and one more call. (Since 2026-10-12 the draw is assembly, lib/source/sprite_meta.asm, and writes each piece itself instead of calling oamSet and oamSetSize: 27,470 master cycles a frame for the two metasprites of this example, 61,460 before — the table above is the history of the C version.) The two attempts above it were slower although they add no call: with the mirrored loop in the same function, the compiler's copies between the two loops fell on every piece. The mirrored draw is therefore a function of its own, and the unflipped one still runs the same loop, now the internal (removed from the API) oamDrawMeta.
devtools/libbench asks both libraries for the same things — what a game asks every frame — and times each request on luna. Master cycles for one frame's worth; docs/BENCHMARK.md has the same comparison for the compilers.
| Row | What it asks for | PVSnesLib | OpenSNES | |
|---|---|---|---|---|
| idle | a frame that only waits (the SDK's vblank handler) | 7,184 | 5,780 | -19.5 % |
| pad | held, pressed, released of pad 0, ten times | 11,427 | 9,722 | -14.9 % |
| scroll | bgSetScroll on three backgrounds, ten times | 39,540 | 27,343 | -30.8 % |
| oamset | oamSet on 32 sprites | 81,061 | 64,939 | -19.9 % |
| oamxy | oamSetXY on 32 sprites | 45,048 | 42,453 | -5.8 % |
| oamsize | the size of 32 sprites | 48,744 | 28,874 | -40.8 % |
| dma | 2 KB to VRAM, one dmaCopyVram | 17,897 | 17,868 | -0.2 % |
| text | 20 characters printed and shown | 84,678 | 17,216 | -79.7 % |
| frame | one frame: pad, three scrolls, 32 sprites, 20 characters | 169,798 | 91,147 | -46.3 % |
| worldc | 19 world-space sprites placed by a loop in C (the same source) | 129,332 | 53,875 | -58.3 % |
| world | the same 19 sprites: oamPlaceWorld here, the C loop there | 129,332 | 31,738 | -75.5 % |
| vramc | six 128-byte VRAM transfers, six dmaCopyVram calls | 17,395 | 15,886 | -8.7 % |
| vramq | the same six, the part paid in VBlank: one vramQueueFlush here, the calls there | 17,395 | 10,738 | -38.3 % |
| worldo | the 19 sprites sorted by depth (an order array): oamPlaceWorld here, the C loop there | 136,701 | 36,299 | -73.4 % |
OpenSNES costs no more than PVSnesLib on 14 of the 14 rows. PVSnesLib at fa758c9b 2025-12-28.
How a row is timed. Each row is built twice per SDK, with its library calls and with the same loops around nothing; both run the same number of frames, and the cost is what the symbols of the first gained over the second. Both SDKs build the scene as LoROM, SlowROM (the build's default, USE_FASTROM=0): a FastROM game pays about a quarter less for the instruction fetches of these routines, on either side. The call, its arguments, the library function and what it calls are counted, the loop is not. text and frame include the frame boundary, where each SDK's handler sends what the calls prepared; idle is that handler alone, in a ROM that only waits.
Reading it.
entries[].per_frame.mean / .max per symbol; sum the function@label entries of one function. The other scenes use the same window (frames 120-420; tetris 300-600, START at frame 100) with the inputs of the table above: likemario RIGHT held from frame 130, rpg RIGHT then DOWN, tetris START. Re-measure after a change to the NMI handler, the sprite or animation modules, and update this page.