Skip to content

gfx: graphics

gfx is everything a game shows on screen: the bundle that carries its art, the palettes and their variants, the sprites of the display list, the pool that keeps bitmaps resident, and the background, a scrolling world of 8×8 tiles that doubles as text mode.

One rule governs all of it, and it is worth repeating: the order of gfx.sprite() calls is the paint order, the last entry is in front. There is no other priority mechanism between sprites. The rules page lists the ones that apply to the whole API. Actors and animations, which feed gfx.sprite through phy.sprite, live in phy; the constants (gfx.FLIP_H, gfx.TILE_FONT8...) and the limits are gathered in constants and limits.

The bundle

A game folder carries its art in palette groups; the file prefixes make the links, there is no manifest:

Files What it is
<g>.pal (768 bytes), <g>.atlas (Kivy format, multi-page), the indexed PNGs the atlas names a group: a palette and the images quantised with it. A game has one or several (game, level2...)
<g>.<v>.pal a variant: same indexes, other colours, for day/night or a flash
<g>-<f>.map (text), <g>-<f>-tiles.png (at most 704 tiles of 8×8, in reading order, tile 0 is the hole) a background: a tileset and its map, in the group's palette

Group and background names use [a-z0-9_]. The App's bundle editor produces these files: the .pal and the .atlas from indexed PNGs, the tileset and the map from a background image.

gfx.bundle() → ok, message

Installs the whole game: every palette and variant, every region of every group into the library, the system fonts first (ids 0..127), then the art, named <g>/<region>, and every background, validated. The first palette (in name order) and the first background become active. On the board: seconds the first time (the NOR flash of RP2 and RP3 is programmed), nothing afterwards thanks to the cache; in the simulator, a few milliseconds.

Everything is counted before anything is installed: too many sprites (1 024, fonts included), too many palettes (12), too many backgrounds (16), or more than 2 MiB of art, and the bundle is refused whole, with the figure; nothing has moved. A true-colour PNG, a malformed map, a region outside its page: same thing, with the file name and the line.

gfx.id(name) → id | nil

The bitmap id of the region "group/region", what gfx.sprite takes. nil if it does not exist: it is up to the game to say so, at load time.

Palettes and colours

gfx.palette(name)true

gfx.fade(name, frames)true

The active palette, right away ("game", "game.night"); or gliding, one palette interpolated in RGB565 per frame, the row modes switching at the end. The game only draws images of the active group: that is its responsibility. An unknown name is an error (with the list of the bundle's palettes), as for gfx.background.

A call restarts the fade from zero

gfx.fade is a trigger, not a state. Called every frame, the fade stays frozen on its first step. Call it once, then let the frames run.

gfx.color(i, r, g, b) / gfx.color_get(i) → r, g, b / gfx.effect(i, mode)

One colour of the current palette (i from 1 to 255, components without clamping: (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3), read back requantised; the blend mode of an index (gfx.TRANSPARENT, OPAQUE, TRANSLUCENT, ADDITIVE, SHADOW, BEHIND). Each of them rebuilds the blend table: keep them for load time, not for the frame.

gfx.backdrop(r, g, b) → nothing

The colour of what is not drawn, a register separate from the palette, since index 0 is reserved on both sides: colour key of the sprites, hole of the background.

gfx.sky(i0, n) / gfx.sky()

The sky: n consecutive palette indexes, starting at i0, form a vertical ramp that every background pixel at 0 takes instead of the backdrop, the empty cell as well as the transparent pixel inside a tile; screen line y renders index i0 + y·n // 480. It is a screen ramp (the horizon does not scroll with the camera), and these are real indexes: translucent, additive and shadow pixels blend with the local colour of the sky, exactly; and since the ramp lives in the palette, gfx.fade does the sunset on its own. Rows "behind the background" cross the sky as they cross the void: a sprite behind stays visible. Without arguments, it switches off: the void goes back to the backdrop. Cost on the console: one row of the blend table copied per line (512 bytes), nothing per pixel. The colours of the ramp come from the bundle or from gfx.color.

Sprites

gfx.sprite(id, x, y [, flags]) → nothing

Adds an entry to the display list. The order of calls is the paint order. x and y are numbers, rounded as on the board (lroundf: to the nearest, halves away from zero, 100.5 → 101, −0.5 → −1). flags: gfx.FLIP_H, gfx.FLIP_V.

Never an error. An unknown id, a sprite entirely off screen, a full list (256 per frame) or a bitmap not yet in the pool are counted, not reported: see gfx.stats(). The last case is the most important one to know: the bitmap is loaded on first use and, if the frame's batch is full, it misses a frame or two (missing). A game that wants to be sure of a sprite asks for it in advance with gfx.load.

For an actor, phy.sprite returns exactly the three values gfx.sprite takes, so gfx.sprite(phy.sprite(a)) draws its current animation image: see phy.

gfx.print(x, y, s [, size]) → boolean

Text as sprites, on top of everything, at any pixel: for a HUD. The system font: size 1 = 8×8, size 2 = 16×16; codes 32..95, lowercase folded to uppercase, any other character = the full block; ink = index 191. One display list entry per character (a space costs none), advance of 8 or 16 px. false if an entry was lost.

gfx.size(id) → w, h

The bitmap's dimensions. Error if the id is unknown.

The pool: resident art

The pool is the SRAM of RP2: 320 KiB, in pages of 64 bytes, a cache of the library. A bitmap enters it on its first gfx.sprite, 16 orders and 96 KiB per frame at most, the rest waits for the next frame, and leaves it by LRU eviction when the pool is full, never if it was drawn in the last three frames. What overflows is therefore the working set of a frame, not the library: a game can carry 2 MiB of art as long as one frame does not show more than 320 KiB of it. Beyond that, sprites go missing, every frame.

gfx.load(id) → boolean

Loads without drawing: true if the bitmap is resident or leaves with this frame's batch, false if it waits (batch full) or cannot fit. Call it when loading a scene, or one frame before a sprite appears.

gfx.unload(id) / gfx.resident(id) → boolean

Unloads (the pages come back three frames later); says whether it is there. Optional since the LRU: advice, not an obligation.

gfx.pool() → used, total, residents

gfx.stats()

The bytes of pages held, 327 680, the number of resident bitmaps; and the counters of gfx.sprite (used: entries of the last frame; the others accumulate).

The background: a world of tiles

The background is not an image: it is a scrolling world of 8×8 tiles, that RP3 rasterises from its NOR flash. It uses the group's palette, tile 0 is the hole (the backdrop), tiles 704..1023 are the system tiles: two fonts, the same glyphs as gfx.print.

gfx.background(name)true

The background "game-level1": its tileset and its map. On the board, about 2 ms with no black screen. The first background of the bundle is active by default. An unknown name is an error (with the list of the bundle's backgrounds): the auto-loaded background would make a silent false invisible, and a stale main.lua would show "the right" background by accident. Same rule for gfx.palette and gfx.fade.

gfx.camera(x, y) → nothing

The camera in world pixels, integers; latched at the end of the frame, the whole next frame is rendered against it, with the sprites of the same frame. In NOR mode, outside the world you see the hole; in local mode, everything is modulo 1 024 × 512.

gfx.bg_local(on) → nothing

Text mode. The window of 128 × 64 tiles that RP3 holds becomes the world itself: toroidal, never reloaded from the NOR flash; what it contained stays visible as long as it is not overwritten. false: back to the background's world, the window reloaded whole.

gfx.tile(x, y, e) / gfx.tiles(x, y, t) / gfx.fill(x, y, w, h, e) / gfx.text(x, y, s [, size]) → boolean

Write into the map, in tiles (x, y modulo 128 × 64). An entry e is an id 0..1023 plus gfx.TILE_FLIP_H / gfx.TILE_FLIP_V; bits 12-15 are reserved, and the simulator refuses them with the line, whereas the board would read outside the tileset. gfx.text writes the system font: size 1 = 8×8 (one tile per character, gfx.TILE_FONT8 + code − 32), size 2 = 16×16 (four tiles, two columns per character).

The channel is slow, and it shows: the writes leave with the video packet, 624 bytes per frame, applied at the end of the frame, visible at the next one. A row of 128 tiles is a run of 260 bytes, a line of 80 characters makes 164; a full screen of 80 × 60 tiles takes 16 to 20 frames. They return false if the FIFO (24 KiB) overflowed. Outside local mode, the same writes are overprints that the next camera move erases.

This is the way for HUD text and menus: no display list entry, no blitter cycle. A counter that changes writes a few tiles; a line rewritten full width costs only one run.