{"id":51882176,"url":"https://github.com/peterhellberg/vex","last_synced_at":"2026-07-25T14:30:35.528Z","repository":{"id":365806411,"uuid":"1273834187","full_name":"peterhellberg/vex","owner":"peterhellberg","description":"A minimal WASM-based fantasy console: write carts in C or Zig, run them in a native window (or a browser) at 320×180 with a 16-color palette.","archived":false,"fork":false,"pushed_at":"2026-06-27T16:06:05.000Z","size":4101,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T17:09:39.728Z","etag":null,"topics":["c","fantasy-console","raylib","wasm","zig"],"latest_commit_sha":null,"homepage":"https://peterhellberg.github.io/vex/","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peterhellberg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-18T23:22:57.000Z","updated_at":"2026-06-27T16:02:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/peterhellberg/vex","commit_stats":null,"previous_names":["peterhellberg/vex"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/peterhellberg/vex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhellberg%2Fvex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhellberg%2Fvex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhellberg%2Fvex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhellberg%2Fvex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peterhellberg","download_url":"https://codeload.github.com/peterhellberg/vex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peterhellberg%2Fvex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35883105,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-25T02:00:06.922Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","fantasy-console","raylib","wasm","zig"],"created_at":"2026-07-25T14:30:34.611Z","updated_at":"2026-07-25T14:30:35.498Z","avatar_url":"https://github.com/peterhellberg.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vex\n\nA minimal WASM-based fantasy console.\n\nA *cart* is any `wasm32` module that exports `update()` and imports a tiny\ndrawing/input API from the host. The host opens a window, runs the cart at\n**60 fps**, and blits the fixed **320×180**, **16-color** framebuffer to the\nscreen.\n\nThere are three interchangeable hosts:\n\n - [`vex`](cmd/vex/main.c) — the reference native host, in C _(raylib + wasm3)_\n - [`vex-run`](cmd/vex-run/main.go) — a native host in Go _(ebitengine + wazero)_\n - [`vex-web`](cmd/vex-web/main.go) — a browser host _(a `\u003ccanvas\u003e` in a small Go server)_\n\nFor the specs and internals, see [How vex works](#how-vex-works).\n\n## Build \u0026 run\n\nNeeds the pinned `zig` (`0.17.0-dev.387+31f157d80`, see [`build.zig.zon`](build.zig.zon)).\n\nDependencies are fetched on first build.\n\n\u003e [!Important]\n\u003e Use the pinned dev build, not `master`. `std.Build`'s API churns between\n\u003e 0.17 nightlies, and a newer/older `zig` will fail to compile this project's\n\u003e *and* raylib's `build.zig` (e.g. `no field named 'args' in struct 'Build'`).\n\u003e Download the exact build from a\n\u003e [community mirror](https://ziglang.org/download/community-mirrors.txt), e.g.\n\u003e `https://pkg.hexops.org/zig/zig-\u003carch\u003e-\u003cos\u003e-0.17.0-dev.387+31f157d80.tar.xz`.\n\n```sh\nzig build --prefix .    # build vex-init + cart.wasm + zcart.wasm into ./bin\ncd cmd/vex \u0026\u0026 zig build --prefix ../..   # build the ./vex host into ../bin\nzig build run           # build, then run the C example cart\nzig build runz          # build, then run the Zig example cart\nzig build run -- -s 5   # forward flags to vex (here: window scale 5)\n```\n\n`--prefix .` installs into `./bin`; a plain `zig build` uses Zig's default\n`zig-out/`.\n\nA `Makefile` wraps these as `make`, `make run`, `make runz`, `make web`,\n`make test-web`, and `make clean`, passing `--prefix .` for you so all\nbinaries — including the Go `vex-web` and `vex-run` — land in `./bin`.\nPlain `make` builds both the SDK package and the `cmd/vex` host package.\n\nThe build is split across two Zig packages on purpose: only `cmd/vex/`\n(the host) declares `raylib` + `wasm3` as dependencies. The SDK package\nitself has no external dependencies, so a cart that just imports `vex`\nfetches nothing heavy.\n\n`make install` copies `vex`, `vex-init`, `vex-web`, and `vex-run` from there to\n`~/.local/bin` _(override with `make install PREFIX=/usr/local`)_.\n\n`vex` is invoked as `vex [-s scale] [-w] \u003ccart.wasm\u003e`. For a dependency-free\nalternative written in Go, see [Native Go version](#native-go-version). \n\nThe window is the 320×180 framebuffer times `scale` \n_(default 3, i.e. 960×540)_; `-s`/`--scale` overrides it. \n\nWith `-w`/`--watch`, vex polls the cart file and reloads it \nautomatically whenever it changes — the native counterpart \nto vex-web's live-reload (see [Web version](#web-version)).\n\nThere are two example carts: \n\n- [`examples/cart/main.c`](examples/cart/main.c) (C)\n- [`examples/zcart/main.zig`](examples/zcart/main.zig) (Zig)\n\nBoth compile to `wasm32` and use the same console API.\n\n### Linux prerequisites\n\nThe host links raylib's default X11 backend, so \nthe matching system libraries must be present. \n\nOn Debian/Ubuntu _(22.04 and newer)_:\n\n```sh\nsudo apt install \\\n    libgl1-mesa-dev libx11-dev libxrandr-dev \\\n    libxinerama-dev libxi-dev libxcursor-dev pkg-config\n```\n\nWithout them the build stops at `unable to find dynamic system library 'GL'`.\n\nThe resulting `vex` is statically linked against raylib and wasm3; only the\nsystem X11/GL libraries (present on any desktop) are needed at runtime.\n\n\u003e [!Note]\n\u003e On Linux the link step prints `warning(link): unexpected LLD stderr` and a\n\u003e few `archive member '…/libGL.so' is neither ET_REL nor LLVM bitcode`\n\u003e warnings. These are harmless — raylib's static archive references the system\n\u003e `.so`s by path — and `zig build` still exits `0` with a working `vex` binary.\n\n## Controls\n\n| Key | Action |\n|-----|--------|\n| `Super`+`Enter` | toggle fullscreen |\n| `Super`+`I` | toggle integer scaling _(crisp pixels vs. fill the screen; on by default in fullscreen)_ |\n| `Super`+`R` | reload the cart from disk _(also automatic with `-w`/`--watch`)_ |\n| `Esc` | quit |\n\n`Super` is the Cmd key on macOS and the Super/Windows key on Linux. \n\nArrow keys, `Z`, and `X` are passed to the cart via `btn()` _(held)_ and\n`btnp()` _(just pressed this frame)_.\n\n\u003e [!Note]\n\u003e These shortcuts apply to the C host `vex`. The Go host `vex-run` only\n\u003e supports `Esc` (quit) and `Cmd`+`Enter` (fullscreen); the C host's\n\u003e `Super`+`I`/`Super`+`R` reload and integer-scale toggles are not\n\u003e implemented there. The web host has no global shortcuts — see the\n\u003e [Web version](#web-version) section.\n\n## Native Go version\n\nThe same carts also run on a native host written entirely in Go —\n[`vex-run`](cmd/vex-run/main.go) uses [wazero](https://wazero.io/) to run the\ncart and [ebitengine](https://ebitengine.org/) to draw into a window, so it\nships as a single self-contained binary with **fewer system dependencies**\nthan the C host — no `libgl1-mesa-dev`, no Zig toolchain, and no `pkg-config`.\n\nThe CLI matches the C host:\n\n```sh\n# straight from GitHub, no checkout needed:\ngo run github.com/peterhellberg/vex/cmd/vex-run@latest mycart.wasm\n\n# or from a checkout of this repo:\nmake                                            # builds bin/vex-run via `go build`\ngo run ./cmd/vex-run -s 5 mycart.wasm           # -s/--scale and -w/--watch work too\ngo run ./cmd/vex-run -w mycart.wasm             # auto-reload on cart changes\n```\n\n`-s`/`--scale` sets the window scale (default 3, i.e. **960×540**) and\n`-w`/`--watch` polls the cart file and reloads it whenever it changes —\nthe same live-reload workflow as `vex` and `vex-web`.\n\n\u003e [!Tip]\n\u003e On Linux, `vex-run` still needs the X11 development headers at build time\n\u003e (ebitengine's vendored GLFW links against them), but neither `libgl1-mesa-dev`\n\u003e nor `pkg-config` are required. You also need **Go ≥ 1.25.0** — a tarball from\n\u003e [go.dev](https://go.dev/dl/) works on any release; check your\n\u003e distro's `golang-go` before relying on `apt install` alone.\n\nThe cart is loaded by `wazero`, and the same console API _(framebuffer,\nSWEETIE-16 palette, drawing, input, **8×8 bitmap font**)_ is reimplemented in\nGo and linked into the cart's `env` imports — so the cart source is\nidentical to what you'd write for `vex` or `vex-web`.\n\n`make install` puts `vex-run` on your `PATH` alongside `vex`, `vex-init`,\nand `vex-web`.\n\n\u003e [!Note]\n\u003e On Linux, ebitengine's X11 backend ([xgb](https://github.com/jezek/xgb))\n\u003e prints two lines on startup if it can't read a valid `~/.Xauthority`:\n\u003e\n\u003e ```\n\u003e XGB: conn.go:69: Could not get authority info: EOF\n\u003e XGB: conn.go:70: Trying connection without authority info...\n\u003e ```\n\u003e\n\u003e The second line means it falls back to an unauthenticated X11 connection,\n\u003e which works on any normal desktop session; the messages are harmless\n\u003e startup output and the run continues normally.\n\n## Web version\n\nThe same carts run unchanged in the browser. \n\n`vex-web` _([`cmd/vex-web/main.go`](cmd/vex-web/main.go))_ is a small self-contained \nGo server that serves a `\u003ccanvas\u003e`-based host — [`vex.js`](cmd/vex-web/assets/vex.js) \nreimplements the console API _(framebuffer, SWEETIE-16 palette, drawing, input,\nand the shared **8×8 bitmap font**)_ in JavaScript and draws into the same fixed\n**320×180** framebuffer, scaled up to fill the window while keeping the aspect\nratio. \n\n`index.html` and `vex.js` are embedded into the binary _(via\n`//go:embed`)_, so `vex-web` needs nothing beside it but a vex cart.\n\n```sh\n# straight from GitHub, no checkout needed:\ngo run github.com/peterhellberg/vex/cmd/vex-web@latest mycart.wasm\n\n# or from a checkout of this repo:\nmake web                          # build, then serve cart.wasm on :8383\nmake web CART=bin/zcart.wasm      # serve a different cart\ngo run ./cmd/vex-web mycart.wasm  # run the server directly\n```\n\nIt serves the page on \u003chttp://localhost:8383/\u003e and opens your browser there\n_(`-no-open` skips that; `-addr host:port` changes the address)_. \n\nThe cart is served on `/cart.wasm`, **read from disk on every request**, \nand the page watches it over Server-Sent Events _(`/reload`)_ — so rebuilding \nthe cart **live-reloads** it in the browser, no refresh or restart needed.\n\n\u003e [!Tip]\n\u003e **Live-reload workflow:** run a watching build in one terminal and the server\n\u003e in another — every rebuild reloads the cart in the browser automatically.\n\u003e\n\u003e ```sh\n\u003e zig build --watch                 # terminal 1: rebuild carts on every change\n\u003e go run ./cmd/vex-web zig-out/bin/zcart.wasm  # terminal 2: serve + auto-reload\n\u003e ```\n\nArrow keys, `Z`, and `X` map to `btn()` and `btnp()`, and the mouse maps to\n`mx()`/`my()`/`mbtn()`, just like the native host.\n\n\u003e [!Tip]\n\u003e **Drag and drop** any `.wasm` onto the page to load it in place of the\n\u003e default cart — handy for trying a build without restarting the server.\n\n\u003e [!Tip]\n\u003e **Virtual gamepad in portrait.** When the viewport is meaningfully\n\u003e taller than it is wide _(roughly 5:6 or narrower, e.g. a phone held\n\u003e upright)_, the page adds a touch-friendly gamepad below the canvas.\n\u003e All six buttons sit inside one square 3×3 grid: the d-pad arrows\n\u003e _(up/left/right/down)_ on the cross arms, `Z` in the centre, and `X`\n\u003e in the bottom-right corner. Every cell is the same size, so the six\n\u003e buttons are identical squares — comfortable to hit with a thumb.\n\u003e Multi-touch works (you can hold, say, left + up for a diagonal),\n\u003e the gamepad disables the browser's pinch-zoom gesture so two-finger\n\u003e input doesn't move the page, and keys pressed on a physical keyboard\n\u003e also highlight the matching on-screen button.\n\n\u003e [!Tip]\n\u003e **Browser tests:** `make test-web` runs the Playwright-driven test\n\u003e suite under [`cmd/vex-web/test/`](cmd/vex-web/test/) — it rebuilds the\n\u003e cart bundle, spins up a tiny static server, and drives the page at\n\u003e several viewport sizes (iPhone SE / 12 / Pro Max, plus near-square\n\u003e and landscape to verify the gamepad hides). On first run it downloads\n\u003e Chromium and the `playwright` npm package; subsequent runs reuse the\n\u003e install. Override the cart with `make test-web CART=path/to/cart.wasm`.\n\n`make install` puts `vex-web` on your `PATH` alongside `vex` and `vex-init`.\n\n## Creating a cart\n\nTwo ways to start: scaffold a Zig project with `vex-init`, or write a cart by\nhand in C or Zig. Either way a cart is a `wasm32` module — see the [API](#api).\n\n### Scaffolding a cart with vex-init\n\n`vex-init` scaffolds a standalone Zig cart project that depends on the `vex`\nSDK which is published at \u003chttps://github.com/peterhellberg/vex\u003e. \n\nWith the binaries installed (`make install`, so `vex` and `vex-init` are on your `PATH`):\n\n```sh\nvex-init mygame      # creates mygame/ (src/cart.zig, build.zig, build.zig.zon)\n                     # and runs `zig fetch --save` to pin the vex dependency\ncd mygame\nzig build            # builds zig-out/bin/mygame.wasm\n\nvex zig-out/bin/mygame.wasm\n```\n\n`vex-init` fetches the vex dependency for you (`zig fetch --save`); \nif that step can't run it prints the command to finish manually. \n\nThe generated `build.zig` depends only on the [`vex`](vex.zig) SDK module,\nwhich has no external dependencies — raylib and wasm3 live in a separate\n`cmd/vex/` package that the cart build doesn't touch.\n\n### Writing a cart by hand\n\nA cart is any `wasm32` module that exports `update()` \n_(and optionally `boot()`)_ and imports the API from `env`.\n\nBoth SDKs are a single file you drop next to your cart:\n\n```sh\ncurl -LO https://raw.githubusercontent.com/peterhellberg/vex/main/vex.h    # C\ncurl -LO https://raw.githubusercontent.com/peterhellberg/vex/main/vex.zig  # Zig\n```\n\nIn C, include [`vex.h`](vex.h):\n\n```c\n#include \"vex.h\"\n\nVEX_EXPORT(\"update\") void update(void) {\n  cls(8);                  // clear to dark blue\n  text(\"VEX C\", 8, 8, 12); // white text\n  rect(60, 60, 8, 8, 6);   // green square\n}\n```\n\nCompile it to WASM:\n\n```sh\nzig cc --target=wasm32-freestanding \\\n  -nostdlib -Os -Wl,--no-entry -I. \\\n  -o mycart.wasm mycart.c\n```\n\nOr in Zig — import the [`vex.zig`](vex.zig) SDK and `export` the entry points:\n\n```zig\nconst vex = @import(\"vex\");\n\nexport fn update() void {\n    // clear to dark blue\n    vex.cls(8);\n\n    // white text\n    vex.text(\"VEX ZIG\", 8, 8, 12);\n}\n```\n\n```sh\nzig build-exe -target wasm32-freestanding \\\n  -O ReleaseSmall -fno-entry -rdynamic \\\n  -femit-bin=mycart.wasm --dep vex \\\n  -Mroot=mycart.zig -Mvex=vex.zig\n```\n\n\u003e [!Note]\n\u003e The [C](base/c/Makefile) and [Zig](base/zig/Makefile) makefiles in `base/`\n\u003e download the SDK and build the cart — handy for one-off projects.\n\n## API\n\n| Function | Description |\n|----------|-------------|\n| `cls(color)` | clear the screen |\n| `pset(x, y, color)` | set one pixel |\n| `rect(x, y, w, h, color)` | filled rectangle |\n| `rectb(x, y, w, h, color)` | rectangle outline |\n| `circ(x, y, r, color)` | filled circle |\n| `circb(x, y, r, color)` | circle outline |\n| `line(x0, y0, x1, y1, color)` | line |\n| `tri(x1, y1, x2, y2, x3, y3, color)` | filled triangle |\n| `trib(x1, y1, x2, y2, x3, y3, color)` | triangle outline |\n| `blit(data, x, y, w, h, key)` | draw a `w`×`h` bitmap of palette indices (one byte/pixel); pixels equal to `key` are skipped |\n| `text(s, x, y, color)` | draw a string |\n| `title(s)` | set the window title |\n| `btn(button) -\u003e int` | `1` if a button is held, else `0` |\n| `btnp(button) -\u003e int` | `1` if a button was just pressed this frame _(edge detection; same button numbers as `btn()`)_, else `0` |\n| `mx() -\u003e int` / `my() -\u003e int` | mouse position in framebuffer pixels (`0..319` / `0..179`) |\n| `mbtn(button) -\u003e int` | `1` if a mouse button is held (0 left, 1 right, 2 middle) |\n| `pal(index, rgb)` | override palette entry `index` (0..15) with a packed `0xRRGGBB` color |\n| `palreset()` | restore the default palette |\n\n`color` is a palette index `0..15`\n\nButtons: \n - `0` left\n - `1` right\n - `2` up\n - `3` down\n - `4` Z\n - `5` X\n\n## How vex works\n\nThe host — a single [`main.c`](main.c) — opens a [raylib](https://www.raylib.com/)\nwindow and runs the cart on the [wasm3](https://github.com/wasm3/wasm3)\ninterpreter. It links a small `env` API into the cart's imports, calls the\ncart's exported `boot()` once at start and `update()` once per frame, then blits\nthe 320×180 framebuffer to the window with nearest-neighbour scaling.\n\n### Specs\n\n- **Display** — 320×180 framebuffer, scaled to the window with nearest-neighbour filtering.\n- **Palette** — 16 colors ([SWEETIE-16](https://lospec.com/palette-list/sweetie-16)), overridable at runtime via `pal()`.\n- **Frame rate** — 60 fps; carts export `update()` (per frame) and optionally `boot()` (once at start).\n- **Input** — 6 buttons (arrow keys + `Z`/`X`) and the mouse (position + 3 buttons).\n- **Cart** — any `wasm32` module that exports `update()` and imports the API from `env`.\n\n### Drawing \u0026 input\n\n- **Lifecycle** — the host calls `boot()` once at start _(optional)_ and\n  `update()` every frame at 60 fps; do your drawing from there.\n- **Coordinates** — the framebuffer origin is the **top-left**; `x` runs\n  `0..319` and `y` runs `0..179`.\n- **The framebuffer persists between frames** — it is *not* cleared for you, so\n  start `update()` with `cls(color)` (or redraw the whole screen) to avoid\n  leftovers from the previous frame.\n- **Color is a palette index** `0..15`, never a raw RGB value. Remap an entry at\n  runtime with `pal(index, 0xRRGGBB)` and restore the defaults with `palreset()`.\n- **Input** — read the buttons with `btn(n)` _(the d-pad and Z/X, mapped to\n  the arrow keys and `Z`/`X`)_ for held state and `btnp(n)` for a one-shot on\n  the frame a button is first pressed. The mouse is read with\n  `mx()`/`my()`/`mbtn(n)`.\n  See the [API](#api) for the exact button numbers.\n\n### Components\n\nThere are three interchangeable hosts — see the [Native Go\nversion](#native-go-version) and [Web version](#web-version) sections for the\nGo and browser alternatives:\n\n- **C host (`vex`)** — the reference implementation in [`main.c`](main.c).\n  - **Runtime:** [wasm3](https://github.com/wasm3/wasm3) — the simplest\n    embeddable WASM interpreter _(pure C, MIT)_; only its core files are\n    compiled.\n  - **Graphics/input:** [raylib](https://www.raylib.com/).\n- **Go host (`vex-run`)** — [`cmd/vex-run/main.go`](cmd/vex-run/main.go).\n  - **Runtime:** [wazero](https://wazero.io/) — a pure-Go WASM runtime\n    _(no cgo)_.\n  - **Graphics/input:** [ebitengine](https://ebitengine.org/) — a pure-Go\n    2D game engine; brings its own OpenGL/DirectX/Metal backends, so no\n    X11/GL system packages are needed at runtime.\n- **Web host (`vex-web`)** — a `\u003ccanvas\u003e` reimplemented in\n  [`vex.js`](cmd/vex-web/assets/vex.js), served by a small Go HTTP server.\n- **Build:** the Zig toolchain. The build is split across two packages:\n  the root [`build.zig`](build.zig) builds the `vex-init` scaffolder and\n  the example carts, while [`cmd/vex/build.zig`](cmd/vex/build.zig) builds\n  the `./vex` C host. `make` builds both plus `vex-run` and `vex-web` with\n  `go build`.\n\nThe C host's dependencies are pulled in via\n[`cmd/vex/build.zig.zon`](cmd/vex/build.zig.zon) and built from source.\nThey live in a separate package on purpose: a cart that depends only on\nthe [`vex`](vex.zig) SDK fetches no external packages and needs no system\npackages on any platform. The Go hosts only need a recent Go toolchain.\n\nOn macOS and Windows the only requirement is `zig`; on Linux the C host also\nlinks the system X11/OpenGL libraries _(see [Linux\nprerequisites](#linux-prerequisites))_. The Go hosts need nothing beyond\nGo itself on any platform.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterhellberg%2Fvex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeterhellberg%2Fvex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeterhellberg%2Fvex/lists"}