Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

raylib-rs is a safe, idiomatic Rust binding to raylib 6.0 — the simple and easy-to-use game programming library. It lets you write 2D/3D games, simulations, and visualizations in Rust with the same low-ceremony feel as the original C library, without requiring unsafe in user code.

Who this is for

This book is for Rust developers who want to build interactive graphical applications — games, simulations, visualizations, or tools — using raylib’s clean API. You should be comfortable with basic Rust (ownership, traits, closures), but you do not need prior game development experience. No C/C++ knowledge is required.

What’s in scope

raylib-rs 6.0 covers the full raylib 6.0 surface:

  • Window management and drawing — 2D and 3D rendering, shapes, textures, render targets.
  • Input — keyboard, mouse, gamepad, touch.
  • Text and fonts — system fonts, custom fonts, SDF rendering.
  • 3D models — meshes, materials, skeletal animation via ModelAnimations.
  • Audio — sounds, music streams, AudioStream for custom mixing.
  • raymath — vectors, matrices, quaternions, easing functions. Math methods come via a C shim so there is no extra dependency by default.
  • raygui — immediate-mode GUI panels, buttons, sliders, text boxes.
  • rlgl — low-level OpenGL abstraction for custom rendering.
  • Software renderer — a headless PLATFORM=Memory backend for pixel-probe testing without a GPU or window.

What’s out of scope here

  • The C raylib reference — see raylib.com for the upstream API docs.
  • The rustdoc API reference — generated from doc comments; published to docs.rs with each crate release.
  • The showcase examples — full ports of the raylib C example set, shipping as a GitHub Pages site in WS9.

How to read this book

Getting Started — set up the dependency and open your first window in under 30 lines. Then follow the install guide for your platform (Windows, macOS, Linux, or Web/Wasm).

Core Concepts — the conventions that show up everywhere: the RaylibHandle/RaylibThread ownership model; RAII resources and how they are cleaned up; strings and allocations; safety rules; feature flags and platform support.

Modules — per-area narrative chapters: window and drawing, input, shapes, textures, text, 3D, audio, raymath, collision, raygui, rlgl, the software renderer, callbacks, and error handling.

Ecosystem — opt-in integrations: glam and mint for interop with other math libraries, serde for serialization, and pointers to what comes next.

Quickstart

This chapter gets you from zero to a working window in under 30 lines of Rust. If you have not installed the native build dependencies yet, see the install guide for your platform first.

Add the dependency

In your Cargo.toml:

[dependencies]
raylib = "6.0.0-rc.2"

Note: 6.0.0-rc.2 is the published release-candidate of the 6.0 line on crates.io while the canonical merge gets additional review. The headings under this book chapter describe what is shipping in the eventual 6.0.0 final release; cargo will not pick up a pre-release tag from raylib = "6.0" alone, so the exact 6.0.0-rc.2 pin is intentional.

Open a window

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Hello, raylib-rs")
        .build();

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::WHITE);
        d.draw_text("Hello, raylib-rs!", 20, 20, 20, Color::BLACK);
    }
}

Walking through the code:

  • raylib::init() returns a RaylibBuilder. Chain .size() and .title() to configure the window before it opens.
  • .build() opens the window and returns a pair: RaylibHandle (your main API handle) and RaylibThread (a token proving you are on the main thread).
  • rl.begin_drawing(&thread) opens a drawing frame and returns a RaylibDrawHandle. All draw calls live inside this block; the frame is committed when d is dropped at the end of the loop body.
  • Color::WHITE and Color::BLACK are constants in raylib::prelude. No import gymnastics needed.

What’s next

Install on Windows

raylib-rs builds on Windows using the MSVC toolchain (Visual Studio Build Tools). The MSVC linker and Windows SDK are required; the MinGW/GNU toolchain is not supported.

Prerequisites

  1. Rust 1.85+ — install via rustup. When rustup asks which toolchain to install, choose stable-x86_64-pc-windows-msvc (the default on Windows).

  2. Visual Studio Build Tools 2019 or newer — download the Build Tools for Visual Studio installer. During setup, select the “Desktop development with C++” workload. This provides cl.exe, the Windows SDK headers, and the linker.

  3. CMake 3.15+ — download from cmake.org and ensure cmake is on your PATH. CMake is required by the raylib-sys build script to compile the vendored raylib C source.

Install steps

# 1. Install rustup (if not already installed)
#    Download from https://rustup.rs/ and run the installer.

# 2. Confirm the MSVC toolchain is active
rustup show

# 3. Create a new project
cargo new my-raylib-game
cd my-raylib-game

# 4. Add raylib as a dependency
cargo add raylib

# 5. Build
cargo build

If the build succeeds you will see raylib compile and link. Replace src/main.rs with the hello-window snippet from the Quickstart and run with cargo run.

Windowing and graphics back end

raylib-rs uses GLFW + OpenGL 3.3 by default on Windows. The wayland and drm features are Linux-only and have no effect here.

Troubleshooting

  • “cannot find libclang” or bindgen errors — bindgen requires a working LLVM/clang installation on some paths. Install the LLVM toolchain for Windows and set LIBCLANG_PATH to the LLVM bin directory. See the bindgen requirements docs and the project backlog issue #254 for known issues.

  • “link.exe not found” or linker errors — confirm that Visual Studio Build Tools are installed with the “Desktop development with C++” workload. Run rustup default stable-x86_64-pc-windows-msvc to ensure you are on the MSVC toolchain.

  • CMake not found — ensure cmake --version works in a new terminal. Add CMake’s bin directory to your PATH or reinstall CMake with the “Add CMake to the system PATH” option checked.

Install on macOS

raylib-rs builds on macOS with the default Apple clang toolchain. Both Apple Silicon (M1/M2/M3) and Intel Macs are supported.

Prerequisites

  1. Rust 1.85+ — install via rustup.

  2. Xcode Command Line Tools — provides clang, ar, and the macOS SDK headers. Install once with:

    xcode-select --install
    

    If you already have a full Xcode install, the command line tools are bundled.

  3. CMake 3.15+ — the easiest way is via Homebrew:

    brew install cmake
    

    Alternatively, download the macOS DMG from cmake.org and add the bin directory to your PATH.

Install steps

# 1. Create a new project
cargo new my-raylib-game
cd my-raylib-game

# 2. Add raylib as a dependency
cargo add raylib

# 3. Build
cargo build

Replace src/main.rs with the hello-window snippet from the Quickstart and run with cargo run.

Apple Silicon vs Intel

Both architectures use the same build path. On Apple Silicon, macOS routes OpenGL calls through Apple’s Metal compatibility layer — performance is excellent for most use cases. The wayland and drm features are Linux-only and have no effect on macOS.

Troubleshooting

  • Framework linker errors (ld: framework not found OpenGL or similar) — your Xcode Command Line Tools may be out of date. Run xcode-select --install to reinstall, or update Xcode from the App Store, then re-run sudo xcode-select -s /Applications/Xcode.app/Contents/Developer.

  • xcrun: error: SDK not found — ensure the macOS SDK is present. Run xcrun --show-sdk-path to check. If it errors, reinstall the Command Line Tools.

  • Slow first build — the raylib-sys build script compiles the vendored raylib C source with CMake. Subsequent builds use the incremental cache and are much faster.

Install on Linux

raylib-rs builds on Linux using the system GCC or Clang toolchain. You need the X11 and/or Wayland development headers, CMake, and the OpenGL headers. The exact package names differ by distribution.

Prerequisites

  1. Rust 1.85+ — install via rustup.

  2. CMake 3.15+ and build essentials — see the distribution-specific instructions below.

  3. Native graphics headers — X11 + OpenGL are required for the default build. Wayland headers are optional (only needed for the wayland feature).

Ubuntu / Debian

sudo apt-get update
sudo apt-get install --no-install-recommends -y \
    cmake \
    libasound2-dev \
    libudev-dev \
    libx11-dev \
    libxrandr-dev \
    libxinerama-dev \
    libxcursor-dev \
    libxi-dev \
    libgl1-mesa-dev

This is the exact package list used by the CI check.yml, test.yml, and book.yml workflows.

Fedora / RHEL / AlmaLinux

sudo dnf install -y \
    cmake \
    gcc-c++ \
    alsa-lib-devel \
    systemd-devel \
    libX11-devel \
    libXrandr-devel \
    libXinerama-devel \
    libXcursor-devel \
    libXi-devel \
    mesa-libGL-devel

Arch Linux

sudo pacman -S --needed \
    cmake \
    base-devel \
    alsa-lib \
    systemd-libs \
    libx11 \
    libxrandr \
    libxinerama \
    libxcursor \
    libxi \
    mesa

Build and run

cargo new my-raylib-game
cd my-raylib-game
cargo add raylib
cargo build

Replace src/main.rs with the hello-window snippet from the Quickstart and run with cargo run.

Wayland and DRM

  • Wayland — opt in with --features wayland. Additionally requires:
    • Ubuntu/Debian: libwayland-dev libxkbcommon-dev
    • Fedora: wayland-devel libxkbcommon-devel
    • Arch: wayland libxkbcommon
  • DRM / tty rendering — opt in with --features drm,opengl_es_20. Renders directly to the framebuffer without a display server; useful for embedded/kiosk targets.

Troubleshooting

  • cannot find libclang — install clang and libclang-dev (Ubuntu: sudo apt-get install clang libclang-dev; Fedora: sudo dnf install clang). Set the LIBCLANG_PATH environment variable if bindgen still cannot find it.

  • cmake: command not found — install cmake from your package manager (see above).

  • Wayland: display server not detected — if you are running under X11 and get Wayland compositor errors, ensure you are either using the wayland feature intentionally or have omitted it (X11 is the default).

Install for the Web

raylib-rs targets wasm32-unknown-emscripten, which compiles your game to WebAssembly and runs it in a browser via OpenGL ES 2 (WebGL). This path is build-verified continuously in the web.yml CI workflow.

Prerequisites

  1. Rust 1.85+ — install via rustup.

  2. The wasm32-unknown-emscripten target:

    rustup target add wasm32-unknown-emscripten
    
  3. emsdk 3.1.64+ — the Emscripten SDK provides emcc, the compiler that cross-compiles C to WebAssembly. Follow the official emsdk install instructions:

    git clone https://github.com/emscripten-core/emsdk.git
    cd emsdk
    ./emsdk install 3.1.64
    ./emsdk activate 3.1.64
    source ./emsdk_env.sh   # add emcc to PATH for this shell session
    

Required environment variable

The raylib-sys build script requires EMCC_CFLAGS to be set when targeting wasm32-unknown-emscripten. Set it before building:

export EMCC_CFLAGS="-O3 -sUSE_GLFW=3 -sASSERTIONS=1 -sWASM=1 -sASYNCIFY -sGL_ENABLE_GET_PROC_ADDRESS=1"

This exact value is used by the CI web.yml workflow.

Build

cargo build -p raylib --target wasm32-unknown-emscripten

This compiles the raylib crate (and the vendored raylib C source) for the WebAssembly target.

Running in a browser

A cargo build produces a .wasm binary and a .js glue file, but running in a browser also requires an HTML shell that loads them. raylib provides an HTML shell template. The WS9 showcase site (the final workstream of the 6.0 upgrade) will provide working web examples and a reference HTML shell.

Known limitation

The software_renderer feature (PLATFORM=Memory / rlsw) is not currently supported on wasm32-unknown-emscripten. The software renderer is a headless desktop-testing path; WebAssembly uses WebGL (GLES2), not the CPU-only rlsw backend. This is a tracked-deferred item from WS6b; see docs/superpowers/notes/ws6b-complete.md item 5.

Known-unsupported / future work

The following targets are tracked in the project backlog but are not currently supported. Builds for these targets may fail or produce incorrect output.

  • aarch64 Linux — the build is currently broken on 64-bit ARM Linux. Tracked in issue #227.

  • Cross-compile Linux → Windows (debug mode) — cross-compilation from Linux to Windows targets fails in debug mode. Tracked in issue #181.

  • NixOS X11 — building under NixOS with the X11 back end requires extra pkgs configuration not yet covered by the official build instructions. Tracked in PR #289 and issue #288. This target is part of the WS6 platform matrix.

  • software_renderer on wasm32-unknown-emscripten — the CPU-only headless renderer (PLATFORM=Memory / rlsw) is incompatible with the WebAssembly target. Tracked as a deferred item in docs/superpowers/notes/ws6b-complete.md (item 5).

These are tracked in the project backlog; community PRs welcome.

Handle and thread

RaylibHandle is the entry point for almost every raylib operation. Together with RaylibThread, it is returned by raylib::init().build(). The two-value return enforces two invariants at the type level: there can only be one active window at a time (single-init), and drawing can only happen on the thread that owns the handle.

RaylibHandle owns the window and the OpenGL context. RaylibThread is a zero-sized marker token that proves you are on the main thread. You pass &thread into begin_drawing and other thread-sensitive calls; the borrow checker ensures they are never called from a background thread.

API surface

Example

The following shows the canonical init-loop-draw pattern, highlighting where the handle and thread token appear:

extern crate raylib;
use raylib::prelude::*;

fn main() {
    // init() returns a RaylibBuilder; .build() opens the window.
    // The two return values are the handle (rl) and the thread token (thread).
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Handle and thread demo")
        .build();

    while !rl.window_should_close() {
        // begin_drawing takes &thread — the borrow checker ensures this is
        // the main thread.  The returned guard commits the frame on drop.
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        d.draw_text("RaylibHandle in action", 20, 20, 20, Color::DARKGRAY);
    }
    // rl is dropped here, tearing down the window and GL context.
}

Gotchas

  • RaylibThread is !Send — never put it in a Mutex, Arc, or thread::spawn payload. The compiler will reject the attempt; this is intentional.
  • Single-init — calling raylib::init().build() while an existing RaylibHandle is live will panic. raylib’s C layer maintains global window state; there is no way to have two windows in one process.
  • Teardown order — window teardown happens when RaylibHandle is dropped. If you mem::forget the handle, raylib’s GL context leaks. Let Rust’s normal drop order handle cleanup; do not try to tear down the window manually.

See also

RAII and resources

Every raylib resource in raylib-rs is a Rust struct that cleans up on drop. The Unload* family of C functions is never exposed in the public API — Drop implementations call them automatically when the resource leaves scope. This eliminates the class of bug where a resource is forgotten or freed twice.

The pattern applies uniformly: Image, Texture2D, RenderTexture2D, Font, Mesh, Shader, Material, and Model all implement Drop. You allocate with load_* or gen_image_*; you free by letting the value go out of scope (or by calling drop() explicitly when order matters).

API surface

  • Image — CPU-side pixel buffer; dropped by calling UnloadImage.
  • Texture2D — GPU texture handle; dropped by calling UnloadTexture.
  • RenderTexture2D — off-screen render target; dropped by UnloadRenderTexture.
  • Font — loaded font; dropped by UnloadFont.
  • Mesh — vertex data; owned by Model (do not drop separately).
  • Shader — GPU shader program; dropped by UnloadShader.
  • Material — material parameters; owned by Model.
  • Model — 3D model including meshes and materials; dropped by UnloadModel.
  • ModelAnimations — new in 6.0, RAII wrapper for the skeletal-animation animation set.

Example

The example below shows resource loading and explicit teardown order. image is dropped first; font is kept alive until the end of the outer scope. Because Drop is deterministic in Rust, the sequence is predictable and correct.

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("RAII demo")
        .build();

    // gen_image_color returns an Image that owns its pixel buffer.
    let image = Image::gen_image_color(64, 64, Color::RED);

    // load_texture_from_image uploads to the GPU.
    let texture = rl.load_texture_from_image(&thread, &image).unwrap();

    // Explicit drop: image is freed here (UnloadImage called).
    // texture remains alive.
    drop(image);

    // texture is freed here when it leaves scope (UnloadTexture called).
    drop(texture);

    // RaylibHandle (rl) is freed here, tearing down the window.
}

Gotchas

  • Lifetime-bound audio resourcesWave, Sound, Music, and AudioStream are bound by lifetime to a RaylibAudio. They cannot outlive the audio device. See the audio chapter.
  • raylib-allocated buffers — functions that return raylib-managed byte slices (e.g., exported image data) wrap them as ManuallyDrop<Box<[T]>> and free via the matching Unload* or MemFree call — never libc::free. This stays correct under custom raylib allocators. See DECISIONS.md for the rationale.
  • Model owns its Mesh and Material arrays — do not call drop() on a Mesh or Material that came from a Model. Dropping the Model handles everything.

See also

Strings and allocations

raylib is a C API, so strings cross an FFI boundary on almost every call. raylib-rs handles the two different use cases separately:

  • Most API functions accept &str and return owned String. The binding converts to a null-terminated CString internally. This is ergonomic and safe; the small allocation is acceptable for infrequent calls like loading a file or setting a window title.
  • Per-frame GUI draw functions take &CStr to avoid allocations inside the render loop. The rstr! macro constructs a &CStr at compile time from a string literal — zero allocation, zero runtime cost.

The WS5 raygui binding goes further: raygui’s string parameters use impl AsRef<str> signatures backed by a thread-local scratch buffer. This means you can pass &str or String directly to GUI calls without wrapping them in rstr!.

API surface

  • rstr! — constructs a &'static CStr from a string literal at compile time, or a CString from a format string.
  • &CStr parameters — the low-level string type used by per-frame GUI draw functions.
  • &str parameters — the ergonomic string type used by most load/config functions.
  • impl AsRef<str> — the raygui parameter convention (WS5); accepts &str, String, and anything that derefs to str.

Example

rstr! is the idiomatic way to create a &CStr literal without a heap allocation:

#![allow(unused)]
fn main() {
extern crate raylib;
use std::ffi::CStr;

// rstr! is a macro exported from the raylib crate root.
let label: &CStr = raylib::rstr!("hello");
assert_eq!(label.to_bytes(), b"hello");
}

The compile-time form (rstr!("literal")) embeds a null byte and wraps the bytes as &CStr without any runtime allocation. The runtime form (rstr!("{} items", count)) allocates a CString and is suitable for dynamic strings outside the hot path.

Gotchas

  • Per-frame GUI calls use &CStr — use rstr! for compile-time labels. For dynamic strings inside a frame, prefer the impl AsRef<str> raygui API (WS5) which uses a thread-local buffer and avoids a heap allocation on every call.
  • Embedded nul bytes — backlog #220 notes that OsStr::to_string_lossy does not replace embedded nul bytes. raylib-rs validates its string inputs, but be aware that C raylib will truncate at the first nul if a nul somehow reaches the C side.
  • raylib-allocated buffers — some functions return raylib-managed byte slices wrapped as ManuallyDrop<Box<[T]>>. These must be freed via the matching Unload* or MemFree, not libc::free. See DECISIONS.md.

See also

Safety

raylib-rs is a safe Rust wrapper over a C library. The safe API surface — everything in raylib::prelude — never requires unsafe from the caller. Internally, unsafe is used sparingly and always justified.

The project follows two conventions enforced by code review:

  • Every unsafe fn has a /// # Safety doc that states the preconditions the caller must uphold.
  • Every unsafe { ... } block has a // SAFETY: comment explaining why the specific call is sound at that site.

These conventions make it easy to audit the unsafety budget during reviews and when upgrading raylib’s C version.

API surface

Conventions, not API items — this chapter describes the disciplines baked into the codebase. Key structural choices that influence safety:

  • RAII wrappers (Image, Texture2D, Font, …) ensure resources are freed exactly once. No raw pointers are exposed to callers.
  • RaylibThread: !Send + !Sync — the main-thread token is not sendable, so thread-sensitive raylib calls can only be made from the thread that called init().
  • No Send/Sync for pointer-owning wrappers — types that own opaque C pointers do not implement AsRef/AsMut in ways that could allow aliased mutable access (partial WS6 fix for #277).
  • Audio lifetimesSound, Music, and AudioStream carry lifetime parameters tying them to the RaylibAudio that created them; the borrow checker prevents use-after-free.

Example

The excerpt below is taken from raylib/src/rlgl/mod.rs and shows both conventions in action:

/// Push the current matrix onto the stack; the returned guard pops it on drop.
/// Apply transforms (rl_translatef/rl_rotatef/rl_scalef) through the guard.
#[inline]
fn rl_push_matrix(&mut self) -> RlMatrix<'_, Self> {
    // SAFETY: the returned RlMatrix's Drop calls the matching rlPopMatrix.
    unsafe { ffi::rlPushMatrix() };
    RlMatrix::new(self)
}

/// Reset the current matrix to identity.
#[inline]
fn rl_load_identity(&mut self) {
    // SAFETY: rlLoadIdentity is an unconditional state mutation; no preconditions.
    unsafe { ffi::rlLoadIdentity() }
}

rl_push_matrix is itself a safe function — callers need no unsafe. The // SAFETY: comment explains the invariant: the RlMatrix guard’s Drop impl guarantees the matching rlPopMatrix call, so the push/pop pair is always balanced.

A full unsafe fn with a # Safety doc follows the same pattern — see raylib/src/core/databuf.rs for the RlManaged type, where each constructor documents its preconditions on pointer provenance and allocator compatibility.

Gotchas

  • Don’t assume FFI calls are safe because the C looks innocuous. raylib’s C functions carry implicit preconditions (e.g., window must be initialised, texture must be uploaded, pointer must not be null). The binding encodes these into the Rust type system where possible; where it cannot (raw FFI escape hatches), # Safety docs spell them out.
  • AsRef/AsMut unsoundness — pointer-owning wrappers do not implement AsRef/AsMut in ways that could permit aliased mutation of the same C object from multiple Rust references. The WS6-prep partial fix (#277) removed the most dangerous impls; a full refactor is tracked-deferred.
  • Skeletal-animation Drop — the pre-6.0 code called UnloadModelAnimations via a raw *mut pointer in a way that could double-free. The WS3 redesign introduced ModelAnimations as a proper RAII struct with a safe Drop impl. If you are upgrading from 5.x, replace any manual animation teardown with the new type.

See also

Features and platforms

raylib-rs uses Cargo features to gate platform back-ends, optional math integrations, and the software renderer. The default feature set mirrors raylib 6.0’s config.h SUPPORT_* configuration: everything a desktop OpenGL 3.3 application needs, and nothing it doesn’t.

Optional integrations — glam, mint, serde — are strictly opt-in. A default build carries zero math-crate dependencies. The full feature alias is the canonical “everything except mutually- exclusive back-ends” target used for CI linting and testing.

Important: --all-features is invalid for raylib-sys. Always use --features full when you want maximum capability in a single build invocation.

API surface

Feature flags (headline set):

  • opengl_33 — OpenGL 3.3 core; this is the default back-end on desktop.
  • opengl_21 — OpenGL 2.1 compatibility; older hardware and some embedded targets.
  • opengl_es_20 — OpenGL ES 2.0; mobile / DRM back-end. Use with drm.
  • drm — DRM/KMS tty rendering; pair with opengl_es_20.
  • wayland — Wayland display server support (Linux).
  • software_renderer — CPU software renderer (PLATFORM=Memory, rlsw). Mutually exclusive with opengl_*. Enables the raylib::test_harness headless render-test module.
  • glamFrom/Into conversions between raylib’s Vector*/Matrix/Quaternion and glam::Vec*/glam::Mat4/glam::Quat.
  • mintFrom/Into conversions with mint::Vector*/mint::ColumnMatrix*.
  • serdeSerialize/Deserialize derives on math and color types.
  • nobuild — skip building the vendored raylib C source (you supply the link target).
  • nobindgen — skip re-generating raylib-sys bindings (read from RAYLIB_BINDGEN_LOCATION).
  • full — curated maximum-capability alias (excludes mutually-exclusive back-ends and escape hatches); use this for cargo test --features full and CI.

Example

Three common Cargo.toml stanzas:

# 1. Default desktop build (OpenGL 3.3, no optional adapters)
[dependencies]
raylib = "6.0.0-rc.2"

# 2. Headless / CI build using the software renderer (no GPU required)
[dependencies]
raylib = { version = "6.0.0-rc.2", features = ["software_renderer"] }

# 3. Game with glam math and serde serialization
[dependencies]
raylib = { version = "6.0.0-rc.2", features = ["glam", "serde"] }

Gotchas

  • opengl_* features are mutually exclusive — by convention, select only one. The build script does not currently diagnose multiple selections (the last #[cfg] wins), so enabling more than one is a footgun.
  • software_renderer is mutually exclusive with opengl_*. The two back-ends cannot be compiled into the same binary. It is also currently incompatible with wasm32-unknown-emscripten — tracked-deferred; see notes/ws6b-complete.md.
  • --all-features is invalid for raylib-sys because it would try to enable multiple mutually-exclusive back-ends simultaneously. Use --features full instead.
  • SUPPORT_* flags — individual raylib capability flags (e.g., SUPPORT_IMAGE_GENERATION, SUPPORT_FILEFORMAT_PNG) map directly to Cargo features of the same name. They are included in full and usually do not need to be set explicitly.

See also

Window and drawing

The window is opened by calling raylib::init().build(). The builder returns a (RaylibHandle, RaylibThread) pair that controls the entire lifecycle: the handle exposes every per-frame operation, and the thread token is the proof that you are on the correct OS thread. Drawing happens between begin_drawing and the end of the returned guard’s lifetime — all draw primitives live on the RaylibDraw trait implemented by RaylibDrawHandle.

API surface

  • RaylibBuilder — fluent builder returned by raylib::init(); call .build() to open the window. Key builder methods: .size(w, h), .title(s), .vsync(), .msaa_4x(), .undecorated().
  • RaylibHandle — the main handle; owns the window and is the receiver for most API calls.
  • RaylibHandle::set_target_fps — cap the frame rate. set_target_fps(0) disables the limiter (busy-spin).
  • RaylibHandle::begin_drawing — returns a scoped RaylibDrawHandle; EndDrawing fires automatically on drop.
  • RaylibDraw — trait with all 2D drawing primitives: clear_background, draw_text, draw_rectangle, draw_line, draw_circle, draw_texture, and more.
  • RaylibHandle::window_should_close — returns true when the OS close event has been sent.

Example

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Hello, raylib-rs!")
        .vsync()
        .build();

    rl.set_target_fps(60);

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        d.draw_text("Hello, world!", 12, 12, 20, Color::DARKGRAY);
    }
    // RaylibHandle is dropped here; CloseWindow() is called automatically.
}

Gotchas

  • Draw only inside the guard. All methods on RaylibDraw are only accessible on the RaylibDrawHandle scope. You cannot call draw_text directly on RaylibHandle — you must enter begin_drawing first.
  • FPS pacing. set_target_fps(0) removes the frame limiter entirely; raylib will busy-spin. Omitting the call defaults to the raylib default (60 on most platforms).
  • Window state queries. is_window_minimized, is_window_resized, and related helpers live on the RaylibWindow trait — check the trait’s documentation for the current set of methods.
  • RaylibThread is !Send. Never put it in a Mutex, Arc, or hand it to another thread. See the Handle and thread chapter for why.

See also

  • Handle and thread — lifecycle details and the !Send invariant.
  • Input — reading keyboard, mouse, and gamepad inside the loop.
  • Shapes — the full 2D primitive catalogue.

Showcase examples

Showcase examples that exercise this module:

Input

Input in raylib-rs is queried per-frame off RaylibHandle — there is no callback or event queue. Every call snapshots the state that raylib collected during the previous EndDrawing call. The API covers keyboard, mouse, gamepad, and touch in a uniform style: is_*_down for level queries and is_*_pressed/is_*_released for edge-triggered queries.

API surface

Example

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Input demo")
        .build();

    while !rl.window_should_close() {
        // Level query — true every frame the key is held.
        if rl.is_key_down(KeyboardKey::KEY_SPACE) {
            println!("SPACE held");
        }

        // Edge query — true only on the first frame of a press.
        if rl.is_key_pressed(KeyboardKey::KEY_ENTER) {
            println!("ENTER pressed");
        }

        let mouse = rl.get_mouse_position();
        let scroll = rl.get_mouse_wheel_move();

        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        d.draw_text(
            &format!("mouse: ({:.0}, {:.0})  scroll: {:.1}", mouse.x, mouse.y, scroll),
            10, 10, 20, Color::DARKGRAY,
        );
    }
}

Gotchas

  • is_key_pressed is per-frame edge-triggered. It returns true only on the single frame where the key transitions from up to down. If you poll it every frame and hold the key, you will see exactly one true unless you also check is_key_pressed_repeat for held-down auto-repeat.
  • Mouse coordinates are Y-down. get_mouse_position().y increases downward, which matches window pixel conventions but is the opposite of mathematical Y-up.
  • get_gamepad_button_pressed transmute UB (tracked-deferred). The current implementation performs a transmute::<u32, GamepadButton> on the raw integer returned by raylib. If raylib returns a value outside the known GamepadButton variants, this is undefined behaviour. A soundness fix is tracked for a future PR. For now, rely only on is_gamepad_button_pressed / is_gamepad_button_down with explicit GamepadButton variants.

See also

Showcase examples

Showcase examples that exercise this module:

Shapes

2D drawing primitives — rectangles, lines, circles, triangles, polygons, and individual pixels — are all methods on the RaylibDraw trait. The trait is implemented by RaylibDrawHandle, so all calls live inside the begin_drawing scope. No extra state or resource loading is required; shapes are drawn immediately with a color and optional parameters for thickness, rounding, or gradients.

API surface

  • draw_rectangle — filled rectangle by position + size.
  • draw_rectangle_v — filled rectangle by Vector2 position + size.
  • draw_rectangle_rec — filled rectangle from a Rectangle value.
  • draw_circle — filled circle by center + radius.
  • draw_circle_v — filled circle by Vector2 center + radius.
  • draw_line — 1-pixel line between two integer points.
  • draw_line_ex — thick line between two Vector2 points with a thickness parameter.
  • draw_triangle — filled triangle from three Vector2 vertices (counter-clockwise winding).
  • draw_poly — filled regular polygon (n sides).
  • draw_pixel — single pixel by integer coordinates.

Example

The example opens a window and draws a rectangle and a circle every frame. A software-renderer version of this is available as part of the WS9 showcase; the live example here requires a display.

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Shapes demo")
        .vsync()
        .build();

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);

        // Filled rectangle at (50, 50), 200×100, blue.
        d.draw_rectangle(50, 50, 200, 100, Color::BLUE);

        // Filled circle at (400, 200), radius 80, red.
        d.draw_circle(400, 200, 80.0, Color::RED);

        // 1-pixel line from (0, 0) to (640, 480), dark gray.
        d.draw_line(0, 0, 640, 480, Color::DARKGRAY);
    }
}

Gotchas

Shapes are stable across raylib 5.x and 6.0; there are no breaking changes to the 2D primitive set. The only points worth noting:

  • Counter-clockwise winding for draw_triangle. raylib expects vertices in counter-clockwise order; clockwise-wound triangles are culled.
  • Integer vs. vector variants. Most primitives have both an integer overload (draw_circle(cx: i32, cy: i32, …)) and a _v overload (draw_circle_v(center: Vector2, …)). Prefer _v when your coordinates are already Vector2.

See also

Showcase examples

Showcase examples that exercise this module:

Textures and images

raylib distinguishes two related types: Image is CPU-side pixel data (a heap-allocated buffer you can read and modify without a GPU or a window), and Texture2D is a GPU-side handle created by uploading an Image. The typical flow is: generate or load an Image, manipulate it, then call load_texture_from_image on RaylibHandle to push it to the GPU.

Both types are RAII resources — they clean up automatically on drop (see RAII and resources).

API surface

Example

CPU-side image operations (generation and pixel read-back) work without a window or GPU. The example below generates a red image and asserts a pixel value — no display required.

extern crate raylib;
use raylib::prelude::*;

fn main() {
    // Generate a 64×64 solid-red Image entirely in CPU memory.
    let img = Image::gen_image_color(64, 64, Color::RED);

    // Read back a pixel and verify its color.
    let pixel = img.get_color(32, 32);
    assert_eq!(pixel.r, 255);
    assert_eq!(pixel.g, 0);
    assert_eq!(pixel.b, 0);
    // `img` is dropped here; UnloadImage is called automatically.
}

Uploading to the GPU requires a window:

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Texture demo")
        .build();

    let img = Image::gen_image_color(64, 64, Color::BLUE);
    // load_texture_from_image uploads to GPU; img can be dropped afterwards.
    let tex = rl.load_texture_from_image(&thread, &img).unwrap();
    drop(img); // CPU buffer freed; GPU copy lives on in `tex`.

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        d.draw_texture(&tex, 100, 100, Color::WHITE);
    }
}

Gotchas

  • Image is CPU; Texture2D is GPU. You cannot modify a Texture2D directly. If you need per-frame CPU edits, keep the Image, modify it, and re-upload with update_texture.
  • RenderTexture2D Y-flip on readback. The WS4 headless harness reads the software-renderer framebuffer as BGRA with Y-inverted rows and then normalises it for you. If you use load_image_from_screen directly, be aware of the GL-style bottom-left origin. See notes/ws4b-complete.md.
  • Memory-leak fix in 6.0. export_image_to_memory had a leak in 5.x (backlog #247). It was fixed by PR #250 in WS3 — the buffer is now wrapped as ManuallyDrop<Box<[u8]>> and freed correctly.

See also

Showcase examples

Showcase examples that exercise this module:

Text and fonts

raylib bundles a default bitmap font that is available as soon as the window is open. Custom fonts are loaded via RaylibHandle::load_font (from a file) or RaylibHandle::load_font_ex (with an explicit codepoint set for preloaded glyph atlases). Both return a RAII Font that is automatically unloaded on drop. Text drawing — including measuring — hangs off RaylibHandle for the default font and off the Font struct or RaylibDraw trait for custom fonts.

API surface

Example

Drawing text requires an open window. The example below opens a 640×480 window, draws a greeting with the default font each frame, and measures its width for centering.

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Text demo")
        .vsync()
        .build();

    let message = "Hello, raylib-rs!";
    let font_size = 24;

    while !rl.window_should_close() {
        // Measure text width for the default font.
        let width = rl.measure_text(message, font_size);

        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);

        // Centered draw using the default font.
        d.draw_text(
            message,
            (640 - width) / 2,
            (480 - font_size) / 2,
            font_size,
            Color::DARKGRAY,
        );
    }
}

Gotchas

  • Custom fonts need a window. load_font and load_font_ex call into raylib’s GPU atlas-building path, so they must be called after raylib::init().build(). Font data files (.ttf, .otf, .fnt, .bdf) must be on disk at the given path.
  • Codepoint preloading. load_font_ex takes an optional &[i32] codepoint list. If None, raylib loads the default Latin set (32–127). Pass an explicit list if your application draws CJK, emoji, or other extended Unicode ranges.
  • raylib 6.0 new text symbols. The 6.0 release added more than 30 new text utility functions in the C library. The Rust parity checklist (docs/superpowers/plans/) tracks which are exposed; check the text module rustdoc for the current set.

See also

Showcase examples

Showcase examples that exercise this module:

3D models

3D rendering in raylib-rs uses Camera3D for view/projection and a RaylibMode3D scope (entered via begin_mode3D) that activates 3D draw calls. A Model bundles one or more Mesh arrays and Material arrays into a single RAII resource. Skeletal animation is exposed through the new 6.0 ModelAnimations wrapper.

API surface

  • RaylibHandle::load_model — load a model from a file (.obj, .glb, .gltf, .iqm, …); returns Result<Model, …>.
  • Mesh — vertex data; owned by Model — do not drop it separately.
  • Material — material parameters (textures, colours, shader); owned by Model.
  • ModelAnimations — RAII wrapper (new in 6.0) for the skeletal-animation set returned by load_model_animations.
  • Camera3D — position, target, up-vector, FOV, and projection mode.
  • RaylibMode3DExt::begin_mode3D — enters 3D mode; returns a RaylibMode3D guard that calls EndMode3D on drop.
  • RaylibDraw3D::draw_model — draw a model at a position with a scale and tint.

Example

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(800, 600)
        .title("3D model demo")
        .vsync()
        .build();

    let model = rl.load_model(&thread, "assets/robot.glb").unwrap();

    let camera = Camera3D {
        position: Vector3::new(5.0, 5.0, 5.0),
        target:   Vector3::new(0.0, 0.0, 0.0),
        up:       Vector3::new(0.0, 1.0, 0.0),
        fovy:     45.0,
        projection: CameraProjection::CAMERA_PERSPECTIVE,
    };

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        {
            let mut d3 = d.begin_mode3D(camera);
            d3.draw_model(&model, Vector3::zero(), 1.0, Color::WHITE);
        }
    }
    // `model` is dropped here; UnloadModel is called automatically.
}

Gotchas

  • ModelAnimations is the new RAII wrapper (6.0). Prior to 6.0, you had to call UnloadModelAnimations manually; the new ModelAnimations struct handles this on drop. Load animations via RaylibHandle::load_model_animations.
  • Model owns its Mesh and Material arrays. Do not call drop() on a Mesh or Material obtained from a Model — the Model’s own Drop impl handles everything. Dropping them separately is a double-free.
  • Mesh accessor soundness (WS6-prep). PRs #257/#118/#256 (folded in WS6) changed the safe accessors to return &[T] slices whose length is guaranteed by the C struct. Out-of-bounds access is no longer possible through the safe API.
  • Tracked-deferred. Issue #283 (improper index calculation) and #207 (broader 3D API improvements) are deferred to future workstreams.

See also

Showcase examples

Showcase examples that exercise this module:

Audio

Audio in raylib-rs sits behind a separate device token: RaylibAudio. Call RaylibAudio::init_audio_device() to open the audio device and get the token. Every audio resource — Wave, Sound, Music, AudioStream — is created through methods on RaylibAudio and carries a lifetime tied to the RaylibAudio token. The borrow checker enforces that no audio resource outlives the device, so teardown order is correct by construction.

Note on the plan’s wording. The plan refers to “AudioHandle” — the real type name in the codebase is RaylibAudio (raylib::core::audio::RaylibAudio).

API surface

  • RaylibAudio::init_audio_device — open the audio device; returns Result<RaylibAudio, …>. Panics if called twice.
  • Wave — loaded wave data (raw PCM). Create via RaylibAudio::new_wave.
  • Sound — short audio clip ready for playback. Create via RaylibAudio::new_sound.
  • Music — audio stream for longer tracks. Create via RaylibAudio::new_music.
  • AudioStream — raw PCM stream for custom audio generation. Create via RaylibAudio::new_audio_stream.
  • Sound::play — play a Sound to completion (fire and forget).
  • Music::play_stream — begin streaming a Music track; call update_music_stream each frame.
  • AudioStream::is_processed — check whether the stream buffer needs refilling.

Example

extern crate raylib;
use raylib::prelude::*;
use raylib::core::audio::RaylibAudio;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Audio demo")
        .build();

    // Open the audio device.
    let audio = RaylibAudio::init_audio_device()
        .expect("failed to init audio device");

    // Load a short sound effect; Sound is lifetime-bound to `audio`.
    let sound = audio.new_sound("assets/coin.wav")
        .expect("failed to load sound");

    while !rl.window_should_close() {
        if rl.is_key_pressed(KeyboardKey::KEY_SPACE) {
            sound.play();
        }

        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);
        d.draw_text("Press SPACE to play a sound", 10, 10, 20, Color::DARKGRAY);
    }
    // `sound` is dropped before `audio` — the compiler enforces this via lifetime.
    // `audio` drop calls CloseAudioDevice.
}

Gotchas

  • RaylibAudio must outlive every audio resource. The lifetime parameters on Wave, Sound, Music, and AudioStream are not just documentation — the compiler will reject code that drops RaylibAudio while any resource is still live.
  • The audio callback was removed in 6.0. The old per-sample callback API is gone. Use AudioStream with is_processed / update_audio_stream for custom PCM generation.
  • WS6-prep soundness fix. The unsound default Sound impl (backlog #277) was removed. Sound no longer implements Default.

See also

Showcase examples

Showcase examples that exercise this module:

raymath

raylib-rs 6.0 ships native #[repr(C)] math types directly from raylib-sys: [Vector2], [Vector3], [Vector4], [Matrix], and [Quaternion]. Their arithmetic comes from raylib’s own raymath via a C shim (binding/raymath_shim.c, RAYMATH_IMPLEMENTATION), wrapped as Rust methods and operators. No third-party math crate is needed by default — glam, mint, and serde are opt-in Cargo features.

You’ll land here when you need vector math, matrix transforms, or quaternion rotation without opening a window.

API surface

  • Vector2 — 2D vector with .dot, .cross, .normalize, .length, .length_sqr, .rotate, and operator overloads.
  • Vector3 — 3D vector with .dot, .cross, .normalize, .length, .rotate_by_quaternion, .rotate_by_axis_angle.
  • Vector4 — 4D vector; also the FFI representation of a quaternion on the C side.
  • Matrix — row-major 4×4 matrix (row-major in memory; see Gotchas); constructors Matrix::identity, Matrix::translate, Matrix::rotate, Matrix::rotate_x/y/z, Matrix::rotate_xyz/zyx.
  • Quaternion — distinct #[repr(C)] struct (C aliases it to Vector4). Constructors: Quaternion::identity, Quaternion::from_axis_angle, Quaternion::normalize, Quaternion::length.
  • Ray / BoundingBox — used by the 3D collision and raycasting helpers.
  • lerpf32 linear interpolation convenience function.
  • rquat — shorthand constructor for Quaternion.

Example

The math types are window-independent — this doctest runs with no GPU context.

#![allow(unused)]
fn main() {
extern crate raylib;
use raylib::math::{Vector3, Matrix};

// Vector operations
let a = Vector3::new(1.0, 0.0, 0.0);
let b = Vector3::new(0.0, 1.0, 0.0);

let d = a.dot(b);
assert_eq!(d, 0.0, "orthogonal vectors have zero dot product");

let c = a.cross(b);
assert!((c.z - 1.0).abs() < 1e-6, "cross product of x and y should be z");

let len = a.normalize().length();
assert!((len - 1.0).abs() < 1e-6, "normalized vector has unit length");

// Matrix identity
let m = Matrix::identity();
// raylib's Matrix is row-major in memory; identity diagonal is m0/m5/m10/m15.
assert_eq!(m.m0, 1.0);
assert_eq!(m.m5, 1.0);
assert_eq!(m.m10, 1.0);
assert_eq!(m.m15, 1.0);
assert_eq!(m.m1, 0.0);

// Translation matrix
let t = Matrix::translate(3.0, 0.0, 0.0);
// m12 is the x-translation entry in raylib's row-major memory layout.
assert_eq!(t.m12, 3.0);
}

Gotchas

  • Quaternion is distinct from Vector4. In C, Quaternion is a typedef for Vector4. raylib-rs uses a distinct #[repr(C)] struct with the same layout for type safety — the compiler will not let you pass a Vector4 where a Quaternion is expected. Use Quaternion::from(v4) / Vector4::from(q) for explicit zero-cost conversion.
  • MintVec* types are deprecated. The MintVec2/MintVec3/MintVec4/MintMatrix/MintQuat type aliases from 5.x are still present but carry #[deprecated(since = "6.0.0")]. Replace them with the native Vector2/Vector3/Vector4/Matrix/Quaternion types. See ecosystem/glam-mint-serde.md for the opt-in integration story.
  • Zero math-crate deps by default. The default build depends only on raylib’s own raymath C implementation. Enable --features glam / --features mint / --features serde for conversions/trait impls. Never assume those features are present in library code.
  • Row-major memory layout, semantic column-major ops. raylib’s Matrix is row-major in memory — the C source (raymath.h) writes translation into m12/m13/m14. The math operations and parameter naming, however, treat the matrix as column-major; the two conventions describe the same data via different access patterns. glam::Mat4 is column-major in memory, so when comparing byte layouts side-by-side they will differ — but the --features glam adapter handles the mapping so values round-trip correctly.

See also

Showcase examples

Showcase examples that exercise this module:

Collision

The collision module provides pure-data helpers for detecting overlap between 2D and 3D shapes. All functions are free functions (or methods on geometry types) — no window, no GPU, no handle required. They map directly to raylib’s CheckCollision* and GetRayCollision* family.

API surface

Example

Rectangle and circle collision — pure Rust, no window needed.

#![allow(unused)]
fn main() {
extern crate raylib;
use raylib::math::Rectangle;
use raylib::core::collision::{check_collision_circles, check_collision_lines};
use raylib::math::Vector2;

// Two overlapping rectangles
let r1 = Rectangle::new(0.0, 0.0, 10.0, 10.0);
let r2 = Rectangle::new(5.0, 5.0, 10.0, 10.0);
assert!(r1.check_collision_recs(r2), "overlapping rects should collide");

// A separated rectangle pair
let r3 = Rectangle::new(20.0, 20.0, 5.0, 5.0);
assert!(!r1.check_collision_recs(r3), "non-overlapping rects should not collide");

// Point inside rectangle
assert!(r1.check_collision_point_rec(Vector2::new(3.0, 3.0)));
assert!(!r1.check_collision_point_rec(Vector2::new(15.0, 15.0)));

// Two overlapping circles (centers 1 apart, radii sum = 4)
assert!(check_collision_circles(
    Vector2::new(0.0, 0.0), 2.0,
    Vector2::new(1.0, 0.0), 2.0,
));

// Line intersection
let hit = check_collision_lines(
    Vector2::new(-1.0, -1.0), Vector2::new(1.0, 1.0),
    Vector2::new(-1.0,  1.0), Vector2::new(1.0, -1.0),
);
assert!(hit.is_some(), "diagonal lines should intersect");
let pt = hit.unwrap();
assert!((pt.x).abs() < 1e-4 && (pt.y).abs() < 1e-4);
}

Gotchas

  • check_collision_recs is a method on Rectangle, not a free function. Call it as rect.check_collision_recs(other). Similarly, check_collision_point_rec, check_collision_circle_rec, and get_collision_rec are methods on Rectangle.
  • check_collision_lines returns Option<Vector2>, not bool. Use .is_some() to test for collision and .unwrap() to read the intersection point.
  • Touch counts as collision. Circles whose edges exactly touch (distance equals the sum of radii) are considered colliding; this matches raylib’s C behaviour.

See also

Showcase examples

Showcase examples that exercise this module:

raygui

raygui is an immediate-mode GUI toolkit bundled with raylib. raylib-rs 6.0 exposes all 57 RAYGUIAPI functions at parity with raygui 6.0. Controls are grouped into sub-traits (RaylibGuiControls, RaylibGuiContainers, RaylibGuiState, RaylibGuiAdvanced, RaylibGuiIcons) and blanket-implemented for every draw handle — just use raylib::prelude::* and the methods are available inside a begin_drawing frame.

String parameters accept impl AsRef<str> (pass &str, String, or any type that derefs to a string slice) — conversion goes through a thread-local scratch buffer so there are no per-frame heap allocations.

Feature gate. raygui is behind the raygui Cargo feature. Add features = ["raygui"] (or "full") to your Cargo.toml dependency.

API surface

Example

raygui controls require an active drawing frame and a GPU context. The example below is marked ignore because the software_renderer feature is mutually exclusive with the default OpenGL build used by the book’s CI. The WS9 showcase will provide a runnable raygui demo once the web gallery is deployed.

extern crate raylib;
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(400, 300)
        .title("raygui demo")
        .build();

    let mut slider_val: f32 = 0.5;

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);

        // Label
        d.gui_label(
            Rectangle::new(20.0, 20.0, 200.0, 30.0),
            "Hello, raygui!",
        );

        // Button — returns true on click
        if d.gui_button(Rectangle::new(20.0, 60.0, 120.0, 30.0), "Click me") {
            println!("button clicked");
        }

        // Slider — mutates slider_val in place
        d.gui_slider(
            Rectangle::new(20.0, 110.0, 200.0, 20.0),
            "Min",
            "Max",
            &mut slider_val,
            0.0,
            1.0,
        );
    }
}

Gotchas

  • String parameters are impl AsRef<str>. Pass &str, String, or any type implementing AsRef<str>. Under the hood, each call writes the string to a thread-local CString scratch buffer — safe and zero-alloc per frame.
  • GUI state is global. raygui is immediate-mode; all style and state (gui_set_state, gui_load_style) affects every control drawn this frame. Call ordering matters.
  • gui_load_style_from_memory is absent from the vendored raygui header (backlog #296 — deferred until the vendored raygui advances).
  • raygui feature required. Without features = ["raygui"] the trait methods are not compiled in.

See also

Showcase examples

Showcase examples that exercise this module:

rlgl

rlgl is raylib’s thin OpenGL abstraction layer — the layer below raylib’s 2D/3D rendering modules that maps draw calls to OpenGL 3.3, OpenGL ES 2.0, or the software renderer depending on the active backend.

raylib-rs 6.0 exposes a safe wrapper for the immediate-mode subset of rlgl.h: a matrix stack, immediate-mode vertex streams, render-state toggles, and ergonomic methods that bind the crate’s safe [Texture2D] and [Shader] handles. The full 161-function rlgl surface remains available as ffi (power-user escape hatch) — the safe layer covers the everyday drawing needs.

All entry points hang off the [RaylibRlgl] trait, which is blanket-implemented for every draw handle, so they’re only callable inside a begin_drawing frame.

API surface

Example

The example requires a live GPU context and so cannot run in the book’s CI build (the software_renderer feature is mutually exclusive with the default OpenGL build). The WS9 showcase will provide a runnable demo.

extern crate raylib;
use raylib::prelude::*;
use raylib::rlgl::{DrawMode, RaylibRlgl};

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("rlgl demo")
        .build();

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        d.clear_background(Color::RAYWHITE);

        // Push a matrix, translate, draw an immediate-mode triangle, pop.
        {
            let mut m = d.rl_push_matrix();
            m.rl_translatef(320.0, 240.0, 0.0);

            m.rl_draw(DrawMode::Triangles, |v| {
                v.color4ub(Color::RED);
                v.vertex2f(-50.0, -50.0);
                v.color4ub(Color::GREEN);
                v.vertex2f( 50.0, -50.0);
                v.color4ub(Color::BLUE);
                v.vertex2f(  0.0,  50.0);
            });
        } // RlMatrix drops here → rlPopMatrix called automatically
    }
}

Gotchas

  • RlMatrix auto-pops on drop. Do not call rl_pop_matrix manually after using rl_push_matrix — the RAII guard handles it. The guard derefs to the parent draw handle so you can call other draw methods through it.
  • Immediate-mode vertices need texcoords under the software renderer. The rlsw backend samples the shapes texture; without texcoord2f calls the default (0,0) coordinate hits a transparent texel and emits zero pixels. Always emit texcoord2f alongside vertex2f/vertex3f when testing with the software renderer.
  • GL-object lifecycle stays with RAII. rl_set_texture / rl_enable_shader bind the existing RAII handles; they do not create or destroy GPU objects. Lifecycle management remains with Texture2D::Drop / Shader::Drop and the raw ffi create/destroy functions.

See also

Showcase examples

Showcase examples that exercise this module:

Software renderer

raylib 6.0 adds rlsw, a software renderer backend that renders into an in-memory framebuffer with no GPU or window required. raylib-rs exposes it via the software_renderer Cargo feature and the test_harness module (source) — a set of helpers that initialise a windowless context, draw a frame, read the framebuffer back, and let you probe pixel values. Note: test_harness is gated on #[cfg(feature = "software_renderer")] and does not appear on docs.rs (docs.rs builds with only the nobuild feature).

This is the mechanism behind raylib-rs’s Tier-2 render tests: the CI software-render job runs on all three platforms (ubuntu/macOS/windows) without a display server.

API surface

  • software_renderer Cargo feature — enables the rlsw backend. Add features = ["software_renderer"] to your Cargo.toml dependency.
  • with_headless(w, h, body) — initialise a w × h windowless context, run body(rl, thread), then tear down. Call at most once per test process (raylib is single-init per process).
  • render_frame(rl, thread, draw) — draw one frame via the draw closure and return a normalized top-left RGBA Image. Coordinates and colors match what you drew: Color::RED at screen (x, y) reads back as red at (x, y).
  • render_frame_raw(rl, thread, draw) — raw readback: BGRA bytes, Y-inverted. Use only when you need the unprocessed rlsw output.
  • pixel_at(img, x, y) — read the pixel at (x, y) as a Px value with r/g/b/a fields.
  • assert_pixel(img, x, y, expected, tol) — assert the RGB channels at (x, y) match expected within a per-channel tolerance tol. Alpha is intentionally ignored (the Memory platform readback does not guarantee alpha fidelity).

Example

The example below is rust,ignore because raylib’s CI build of the rlib that backs mdbook test uses the full feature, which falls through to PLATFORM=Desktop with the OpenGL backend. test_harness lives under #[cfg(feature = "software_renderer")], so it isn’t compiled into that rlib. To actually run a software-renderer test, compile the crate with --no-default-features --features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_IMAGE_GENERATION,raygui (this is what check.yml/test.yml do for the Tier-2 render tests). The WS9 showcase pipeline is the eventual home for runnable demos.

extern crate raylib;
use raylib::prelude::*;
use raylib::test_harness::{with_headless, render_frame, assert_pixel};

#[test]
fn red_rectangle_center_pixel() {
    with_headless(256, 256, |rl, thread| {
        let img = render_frame(rl, thread, |d| {
            d.clear_background(Color::BLACK);
            d.draw_rectangle(64, 64, 128, 128, Color::RED);
        });

        // Center of the rectangle should be red.
        assert_pixel(&img, 128, 128, Color::RED, 0);
        // A corner well outside the rectangle should be black.
        assert_pixel(&img, 4, 4, Color::BLACK, 0);
    });
}

Gotchas

  • software_renderer is not enabled by the full feature alias. The full alias explicitly excludes mutually-exclusive backend selectors (opengl_*, sdl, wayland, drm, software_renderer) per raylib/Cargo.toml. A --features full build therefore defaults to PLATFORM=Desktop with OpenGL — test_harness won’t be compiled in. To use the software renderer, build with --no-default-features --features software_renderer,... (see the canonical command below). The compile_error! in raylib-sys/build.rs only fires when an explicit opengl_* (or drm) feature is combined with software_renderer; full alone does not trigger it.
  • software_renderer is mutually exclusive with wasm32-unknown-emscripten (tracked-deferred). rlsw-on-Emscripten is not yet supported; see docs/superpowers/notes/ws6b-complete.md.
  • All required raylib modules must be linked. The harness requires SUPPORT_MODULE_RSHAPES, SUPPORT_MODULE_RTEXTURES, SUPPORT_MODULE_RTEXT, SUPPORT_MODULE_RMODELS, SUPPORT_MODULE_RAUDIO, plus SUPPORT_IMAGE_GENERATION (the safe gen_image_* family is currently ungated; MSVC link fails without it). The raygui feature is also enabled in CI so render_gui tests link cleanly. Disabling any required module causes a link error; see the memory note software-renderer-headless-testing.
  • with_headless is single-init. raylib can only be initialised once per process. Run software-renderer tests with:
    cargo test -p raylib --no-default-features \
      --features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_IMAGE_GENERATION,raygui \
      -- --test-threads=1
    
    --test-threads=1 is required because the harness initialises raylib’s platform layer once per process. In a test suite, wrap all headless tests inside a single with_headless call or use a process-global init strategy (e.g., std::sync::OnceLock).

See also

  • Features and platforms — feature flag reference.
  • test_harness source — module is cfg-gated on software_renderer; not surfaced on docs.rs.
  • docs/superpowers/notes/ws4b-complete.md — WS4b harness design rationale.
  • docs/superpowers/notes/ws5-complete.md — readback normalization details.

Callbacks and logging

raylib exposes hooks for several behaviours: trace-log output, custom file I/O loaders/savers, and audio stream processing. raylib-rs wraps the trace-log hook as a plain Rust function pointer so you can redirect raylib’s diagnostic output to your own logger.

The logging free functions (trace_log, set_trace_log) live in raylib::core::logging and are re-exported through raylib::prelude.

API surface

Example

Install a trace-log callback and emit one message. No window needed to install the callback, but trace_log calls into ffi::TraceLog which does require an active raylib context, so this example is no_run.

extern crate raylib;
use raylib::core::callbacks::set_trace_log_callback;
use raylib::core::logging::trace_log;
use raylib::consts::TraceLogLevel;

fn my_logger(level: TraceLogLevel, msg: &str) {
    eprintln!("[{level:?}] {msg}");
}

fn main() {
    // Install the callback before init so we capture startup messages too.
    set_trace_log_callback(my_logger).expect("callback already set");

    let (_rl, _thread) = raylib::init()
        .size(1, 1)
        .title("callback demo")
        .build();

    // Emit a custom message through the same callback.
    trace_log(TraceLogLevel::LOG_INFO, "Hello from the custom logger");
}

Gotchas

  • Callbacks are global state. Installing a new trace-log callback replaces the previous one. set_trace_log_callback always returns Ok(()). The old methods on RaylibHandle (e.g., rl.set_trace_log_callback(...)) are #[deprecated] — use the free function form instead.
  • The audio callback was removed in 6.0. The old per-sample audio callback API is gone entirely. Use AudioStream with is_processed / update_audio_stream for custom PCM generation, or attach a processor via attach_audio_stream_processor_to_music.
  • File I/O callbacks replace raylib’s I/O for the lifetime of the process. There is no “unset” for set_load_file_data_callback etc. — they can only be set once per callback slot.

Routing raylib logs through the log crate

With the opt-in log feature, the window builder can forward raylib’s TraceLog output into the log facade, so your application’s logger (env_logger, tracing-log, …) receives raylib’s logs alongside your own:

[dependencies]
raylib = { version = "6", features = ["log"] }
log = "0.4"
env_logger = "0.11"
env_logger::init(); // any `log`-compatible logger

let (mut rl, thread) = raylib::init()
    .size(800, 450)
    .title("game")
    .log_to_rust()
    .build();

log::info!("app and raylib logs share one pipeline now");

Messages arrive under the target "raylib", so RUST_LOG=raylib=debug scopes raylib’s output independently of your app’s.

The bridge makes the log facade the single level filter: it sets raylib’s own threshold to LOG_ALL (overriding .log_level()), so what you see is controlled entirely by your logger’s configuration. Raylib levels map TRACE→trace, DEBUG→debug, INFO→info, WARNING→warn, and both ERROR and FATAL to error (the facade has no fatal level; raylib still aborts after a fatal log, unchanged).

Two caveats:

  • The bridge claims the same single callback slot as set_trace_log_callback — they are mutually exclusive, last writer wins.
  • The bridge only emits into the facade. Without a logger installed, the messages are silently dropped, like any library using log. (It also requires the SUPPORT_TRACELOG feature, which is in the default set.)

See also

Showcase examples

Showcase examples that exercise this module:

Error handling

raylib-rs uses structured error types for all fallible operations. The safe crate defines a rich set of typed errors in raylib::core::error — each domain (images, textures, fonts, audio, models, etc.) has its own error enum. A top-level RaylibError aggregates all of them via #[from] conversions for callers that don’t need to distinguish the source.

Drawing operations are infallible — there is no error path for draw_rectangle, draw_text, etc. Errors arise only at resource load time.

API surface

  • InvalidImageError — returned by Image::load_image, Image::load_image_from_mem, etc. Variants include NullDataFromFile (file not found / unsupported format), ZeroWidth/ZeroHeight, UnsupportedFormat.
  • LoadTextureError — returned by load_texture, load_texture_from_image, load_render_texture.
  • LoadFontError — returned by load_font, load_font_ex.
  • LoadSoundError — returned by RaylibAudio::new_sound, new_wave, new_music.
  • LoadModelError — returned by load_model, load_model_from_mesh.
  • RaylibError — top-level enum aggregating all domain errors via From impls; useful when propagating with ? through a function that loads multiple resource types.
  • raylib::init().build() panics on a second call (window context is process-global). All other APIs are Result-based.

Example

extern crate raylib;
use raylib::core::texture::Image;
use raylib::core::error::InvalidImageError;

fn main() {
    match Image::load_image("nonexistent.png") {
        Ok(img) => {
            println!("loaded {}x{} image", img.width(), img.height());
        }
        Err(InvalidImageError::NullDataFromFile) => {
            eprintln!("file not found or unsupported format");
        }
        Err(e) => {
            eprintln!("unexpected image error: {e}");
        }
    }
}

Gotchas

  • Error variants are typed, not stringly-typed. Unlike some thin C-binding layers, raylib-rs maps sentinel return values to named enum variants. This makes match exhaustive and lets you distinguish “file not found” from “GPU upload failed” without parsing a message string.
  • Errors use thiserror. Every error type implements std::error::Error and Display. They compose naturally with anyhow, eyre, or any Box<dyn Error> handler.
  • ? propagation works if you use RaylibError as your function’s error type — every domain error has a From impl into it. Alternatively, the individual domain errors can be mapped with .map_err(RaylibError::from).
  • raylib::init().build() panics, not Err. Double-init is a programming error (not a runtime condition), so it panics rather than returning Err.

See also

glam, mint, serde

raylib-rs 6.0 makes math-ecosystem integration strictly opt-in. The default build ships zero extra math-crate dependencies — native #[repr(C)] types (Vector2, Vector3, Vector4, Matrix, Quaternion) cover every API surface with no third-party crate required.

Enable glam, mint, or serde as Cargo features to unlock From/Into conversions and serialization trait derives on those native types.

API surface

  • --features glam — adds From/Into between:
    • Vector2glam::Vec2
    • Vector3glam::Vec3
    • Vector4glam::Vec4
    • Quaternionglam::Quat
    • Matrixglam::Mat4 (column-major round-trip; see Gotchas)
  • --features mint — adds From/Into between:
    • Vector2mint::Vector2<f32>
    • Vector3mint::Vector3<f32>
    • Vector4mint::Vector4<f32>
    • Quaternionmint::Quaternion<f32>
    • Matrixmint::ColumnMatrix4<f32>
  • --features serde — adds Serialize/Deserialize derives on the math types (Vector2/Vector3/Vector4/Matrix/Quaternion) and on Color.

Example

full includes glam, so this doctest runs under cargo test --features full and under mdbook test -L target/debug/deps.

#![allow(unused)]
fn main() {
extern crate raylib;
extern crate glam;
use raylib::math::Vector3;

// Convert to glam for library interop, then convert back.
let rl_v = Vector3::new(1.0, 2.0, 3.0);
let gv: glam::Vec3 = rl_v.into();
assert_eq!(gv, glam::Vec3::new(1.0, 2.0, 3.0));

let round_trip: Vector3 = gv.into();
assert_eq!(round_trip.x, rl_v.x);
assert_eq!(round_trip.y, rl_v.y);
assert_eq!(round_trip.z, rl_v.z);
}

Gotchas

  • MintVec* / MintMatrix / MintQuat are deprecated. The MintVec2, MintVec3, MintVec4, MintMatrix, and MintQuat type aliases from the 5.x era are still present but carry #[deprecated(since = "6.0.0")]. Replace them with the native types (Vector2, Vector3, Vector4, Matrix, Quaternion) and, if you need mint interop, enable --features mint.
  • glam::Mat4 column-major vs. raylib Matrix row-major. raylib’s Matrix is row-major in memory (translation lives in m12/m13/m14; see raymath chapter). glam::Mat4 is column-major in memory. The --features glam adapter handles the field permutation so values round-trip correctly — but if you inspect raw byte layouts side-by-side, m0..m15 will differ from glam’s internal storage order.
  • Zero math-crate deps by default. Library crates built on raylib-rs should not enable glam/mint/serde in their own [features] defaults unless they genuinely need them — doing so forces the dependency onto every downstream consumer.

See also

What next?

You’ve read through the getting-started guides, the core concepts, the module chapters, and the ecosystem integrations. Here’s where to go from here.

Pointers

  • docs.rs API reference — the full rustdoc for every public type, function, and trait in raylib. Use the search bar and the Feature flags panel to browse feature-gated items (glam, mint, serde, raygui, …).

  • Upstream raylib — the C library reference, the official cheatsheet, examples, and a community forum. When a function isn’t documented in the Rust side, the C docs are the authoritative source.

  • The showcase — WS9 (the 6.0 finale) will port the full set of official raylib examples to Rust and deploy them as a GitHub Pages gallery. The portfolio lives under showcase/ in the repository today; the hosted gallery will appear when WS9 ships.

  • The repository — source code, issue tracker, and the docs/superpowers/ directory which holds the spec and plan archive for the entire 6.0 upgrade effort.

  • CONTRIBUTE.md — contribution guide covering unsafe conventions, RAII expectations, the # Safety / // SAFETY: comment discipline, and the PR process.

See also

Showcase examples

This is the full inventory of the 229 examples in the raylib-rs showcase (217 raylib core + 12 raygui), grouped by category. Each entry links to the example’s WASM-rendered page (or a desktop-only placeholder if not wasm-buildable).

Live gallery: https://raylib-rs.github.io/raylib-rs/

Audio (11)

Core (49)

Models (30)

Others (3)

raygui (12)

Shaders (35)

Shapes (41)

Text (16)

Textures (32)