Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/examples/src/examples/platformer/createGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const createGame = () => {
renderer: video.AUTO,
preferWebGL1: false,
subPixel: false,
highPrecisionShader: !device.isMobile,
highPrecisionShader: false,
});

// register the debug plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const createGame = () => {
!video.init(1024, 768, {
parent: "screen",
scaleMethod: "flex",
preferWebGL1: false,
})
) {
alert("Your browser does not support HTML5 canvas.");
Expand Down
18 changes: 17 additions & 1 deletion packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,24 @@

## [19.4.0] (melonJS 2) - _unreleased_

**Highlights:** rendering-focused release. The headline is GPU-accelerated WebGL 2 tile rendering for orthogonal TMX maps: visible layers now render as a single quad through a fragment shader instead of one draw per tile. Combined with the new shader-wide uniform cache, the per-fragment fast path, and the flat `Uint16Array`-backed tile data, a typical 3-layer 800×600 game on mid-tier mobile reclaims roughly **1.5–3.5 ms per frame** (~10–20% of the 60 fps budget). Dense large maps should see ~5–8× speedups on the rendering portion.

### Added
- GPU-accelerated WebGL 2 tile rendering for orthogonal TMX maps. Each visible layer renders as a single quad whose fragment shader walks the per-layer GID index texture and samples the tileset atlas, with no per-tile draw loop. Supports animated tiles, flip bits (H/V/AD), per-layer opacity/tint, per-layer blend mode, and oversized bottom-aligned tiles. Enabled by default via `Application.settings.gpuTilemap`; falls back transparently to the legacy CPU renderer on isometric/staggered/hexagonal layers, collection-of-image tilesets, non-zero `tileoffset`, or non-WebGL-2 contexts. Rough win on a mid-tier mobile GPU with a 3-layer 800×600 viewport: ~2–4 ms down to ~0.3–0.8 ms per frame; up to ~5–8× on dense large maps; effectively free on desktop GPUs.
- WebGL: custom shaders can now be written in GLSL ES 3.00 (`#version 300 es`). Construct a `GLShader` with both vertex and fragment source in 3.00 form. The precision injector and attribute extractor handle both versions. **Note:** `ShaderEffect` is still 1.00-only since WebGL requires both stages of a program to share a version, and it pairs the user's fragment with the built-in 1.00 quad vertex shader.
- `TextureResource` / `BufferTextureResource`: a renderer-agnostic source for textures synthesized from raw byte buffers rather than loaded from an image. Flows through the standard `TextureCache` and batcher path. Supports `rgba8` and `rgba8ui` (WebGL 2) formats. Used internally by the GPU TMX renderer.

### Fixed
- WebGL: `MaterialBatcher.uploadTexture` was using its `w` and `h` parameters (the destination quad size, not the texture's) for the `isPOT` check, which drives both the wrap-mode fallback and the `generateMipmap` gate. Visible as a `GL_INVALID_OPERATION` from `gl.generateMipmap` on WebGL 1; silent wasted work (unnecessary mipmaps, wrong `isPOT`-derived state) on WebGL 2. Texture dimensions are now derived from the source itself.

### Changed
- `throttle(fn, wait)` is now generic over its argument tuple — `throttle<T extends unknown[]>((...args: T) => void, wait)` preserves the wrapped function's parameter types. Drops the `as unknown as () => void` cast that the pointer-event handler used to need.
- WebGL 1: removed the unconditional `[Texture] ... is not a POT texture` warning. The engine handles NPOT correctly (clamp wrap, non-mipmapped filters). A targeted warning now fires only when `repeat: "repeat*"` is requested on an NPOT texture under WebGL 1, the one case where the user's intent is silently downgraded.
- `throttle(fn, wait)` is now generic over its argument tuple. `throttle<T extends unknown[]>((...args: T) => void, wait)` preserves the wrapped function's parameter types.

### Performance
- TMX tile layers now back `layerData` with a flat `Uint16Array` and the orientation renderers read directly from it, with no `Tile` allocations during map parse or per-frame rendering. Per-layer memory drops ~25× (40 KB vs ~1 MB on a 100×100 layer); modest FPS gain on Canvas (~2–5% in tile-heavy scenes). Public API is unchanged.
- WebGL: every shader the engine builds (sprite batchers, light effects, post-effect chains, the TMX GPU renderer, user-authored `GLShader` / `ShaderEffect`) now caches the last value sent for each uniform and skips redundant `gl.uniform*` calls. Vec/mat values compare element-wise so a reused scratch `Float32Array` is detected correctly. Biggest beneficiaries are the per-frame projection-matrix upload (now skipped after the first frame) and the TMX GPU renderer's layer-lifetime constants. Modest on its own, typically ~0.1–0.5 ms saved per frame on mid-tier mobile, more in scenes with many custom shaders or post-effect chains, but stacks cleanly with every other rendering win.
- TMX GPU renderer: fragment shader branches on `uOverflow == (0, 0)` and uses a single-cell fast path for tilesets whose tiles fit the cell exactly (the common case), skipping the worst-case 25-iteration candidate-cell loop entirely. The slow path (oversized bottom-aligned tiles) is unchanged. Roughly 10–25% fragment-shader cost reduction for the common case (~0.05–0.2 ms per frame on mid-tier mobile, lost in the noise on desktop GPUs); the win compounds with viewport size since fragment work scales with pixel count.

## [19.3.0] (melonJS 2) - _2026-05-08_

Expand Down
18 changes: 18 additions & 0 deletions packages/melonjs/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@
this.settings = settings;

// identify parent element and/or the html target for resizing
this.parentElement = device.getElement(this.settings.parent!);

Check warning on line 276 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 276 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 276 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
if (typeof this.settings.scaleTarget !== "undefined") {
this.settings.scaleTarget = device.getElement(this.settings.scaleTarget);
}
Expand Down Expand Up @@ -379,6 +379,24 @@
this.world.app = this;
// set the reference to this application instance
this.world.physic = this.settings.physic;
this.world.gpuTilemap = this.settings.gpuTilemap;

// The GPU tilemap path needs a WebGL 2 renderer. Warn once at app
// startup when the user asked for it but the active renderer
// can't honor it (Canvas mode, WebGL 1 driver, `preferWebGL1`
// override, etc.) — individual layers will silently fall through
// to the legacy renderer, but the user gets one heads-up that
// the feature they enabled isn't actually in effect.
if (
this.settings.gpuTilemap &&
// duck-type rather than `instanceof WebGLRenderer` to avoid a
// runtime import; only the WebGL renderer carries `WebGLVersion`
(this.renderer as unknown as { WebGLVersion?: number }).WebGLVersion !== 2
) {
console.warn(
"melonJS: gpuTilemap is enabled but the active renderer is not WebGL 2 — falling back to the legacy tile renderer for every tile layer",
);
}

// app starting time
this.lastUpdate = globalThis.performance.now();
Expand Down Expand Up @@ -412,7 +430,7 @@
// point to the current active stage "default" camera
const current = state.get();
if (typeof current !== "undefined") {
this.viewport = current.cameras.get("default")!;

Check warning on line 433 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 433 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 433 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
}

// publish reset notification
Expand Down Expand Up @@ -519,21 +537,21 @@
globalThis.removeEventListener("resize", this._onResize);
globalThis.removeEventListener(
"orientationchange",
this._onOrientationChange!,

Check warning on line 540 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 540 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 540 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
);
globalThis.removeEventListener("scroll", this._onScroll!);

Check warning on line 542 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 542 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 542 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion
if (device.screenOrientation) {
globalThis.screen.orientation.onchange = null;
}
}

// destroy the world and all its children
if (this.world) {

Check warning on line 549 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 549 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 549 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, value is always truthy
this.world.destroy();
}

// remove the canvas from the DOM
if (removeCanvas && this.renderer) {

Check warning on line 554 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 554 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy

Check warning on line 554 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Unnecessary conditional, value is always truthy
const canvas = this.renderer.getCanvas();
if (canvas.parentElement) {
canvas.parentElement.removeChild(canvas);
Expand Down Expand Up @@ -652,7 +670,7 @@
// update all objects (and pass the elapsed time since last frame)
this.isDirty = this.world.update(this.updateDelta);
this.isDirty =
state.current()!.update(this.updateDelta) || this.isDirty;

Check warning on line 673 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 673 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 673 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion

this.lastUpdate = globalThis.performance.now();
this.updateAverageDelta = this.lastUpdate - this.lastUpdateStart;
Expand Down Expand Up @@ -681,7 +699,7 @@
this.renderer.clear();

// render the stage
state.current()!.draw(this.renderer, this.world);

Check warning on line 702 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 702 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion

Check warning on line 702 in packages/melonjs/src/application/application.ts

View workflow job for this annotation

GitHub Actions / test

Forbidden non-null assertion

// set back to flag
this.isDirty = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const defaultApplicationSettings = {
consoleHeader: true,
blendMode: "normal",
physic: "builtin",
gpuTilemap: true,
failIfMajorPerformanceCaveat: true,
highPrecisionShader: true,
subPixel: false,
Expand Down
13 changes: 13 additions & 0 deletions packages/melonjs/src/application/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ export type ApplicationSettings = {
* @default "builtin"
*/
physic: PhysicsType;

/**
* Enable the WebGL2 procedural shader path for orthogonal tile layers.
* When `true` (default), eligible layers render via a single quad per
* tileset + a fragment shader doing per-fragment GID lookup, bypassing
* the per-tile draw loop entirely. Layers that don't qualify
* (Canvas/WebGL1, non-orthogonal, collection-of-image tilesets,
* tilerendersize "grid", non-zero tileoffset, oversampled beyond the
* shader's overflow window) fall back to the legacy path automatically.
* Set to `false` to disable globally.
* @default true
*/
gpuTilemap: boolean;
/**
* if true, the renderer will fail if the browser reports a major performance caveat
* (e.g. software WebGL). Set to false to allow WebGL on machines with
Expand Down
Loading
Loading