All docs

VPy and C: how they correspond

One runtime, libvpy, behind both languages: project files, program structure, every builtin and its C call, assets, targets, and VPy built through C.

VPy and C are two ways into the same runtime. The VPy builtins are implemented once, in C, in libvpy (vpy.h in the SDK). VPy programs on the ARM targets and C games both link it, and a C game calls its functions directly. The 3D builtins call the SDK's vpy3d, which C games use too. So when you learn one language you have learned most of the other: the same coordinates, the same assets, the same frame model, often the same names.

This page lines the two up. For the C side in the IDE see C Projects, and for the full C API see libvpy and vpy3d.


Project files

VPyC
Project file<name>.vpyproj<name>.cvproj
Entrysrc/main.vpysrc/main.c
Who builds itvpy_cli, the IDE's compilermake, running the command in the .cvproj
TargetChosen in Settings (or --target on the command line)Chosen in Settings; each target has its own [targets.<t>] recipe
AssetsFound by folder under assets/ and named by stringCompiled to gen/<name>.h by the Makefile and named by symbol
SDK pin[sdk] version[sdk] version (same table)

A new VPy project (File → New → Project, language VPy):

[project]
name = "ship"
version = "0.1.0"
entry = "src/main.vpy"
 
[build]
output = "build/ship.bin"
optimization = 2
debug_symbols = true
 
[sources]
vpy = ["src/**/*.vpy"]
 
[resources]
vectors = ["assets/vectors/*.vec"]
music = ["assets/music/*.vmus"]
# ... one line per asset folder
 
[sdk]
version = "v1.4.4"

A new C project (language C (SDK)):

[project]
name = "ship"
type = "c-external"
target = "rp2350"
entry = "src/main.c"
 
[build]
command = "make"
args = ["rp2350"]
artifact = "build_rp2350/ship_sd.bin"
 
[targets.uvm2]
command = "make"
args = ["uvm2"]
artifact = "build_uvm2/ship.um2"
 
[simulate]
command = "make"
args = ["sim"]
module = "build_wasm/ship.js"
 
[sdk]
version = "v1.4.4"

The full C manifest is in C Projects, and the pin in Projects and the SDK.


Program structure

Both languages have a run-once function and a per-frame function. Neither one has a frame loop to write.

# VPy
x = 0
 
def main():
    SET_INTENSITY(127)
 
def loop():
    x = clamp(x + J1_X() / 32, -100, 100)
    PRINT_TEXT(-60, 60, "HELLO")
    DRAW_LINE(x - 10, 0, x + 10, 0, 127)
/* C */
#define VPY_SHORT_NAMES
#include <vpy.h>
 
static int x;
 
static void setup(void) { SET_INTENSITY(127); }
 
static void loop(void)
{
    x = vpy_clamp(x + J1_X() / 32, -100, 100);
    PRINT_TEXT(-60, 60, "HELLO");
    DRAW_LINE(x - 10, 0, x + 10, 0, 127);
}
 
int main(void) { vpy_run(setup, loop); return 0; }
VPyC
Run oncedef main():setup(), passed to vpy_run
Every framedef loop():loop(), passed to vpy_run
Frame syncWAIT_RECAL injected by the compiler at the start of loop()vpy_run calls vpy_frame_begin() before loop()
ControlsRead once per frame by injected codeRead once per frame by vpy_frame_begin()
Music, SFX, notesUpdated every frame by injected codeUpdated every frame by vpy_run

Don't add a frame sync of your own in either language. In C, calling v_WaitRecal() directly skips the flush and nothing is drawn. If you need the boundary yourself, use vpy_wait_recal().


Builtins and their C calls

In C, #define VPY_SHORT_NAMES before #include <vpy.h> gives you VPy's upper-case names as macros. Without it, use the vpy_* names. The tables below list only pairs confirmed in vpy.h, in vpy3d's header or in the compiler's C backend. Short name is the C macro, and — means there is none: call the long name. Where the arguments differ, the C form is shown.

Drawing

VPyC short nameC functionNotes
SET_INTENSITY(b)SET_INTENSITYvpy_set_intensity
MOVE(x, y)MOVEvpy_move
DRAW_LINE(x0, y0, x1, y1, b)DRAW_LINEvpy_draw_line
DRAW_CIRCLE(cx, cy, r, b)DRAW_CIRCLEvpy_draw_circle16 segments
DRAW_RECT(x, y, w, h, b)DRAW_RECTvpy_draw_rectx, y is the lower-left corner
DRAW_FILLED_RECT(x, y, w, h, b)DRAW_FILLED_RECTvpy_draw_filled_rect
DRAW_ELLIPSE(cx, cy, rx, ry, b)DRAW_ELLIPSEvpy_draw_ellipse
DRAW_POLYGON(n, b, x0, y0, …)DRAW_POLYGONvpy_draw_polygonC takes an array: (const int *xy, int n, int b)
DRAW_VECTOR("ship", x, y)DRAW_VECTORvpy_draw_vectorC: DRAW_VECTOR(SHIP_vec, x, y)
DRAW_VECTOR_EX("ship", x, y, mirror, b)DRAW_VECTOR_EXvpy_draw_vector_ex
DRAW_VECTOR_ROT("ship", x, y, angle)DRAW_VECTOR_ROTvpy_draw_vector_rot
DRAW_VECTOR_ROT("ship", x, y, angle, mirror, b)DRAW_VECTOR_ROT_EXvpy_draw_vector_rot_exVPy has one name with optional arguments
DRAW_ANIM("walk", x, y, mirror)DRAW_ANIMvpy_draw_animC: DRAW_ANIM(WALK_anim, WALK_anim_sprites, x, y, mirror)
DRAW_RECORDING("clip", x, y, scale, frame)DRAW_RECORDINGvpy_draw_recordingC always uses the recorded intensities
WAIT_RECAL() (injected)WAIT_RECALvpy_wait_recalDone for you in both

Text and debug

VPyC short nameC functionNotes
PRINT_TEXT(x, y, "text")PRINT_TEXTvpy_print_text
PRINT_NUMBER(x, y, n)PRINT_NUMBERvpy_print_number
SET_TEXT_SIZE(n)SET_TEXT_SIZEvpy_set_text_size
DEBUG_PRINT(v)—vpy_debug_printrp2350 builds: text to the IDE while debugged on the cartridge
DEBUG_PRINT_LABELED("x", v)DEBUG_PRINT_LABELEDvpy_debug_print_labeledA no-op on targets with no console to print to
DEBUG_PRINT_STR("text")—vpy_debug_print_str
——vpy_log(fmt, …)C only: %d %i %u %x %c %s

Input

VPyC short nameC functionNotes
J1_X() / J1_Y()J1_X / J1_Yvpy_j1_x / vpy_j1_y-127..127, +Y up
J1_BUTTON_1() … J1_BUTTON_4()J1_BUTTON_1() … J1_BUTTON_4()vpy_j1_button(n)1 while held (level, not edge)
UPDATE_BUTTONS()UPDATE_BUTTONSvpy_update_buttonsNot needed normally in either
J2_X(), J2_BUTTON_1() …——libvpy has no joystick 2 calls

Sound

VPyC short nameC functionNotes
PLAY_MUSIC("theme")PLAY_MUSICvpy_play_musicC: PLAY_MUSIC(THEME_music)
STOP_MUSIC()STOP_MUSICvpy_stop_music
PLAY_SFX("laser")PLAY_SFXvpy_play_sfxChannel C. C: PLAY_SFX(LASER_sfx)
PLAY_NOTE("bell", ch, note)PLAY_NOTEvpy_play_noteC: PLAY_NOTE(BELL_instr, ch, note)
PLAY_SAMPLE("voice")PLAY_SAMPLEvpy_play_sampleC: PLAY_SAMPLE(VOICE_smp)
SAMPLE_POS(fps)SAMPLE_POSvpy_sample_pos

Levels, camera and enemies

VPyC short nameC functionNotes
LOAD_LEVEL("level1")LOAD_LEVELvpy_load_levelC: LOAD_LEVEL(LEVEL1_level, LEVEL1_level_sprites)
SHOW_LEVEL()SHOW_LEVELvpy_show_level
UPDATE_LEVEL()UPDATE_LEVELvpy_update_level
SET_CAMERA_X(x) / SET_CAMERA_Y(y)SET_CAMERA_X / SET_CAMERA_Yvpy_set_camera_x / vpy_set_camera_y
GET_CAMERA_X() / GET_CAMERA_Y()GET_CAMERA_X / GET_CAMERA_Yvpy_get_camera_x / vpy_get_camera_y
GET_SCROLL_LIMIT_LEFT() (and _RIGHT, _TOP, _BOTTOM)same namesvpy_get_scroll_limit_left …
GET_LEVEL_FLOOR_Y()GET_LEVEL_FLOOR_Yvpy_get_level_floor_y
LEVEL_COLLISION_Y(x, y, hh)LEVEL_COLLISION_Yvpy_level_collision_y
LEVEL_COLLISION_X(x, y, hw, hh)LEVEL_COLLISION_Xvpy_level_collision_x
SPAWN_ENEMIES("level1")SPAWN_ENEMIESvpy_spawn_enemiesC: SPAWN_ENEMIES(LEVEL1_enemies, LEVEL1_enemy_sprites)
UPDATE_ENEMIES() / DRAW_ENEMIES()UPDATE_ENEMIES / DRAW_ENEMIESvpy_update_enemies / vpy_draw_enemies
KILL_ENEMY(i)KILL_ENEMYvpy_kill_enemy
GET_ENEMY_ACTIVE(i), GET_ENEMY_X(i), GET_ENEMY_Y(i), GET_ENEMY_STATE(i)same namesvpy_get_enemy_active, _x, _y, _state
SET_ENEMY_X(i, x), SET_ENEMY_Y(i, y), SET_ENEMY_STATE(i, s), SET_ENEMY_DIR(i, d)same namesvpy_set_enemy_x, _y, _state, _dir
ENEMY_FIRE_EVENT(i, "hit")ENEMY_FIRE_EVENTvpy_enemy_fire_event_strHashed at run time like the VPy builtin

Math

libvpy has no short names for the math helpers. Use the vpy_* names in C.

VPyC functionNotes
abs(x)vpy_abs
min(a, b) / max(a, b)vpy_min / vpy_max
clamp(v, lo, hi)vpy_clamp
sin(a) / cos(a)vpy_sin / vpy_cos0..127 is a full turn; result -127..127
sqrt(x)vpy_sqrtInteger
atan2(y, x)vpy_atan20..127
rand() / rand_range(lo, hi)vpy_rand / vpy_rand_range
SIN_Q14(a) / COS_Q14(a)vpy_sin_q14 / vpy_cos_q144096 per turn, 16384 = 1.0. C short names SIN_Q14 / COS_Q14
—vpy_seed(s)C only

3D

The VPy 3D builtins call vpy3d, the same layer a C game includes with #include <vpy3d.h> (add $(VPY_C_SDK)/vpy3d.c to the sources). They are shaped differently, though. VPy names a mesh by string and keeps transforms in 12-element i32 arrays. C keeps a vpy_mesh object per mesh and passes vpy_xf values.

VPyC (vpy3d)
XF_IDENTITY(xf)xf = vpy3d_identity()
XF_TRANSLATE(xf, x, y, z)xf = vpy3d_translate(x, y, z)
XF_ROT_X(xf, a) / XF_ROT_Y / XF_ROT_Zvpy3d_rot_x(a) / vpy3d_rot_y(a) / vpy3d_rot_z(a)
XF_MUL(dst, a, b)dst = vpy3d_mul(&a, &b)
XF_TO_MODEL(out, xf, x, y, z)vpy3d_world_to_model
CAMERA_LOOK_AT(ex, ey, ez, tx, ty, tz[, ux, uy, uz])vpy3d_look_at(ex, ey, ez, tx, ty, tz, ux, uy, uz)
CAMERA_SET(xf) / CAMERA_GET(xf)vpy3d_set_camera(&xf) / vpy3d_camera()
CAMERA_FOCAL(f) / CAMERA_NEAR(z) / CAMERA_CLIP(hx, hy)vpy3d_set_focal / vpy3d_set_near / vpy3d_set_clip_xy
PROJECT(out, x, y, z)vpy3d_to_camera then vpy3d_project
PROJECT_CAM(out, x, y, z)vpy3d_project
SCREEN_SIZE(x, y, z, r)vpy3d_screen_size
LOAD_MESH("cube")vpy3d_load_mesh(&cube, CUBE_vmesh)
DRAW_MESH("cube", xf, b)vpy3d_draw_mesh(&cube, &xf, b)
MESH_OCCLUSION(on)vpy3d_set_mesh_occlusion
MESH_POOL_MARK() / MESH_POOL_RELEASE()vpy3d_pool_mark() / vpy3d_pool_release(mark)
LOAD_SCENE("tunnel")vpy3d_load_scene(&s, TUNNEL_vscene, TUNNEL_vscene_meshes)
DRAW_SCENE("tunnel", x, y, z, b[, groups])vpy3d_draw_scene(&s, 0, pos, groups, b)
DRAW_SCENE_AT("tunnel", xf, x, y, z, b[, groups])vpy3d_draw_scene(&s, &xf, pos, groups, b)
LINE_3D(…) / LINE_CAM(…)vpy3d_line_world / vpy3d_line_cam
OCCL_RESET() / OCCL_LINE(…)vpy3d_occl_reset / vpy3d_occl_line
OCCL_MESH("cube", xf) / OCCL_ADD(corners, n)vpy3d_occl_add_mesh / vpy3d_occl_add
RAY_MESH(out, "cube", xf, …)vpy3d_ray_mesh
MARKS_CLEAR / MARKS_ADD / MARKS_DRAWvpy3d_marks_clear / vpy3d_marks_add / vpy3d_marks_draw
FOG(b, x, y, z, full, gone) / SCENERY_FOG(full, gone)vpy3d_fog / vpy3d_set_scenery_fog
TEXT_BILLBOARD(…) / TEXT_3D(…)vpy3d_text_billboard / vpy3d_text
VEC_3D("ship", xf, scale, b)vpy3d_draw_vec
FX_RESET(), FX_SEED(s), FX_STEP(), FX_DRAW(), FX_DRAW2D()vpyfx_reset, vpyfx_seed, vpyfx_step, vpyfx_draw, vpyfx_draw2d
FX_RATE, FX_GRAVITY, FX_FLOOR, FX_BUDGETvpyfx_set_rate, vpyfx_set_gravity, vpyfx_set_floor, vpyfx_set_budget
SOFT_RESET(), SOFT_STEP(), SOFT_DRAW(…), SOFT_FREE(b), SOFT_PUSH(…), SOFT_UNPIN(…), SOFT_POINTS(b)vpysoft_reset, vpysoft_step, vpysoft_draw, vpysoft_free, vpysoft_push, vpysoft_unpin, vpysoft_points

The skeleton builtins (BONE_*) sit on vpybone, and the jukebox builtins (MUSIC_TRACK, SFX_PCM…) on the same player C games use, but through small adapters of the compiler's own, so their C calls are not one-to-one. See VPy 3D and vpy3d.


Assets

Both languages use the same files, made in the same editors. VPy names an asset with a string and the compiler embeds it. C includes the header vpy_cli compile-asset writes to gen/<name>.h and passes its arrays. In the table, N is the name in upper case.

AssetVPyCWhere it builds
.vec spriteDRAW_VECTOR("n", x, y), DRAW_VECTOR_EX, DRAW_VECTOR_ROT, DRAW_VECTOR_3DDRAW_VECTOR(N_vec, x, y), DRAW_VECTOR_ROT(N_vec, x, y, a)VPy: all targets. C: sim, rp2350, uvm2
.vanim animationDRAW_ANIM("n", x, y, mirror)DRAW_ANIM(N_anim, N_anim_sprites, x, y, mirror)All. C needs named .vec frames: inline paths are VPy only
.vplay levelLOAD_LEVEL("n"), SHOW_LEVEL(), UPDATE_LEVEL()LOAD_LEVEL(N_level, N_level_sprites), SHOW_LEVEL()All
.venemy enemy typePlaced by a level; SPAWN_ENEMIES("level")SPAWN_ENEMIES(L_enemies, L_enemy_sprites), from the level's headerAll
.vmus musicPLAY_MUSIC("n"), STOP_MUSIC()PLAY_MUSIC(N_music)All
.vsfx sound effectPLAY_SFX("n")PLAY_SFX(N_sfx)All
.vinstr instrumentPLAY_NOTE("n", ch, note)PLAY_NOTE(N_instr, ch, note)All
.vrec recordingDRAW_RECORDING("n", x, y, scale, frame)DRAW_RECORDING(N_vrec, x, y, scale, frame)VPy: rp2350, uvm2, pitrex; video only on m6809. C: sim, rp2350, uvm2
.vsmp samplePLAY_SAMPLE("n"), SAMPLE_POS(fps)PLAY_SAMPLE(N_smp), SAMPLE_POS(fps)Plays on rp2350 (and in the C simulator). Builds but is silent on uvm2. Not on m6809
.vmesh meshLOAD_MESH("n"), DRAW_MESH("n", xf, b)vpy3d_load_mesh(&m, N_vmesh), vpy3d_draw_meshVPy: uvm2, and rp2350 with the C backend. C: sim, rp2350, uvm2
.vscene sceneLOAD_SCENE("n"), DRAW_SCENE("n", x, y, z, b)vpy3d_load_scene(&s, N_vscene, N_vscene_meshes), vpy3d_draw_sceneAs .vmesh; SDK v1.3.5 or later
.vskel skeletonimport n, then n.T_BONES, n.T_TURNS, n.T_BONE_MESHES…#include "n.h", then N_T_BONES…VPy: uvm2. C: sim, rp2350, uvm2
.vclip poses and clipsimport n, then n.T_POSES[n.POSE_X][n.JOINT_Y], n.T_KEYS…#include "n.h", then N_T_POSES[N_POSE_X][N_JOINT_Y]…VPy: uvm2. C: sim, rp2350, uvm2

"All" means m6809, rp2350, uvm2 and pitrex for VPy, and sim, rp2350 and uvm2 for C. In C, SET_INTENSITY does not override a sprite's own intensities. Give each asset of a C project its own name: ship.vec and ship.vmus would both become gen/ship.h.


Targets and languages

TargetVPyC
m6809 (Vectrex cartridge ROM)Yes: .bin, single or multibankNo
pitrex (Raspberry Pi expansion; not yet tested with the v0.4 SDK)Yes: .img, ARM32Only through the examples' common.mk (kernel7l.img, needs a PiTrex SDK); not in the new-project template
rp2350 (debug cartridge)Yes: ARM Thumb-2, or C with [rp2350] backend = "c"Yes: make rp2350, an SD card RAM image
uvm2 (UVMC2 multicart)Yes: .um2, ARM Thumb-2 or C with [uvm2] backend = "c"Yes: make uvm2, a .um2
WASM simulator (Shift+F5)No: VPy runs in the target's emulatorYes: make sim, the fastest loop (no debugger)

See Compiler & Build Targets for what each VPy target produces, and Cartridges for the hardware.


VPy built through C

On uvm2 and rp2350, a VPy program can be compiled as C. The compiler writes the whole program as C and builds it with gcc against libvpy, the same runtime C games link:

[uvm2]
backend = "c"
sdk_opt = "O2"                         # optional: "O2", "Os" or "O1"
defs = "UVM2_DRAW_SCALE=127"           # optional; several are separated by ;
 
[rp2350]
backend = "c"                          # if absent, [uvm2] backend is used
  • Size. On a large game we measured 224 KB of code with the C backend against 323 KB with the ARM backend. A .um2 runs entirely from the cartridge's SRAM, code included, so this is room for levels. An untyped const array whose values all fit in a byte is stored as int8_t with this backend.
  • Same drawing as C games. The 2D builtins are libvpy's own, so text and vectors draw exactly as in a C game.
  • rp2350 gets the 3D, effects and jukebox builtins this way. The result is always the SD card's RAM image. The cartridge has no audio jack, so the jukebox builtins compile and stay silent (JUKEBOX_LIVE() returns 0).
  • sdk_opt builds the SDK's C libraries (libvpy, vpy3d and the layers on it) at that level instead of the build's own.
  • defs passes defines to the SDK (pool sizes, limits). The [rp2350] keys fall back to [uvm2] ones.
  • Semantics are the ARM backend's: 32-bit wrapping integers, truncating /, typed arrays.
  • Not everything is there yet. Builtins with no libvpy counterpart in this backend (levels, enemies, animations, samples) are a compile error for now.
  • Debugging. gcc optimises, so a line may be skipped, a breakpoint on a call can stop in the inlined function, and some locals show as optimised out here. main() and loop() appear in the call stack inlined into the runtime. See Debugging.

Shared conventions

Both languages
Screenx and y from -127 to 127, origin at the centre
Y axis+Y is up
Brightness0..127. In libvpy, 0 means "do not draw"
2D angles0..127 is one full turn (sin, cos; DRAW_VECTOR_ROT turns counter-clockwise)
3D angles4096 per turn (SIN_Q14, XF_ROT_*)
Frame50 Hz by default. Everything drawn in one loop() call is one frame
Asset coordinatesIntegers, relative to the sprite's centre, +Y up

On the ARM targets the C calls behave like the VPy ones, with the same bytes and the same runtime arithmetic, so positions and timings carry over when you port a game from one language to the other.