This document summarizes the current public C API exposed by include/*.h. As with the rest of this library, it is a work in progress. Caveat Emptor!
- Most resource APIs use
rl_handle_t(unsigned int) as an opaque handle. 0is generally an invalid handle unless a subsystem documents otherwise.- Public API names use the default-name-as-synchronous convention. If a function may start work and return before completion, it should use an
_asyncsuffix and provide a handle, poll/finish flow, or explicit readiness state. Synchronous/default functions may still suspend on wasm when reached through a JSPI-exported entry point. - Call
rl_init(NULL)(orrl_init(&config)/rl_init_values(...); seeinclude/rl_config.h) before using subsystem APIs, andrl_deinit()at shutdown.rl_init()is the default synchronous contract: it returns only after loader restore readiness is satisfied.rl_init_values(...)is the flattened convenience entrypoint for bindings that do not want to marshalrl_init_config_tacross the FFI boundary.rl_init_async()preserves the polling contract: it starts runtime init and returns immediately.rl_init_values_async(...)is the flattened polling-style counterpart torl_init_async(). Both returnRL_INIT_OK(0) on success or a negativeRL_INIT_ERR_*code on failure (RL_INIT_ERR_UNKNOWN,RL_INIT_ERR_ALREADY_INITIALIZED,RL_INIT_ERR_LOADER,RL_INIT_ERR_ASSET_HOST,RL_INIT_ERR_WINDOW). - Use
rl_is_initialized()to query whether the runtime is currently initialized. - Use
rl_get_platform()to query the build/runtime platform string ("desktop"or"web").
- Not all handle-backed resources use the same storage/lifetime policy.
- Colors are lightweight pooled value handles (RGBA) and are not refcounted shared GPU assets.
- Textures are shared GPU assets with path-based deduplication and internal refcounting.
- Music streams are handle-backed runtime resources; each handle owns its decoded stream/data until destroyed.
- Sounds are handle-backed runtime resources intended for short one-shot SFX.
- This difference is intentional: textures are expensive to allocate/upload, colors are cheap values.
Main responsibilities:
- Runtime lifecycle (
rl_init,rl_deinit) - App loop lifecycle (
rl_start,rl_tick,rl_stop,rl_run,rl_set_target_fps) - Render lifecycle (
rl_render_begin,rl_render_end) - Basic drawing (text, fps helpers)
- 2D/3D mode switching
- Mouse/keyboard input helpers (
rl_input_get_mouse*,rl_input_get_keyboard_state) - Basic lighting toggles and parameters
- Timing helpers (
rl_get_time,rl_get_delta_time) - Text measurement helpers
Header note:
rl.his the primary/core header for runtime/frame/draw APIs.- Shared base types (
rl_handle_t, math/data structs) live inrl_types.h. - For many integrations, including only
rl.his enough. - If you need subsystem-specific APIs (window/model/font/color/loader/scratch/shape), include their corresponding headers explicitly.
Main responsibilities:
- Window size/position/title helpers
- Monitor/screen/window queries
- Close-request polling for externally pumped runtimes (
rl_window_close_requested())
Notes:
- The OS window is created by
rl_init()(driven byinclude/rl_config.h), and destroyed byrl_deinit(). - There is no public
rl_window_open/rl_window_closeAPI anymore. rl_window_close_requested()mirrors the platform close-request latch on desktop; on web it always returns0.include/rl_window.hincludes an explicit comment with the internal-only open/close signatures for reference.
Main responsibilities:
- Immediate shape drawing helpers
- Current primitives:
rl_shape_draw_cube(...)rl_shape_draw_rectangle(...)
Main responsibilities:
- Handle-based camera creation/update/activation
- Active camera tracking (
rl_camera3d_get_active) - Reserved default camera handle (
RL_CAMERA3D_DEFAULT/rl_camera3d_get_default)
Notes:
rl_render_begin_mode_3d()uses the current active camera.- If no active camera is set,
rl_render_begin_mode_3d()falls back to the default camera.
Main responsibilities:
- Built-in color handles (e.g.
RL_COLOR_WHITE,RL_COLOR_BLACK, etc.) - Create/destroy runtime color handles
Notes:
- Built-in color constants are stable
rl_handle_tvalues, not raw array indices. - Color handles resolve through the shared handle-pool machinery and get stale-handle protection after destroy.
- Built-in colors are global singleton handles and are not caller-owned; destroying them is invalid.
- Runtime colors created with
rl_color_create(...)are lightweight value handles, not shared/refcounted assets.
Main responsibilities:
- Font creation/destruction by filename + size
- Default font handle access
Main responsibilities:
- Model creation/destruction by filename
- Model draw
- Validity checks (
rl_model_is_valid,rl_model_is_valid_strict) - Animation clip/frame queries
- Animation control (
set_animation, speed/loop, update/tick)
Notes:
rl_model_create()requires a ready window/graphics context.- On model load failure, the implementation substitutes a visible placeholder cube.
- Model instances now own transform state.
- Use
rl_model_set_transform(...)to update position / XYZ rotation / non-uniform scale on the instance. rl_model_draw(handle, tint)draws using the stored transform.
Main responsibilities:
- Model picking from screen-space mouse coordinates with camera + model handles
- Sprite3D billboard picking from screen-space mouse coordinates with camera + sprite handles
- Return collision details (
hit,distance, local-spacepointandnormal) - Wasm bridge helpers for JS (
rl_pick_model_to_scratch,rl_pick_sprite3d_to_scratch)
Notes:
rl_pick_model(camera, model, mouse_x, mouse_y)— reads the model's stored transform from the instance; no explicit position/rotation/scale params needed.rl_pick_sprite3d(camera, sprite3d, mouse_x, mouse_y)— reads the sprite's stored transform from the instance; no explicit position/size params needed.rl_pick_result_t.pointand.normalare returned in local/object space:- model picks are transformed back through the full model world matrix used during collision
- sprite3d picks use billboard-local coordinates centered on the sprite, with normals on the local Z axis
- Picking now uses broad-phase culling before narrow-phase tests:
- models: world-space AABB ray test
- sprite3d billboards: bounding-sphere ray test
rl_pick_model_to_scratch(...)writes:- hit point into scratch
vector3 - hit normal + distance into scratch
vector4(x,y,z = normal,w = distance)
- hit point into scratch
- Pick telemetry helpers:
rl_pick_reset_stats()rl_pick_get_broadphase_tests()rl_pick_get_broadphase_rejects()rl_pick_get_narrowphase_tests()rl_pick_get_narrowphase_hits()
Main responsibilities:
- Music stream creation/destruction by filename
- Playback control (
play,pause,stop) - Runtime control (
set_loop,set_volume) - Status/update (
is_playing,update,update_all)
Notes:
- Music loading uses the same loader/file callback flow as other assets.
rl_music_update()(orrl_music_update_all()) should be called each frame while playing.
Main responsibilities:
- Sound creation/destruction by filename
- Playback control (
play,pause,resume,stop) - Runtime control (
set_volume,set_pitch,set_pan) - Status query (
is_playing)
Notes:
- Sounds are intended for short SFX and do not require a per-frame update call.
- Sound loading uses the same loader/file callback flow as other assets.
Main responsibilities:
- Texture creation/destruction by filename
- Shared texture-handle reuse for identical normalized paths
Main responsibilities:
- 3D sprite creation from path or texture handle
- Instance-owned billboard transform (
rl_sprite3d_set_transform(...)) - Billboard drawing in active 3D camera context
- Sprite handle destruction
Notes:
- Sprite3D instances now own position/size state.
rl_sprite3d_draw(handle, tint)draws using the stored transform.
Main responsibilities:
- 2D sprite creation from path or texture handle
- Instance-owned 2D transform (
rl_sprite2d_set_transform(...)) - 2D drawing with position, scale, and rotation
- Sprite handle destruction
API:
rl_handle_t rl_sprite2d_create(const char *filename); // load from file
rl_handle_t rl_sprite2d_create_from_texture(rl_handle_t texture); // from existing texture
void rl_sprite2d_set_transform(rl_handle_t sprite, float x, float y, float scale, float rotation);
void rl_sprite2d_draw(rl_handle_t sprite, rl_handle_t tint);
void rl_sprite2d_destroy(rl_handle_t sprite);Notes:
- Sprite2D instances own position/scale/rotation state separate from the underlying texture.
- Use
rl_sprite2d_set_transform()to update transform;rl_sprite2d_draw()uses the stored values. - This matches the split transform/draw pattern used by
rl_modelandrl_sprite3d. - For Lua/remote contexts, emit
SET_SPRITE2D_TRANSFORMthenDRAW_SPRITE2Dframe commands.
Main responsibilities:
- Optional debug overlay helpers
- Built-in FPS display configuration
Notes:
rl_debug_enable_fps(x, y, font_size, font_path)enables an FPS overlay and optionally loads a custom font for it.rl_debug_disable()turns the overlay off and releases any font owned by the debug subsystem.- The current implementation draws the debug overlay automatically during
rl_render_end().
Main responsibilities:
- One-time app startup (
rl_start(init_fn, tick_fn, shutdown_fn, user_data)) - Manual stepping (
rl_tick()) - Loop stop/teardown (
rl_stop()) - Convenience one-stop run (
rl_run(init_fn, tick_fn, shutdown_fn, user_data))
Notes:
rl_start(...)registersinit_fn/tick_fn/shutdown_fnand returns immediately. Loader readiness, one-timeinit_fn, and per-frametick_fnall run fromrl_tick()or from the internalrl_run()loop (so async loader work, e.g. wasm IDBFS, can complete between host frames).rl_tick()is for manual stepping and returns0on success,-1for invalid usage/state.rl_stop()breaksrl_run(...)loops; outside loop mode it performs shutdown teardown.rl_run(...)is the convenience wrapper that starts, loops, and stops for you.- Emscripten / web: the internal
rl_runframe callback does not callWindowShouldClose(). In raylib’srcore_web.c, that function performsemscripten_sleep(12)on every call (it expects an Asyncify/JSPI-capable app link) even though it always returnsfalseon the web. Desktop builds still useWindowShouldClose()in therl_runloop.
Main responsibilities:
- Fixed-capacity per-frame command buffer type (
rl_frame_command_buffer_t) - Append helper for module-emitted frame commands
- Ordered execution helpers for clear / audio / 3D / 2D passes
Notes:
rl_frame_commands_append(...)appends onerl_module_frame_command_tinto the buffer.- The command buffer is intentionally host-owned;
rl_frame_runnerdoes not depend on it. - The C example uses this subsystem to capture commands emitted by the Lua module, then drains them in phase order during each frame.
Main responsibilities:
- Loader-only bootstrap:
rl_loader_init(mount_point)rl_loader_init_async(mount_point)rl_loader_deinit()rl_loader_is_initialized()— returnstrueif the loader is currently initialized
- Asset-host configuration:
rl_loader_set_asset_host(asset_host)rl_loader_get_asset_host()
- Async restore / import operations:
rl_loader_restore_fs_async()→rl_handle_trl_loader_create_import_task(filename)→rl_handle_trl_loader_import_assets_async(filenames, count)→rl_handle_t
- Synchronous asset import:
rl_loader_import_asset(filename)→int
- Async task lifecycle:
rl_loader_add_task(task, on_success, on_failure, user_data)rl_loader_poll_task(task)rl_loader_finish_task(task)rl_loader_get_task_path(task)rl_loader_free_task(task)
- Local cache queries and maintenance:
rl_loader_is_asset_cached(filename)
Notes:
rl_loader_add_task(...)now derives the callback path from the queued task itself. Callers no longer pass a separate path string when registering a managed task.- Loader tasks are public
rl_handle_tvalues. The internalrl_loader_task_tstate remains private to the loader. rl_loader_add_task(...)consumes the task handle on success, matching the previous ownership transfer where the managed queue owned and freed the task pointer.- On wasm,
rl_loader_deinit()waits for any in-flight IDBFS restore to settle, flushes IDBFS, then deinitializes file I/O. Any exported entry point that can reachrl_loader_deinit()/rl_deinit()must be listed inJSPI_EXPORTS. - On wasm,
rl_loader_import_asset(...)uses the JSPI sync fetch path. Any exported entry point that can reach it must be listed inJSPI_EXPORTS.rl_loader_uncache_asset(filename)rl_loader_clear_cache()
Notes:
rl_loader_init(...)is the default synchronous contract. It returns only after loader restore readiness is satisfied.rl_loader_init_async(...)preserves the polling contract. Callers must continue checkingrl_loader_is_ready()and pumpingrl_loader_tick().- The loader no longer performs hidden blocking fetches during synchronous file reads.
- Blocking wait helpers were removed from the public C API:
rl_loader_wait_task(...)rl_loader_wait_tasks(...)
- Synchronous consumers like raylib file callbacks are expected to read already-local files.
- On wasm, the intended flow is:
- start restore once
- begin one or more prepare operations
- poll/finish those ops across frames
- only then call APIs that synchronously consume the prepared assets
- Higher-level "task group" ergonomics are intentionally binding-level (Haxe/JS/Nim/Lua helpers), not part of the C loader API.
rl_loader_create_import_task(...)is dependency-aware for assets like.gltfthat may require additional files at load time.rl_loader_import_assets_async(...)is the convenience batch entry point used by the C example bootstrap flow.- URL and file-path normalization flow is centralized through
path_normalize(). - URL normalization preserves scheme/authority/query/fragment and normalizes only URL path segments.
Main responsibilities:
- Generic module ABI for host-driven plugin lifecycle (
init, optionalget_config, optionalstart,update,deinit) - Host services passed to modules through
rl_module_host_api_t:- logging (
log) - allocation (
alloc/free) - immediate event pub/sub (
event_on/event_off/event_emit)
- logging (
- Module registry lookup via module name (
rl_module_init("lua", ...), etc.)
Minimal usage pattern:
#include "rl.h"
#include "rl_module.h"
#include "rl_event.h"
typedef struct module_runtime_t {
const rl_module_api_t *api;
void *state;
} module_runtime_t;
static int host_event_on(void *host_user_data, const char *event_name,
rl_module_event_listener_fn listener, void *listener_user_data)
{
(void)host_user_data;
return rl_event_on(event_name, listener, listener_user_data);
}
static int host_event_off(void *host_user_data, const char *event_name,
rl_module_event_listener_fn listener, void *listener_user_data)
{
(void)host_user_data;
return rl_event_off(event_name, listener, listener_user_data);
}
static int host_event_emit(void *host_user_data, const char *event_name, void *payload)
{
(void)host_user_data;
return rl_event_emit(event_name, payload);
}
static void host_log(void *user_data, int level, const char *message)
{
(void)user_data;
(void)level;
fprintf(stderr, "[module] %s\n", message ? message : "(null)");
}
/* The real Lua-driven host example lives in examples/c-lua/main.c. */
void run_lua_module_example(void)
{
rl_init_config_t init_cfg = {0};
rl_module_host_api_t host = {0};
rl_module_config_t config = {800, 600, 60, 0, "module example"};
module_runtime_t lua = {0};
char error[256] = {0};
init_cfg.window_width = config.width;
init_cfg.window_height = config.height;
init_cfg.window_title = config.title;
(void)rl_init(&init_cfg);
host.log = host_log;
host.event_on = host_event_on;
host.event_off = host_event_off;
host.event_emit = host_event_emit;
if (rl_module_init("lua", &host, &lua.api, &lua.state, error, sizeof(error)) != 0) {
fprintf(stderr, "failed to init lua module: %s\n", error);
rl_deinit();
return;
}
(void)rl_event_emit("lua.do_string", "print('hello from lua module')");
(void)rl_event_emit("lua.add_path", "assets/scripts/lua");
(void)rl_event_emit("lua.do_file", "boot.lua");
if (rl_module_get_config_instance(lua.api, lua.state, &config) == 0) {
/* host can now create the window/runtime from config */
}
if (rl_module_start_instance(lua.api, lua.state) != 0) {
fprintf(stderr, "failed to start lua module\n");
}
if (lua.api != NULL && lua.api->update != NULL) {
(void)lua.api->update(lua.state, 1.0f / 60.0f);
}
rl_module_deinit_instance(lua.api, lua.state);
rl_deinit();
}Notes:
- Events are immediate/synchronous today (no queue yet).
lua.do_filein current Lua module usesfileio_read(...), so file paths should be loader/fileio-relative.- In the current example host, the Lua search root is
assets/scripts/lua, so entry scripts are emitted relative to that path. - Lua module is built as a separate archive (
modules/lua/lib/librl_lua.a/.wasm.a) and linked by the host app. - The module host API also includes
frame_command, which is the current typed path for transient draw/audio commands emitted by scripting modules.
Current Lua module responsibilities:
- embeds the Lua VM behind the generic module ABI
- installs a Lua
require(...)searcher backed bylua.add_path - exposes script-facing bindings for:
- frame commands (
clear,draw_text,draw_texture,draw_sprite3d,draw_model,play_sound) - resource lifecycle (
create_color/destroy_color,load_*,destroy_*) - stateful music control
- camera creation/update/activation
- model/sprite picking
- immediate event pub/sub (
event_on,event_off,event_emit) - logging with source-aware file/line reporting
- frame commands (
Current status notes:
- The frame-command path is already active, not just planned:
- Lua emits typed transient commands through the host
frame_commandcallback - the current reference host in
examples/c-lua/main.cbuffers and drains them each tick
- Lua emits typed transient commands through the host
- The Lua module also keeps its own small resource/color caches so repeated script requests can reuse the same script-visible handles across HCR/reload within the same module lifetime.
- For HCR-friendly scripts,
load()should generally reacquire cached handles whileunload()removes side effects and script-local references rather than aggressively destroying shared cached resources. - Lua scripts can subscribe and emit through the host event bus with
event_on,event_off, andevent_emit. - Current event payload support is intentionally narrow: Lua currently treats event payloads as
stringornil. - Current reload caveat: Lua event listeners are not yet tracked by script/generation, so listener teardown is currently script-managed rather than auto-pruned on reload.
- Current command set is intentionally small:
- clear background
- draw text
- draw sprite3d
- draw model
- draw texture
- play sound
- Music control currently remains stateful resource API usage rather than a transient frame command.
Current Lua script entrypoints:
get_config()init()load()update(frame)unload()serialize()unserialize(state)shutdown()
Current host ordering in the C example:
- initialize
librl - initialize Lua module with
rl_module_init("lua", ...) - emit
lua.add_path - emit
lua.do_filefor the entry script - call
rl_module_get_config_instance(...) - create the window / set target FPS
- call
rl_module_start_instance(...) - Lua runs one-time
init()if present - Lua runs
load()for the active script if present - call
api->update(...)every frame - on reload, Lua runs
serialize()->unload()-> new chunk ->load()->unserialize(state) - on module teardown, Lua runs
unload()and then one-timeshutdown()
Lifecycle intent:
init()is the one-time constructor for the script runtime instanceload()/unload()are the code-lifetime hooks used for first load and HCRserialize()/unserialize(state)are optional state-transfer hooks for HCRshutdown()is the one-time destructor for the script runtime instance
get_config() currently supports:
widthheighttitletarget_fpsflags
update(frame) currently receives a reused Lua table with:
frame.dtframe.screen_wframe.screen_hframe.mouseframe.keyboard
frame.mouse currently contains:
xywheelleftrightmiddlebuttons
Mouse button values use the shared RL_BUTTON_* state encoding:
0: up1: pressed2: down3: released
frame.keyboard currently contains:
keyspressed_keypressed_charnum_pressed_keyspressed_keysnum_pressed_charspressed_chars
Notes:
pressed_key/pressed_charare event-style values, not persistent held-state indicators.- held-state lives in
keyboard.keys[keycode]. pressed_keys[]drains the fullGetKeyPressed()queue for the frame.pressed_chars[]drains the fullGetCharPressed()queue for the frame.- The host is expected to rebuild transient frame commands every tick rather than treating them as persistent render state.
The Lua module currently injects a small set of constants/globals, including:
- event helpers:
event_onevent_offevent_emit
- colors:
COLOR_WHITECOLOR_BLACKCOLOR_BLUECOLOR_RAYWHITECOLOR_DARKBLUE
- font:
FONT_DEFAULT
- camera:
CAMERA_PERSPECTIVECAMERA_ORTHOGRAPHICCAMERA3D_DEFAULT
- window flags for
get_config():FLAG_VSYNC_HINTFLAG_FULLSCREEN_MODEFLAG_WINDOW_RESIZABLEFLAG_WINDOW_UNDECORATEDFLAG_WINDOW_HIDDENFLAG_WINDOW_MINIMIZEDFLAG_WINDOW_MAXIMIZEDFLAG_WINDOW_UNFOCUSEDFLAG_WINDOW_TOPMOSTFLAG_WINDOW_ALWAYS_RUNFLAG_WINDOW_TRANSPARENTFLAG_WINDOW_HIGHDPIFLAG_MSAA_4X_HINTFLAG_INTERLACED_HINT
The example Lua runtime now layers small object-style wrappers on top of the flat C bindings:
color.luamodel.luatexture.luasprite3d.luasound.luamusic.luacamera3d.luafont.lua
These currently live under examples/www/public/assets/scripts/lua/.
These are not part of the C ABI, but they are the current recommended Lua-side usage pattern.
Notes:
- On wasm,
fileio_init()mounts IDBFS and starts an async restore (FS.syncfs(true, ...)) through a single internal sync path. Module.fileio_idbfs_readyisfalsefrom init start until restore succeeds.- Direct fileio callers that need deterministic persistence before teardown should start
fileio_flush_async()and wait forfileio_sync_finish(...)beforefileio_deinit(). rl_loader_deinit()handles that flush for librl callers, sorl_deinit()is the normal deterministic teardown boundary for librl applications.Module.fileio_idbfs_readyis set back tofalseat fileio deinit.- A sync-overlap guard (
Module.fileio_idbfs_syncing) prevents concurrent sync operations. - JS callers that need cache-first behavior should wait for readiness before first asset-backed load.
Notes:
- Logging goes through the shared wrapper logger (
deps/wgutils/include/logger/logger.h), not directfprintf/printf. - Log level is carried by logger calls (
log_error,log_warn,log_info,log_debug) or explicitlog_message(...). - Log messages should include a subsystem scope prefix in message text, e.g.
FILEIO: ....
Mouse button state values are shared across C/JS/Nim:
RL_BUTTON_UP(0)RL_BUTTON_PRESSED(1)RL_BUTTON_DOWN(2)RL_BUTTON_RELEASED(3)
This provides a method of avoiding the expensive data exchange between JS and wasm (aka: "Border Tax"). It is a shared memory block with known offsets so each system can read/write to that shared memory and not invoke another boundry call. It's especially useful when trying to share structures that are constantly being mutated (like keyboard, mouse info, etc).
Main responsibilities:
- Shared memory struct for high-frequency data exchange with host runtimes:
- vectors, matrices, quaternions, colors, rectangles
- mouse/keyboard/gamepad/touch state
- Direct pointer access (
rl_scratch_get_base) - Layout metadata (
rl_scratch_get_offsets) for JS/wasm interop - Set/get helpers and refresh/clear functions
Wasm/JS boundary conventions:
- Scratch bridge entrypoints follow explicit naming:
*_to_scratch: write computed data into scratch (for JS to read through the JS binding layer).*_from_scratch: read host-provided scratch data (where applicable).
- JS bindings keep scratch abstracted:
bindings/js/rl.jsexposes high-level methods.- Scratch-backed methods are grouped and implemented through bridge calls + reads in
bindings/js/rl.js.
- Scratch refresh is explicit via
rl_scratch_refresh(). - Most vec-return helpers used by JS follow this bridge pattern:
- C computes native value return (
vec2_t, etc.) - wasm bridge writes to scratch (
*_to_scratch) - JS wrapper reads from
bindings/js/rl.js
- C computes native value return (