chore: clean recommit
This commit is contained in:
parent
8024c6ac40
commit
60ad2e7fb1
122 changed files with 23950 additions and 323 deletions
|
|
@ -8,13 +8,18 @@ namespace Examples.Web;
|
|||
/// Browser host: owns the single raylib window and the "current" example. The frame loop is
|
||||
/// driven from JavaScript (main.js, via requestAnimationFrame) since a blocking C# loop would
|
||||
/// freeze the page. Methods marked [JSExport] are called from main.js.
|
||||
///
|
||||
/// <para>
|
||||
/// Every switch applies <see cref="IExample.TargetFps"/> and resets the cursor to visible.
|
||||
/// Cursor hiding, <see cref="IExample.ConfigFlags"/>, and per-example window size are not
|
||||
/// honored in the browser; the canvas stays 800x450 and the pointer stays visible.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public partial class Host
|
||||
{
|
||||
public const int screenWidth = 800;
|
||||
public const int screenHeight = 450;
|
||||
|
||||
// Examples discovered from the assembly, minus desktop-only ones (see ExampleRegistry).
|
||||
private static readonly List<IExample> _examples = new(ExampleRegistry.BrowserExamples);
|
||||
|
||||
private static IExample _current;
|
||||
|
|
@ -24,48 +29,76 @@ public partial class Host
|
|||
InitWindow(screenWidth, screenHeight, "raylib-cs web examples");
|
||||
SetTargetFPS(60);
|
||||
|
||||
SetExample(_examples[0].Name);
|
||||
// main.js selects the first example, so its init failures surface through SetExample.
|
||||
}
|
||||
|
||||
/// <summary>Render one frame of the current example (called every requestAnimationFrame tick).</summary>
|
||||
/// <summary>Render one frame of the current example; false if it threw and was torn down.</summary>
|
||||
[JSExport]
|
||||
public static void UpdateFrame()
|
||||
public static bool UpdateFrame()
|
||||
{
|
||||
if (_current == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_current?.Update();
|
||||
_current.Update();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't let one misbehaving example kill the whole page; log and stop driving it.
|
||||
Console.WriteLine($"[Examples.Web] '{_current?.Name}' threw during Update: {ex}");
|
||||
Console.WriteLine($"[Examples.Web] '{_current.Name}' threw during Update: {ex}");
|
||||
TryUnload(_current);
|
||||
_current = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Switch the active example by name (called from the nav dropdown).</summary>
|
||||
/// <summary>Switch the active example by name; false on unknown name or failed Init.</summary>
|
||||
[JSExport]
|
||||
public static void SetExample(string name)
|
||||
public static bool SetExample(string name)
|
||||
{
|
||||
var next = _examples.Find(e => e.Name == name);
|
||||
if (next == null)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_current != null)
|
||||
{
|
||||
TryUnload(_current);
|
||||
_current = null;
|
||||
}
|
||||
|
||||
// Reset the baseline; an example may hide/lock the cursor from its own Update(), and
|
||||
// CursorDisabled/CursorHidden are deliberately never applied in the browser.
|
||||
EnableCursor();
|
||||
ShowCursor();
|
||||
SetTargetFPS(next.TargetFps);
|
||||
|
||||
try
|
||||
{
|
||||
_current?.Unload();
|
||||
next.Init();
|
||||
_current = next;
|
||||
_current.Init();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Examples.Web] '{next.Name}' failed to initialize: {ex}");
|
||||
_current = null;
|
||||
TryUnload(next);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Target FPS of the current example; main.js paces the frame loop with it.</summary>
|
||||
[JSExport]
|
||||
public static int GetCurrentTargetFps()
|
||||
{
|
||||
return _current?.TargetFps ?? 60;
|
||||
}
|
||||
|
||||
/// <summary>Newline-separated example names, used to populate the nav dropdown.</summary>
|
||||
[JSExport]
|
||||
public static string GetExampleNames()
|
||||
|
|
@ -83,4 +116,17 @@ public partial class Host
|
|||
SetMouseOffset(0, 0);
|
||||
SetMouseScale(scaleX, scaleY);
|
||||
}
|
||||
|
||||
/// <summary>Best-effort Unload that never lets a failure cascade.</summary>
|
||||
private static void TryUnload(IExample example)
|
||||
{
|
||||
try
|
||||
{
|
||||
example.Unload();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Examples.Web] '{example.Name}' threw during Unload: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
# 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.
|
||||
Runs the raylib examples in the browser via WebAssembly, with a dropdown to switch between them.
|
||||
It proves the `Raylib-cs` NuGet package's `browser-wasm` support end-to-end: the package's
|
||||
`buildTransitive` targets link the shipped `raylib.a` into the .NET wasm runtime.
|
||||
|
||||
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.
|
||||
The browser-wasm configuration only activates when publishing with
|
||||
`RuntimeIdentifier=browser-wasm`; normal solution builds don't need the `wasm-tools` workload.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -27,23 +23,11 @@ dotnet publish Examples -f net10.0 -r browser-wasm -c Release
|
|||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
If the link step fails with `wasm-opt: Unknown option '--enable-bulk-memory-opt'`, the installed
|
||||
`wasm-tools` workload is out of sync with the SDK (stale workload band or a system EMSDK on
|
||||
PATH); fix with `dotnet workload update`. As a safety net, `Examples.csproj` defaults
|
||||
browser-wasm publishes to unoptimized native linking; an optimized publish can override those
|
||||
properties.
|
||||
|
||||
## Run
|
||||
|
||||
|
|
@ -58,51 +42,40 @@ 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:
|
||||
The render buffer stays fixed at `800x450`; display scaling is CSS-only. Pick with the **Scale**
|
||||
dropdown (or `?scale=` query param):
|
||||
|
||||
- `native` (default): exact `800x450` CSS 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).
|
||||
- `native` (default): exact `800x450` CSS pixels.
|
||||
- `integer`: largest whole-number multiple that fits, centered with letterboxing (pixel-perfect).
|
||||
- `fit`: fills the viewport preserving aspect ratio (can be fractional, less crisp).
|
||||
|
||||
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 zoom `100%`: `integer` should appear crisp and stable (`Scale 1.00` or
|
||||
higher if viewport allows).
|
||||
- OS scale `125%` and `150%`, browser zoom `100%`: `integer` should stay crisp (no subpixel CSS
|
||||
scaling); perceived physical size changes with OS scale are expected.
|
||||
- Browser zoom `80%`, `100%`, `125%`: `integer` should continue using whole-number scaling;
|
||||
`fit` may show softening at non-integer effective scales.
|
||||
- `native` mode should always report `CSS 800x450` and `Scale 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.
|
||||
Scaling is computed in device pixels and converted back to CSS via `devicePixelRatio`, so the
|
||||
modes behave consistently across OS scale and browser zoom. The status bar shows the live numbers
|
||||
(`DPR`, CSS size, backing size, scale): `integer` should stay crisp at any OS scale or zoom, and
|
||||
`native` should always report `Scale 1.00`.
|
||||
|
||||
## 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()` calls `InitWindow` once and selects the default example (`main.js` runs it via
|
||||
`runMain()`).
|
||||
- `main.js` binds the page `<canvas>` to the runtime, then calls `Host.UpdateFrame()` every
|
||||
`requestAnimationFrame` tick. `UpdateFrame`, `SetExample`, and `GetExampleNames` are `[JSExport]`.
|
||||
- Each base example implements `IExample` (`Init` / `Update` / `Unload`) in `#if BROWSER` partials.
|
||||
The host owns the window;
|
||||
examples never call `InitWindow`/`CloseWindow`.
|
||||
- `Host.Main()` calls `InitWindow` once; `main.js` then populates the dropdown and selects the
|
||||
first example via `Host.SetExample`, so init failures surface in the on-page error banner.
|
||||
- `main.js` binds the page `<canvas>` to the runtime and calls `Host.UpdateFrame()` from a
|
||||
`requestAnimationFrame` loop paced to the current example's `TargetFps` (raylib's own limiter
|
||||
busy-waits and would peg the main thread). The `Host` methods it calls are `[JSExport]`.
|
||||
- On switch, `Host.SetExample` unloads the previous example, resets the cursor to visible, and
|
||||
applies the next `TargetFps`. Cursor hiding and `ConfigFlags` are not honored in the browser.
|
||||
If `Init` or `Update` throws, the example is unloaded and an error banner is shown.
|
||||
- Each example is a single `.cs` file implementing `IExample` (`Init` / `Update` / `Unload`); the
|
||||
host owns the window, so examples never call `InitWindow`/`CloseWindow`. Platform differences
|
||||
(e.g. GLSL 100 vs 330) are handled inline with `#if BROWSER` guards, not separate files.
|
||||
|
||||
## 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`:
|
||||
Examples are **auto-discovered by reflection** (`ExampleRegistry.DiscoverAll`) — no list to edit.
|
||||
Drop a new `.cs` file in the matching category folder implementing `IExample`, splitting the
|
||||
original monolithic `Main` as:
|
||||
|
||||
```
|
||||
Main() { <setup>; while(!WindowShouldClose()){ <body> } <cleanup> }
|
||||
|
|
@ -111,8 +84,8 @@ Main() { <setup>; while(!WindowShouldClose()){ <body> } <cleanup> }
|
|||
Unload(){ <cleanup, minus CloseWindow> }
|
||||
```
|
||||
|
||||
Then add `new YourExample()` to the `Examples` list in `Host.cs`.
|
||||
Keep the standalone `static Main()` as a thin driver so the example still runs on its own.
|
||||
|
||||
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`.
|
||||
If an example can't run on single-threaded wasm, add its type to `DesktopExcludedFromBrowser` in
|
||||
`ExampleRegistry.cs`; `BrowserOnly` is the inverse list. Assets under `resources/` are bundled
|
||||
into the wasm virtual filesystem automatically.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@
|
|||
background: #000;
|
||||
}
|
||||
#status { color: #888; font-size: .85rem; margin: 0; }
|
||||
#error {
|
||||
color: #ff6b6b;
|
||||
background: #3a1d1d;
|
||||
border: 1px solid #6b2b2b;
|
||||
border-radius: 4px;
|
||||
padding: .4rem .6rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -60,6 +68,7 @@
|
|||
<option value="fit">Fit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="error" hidden>This example crashed — see the browser console for details. Select another example to continue.</div>
|
||||
<div id="viewport">
|
||||
<canvas id="canvas" width="800" height="450" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from './scaleUtils.js'
|
||||
|
||||
const status = document.getElementById('status');
|
||||
const errorBanner = document.getElementById('error');
|
||||
const select = document.getElementById('examples');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const viewport = document.getElementById('viewport');
|
||||
|
|
@ -74,7 +75,6 @@ function applyDisplayScale() {
|
|||
canvas.style.width = `${display.cssWidth}px`;
|
||||
canvas.style.height = `${display.cssHeight}px`;
|
||||
|
||||
// Center canvas inside viewport, leaving letterboxing around it.
|
||||
viewport.style.justifyContent = 'center';
|
||||
viewport.style.alignItems = 'center';
|
||||
|
||||
|
|
@ -133,11 +133,17 @@ Host = exports.Examples.Web.Host;
|
|||
// raylib's GLFW/emscripten backend renders into this canvas.
|
||||
dotnet.instance.Module['canvas'] = canvas;
|
||||
|
||||
// Runs Host.Main() -> InitWindow + default example Init(). Must come after the canvas is bound.
|
||||
// Runs Host.Main() -> InitWindow. Must come after the canvas is bound.
|
||||
await runMain();
|
||||
applyDisplayScale();
|
||||
|
||||
// Populate the navigation dropdown from the registered examples.
|
||||
let targetFps = 60;
|
||||
function selectExample(name) {
|
||||
const ok = Host.SetExample(name);
|
||||
targetFps = Host.GetCurrentTargetFps();
|
||||
errorBanner.hidden = ok;
|
||||
}
|
||||
|
||||
select.innerHTML = '';
|
||||
for (const name of Host.GetExampleNames().split('\n').filter(n => n.length > 0)) {
|
||||
const opt = document.createElement('option');
|
||||
|
|
@ -145,13 +151,26 @@ for (const name of Host.GetExampleNames().split('\n').filter(n => n.length > 0))
|
|||
opt.textContent = name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.addEventListener('change', () => Host.SetExample(select.value));
|
||||
select.addEventListener('change', () => selectExample(select.value));
|
||||
selectExample(select.value);
|
||||
|
||||
setScaleMode(scaleMode, false);
|
||||
|
||||
// Drive raylib one frame per animation tick (never block the browser).
|
||||
function mainLoop() {
|
||||
Host.UpdateFrame();
|
||||
// Drive raylib one frame per animation tick, paced to the example's target FPS here rather
|
||||
// than by raylib's limiter (which busy-waits in EndDrawing and would peg the main thread).
|
||||
let nextFrameTime = 0;
|
||||
function mainLoop(timestamp) {
|
||||
requestAnimationFrame(mainLoop);
|
||||
|
||||
if (timestamp < nextFrameTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule one interval ahead; resync to now if we've fallen behind (hidden tab).
|
||||
nextFrameTime = Math.max(nextFrameTime + 1000 / targetFps, timestamp);
|
||||
|
||||
if (!Host.UpdateFrame()) {
|
||||
errorBanner.hidden = false;
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(mainLoop);
|
||||
|
|
|
|||
Loading…
Reference in a new issue