Yet Another Raycasting Implementation
A small C game engine for building vintage first-person games in the spirit of Wolfenstein 3D. YARI is focused on ESP32 hardware, but the same game code also runs on macOS, Linux and WebAssembly through raylib or SDL2.
Try the browser demo
- What is YARI?
- Quick Start
- Supported Targets
- Project Layout
- Writing a Game
- Core API
- Utils
- Maps, Assets and Fonts
- Map Builder
- Build Commands
- ESP32 Configuration
- Compatibility and Current Limits
- Included Examples
- Acknowledgements
yari is a compact raycasting engine for old-school 2.5D games.
It is not a general-purpose game engine. The goal is to stay small, understandable and practical on constrained hardware, while keeping desktop development fast enough to iterate without flashing an ESP32 after every change.
The repository includes:
- the engine core in
src/yari; - renderer and input backends for ESP32, raylib and SDL2;
- desktop, web and ESP-IDF examples;
- tools that convert images and fonts into C headers;
- a visual level editor that generates YARI-compatible
level.hfiles.
- C raycasting renderer: column-based wall rendering, sprite z-buffering, distance shading and floor/ceiling casting.
- ESP32-first design: ST7789 SPI backend, RGB565 framebuffer, configurable LCD pins and ready-to-build ESP-IDF examples.
- Desktop iteration: the same game can run on macOS/Linux through raylib or SDL2, which makes development and debugging much faster.
- Web output: the full game example can be compiled to WebAssembly and run in a browser.
- Static assets: textures and fonts are packed into C arrays, making them easy to ship inside embedded firmware.
- Built-in map editor:
map_buildergenerates map data, player settings, surfaces, entities, collision layers and reloadable editor metadata. - Small game-facing API: a game implements
yr_init_game()andyr_update_game(). YARI owns the platform loop, rendering setup and input setup.
Requirements:
- ESP-IDF installed and configured;
- a connected ESP32 board;
- an ST7789 display matching the default pin configuration, or custom LCD/pin macros supplied at build time.
The project has been developed with ESP-IDF 5.5.2.
make esp32-build
make esp32-flash-monitorIf ESP-IDF is not installed under the path used by the Makefile, pass
ESP32_HOME explicitly:
make ESP32_HOME="$HOME/esp/v5.5.2/esp-idf" esp32-buildRequirements:
- a C compiler;
make;pkg-config;- raylib and/or SDL2 available through
pkg-config.
For the default raylib backend on macOS with Homebrew:
brew install raylib
make runFor the SDL2 backend on macOS with Homebrew:
brew install sdl2
make run-sdlOn Linux, install the raylib or SDL2 development package with your distribution's package manager, then run one of:
make run
make run-sdlmake run builds the raylib backend. make run-sdl builds the SDL2 backend.
Both start the complete example in example/fps/main/main.c.
Requirements:
- Emscripten active in the shell (
emccandemarinPATH); npxif you want to serve locally.
make wasm
make run-wasmThe web build writes docs/index.html, docs/index.js and docs/index.wasm.
| Target | Backend | Status |
|---|---|---|
| ESP32 | ST7789 renderer + GPIO/ADC input | supported |
| macOS | raylib or SDL2 | supported |
| Linux | raylib or SDL2 | supported |
| WebAssembly | raylib PLATFORM_WEB + Emscripten | supported |
| Windows | raylib | supported |
.
├── example/ # Example games
├── src/
│ ├── tools/ # assets_packer, font_baker, map_builder
│ └── yari/ # Engine source
│ ├── platform/esp32/ # ESP32 backend
│ ├── platform/raylib/ # Desktop/web backend
│ └── platform/sdl/ # Desktop SDL2 backend
└── Makefile # Desktop, WASM, ESP32 and tool builds
A YARI game includes yari.h, defines YARI_MAIN and implements two
functions:
#define YARI_MAIN
// #define YARI_NO_PREFIX
#include <yari.h>
...
void yr_init_game(YrContext *ctx) {
// configure the engine
ctx->map = (YrMap){
.walls = (YrWall *)walls,
.cols = COLS,
.rows = ROWS,
};
ctx->camera = (YrCamera){.pos = {14.5, 5.5}, .dir = {-0.8, 0.5}};
}
void yr_update_game(YrContext *ctx) {
// update the game state and draw the frame
yr_draw_game(ctx);
}You can find a minimal example in example/base/main/main.c:
YARI_NO_PREFIX is optional. Without it, use the explicit yr_ and Yr
symbols, such as yr_draw_game() and YrContext.
Full function reference: CHEATSHEET.md.
YrContext is the central structure used by the engine.
| Field | Purpose |
|---|---|
camera |
camera position, direction, offset and rotation |
screen_width, screen_height |
framebuffer resolution |
game_title |
desktop window title |
target_fps |
target frame rate, zero for unlimited |
map |
tile map: a YrMap (walls, cols, rows, plus floor_texture/ceil_texture defaults and optional per-cell floor/ceil texture overrides - see Map Format) |
entities |
hash map of sprites/objects, keyed by a stable size_t id (see Entities) |
ray_res |
pixel width of each cast ray; larger values are faster but blockier |
zbuffer |
internal wall-depth buffer allocated by YARI |
assets_map |
texture lookup table generated by assets_packer |
game_data |
user-defined pointer for custom game state |
yr_draw_game(ctx) draws background, walls and entities.
The raycaster assumes 64x64 textures for walls, floors, ceilings and
entities (YR_TEXTURE_SIZE). HUD can be drawn with yr_draw_texture(...) and with the other drawing functions.
yr_pixel_t is the pixel format used by the framebuffer:
| Build | yr_pixel_t |
Notes |
|---|---|---|
YR_RGB565 (ESP32 default) |
uint16_t |
RGB565 5-6-5 format |
YR_L8 (desktop/web/mono) |
uint8_t |
8-bit grayscale luminance |
| default (desktop/web) | uint32_t |
ARGB 8-8-8-8 format |
On ESP32, YR_RGB565 is defined automatically unless YR_L8 (or YR_MONOCROME) is set. Desktop/web default to ARGB32.
YrColorFilterCallback is a per-pixel color callback:
typedef void (*YrColorFilterCallback)(int x, int y, yr_pixel_t *color, void *user_data);yr_apply_color_filter(apply, user_data) touches every pixel exactly once, so its cost
is fixed at screen_width * screen_height regardless of overdraw.
Use it for full-screen effects (color grading, vignette, scanlines, day/night tinting):
yr_draw_game(ctx);
yr_apply_color_filter(sepia_filter, NULL);
draw_hud(ctx); // drawn after, so the HUD stays unfilteredThe apply callback receives the pixel coordinates and a pointer to the color
value, which can be modified in-place. The user_data pointer is passed through unchanged and can be used to pass extra parameters to the callback.
The pixel format is yr_pixel_t, which is uint16_t in RGB565 builds and uint32_t in desktop/web builds, take care of that in your callback.
Define YR_L8 or YR_MONOCROME (which implies YR_L8) to use 8-bit grayscale pixels everywhere. Colors are stored as luminance (0–255). The pixel type is uint8_t.
YR_MONOCROME simulates on desktop the dithered 1bpp path used by the ESP32 backend for monochrome panels (e.g. SSD1306). The engine runs normally in L8; only the final blit to the display thresholds each pixel to black/white using a Bayer 4×4 ordered dithering matrix (yr_bayer4x4, accessible as yr_mono_dither_lit(luma, x, y)).
On ESP32 an #elif branch for LCD_CONTROLLER_SSD1306 in lcd_init() selects the monochrome panel driver; the same Bayer dithering is applied in the blit path.
Regardless of the mode, keep apply in integer math on ESP32: the Xtensa FPU
is single-precision only, so double arithmetic (bare float literals like
0.393, fmin) is emulated in software and can drop frame rate from ~30 FPS
to single digits. example/fps/main/main.c has a fixed-point sepia filter
that budgets coefficients as integers scaled by 256:
int r = (*color >> 11) & 0x1F, g = (*color >> 6) & 0x1F, b = *color & 0x1F;
int nr = (101 * r + 197 * g + 48 * b) >> 8;
if (nr > 31) nr = 31;
// ... same for g (>> 7, clamp 63) and b (>> 8, clamp 31)All collision functions return or populate a YrCollisionInfo:
typedef struct {
YrCollisionType type; // YR_COLLISION_NONE, YR_COLLISION_WALL, YR_COLLISION_ENTITY
union {
// valid only when type == YR_COLLISION_WALL
struct {
int cell_x, cell_y; // map cell of the hit wall
YrWall tile; // the hit wall itself (kind, texture_id/color, slide_x/y, ...)
};
// valid only when type == YR_COLLISION_ENTITY - shares memory with the struct above
struct {
YrEntity *entity; // pointer to the hit entity
size_t entity_index; // id of the hit entity in ctx->entities
};
};
} YrCollisionInfo;Built-in collision masks:
#define YR_CMSK_NONE 0
#define YR_CMSK_WALL 1
#define YR_CMSK_ALL -1Entities can use custom bit masks for collision layers. The map builder can
define custom layers and generate macros such as YR_CMSK_ENTITY, YR_CMSK_PLAYER...
Map your esp32 key bindings in yr_init_game(ctx) using yr_esp_key_init and yr_esp_joystick_init
Key codes are defined in src/yari/inputs.h and follow raylib values. The SDL2
backend maps SDL key events into the same YARI key enum, so game code can be
shared across desktop backends and embedded targets.
YrEntity represents a sprite in the world.
| Field | Purpose |
|---|---|
pos |
position in map space |
texture_id |
index inside assets_map |
kind |
user-defined entity kind id, set by the map builder (YR_KIND_*) or game code |
dist |
distance from the player, maintained by the renderer |
vdiv, hdiv |
vertical/horizontal sprite size reduction |
vmove |
perspective vertical offset |
disabled |
if true, the entity is skipped |
entity_data |
user-defined pointer |
collision_mask |
entity collision layer |
collision_threshold |
collision radius |
animation |
embedded YrAnimationStack |
init |
optional callback run once by yr_create_entity_ex when the entity is spawned: void(YrEntity *self, void *data) |
update |
optional callback invoked every frame: void(YrContext *ctx, YrEntity *self, size_t id) |
cleanup |
optional callback run by yr_remove_entity: void(YrEntity *self); use it to free entity_data |
ctx->entities is a YrEntityMap (a yr_Hm(size_t, YrEntity) hash map - see the Hash Map and Hash Set section under Utils), not a plain array. Entities are addressed by a stable id that keeps working across other insertions/removals, so it's safe to remove one while iterating or holding onto its id across frames:
size_t id = yr_create_entity(state, entity); // inserts entity, returns its new id
size_t id2 = yr_create_entity_ex(state, entity, p); // same, and passes p to entity.init (if set)
yr_remove_entity(state, id); // runs entity->cleanup (if set), then removes itThe id/index argument passed to update callbacks, and YrCollisionInfo.entity_index, are this same persistent id. Use it with yr_remove_entity or yr_hm_try(&ctx->entities, id) - it is not an array offset. Given a live YrEntity *, yr_get_entity_id(e) recovers its id.
To iterate all entities:
yr_foreach(&ctx->entities, kv) {
YrEntity *e = &kv->value; // kv->key is the entity id
}YrTimer is a lightweight countdown timer. See CHEATSHEET.md for the full function list.
The count field on YrTimer increments each time yr_timer_loop fires.
YrAnimationStack manages a stack of animations. An animation is described by a YrAnimation value (frames, frame_count, duration); push one with the helpers in CHEATSHEET.md. Popping the top animation (once its lifetime expires) resumes the one beneath it.
Every YrEntity has its own animation stack, and yr_draw_entities advances each entity's stack and writes the result into texture_id automatically every frame - game code only ever pushes animations, it never needs to call an "advance" function for entities.
Designate HUD/UI elements in normalized (-1..1) or absolute screen coordinates, with alignment and an optional bounding box, through variadic macros:
// Screen coordinate conversions
Vector2 yr_screen_coord(Vector2 pos); // normalized -1..1 → screen pixels from center origin
Vector2 yr_screen_coord_abs(Vector2 pos); // normalized 0..1 → screen pixels
// Layout-aware text and texture drawing (variadic struct params)
yr_draw_text_ex(txt, font, .color=YR_WHITE, .align=YR_LAY_CENTER, ...);
yr_draw_texture_ex(texture, txw, txh, .align=YR_LAY_CENTER, .box={200, 100}, ...);Alignment values (enum lay_pos): YR_LAY_NONE, YR_LAY_CENTER, YR_LAY_CB (center-bottom), YR_LAY_CT (center-top), YR_LAY_CL (center-left), YR_LAY_CR (center-right), YR_LAY_TL (top-left), YR_LAY_TR, YR_LAY_BR, YR_LAY_BL.
Display modes (enum lay_dis): YR_LAY_SCREEN (position as absolute pixel x, y), YR_LAY_NORM (position as normalized 0..1 nx, ny).
The struct params accept .color, .align, .display, .box (bounding box), and .length/.draw_empty as needed. See CHEATSHEET.md for all fields.
Example - looping idle with a one-shot attack that auto-pops, driven from an entity's init/update callbacks:
// the map builder can generate these as named YrAnimation constants instead
static const int idle_frames[] = {tx_idle0, tx_idle1, tx_idle2};
static const int attack_frames[] = {tx_atk0, tx_atk1, tx_atk2, tx_atk3};
YrAnimation idle_anim = {.frames = idle_frames, .frame_count = 3, .duration = 0.15f};
YrAnimation attack_anim = {.frames = attack_frames, .frame_count = 4, .duration = 0.1f};
void init_enemy(YrEntity *self, void *data) {
(void)data;
yr_start_loop_animation(&self->animation, idle_anim); // push idle on spawn
}
void update_enemy(YrContext *ctx, YrEntity *self, size_t id) {
(void)state;
if (/* attack triggered */ false) {
// push attack on trigger - auto-pops, idle resumes underneath
yr_start_animation_once(&self->animation, attack_anim);
}
}For an animation not tied to an entity (a HUD weapon sprite, say), call yr_get_animation_texture(&stack) yourself each frame to read the current frame's texture id (or -1 if the stack is empty).
ctx->map is a YrMap: a walls array of rows * cols YrWall cells, plus
floor_texture/ceil_texture (default floor/ceiling texture ids, 0 for
black) and optional per-cell floor/ceil uint8_t arrays that override the
default texture for individual cells (0 means "use the default").
| Attribute | Meaning |
|---|---|
| kind | YR_WK_EMPTY (empty cell), YR_WK_FULL, ... |
| textured | if false color will be applied instead of texture_id |
| texture_id | texture id, indexed through assets_map |
| color | solid color |
Player and entity coordinates are floating-point values in the same map space.
Cell (x, y) covers the area [x, x+1), [y, y+1).
Source assets are converted into static C arrays by assets_packer.
make assets
# it runs build/yari/bin/assets_packer example/fps/assets example/fps/main/assets.hassets_packer:
- reads
.pngand.jpgfiles from the directory passed as the first argument; - generates one
yr_pixel_tarray per image; - generates a
TextureIdenum withtx_<file_name>symbols; - generates
assets_map[]; - emits both RGB565 data for
YR_RGB565builds and 32-bit data for desktop/web builds. - the output file is written to the path passed as the second argument.
Use simple C-friendly file names, for example wal_001.png, wep_gun0.png or
door_metal.png.
.ttf files under assets/font/ are baked into
example/fps/main/fonts.h:
make assets
# it runs build/yari/bin/font_baker example/fps/assets/font example/fps/main/fonts.h- reads
.ttffiles from the directory passed as the first argument; - the output file is written to the path passed as the second argument;
font_baker generates four font sizes:
YR_FONT_SM;YR_FONT_MD;YR_FONT_LG;YR_FONT_XL.
yr_get_text_length(text, len, font) measures the pixel width of len characters in the given font (pass SIZE_MAX or 0 to measure the full null-terminated string).
Example:
yr_draw_text("HP: 100", 10, 15, fonts[YR_FONT_SM], YR_GREEN);
float w = yr_get_text_length("HP: 100", 0, fonts[YR_FONT_SM]); // pixel widthYARI includes a raylib/raygui visual level editor:
make edit-fps
make edit-kartTo build the editor without launching it:
make map-buildermake edit-fps builds the tool and runs:
build/yari/bin/map_builder assets example/fps/main/level1.hThe executable accepts optional paths:
build/yari/bin/map_builder [assets_dir] [output_file]If omitted, assets_dir defaults to assets and output_file defaults to
level.h. The asset directory is scanned for .png, .jpg and .jpeg files;
their names are converted to the same tx_<file_name> symbols generated by
assets_packer, so run make assets after adding or renaming textures.
On startup, the editor tries to load the MAP_BUILDER_STATE_BEGIN/END metadata
from the output files. Press Save to overwrite the output header and Load to
reload the last saved state.
It generate a level_gen.h that contains:
- custom collision layers;
- entity kinds (
YR_KIND_*); - init/update/cleanup callback forward declarations;
- animations as named
YrAnimationconstants (frames, frame count and duration);
And a level file contains:
- map dimensions (
YR_MAP_COLS,YR_MAP_ROWS); - wall grid data;
- floor and ceiling texture ids;
- player start position;
- inline factories for entities (the generated factories also starts the idle animation if setted and setup configured callbacks);
level_append_exported_entities(); // appends entities marked asexportedin the editor to the game statelevel_get_map();
Include the generated level file header from game code and wire it into yr_init_game():
#include "assets.h" // generated by assets_packer
#include "level.h" // generated by map_builder
void yr_init_game(Context *ctx) {
load_level(ctx);
// ...
}Entities marked as exported in the editor are appended in the game state entities.
If you want to spawn entities at runtime you can remove the exported flag and call the factory functions directly, for example:
YrEntity enemy = create_enemy_pos((Vector2){10.0f, 5.0f}, NULL, init_enemy, update_enemy, cleanup_enemy);
yr_create_entity(state, enemy);A factory only takes an explicit init/update/cleanup parameter (in that order) when the entity has no fixed callback of that kind assigned in the editor; when one is assigned, it's baked into the factory and the corresponding parameter disappears from its signature. e.g. an entity with all three assigned generates create_enemy_pos(pos, data), taking no callback parameters at all.
Entities with a named update callback generate a forward declaration for that function, so implement it in game code with this signature:
void update_enemy(YrContext *ctx, YrEntity *self, size_t id) {
(void)state;
(void)self;
(void)id;
}Entities can also be assigned init and cleanup callbacks (set in the editor's Functions tab, next to update). Both also generate forward declarations. init runs once when the entity is spawned via yr_create_entity/yr_create_entity_ex, typically to calloc and populate entity_data; cleanup runs once when the entity is removed via yr_remove_entity, typically to free it:
void init_enemy(YrEntity *self, void *data) {
(void)data;
self->entity_data = calloc(1, sizeof(EnemyData));
}
void cleanup_enemy(YrEntity *self) {
free(self->entity_data);
}Useful map builder controls:
| Action | Control |
|---|---|
| save | Save button or Ctrl/Cmd+S |
| reload level | Load button |
| fit view | Fit button |
| copy selection | Ctrl/Cmd+C |
| paste selection | Ctrl/Cmd+V |
| delete selection | Delete or Backspace |
| pan | middle mouse button, or Space + left drag |
| zoom | mouse wheel with Ctrl/Cmd |
Main editor modes:
Wall: draw walls as points, rectangles or circles.Entity: place sprites with texture, collision mask and kind on thePropertiestab, init/update/cleanup callbacks on theFunctionstab.Player: edit player position, direction, collision radius and collision layers.Floor/Ceil: assign floor and ceiling textures.Anim: create and edit animations for entities, including frame list, duration.
| Command | Effect |
|---|---|
make run |
builds and runs example/fps with raylib on desktop |
make run-sdl |
builds and runs example/fps with SDL2 on desktop |
make run-base |
builds and runs the minimal example |
make assets |
regenerates assets.h and fonts.h |
make edit-fps |
builds and runs the map editor for example/fps |
make edit-kart |
builds and runs the map editor for example/kart |
make run-wasm |
builds and serves the WebAssembly example/fps |
make esp32-build |
builds example/fps with ESP-IDF |
make esp32-flash |
builds and flashes example/fps |
make esp32-monitor |
opens the serial monitor |
make esp32-flash-monitor |
flashes and opens the serial monitor |
make esp32-clean |
runs idf.py fullclean in example/fps |
make esp32-base-build |
builds example/base for ESP32 |
make esp32-base-flash-monitor |
flashes and monitors the base example |
make all builds desktop, WebAssembly and ESP32 targets. For day-to-day work,
use narrower targets such as make run or make esp32-flash.
The ESP32 backend is implemented in src/yari/platform/esp32/renderer.c on
top of ESP-IDF's esp_lcd component, over SPI. It defaults to an ST7789
panel in landscape orientation (the T-Display's display), but every pin,
bus parameter and panel trait is a #define so it can be reconfigured, or
retargeted to a different controller, from your build without touching the
file.
Main configuration macros:
// Framebuffer / active area
#define YR_LCD_W 240
#define YR_LCD_H 136
#define LCD_X_OFF 40 // gap between GRAM origin and visible glass
#define LCD_Y_OFF 53
// SPI pins and bus
#define PIN_MOSI 19
#define PIN_CLK 18
#define PIN_CS 5
#define PIN_DC 16
#define PIN_RST 23 // set to -1 if the panel has no reset line
#define PIN_BL 4 // set to -1 if the backlight isn't GPIO-controlled
#define LCD_BL_ACTIVE_LOW false
#define LCD_SPI_HOST SPI2_HOST
#define LCD_SPI_MODE 0
#define SPI_CLOCK_SPEED (80 * 1000 * 1000)
// Panel controller: define one to select it (defaults to LCD_CONTROLLER_ST7789)
#define LCD_CONTROLLER_ST7789
// #define LCD_CONTROLLER_SSD1306
// Rotation / color, applied through esp_lcd's portable panel ops
#define LCD_MIRROR_X true
#define LCD_MIRROR_Y false
#define LCD_SWAP_XY true
#define LCD_BGR_ORDER false
#define LCD_INVERT_COLOR true // defaults to false for non-ST7789 controllers
// Color depth: 1 = monochrome (packed per the controller's native GDDRAM
// layout), 16 = RGB565 (fast path, zero-copy blit), anything else is
// treated as N bytes/pixel and produced by expanding RGB565 per channel.
#define LCD_BITS_PER_PIXEL 16esp_lcd ships ST7789 and SSD1306 (monochrome) drivers with no extra
dependencies; both are wired up above. To target another esp_lcd-compatible
controller (e.g. ILI9341, GC9A01, either built into esp_lcd or added as a
managed component via idf_component.yml), add an #elif branch next to
esp_lcd_new_panel_st7789(...) in lcd_init() calling its own
esp_lcd_new_panel_xxx() constructor — the SPI bus setup, framebuffer,
rotation and blit/pack code are all controller-agnostic already.
ESP32 is defined automatically by the yari component itself. The pixel
format is your choice, so the ESP-IDF examples add it explicitly:
idf_build_set_property(COMPILE_OPTIONS "-DYR_RGB565" APPEND)To use YARI as an ESP-IDF component in another project, add it as a
dependency in your main/idf_component.yml:
dependencies:
idf: ">=4.1.0"
yari:
git: "https://github.com/monade/esp32-3D.git"
path: "src/yari"
version: "main" # pin to a tag or commit for reproducible buildsFor optimal performance you should keep the esp32 cpu freq to max 240mhz, use compiler optimization flags and define -DYR_MULTITHREAD, check examples CMakeList.txt and sdkconfig.
If you need more space for assets you should define a custom partition table according to your device flash storage capability using idf.py menuconfig.
Digital buttons are configured as pull-up GPIOs:
yr_esp_key_init(25, YR_KEY_Q);
yr_esp_key_init(2, YR_KEY_E);
yr_esp_key_init(15, YR_KEY_X);
yr_esp_key_init(26, YR_KEY_SPACE);The analog joystick uses two ADC pins:
int joystick_id = yr_joystick_init(32, 36);
float x = yr_joystick_get_axis(joystick_id, YR_X_AXIS);
float y = yr_joystick_get_axis(joystick_id, YR_Y_AXIS);- The project is plain C.
- The ESP32 renderer currently targets ST7789 SPI displays with an RGB565 framebuffer.
- Desktop rendering can use raylib or SDL2; web rendering uses raylib.
- Raycaster textures are expected to be
64x64. - Map cells are
YrWallstructs (kind,textured,texture_id/color,slide_x/slide_y), not raw byte values - see Map Format. - Desktop joystick backends are currently stubs.
A minimal example with an in-memory map and solid-color walls. Use it to learn the engine contract without the asset pipeline.
make run-baseTwo complete examples with:
- packed assets from
assets/; - bitmap fonts;
- a level generated by the map builder;
- player movement;
- HUD rendering;
- a weapon pickup;
- desktop, ESP32 and WebAssembly builds.
# fps
make esp32-flash
make run
make run-sdl
make run-wasm
# kart
make esp32-kart-flash
make run-kart
make run-sdl-kart


