| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .. | ||
| Host.cs | ||
| index.html | ||
| main.js | ||
| README.md | ||
| scaleUtils.js | ||
| scaleUtils.test.js | ||
Examples/Web — raylib-cs in the browser (WebAssembly)
A small browser app that runs raylib examples in the browser via WebAssembly, with a dropdown to
switch between them. It exists to prove the Raylib-cs NuGet package's browser-wasm support
works end-to-end: the package's buildTransitive/Raylib-cs.targets automatically links the
shipped runtimes/browser-wasm/native/raylib.a (and adds -sUSE_GLFW=3) into the .NET wasm
runtime. The browser host lives in Examples/Web, while Examples.csproj remains the single
examples project.
The browser-wasm configuration is enabled only when publishing Examples with
RuntimeIdentifier=browser-wasm, so normal solution builds do not require the wasm-tools
workload.
Prerequisites
- .NET 10 SDK
dotnet workload install wasm-tools- The
Raylib-cspackage matching$(RaylibCsVersion)(seeDirectory.Build.props) — from nuget.org, or built locally into the repo's./nugetfeed (dotnet pack Raylib-cs -c Release --output nuget).
Build
dotnet publish Examples -f net10.0 -r browser-wasm -c Release
# -> Examples/bin/Release/net10.0/browser-wasm/AppBundle/
Toolchain caveat
If the link step fails with:
wasm-opt: Unknown option '--enable-bulk-memory-opt'
your installed wasm-tools workload (Binaryen) is out of sync with the SDK — emscripten passes a
feature flag the bundled wasm-opt doesn't understand (commonly caused by a stale workload band
or a conflicting system EMSDK on PATH/$EMSDK). Proper fix:
dotnet workload update
Examples.csproj defaults browser-wasm publishes to unoptimized native linking so the standard
publish command works on affected local toolchains. A fully optimized publish can override those
MSBuild properties after updating the workload/toolchain.
Run
WebAssembly must be served over HTTP (not file://):
dotnet serve -d Examples/bin/Release/net10.0/browser-wasm/AppBundle # dotnet tool install -g dotnet-serve
# or: npx http-server Examples/bin/Release/net10.0/browser-wasm/AppBundle
Open the printed URL and use the Example dropdown to switch examples.
Canvas scaling modes
The browser host keeps raylib's internal render buffer fixed to 800x450 and applies display
scaling in CSS. Use the Scale dropdown (or ?scale= query param) to choose behavior:
native(default): exact800x450CSS pixels (1x), no resizing.integer: largest whole-number multiple that fits the viewport, centered with letterboxing. Best for pixel-perfect presentation.fit: fills available viewport while preserving aspect ratio (can be fractional, less crisp on some DPI/zoom combinations).
Scaling is computed in device pixels first and then converted back to CSS pixels using current
devicePixelRatio, which decouples the modes from needing a specific browser zoom level on
125%/150% OS scale displays.
main.js also shows runtime diagnostics in the status bar (DPR, CSS size, backing size, scale)
to help debug OS scaling / browser zoom behavior across monitors.
Manual validation matrix (expected behavior)
- OS scale
100%, browser zoom100%:integershould appear crisp and stable (Scale 1.00or higher if viewport allows). - OS scale
125%and150%, browser zoom100%:integershould stay crisp (no subpixel CSS scaling); perceived physical size changes with OS scale are expected. - Browser zoom
80%,100%,125%:integershould continue using whole-number scaling;fitmay show softening at non-integer effective scales. nativemode should always reportCSS 800x450andScale 1.00.- During window resize, there should be no frame-by-frame jitter because scale updates run on resize/mode changes, not per animation frame.
How it works
A browser can't run raylib's blocking while (!WindowShouldClose()) loop (it would freeze the
page), so frames are driven from JavaScript:
Host.Main()callsInitWindowonce and selects the default example (main.jsruns it viarunMain()).main.jsbinds the page<canvas>to the runtime, then callsHost.UpdateFrame()everyrequestAnimationFrametick.UpdateFrame,SetExample, andGetExampleNamesare[JSExport].- Each base example implements
IExample(Init/Update/Unload) in#if BROWSERpartials. The host owns the window; examples never callInitWindow/CloseWindow.
Adding more examples
For a new desktop example (../Core/..., ../Shaders/..., etc.), add a browser partial
(*.Browser.cs) that implements IExample and mirrors the split of its monolithic Main:
Main() { <setup>; while(!WindowShouldClose()){ <body> } <cleanup> }
-> Init() { <setup, minus InitWindow/SetTargetFPS> } // loop-spanning locals become fields
Update(){ <body> } // keep BeginDrawing..EndDrawing
Unload(){ <cleanup, minus CloseWindow> }
Then add new YourExample() to the Examples list in Host.cs.
Examples that load files (resources/...) also need those assets in the wasm virtual filesystem.
Examples.csproj bundles ../resources/ into the browser app when publishing with
RuntimeIdentifier=browser-wasm.