No GPU is available

This page is normally drawn by Webwin itself. Without WebGPU or WebGL 2 it falls back to the plain document below — the same words, laid out by the browser.

Use Webwin in your project

Everything the demos are built on ships as a library beside them: a game core that draws with three.js, and a windowing front-end that lays windows, controls and a HUD over it. Copy the files, or load them straight off this site. The one thing your page brings is three.js itself.

The files

FileWhat it is
webwin.js ES module — the game core: Game, the renderer, the loop, input, images
webwin-ui.js ES module — the windowing front-end (webwin/ui): desktop, windows, controls, themes, flares
webwin-markup.js ES module — the XML layout language (webwin/markup)
webwin-gaming.js ES module — the HUD widget set (webwin/gaming)
webwin-engine2d.js ES module — the 2D scene graph and sprite renderer
webwin-engine3d.js ES module — three.js scenes as game layers, model loading, 3D physics
webwin-shared-*.js Code the entries above share. Not imported directly — but copy them
webwin.global.js Classic script — defines window.Webwin: core and UI, three.js included
types/ TypeScript declarations, mirroring the source tree
Keep the directory together. The six entry files are separate downloads but not independent: the code they share lives in the webwin-shared-*.js chunks, which they import by relative path. That sharing is what makes a gaming widget and a core control the same class at runtime, so instanceof tells the truth.

The files here are Webwin 0.6.0. A vendored copy keeps saying so: every bundle opens with the release in its banner comment, and the library exports it as VERSION, which is worth putting in a bug report.

Building with Claude

webwin.skill is an agent skill for Claude Code. It carries the mental model, a reference for each part of the API, and a worked application to read. Most of it, though, is the list of things here that go wrong quietly instead of throwing, because that is what a model guesses worst: it will happily set a splitter position before the splitter has a size. Drop the folder inside it into your .claude/skills directory. The file is an ordinary zip, so unzipping it by hand works as well as any installer does.

Built from the source, not written about it. The skill is packed out of the repository on every release, so it ages at exactly the rate the bundles beside it do. It will not save you from the GPU requirement, though. Read Before you start whether or not a model is doing the typing.

As ES modules

The ES bundles import three.js by its usual names rather than carrying a copy, so a game that builds its own scenes uses the same three.js Webwin does. Without a bundler, an import map says where it is:

<script type="importmap">
{ "imports": {
  "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
  "three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.webgpu.js",
  "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.tsl.js",
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
} }
</script>

An application that is all windows makes a desktop, which runs a game of its own:

<canvas id="desktop"></canvas>
<script type="module">
  import { Desktop, ModernTheme, Window, Button }
    from "https://jetsonsoft.com/Webwin/lib/webwin-ui.js";

  const desktop = await Desktop.create(document.getElementById("desktop"), {
    theme: new ModernTheme("light"),
  });

  const win = new Window({ title: "Hello", x: 40, y: 40, width: 320, height: 180 });
  win.controls.add(new Button({ x: 16, y: 16, width: 88, height: 30, text: "OK" }));
  desktop.addWindow(win);
</script>

A game starts the game, draws its world as a layer, and lays the windows over it. The desktop is offered every click and key first, and whatever lands on open ground reaches the game:

import * as THREE from "three/webgpu";
import { Game } from "./lib/webwin.js";
import { SceneLayer } from "./lib/webwin-engine3d.js";
import { Desktop, DungeonTheme } from "./lib/webwin-ui.js";

const game = await Game.create(canvas);
const world = new SceneLayer(game.gpu, { bloom: { strength: 0.6 } });
world.scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial()));
game.addLayer(world);

const hud = new Desktop(game, { theme: new DungeonTheme() });
game.input.on("keydown", (e) => { if (e.code === "Space") jump(); });

A bundler-based project imports the same files by path. If you re-bundle, do not mark the package sideEffects: false. The flare and glyph registries fill themselves at import time, and dropping those modules leaves nothing to look up.

As a script tag

<canvas id="desktop"></canvas>
<script src="https://jetsonsoft.com/Webwin/lib/webwin.global.js"></script>
<script>
  const desktop = await Webwin.Desktop.create(
    document.getElementById("desktop"),
    { theme: new Webwin.ModernTheme("dark") },
  );
</script>

The global build is the core and the UI in one, with three.js inside it, deliberately. A classic script cannot share code across files, so shipping one per entry would give each its own copy of the shared core, and a page loading two of them would end up with duplicate classes and forked registries. If you need the HUD or an engine, use the ES modules.

TypeScript

Copy lib/types/ alongside the bundles and point at it:

{
  "compilerOptions": {
    "paths": {
      "webwin": ["./vendor/webwin/types/index.d.ts"],
      "webwin/ui": ["./vendor/webwin/types/ui/index.d.ts"],
      "webwin/markup": ["./vendor/webwin/types/ui/markup/index.d.ts"],
      "webwin/gaming": ["./vendor/webwin/types/ui/gaming/index.d.ts"],
      "webwin/engine2d": ["./vendor/webwin/types/engine2d/index.d.ts"],
      "webwin/engine3d": ["./vendor/webwin/types/engine3d/index.d.ts"]
    }
  }
}

Layouts as XML

webwin/markup loads a layout written as XML, at runtime. Its one rule is that a layout carries no coordinates: a size nobody writes is measured, * divides up what is left, % takes a share, and em follows the theme's font. So one document survives a resize, a theme swap and a change of uiScale without naming any of them.

<Window title="{brand}" size="520x360">
  <Values>
    <Value name="brand">Notes</Value>
    <Value name="gutter">8</Value>
  </Values>

  <VStack padding="{gutter}" gap="{gutter}">
    <Grid cols="auto *" rows="auto auto">
      <Label row="0" col="0" vAlign="center">Title:</Label>
      <TextBox row="0" col="1" name="title"/>
      <Label row="1" col="0" vAlign="center">Tags:</Label>
      <ComboBox row="1" col="1" items="Work, Home, Ideas"/>
    </Grid>

    <TextArea name="body" height="*" wrap="true"/>

    <HStack hAlign="end" gap="{gutter}">
      <Button width="7em" isDefault="true" onClick="save">Save</Button>
    </HStack>
  </VStack>
</Window>
import { loadMarkup } from "./lib/webwin-markup.js";

const view = loadMarkup(xml, {
  desktop,
  handlers: { save: () => store.save() },
});

view.get("title").text = "Untitled";

Names do the binding. name gives code a typed handle on a control, and an on* attribute names a function in handlers. Attributes hold no expressions, so there is nothing between the two halves to keep in sync, and a handler name nobody passed is a load-time error rather than a button that silently does nothing.

It is strict XML. Quoted values, matched tags, the five named entities — so an ordinary XML editor validates a layout. The one convention on top is the mnemonic: write _File rather than &File, because a caption that reads &File in the source is one nobody can scan.

The Markup page is the whole of it running: edit the document on the left and the preview on the right is rebuilt as you type, with a parse error reported at its line and column.

Capturing a frame

The desktop can hand back what it just drew — as a PNG, as a data: URL, or as raw RGBA8 pixels with no encoder in the way:

const png = await desktop.capture({ maxWidth: 1600 });  // a Blob
const url = await desktop.captureDataUrl();             // survives page.evaluate
const raw = await desktop.capturePixels();              // for a golden-image diff

A page-level screenshot of the canvas works too, and matches these to within a channel or so. What it cannot do is pin a chosen frame, which is the difference between a golden-image test that is stable and one that is flaky. It also does not exist at all inside an embedder such as Electron, a Node harness, or an offscreen canvas in a worker. Each call draws the frame again into a target of its own and reads that back, so it works on either backend and on a phone that will not hand out its swapchain.

Before you start

Webwin draws with WebGPU where the browser has it and falls back to WebGL 2 where it does not, on its own. Only where neither is available do Game.create and Desktop.create reject, with GpuUnavailableError, and your app has to catch that and show a fallback of its own, the same way every page here does.

import { GpuUnavailableError } from "./lib/webwin.js";
import { Desktop, ModernTheme } from "./lib/webwin-ui.js";

let desktop;
try {
  desktop = await Desktop.create(canvas, { theme: new ModernTheme("dark") });
} catch (err) {
  showFallback(err instanceof GpuUnavailableError ? err.message : String(err));
  return;
}

// Glyphs are cached by face, so the first raster must use the real font.
await document.fonts.ready;

Two smaller things. The canvas owns its own input and text rendering, so do not try to overlay DOM widgets on it. And glyph rasterisation caches by font face, so await document.fonts.ready before the first frame if your page uses a webfont — otherwise the atlas caches the fallback.