raylibr can be compiled to WebAssembly and run in the browser via webR. This page explains the build process and how the WASM demos on this site work.
Prerequisites
- Emscripten SDK (tested with 4.0+)
- R with
Rcppinstalled natively (for headers and the R lazy-load database) - The
raylibrpackage installed natively (R CMD INSTALL .)
How it works
Raylib 6.0 includes a PLATFORM_WEB_EMSCRIPTEN backend
that uses direct Emscripten HTML5 APIs instead of GLFW. The build script
compiles both Raylib and raylibr’s C++ sources to WebAssembly as an
Emscripten side
module, which webR can load at runtime.
Key components:
-
Raylib as a static WASM library
(
libraylib.web.a) compiled withemccand-DPLATFORM_WEB_EMSCRIPTEN -
raylibr C++ sources compiled with
em++as a side module (-s SIDE_MODULE=1) -
Raylib patches
(
inst/wasm/patch_raylib.py) that make the platform code worker-compatible:- Creates an
OffscreenCanvasand WebGL context in the worker (no DOM required) - Replaces
emscripten_sleep()withAtomics.wait()for non-blocking sleep - Sends rendered frames to the main thread via
BroadcastChannelandreadPixels() - Reads mouse and keyboard input from a
SharedArrayBufferwritten by the main thread
- Creates an
-
HTML host pages that display frames on a
<canvas>and forward input events
Building
Run the build script from the repository root:
This produces a webR-compatible package tarball at
pkgdown/assets/wasm/repo/bin/emscripten/contrib/4.6/raylibr_6.0.0.9000.tgz.
The build script:
- Extracts Raylib 6.0 from the bundled tarball
- Patches Raylib’s
rcore.candrcore_web_emscripten.cfor worker mode - Compiles Raylib with
emcc(-DPLATFORM_WEB_EMSCRIPTEN -DGRAPHICS_API_OPENGL_ES2) - Compiles raylibr’s C++ sources with
em++ - Links everything into
raylibr.so(a WASM side module) - Packages it as a
.tgzwith R’s lazy-load database from the native install
Architecture
Browser main thread webR Web Worker
┌──────────────────────┐ ┌──────────────────────┐
│ <canvas> │ │ R interpreter │
│ 2D rendering ctx │◄──frames──│ raylibr (WASM) │
│ │ │ Raylib (WASM) │
│ Mouse/keyboard │ │ OffscreenCanvas │
│ event listeners │──input──►│ WebGL context │
│ │ (SAB) │ │
└──────────────────────┘ └──────────────────────┘
Frame transfer: Raylib’s
SwapScreenBuffer() calls gl.readPixels() on
the OffscreenCanvas, then sends the pixel data to the main thread via
BroadcastChannel.postMessage(). The main thread flips the
rows (WebGL is bottom-up) and draws to the visible
<canvas> with putImageData().
Input forwarding: The main thread writes mouse
coordinates and key codes to a SharedArrayBuffer. Raylib’s
PollInputEvents() reads from this buffer on each frame
using Atomics.load() and
Atomics.exchange().
Sleeping: WindowShouldClose() and
WaitTime() use Atomics.wait() instead of
emscripten_sleep(), which requires ASYNCIFY. This works in
Web Workers (where Atomics.wait is allowed) and avoids
busy-waiting.
HTML host page
Each demo page follows this pattern:
<canvas id="canvas" width="600" height="400"></canvas>
<script type="module">
import { WebR } from 'https://webr.r-wasm.org/v0.6.0/webr.mjs';
const canvas = document.getElementById('canvas');
const ctx2d = canvas.getContext('2d');
let inputSAB = null;
// Receive frames and SAB from worker
const bc = new BroadcastChannel('raylibr-frames');
bc.onmessage = (e) => {
if (e.data.t === 'f') {
// Flip rows (WebGL readPixels is bottom-up)
const src = new Uint8ClampedArray(e.data.px);
const w = e.data.w, h = e.data.h, stride = w * 4;
const dst = new Uint8ClampedArray(src.length);
for (let y = 0; y < h; y++) {
dst.set(src.subarray((h - 1 - y) * stride, (h - y) * stride), y * stride);
}
ctx2d.putImageData(new ImageData(dst, w, h), 0, 0);
} else if (e.data.t === 'sab') {
inputSAB = new Int32Array(e.data.sab);
}
};
// Forward mouse input to worker via SAB
canvas.addEventListener('mousemove', (e) => {
if (!inputSAB) return;
const rect = canvas.getBoundingClientRect();
Atomics.store(inputSAB, 0, Math.round(e.clientX - rect.left));
Atomics.store(inputSAB, 1, Math.round(e.clientY - rect.top));
Atomics.store(inputSAB, 2, 1); // cursor on screen
});
// Forward keyboard input (Raylib key codes)
const KEY_MAP = { ArrowUp: 265, ArrowDown: 264, ArrowLeft: 263, ArrowRight: 262 };
canvas.addEventListener('keydown', (e) => {
if (!inputSAB) return;
const raylibKey = KEY_MAP[e.key] || e.key.toUpperCase().charCodeAt(0);
Atomics.store(inputSAB, 3, raylibKey);
e.preventDefault();
});
// Start webR and run the demo
const webR = new WebR();
await webR.init();
await webR.installPackages(['Rcpp'], { repos: ['https://repo.r-wasm.org'], quiet: true });
await webR.installPackages(['raylibr'], { repos: ['./repo'], quiet: true });
await webR.evalR('library(raylibr)');
// Run the exact same R code as the native demo
await webR.evalR(`
init_window(600, 400, "My Demo")
while (!window_should_close()) {
begin_drawing()
clear_background("black")
draw_text("Hello from R + WASM!", 100, 180, 30, "white")
end_drawing()
}
close_window()
`);
</script>SharedArrayBuffer layout
The input SAB is an Int32Array with 8 elements:
| Index | Contents | Access |
|---|---|---|
| 0 | Mouse X |
Atomics.store / Atomics.load
|
| 1 | Mouse Y |
Atomics.store / Atomics.load
|
| 2 | Cursor on screen (0/1) |
Atomics.store / Atomics.load
|
| 3 | Last key pressed (Raylib key code) |
Atomics.store / Atomics.exchange
(consumed) |
| 4-7 | Reserved |
Raylib key codes
Common Raylib key codes for the KEY_MAP:
| Key | Code |
|---|---|
| A-Z | 65-90 |
| 0-9 | 48-57 |
| Space | 32 |
| Enter | 257 |
| Escape | 256 |
| ArrowRight | 262 |
| ArrowLeft | 263 |
| ArrowDown | 264 |
| ArrowUp | 265 |
Cross-origin headers
SharedArrayBuffer requires cross-origin isolation.
Your server must send:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
For local development, use a server that sets these headers. webR’s
PostMessage channel works without them but is slower.
