Skip to content

Commit 200eb81

Browse files
obiotclaude
andcommitted
feat(webgl)!: WebGL 2 only renderer + Vertex Array Objects (#1509)
Phase 0 of the WebGPU groundwork: drop the WebGL 1 path and give every batcher an immutable vertex state, so the layout description stops being re-issued per draw and starts looking like a pipeline descriptor. BREAKING: the WebGL renderer now requires WebGL 2. - `video.AUTO` falls back to the Canvas renderer on WebGL-1-only devices; `video.WEBGL` throws there - `preferWebGL1` setting and the `#webgl1` URL flag removed - `device.isWebGLSupported()` probes for WebGL 2 — it now agrees with what renderer construction actually requests (it probed WebGL 1 before, so the gate and the context could disagree) - `renderer.type` is always "WebGL2"; `renderer.WebGLVersion` deprecated - corrections on ex-WebGL-1 configs: NPOT `repeat` genuinely tiles, darken/lighten use true MIN/MAX, `createPattern()` accepts NPOT - user shaders need NO changes: GLSL ES 1.00 compiles on WebGL 2 contexts Vertex Array Objects: - new `WebGLVertexState` (buffer/vertexstate.js, sibling of WebGLIndexBuffer) owns the VAO lifecycle; its binding save/restore is private, so no caller can half-apply the protocol - built from a {attributes, stride, buffer, indexBuffer} descriptor — a GPUVertexBufferLayout + arrayStride, so #1492 becomes a class swap - batcher switches cost one bindVertexArray; steady-state frames issue zero attribute-specification calls (19 at startup, 0 per frame after) - the attribute-leak machinery is gone: state is swapped, not disabled - custom shaders on a built-in batcher must declare that batcher's attributes first, in layout order — warns once per shader on mismatch - TMX GPU tilemap eligibility now uses `renderer.supportsShaderTileLayers` (a backend capability flag) instead of a WebGL-version check Tests: 6 new specs (vertex-state units, VAO state/call-counts/recreation/ contract/adversarial, teardown), the attribute-leak spec retired and its invariant re-expressed, 4888 passing. Closes #1509 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JvxaXQRgQn5AoR8DNkv6Wg
1 parent ad2f2bd commit 200eb81

42 files changed

Lines changed: 2055 additions & 885 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/melonjs/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [20.0.0] (melonJS 2) - _unreleased_
4+
5+
### Changed (breaking)
6+
- **The WebGL renderer is now WebGL 2 only** ([#1509](https://github.com/melonjs/melonJS/issues/1509)) — the WebGL 1 fallback path is removed. `renderer: video.AUTO` falls back to the Canvas renderer on WebGL-1-only devices; `renderer: video.WEBGL` throws there. **User shaders need no changes**`ShaderEffect` bodies and raw `GLShader` sources (GLSL ES 1.00) compile unchanged on WebGL 2 contexts.
7+
- `preferWebGL1` setting and the `#webgl1` URI flag are removed (`#webgl` / `#webgl2` are synonyms)
8+
- `device.isWebGLSupported()` now probes for a WebGL **2** context — it finally agrees with what renderer construction actually requests (the gate probed WebGL 1 before, so the two could disagree)
9+
- `renderer.type` is always `"WebGL2"` for the WebGL renderer; `renderer.WebGLVersion` is deprecated (always `2`)
10+
- behavior corrections on ex-WebGL-1 configs: `repeat` wrap now genuinely tiles non-power-of-two textures (was clamp + warning), `"darken"` / `"lighten"` blend modes use true MIN/MAX equations (were silently downgraded to `"normal"`), and `createPattern()` accepts non-power-of-two sources (threw before)
11+
- TMX GPU tilemap eligibility is now advertised through `renderer.supportsShaderTileLayers` (a backend capability flag) instead of a WebGL-version check
12+
- **each `Batcher` now owns an immutable Vertex Array Object** built at init ([#1509](https://github.com/melonjs/melonJS/issues/1509)): vertex attribute layout is frozen once built, batcher switches cost a single `bindVertexArray` (steady-state frames issue zero attribute-specification calls, measured), and `Batcher.unbind()` no longer disables attribute arrays. Custom batchers inheriting `Batcher.init()`/`bind()` need no changes. Custom shaders hosted by a built-in batcher must declare that batcher's attributes first, in layout order (ShaderEffect-generated vertex shaders already comply) — a console warning fires on mismatch. `GLShader.setVertexAttributes` is no longer called by the engine (still public)
13+
14+
### Performance
15+
- **Vertex Array Objects for every batcher** ([#1509](https://github.com/melonjs/melonJS/issues/1509)) — vertex attribute layout is specified once at init rather than on every batcher switch and every mesh flush, so steady-state frames issue **zero** attribute-specification calls. How much GL traffic this saves depends on how often a scene alternates batchers: a scene that stays on one batcher saves about 2 calls per frame, one mixing sprites, meshes and primitives about 40 — in both cases well under a millisecond. The structural benefit is the larger one: attribute-state leaks between batchers become impossible by construction.
16+
317
## [19.9.1] (melonJS 2) - _2026-07-28_
418

519
### Fixed

packages/melonjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "melonjs",
3-
"version": "19.9.1",
3+
"version": "20.0.0",
44
"description": "melonJS Game Engine",
55
"homepage": "http://www.melonjs.org/",
66
"type": "module",

packages/melonjs/src/application/application.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -316,16 +316,11 @@ export default class Application {
316316
} as ResolvedApplicationSettings;
317317

318318
// override renderer settings if &webgl or &canvas is defined in the URL
319+
// (the WebGL renderer is WebGL 2 only since 20.0 — the legacy
320+
// `#webgl1` flag is gone, `#webgl`/`#webgl2` are synonyms)
319321
const uriFragment = getUriFragment();
320-
if (
321-
uriFragment.webgl === true ||
322-
uriFragment.webgl1 === true ||
323-
uriFragment.webgl2 === true
324-
) {
322+
if (uriFragment.webgl === true || uriFragment.webgl2 === true) {
325323
settings.renderer = WEBGL;
326-
if (uriFragment.webgl1 === true) {
327-
settings.preferWebGL1 = true;
328-
}
329324
} else if (uriFragment.canvas === true) {
330325
settings.renderer = CANVAS;
331326
}
@@ -381,10 +376,11 @@ export default class Application {
381376
if (!device.isWebGLSupported(this.settings)) {
382377
throw new Error(
383378
"Application: `renderer: video.WEBGL` was requested " +
384-
"but the WebGL context could not be created in this " +
385-
"environment (driver-blocklisted GPU, " +
386-
"`failIfMajorPerformanceCaveat: true` on a software " +
387-
"renderer, etc.). Use `video.AUTO` if Canvas " +
379+
"but a WebGL 2 context could not be created in this " +
380+
"environment (WebGL-1-only device, driver-blocklisted " +
381+
"GPU, `failIfMajorPerformanceCaveat: true` on a " +
382+
"software renderer, etc.). The WebGL renderer is " +
383+
"WebGL 2 only since 20.0. Use `video.AUTO` if Canvas " +
388384
"fallback is acceptable, or check " +
389385
"`device.isWebGLSupported()` before construction.",
390386
);

packages/melonjs/src/application/defaultApplicationSettings.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const defaultApplicationSettings = {
66
renderer: AUTO,
77
scale: 1.0,
88
scaleMethod: ScaleMethods.Manual,
9-
preferWebGL1: false,
109
powerPreference: "default",
1110
transparent: false,
1211
antiAlias: false,

packages/melonjs/src/application/settings.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,21 @@ type PhysicsType =
3232
| PhysicsAdapter
3333
| { adapter: PhysicsAdapter };
3434

35-
type PowerPreference = "default" | "low-power";
35+
type PowerPreference = "default" | "low-power" | "high-performance";
3636

3737
export type ApplicationSettings = {
3838
/**
3939
* Renderer to use. Three built-in modes (constants from `me.video`):
4040
*
4141
* - {@link CANVAS} — HTML5 Canvas backend. No shader / mesh /
4242
* Camera3d support.
43-
* - {@link WEBGL} — **requires WebGL**. Throws at `new Application(...)`
44-
* time if WebGL is unavailable (driver-blocklisted GPU, perf-caveat
45-
* failure, etc.). Use this when your scene needs Camera3d, Mesh,
46-
* ShaderEffect, Light2d or GPU tilemap — you'd rather fail fast
47-
* than render a stuck blank canvas.
48-
* - {@link AUTO} — try WebGL, silently fall back to Canvas if
43+
* - {@link WEBGL} — **requires WebGL 2** (the WebGL renderer is
44+
* WebGL 2 only since 20.0). Throws at `new Application(...)`
45+
* time if a WebGL 2 context is unavailable (WebGL-1-only device,
46+
* driver-blocklisted GPU, perf-caveat failure, etc.). Use this when
47+
* your scene needs Camera3d, Mesh, ShaderEffect, Light2d or GPU
48+
* tilemap — you'd rather fail fast than render a stuck blank canvas.
49+
* - {@link AUTO} — try WebGL 2, silently fall back to Canvas if
4950
* unavailable. Application construction always succeeds. The
5051
* WebGL-only subsystems (Camera3d, Mesh, ShaderEffect, Light2d,
5152
* GPU tilemap) silently stop working under the Canvas fallback —
@@ -74,14 +75,20 @@ export type ApplicationSettings = {
7475
scaleTarget: HTMLElement;
7576

7677
/**
77-
* if true the renderer will only use WebGL 1
78-
* @default false
79-
*/
80-
preferWebGL1: boolean;
81-
82-
/**
83-
* a hint to the user agent indicating what configuration of GPU is suitable for the WebGL context. To be noted that Safari and Chrome (since version 80) both default to "low-power" to save battery life and improve the user experience on these dual-GPU machines.
84-
* @default default
78+
* A hint to the user agent about which GPU to use on multi-GPU systems
79+
* (discrete vs integrated). Browsers generally favour the low-power GPU
80+
* unless asked otherwise, to preserve battery life.
81+
*
82+
* - `"default"` — no hint; let the user agent decide.
83+
* - `"low-power"` — prefer the integrated GPU.
84+
* - `"high-performance"` — prefer the discrete GPU. Note that browsers
85+
* only honour this for pages that handle context loss, since switching
86+
* GPU can drop the context; melonJS registers those handlers itself,
87+
* so the request is respected.
88+
*
89+
* The same hint (and the same values, minus `"default"`) is used by
90+
* WebGPU's adapter request, so this setting is backend-neutral.
91+
* @default "default"
8592
*/
8693
powerPreference: PowerPreference;
8794

@@ -166,7 +173,7 @@ export type ApplicationSettings = {
166173
* When `true` (default), eligible layers render via a single quad per
167174
* tileset + a fragment shader doing per-fragment GID lookup, bypassing
168175
* the per-tile draw loop entirely. Layers that don't qualify
169-
* (Canvas/WebGL1, non-orthogonal, collection-of-image tilesets,
176+
* (Canvas renderer, non-orthogonal, collection-of-image tilesets,
170177
* tilerendersize "grid", non-zero tileoffset, oversampled beyond the
171178
* shader's overflow window) fall back to the legacy path automatically.
172179
* Set to `false` to disable globally.

packages/melonjs/src/lang/deprecated.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export class CanvasTexture extends CanvasRenderTarget {
2121
* @param {number} width - the desired width of the canvas
2222
* @param {number} height - the desired height of the canvas
2323
* @param {object} attributes - The attributes to create both the canvas and context
24-
* @param {boolean} [attributes.context="2d"] - the context type to be created ("2d", "webgl", "webgl2")
24+
* @param {boolean} [attributes.context="2d"] - the context type to be created ("2d", "webgl" — creates a WebGL 2 context)
2525
* @param {boolean} [attributes.offscreenCanvas=false] - will create an offscreenCanvas if true instead of a standard canvas
2626
* @param {boolean} [attributes.willReadFrequently=false] - Indicates whether or not a lot of read-back operations are planned
2727
* @param {boolean} [attributes.antiAlias=false] - Whether to enable anti-aliasing, use false (default) for a pixelated effect.

packages/melonjs/src/level/tiled/TMXLayer.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const FLIP_AD_BIT = 1 << 2;
2222
// the fallback so apps without any tilemap stay quiet (the prior heads-up
2323
// at Application init time fired regardless of whether a TMX layer was
2424
// ever loaded).
25-
let _warnedNoWebGL2Once = false;
25+
let _warnedNoGpuTileSupportOnce = false;
2626

2727
/**
2828
* extract a 3-bit flip mask from a raw 32-bit GID (Tiled's flip bits live in
@@ -409,11 +409,11 @@ export default class TMXLayer extends Renderable {
409409
// — emitted from the first TMX layer that hits it, so apps with
410410
// no tilemap loaded stay quiet.
411411
if (gpuAllowed) {
412-
if (elig.reason === "no-webgl2-renderer") {
413-
if (!_warnedNoWebGL2Once) {
414-
_warnedNoWebGL2Once = true;
412+
if (elig.reason === "no-gpu-renderer") {
413+
if (!_warnedNoGpuTileSupportOnce) {
414+
_warnedNoGpuTileSupportOnce = true;
415415
console.warn(
416-
"melonJS: gpuTilemap is enabled but the active renderer is not WebGL 2 — falling back to the legacy tile renderer for every tile layer",
416+
"melonJS: gpuTilemap is enabled but the active renderer has no GPU tile-layer support — falling back to the legacy tile renderer for every tile layer",
417417
);
418418
}
419419
} else {
@@ -440,8 +440,8 @@ export default class TMXLayer extends Renderable {
440440
if (!gpuAllowed) {
441441
return { ok: false, reason: "gpuTilemap disabled" };
442442
}
443-
if (!renderer || renderer.WebGLVersion !== 2) {
444-
return { ok: false, reason: "no-webgl2-renderer" };
443+
if (!renderer || renderer.supportsShaderTileLayers !== true) {
444+
return { ok: false, reason: "no-gpu-renderer" };
445445
}
446446
if (this.orientation !== "orthogonal") {
447447
return {

packages/melonjs/src/system/device.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -655,10 +655,13 @@ export function isWebGLSupported(options?: {
655655
failIfMajorPerformanceCaveat: options.failIfMajorPerformanceCaveat,
656656
}),
657657
};
658+
// WebGL 2 only since 20.0 (#1509) — this gate must agree with
659+
// the context actually created by the WebGL renderer (which
660+
// requests "webgl2"), so AUTO falls back to Canvas exactly when
661+
// renderer construction would fail
658662
const _supported = !!(
659-
globalThis.WebGLRenderingContext &&
660-
(canvas.getContext("webgl", ctxOptions) ||
661-
canvas.getContext("experimental-webgl", ctxOptions))
663+
globalThis.WebGL2RenderingContext &&
664+
canvas.getContext("webgl2", ctxOptions)
662665
);
663666
WebGLSupport = _supported ? 1 : 0;
664667
} catch {

packages/melonjs/src/video/renderer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ export default class Renderer {
110110
*/
111111
this.type = "Generic";
112112

113+
/**
114+
* Whether this renderer backend can draw TMX tile layers through a
115+
* GPU shader path (see the `gpuTilemap` application setting).
116+
* `false` here on the base/Canvas renderer; GPU backends flip it.
117+
* A capability flag rather than a version/class check so future
118+
* backends advertise support without consumers growing type checks.
119+
* @type {boolean}
120+
* @default false
121+
*/
122+
this.supportsShaderTileLayers = false;
123+
113124
/**
114125
* The background color used to clear the main framebuffer.
115126
* Note: alpha value will be set based on the transparent property of the renderer settings.

packages/melonjs/src/video/rendertarget/canvasrendertarget.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,9 @@ const defaultAttributes = {
2020
stencil: true,
2121
blendMode: "normal",
2222
failIfMajorPerformanceCaveat: true,
23-
preferWebGL1: false,
2423
powerPreference: "default",
2524
};
2625

27-
// WebGL version (if a gl context is created)
28-
let WebGLVersion;
29-
3026
// a helper function to create the 2d/webgl context
3127
function createContext(canvas, attributes) {
3228
let context;
@@ -50,24 +46,13 @@ function createContext(canvas, attributes) {
5046
failIfMajorPerformanceCaveat: attributes.failIfMajorPerformanceCaveat,
5147
};
5248

53-
// attempt to create a WebGL2 context unless not requested
54-
if (attributes.preferWebGL1 !== true) {
55-
context = canvas.getContext("webgl2", attr);
56-
if (context) {
57-
WebGLVersion = 2;
58-
}
59-
}
49+
// WebGL 2 only since 20.0 (#1509) — no WebGL 1 fallback; callers
50+
// (autoDetectRenderer / Application) fall back to the Canvas
51+
// renderer instead when this throws
52+
context = canvas.getContext("webgl2", attr);
6053

61-
// fallback to WebGL1
6254
if (!context) {
63-
WebGLVersion = 1;
64-
context =
65-
canvas.getContext("webgl", attr) ||
66-
canvas.getContext("experimental-webgl", attr);
67-
}
68-
69-
if (!context) {
70-
throw new Error("A WebGL context could not be created.");
55+
throw new Error("A WebGL 2 context could not be created.");
7156
}
7257
} else {
7358
throw new Error("Invalid context type. Must be one of '2d' or 'webgl'");
@@ -89,8 +74,7 @@ class CanvasRenderTarget extends RenderTarget {
8974
* @param {number} width - the desired width of the canvas
9075
* @param {number} height - the desired height of the canvas
9176
* @param {object} attributes - The attributes to create both the canvas and context
92-
* @param {string} [attributes.context="2d"] - the context type to be created ("2d", "webgl")
93-
* @param {boolean} [attributes.preferWebGL1=false] - set to true for force using WebGL1 instead of WebGL2 (if supported)
77+
* @param {string} [attributes.context="2d"] - the context type to be created ("2d", "webgl" — a "webgl" request creates a WebGL 2 context)
9478
* @param {boolean} [attributes.transparent=false] - specify if the canvas contains an alpha channel
9579
* @param {boolean} [attributes.offscreenCanvas=false] - will create an offscreenCanvas if true instead of a standard canvas
9680
* @param {boolean} [attributes.willReadFrequently=false] - Indicates whether or not a lot of read-back operations are planned
@@ -120,7 +104,14 @@ class CanvasRenderTarget extends RenderTarget {
120104
// create the context
121105
this.context = createContext(this.canvas, this.attributes);
122106

123-
this.WebGLVersion = WebGLVersion;
107+
if (this.attributes.context === "webgl") {
108+
/**
109+
* The WebGL version of the created context.
110+
* @type {number}
111+
* @deprecated since 20.0.0 — the WebGL renderer is WebGL 2 only, this is always `2`
112+
*/
113+
this.WebGLVersion = 2;
114+
}
124115

125116
// enable or disable antiAlias if specified
126117
this.setAntiAlias(this.attributes.antiAlias);

0 commit comments

Comments
 (0)