Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

### Fixed
- Application: `Object.assign(defaultApplicationSettings, options)` mutated the shared defaults object in both `Application.init()` and `video.init()` — creating multiple Application instances would corrupt settings. Fixed with object spread.
- Text/Light2d: fix invalid `pool.push` on CanvasRenderTarget instances that were never pool-registered (would throw on destroy)
- CanvasRenderTarget: `destroy(renderer)` now properly cleans up WebGL GPU textures and cache entries (previously leaked in Light2d)
- Text: always use power-of-two texture sizes for offscreen canvas (removes WebGL version check dependency)
- Application: prevent white flash on load by setting a black background on the parent element when no background is defined
- WebGLRenderer: `setBlendMode()` now tracks the `premultipliedAlpha` flag — previously only the mode name was checked, causing incorrect GL blend function when mixing PMA and non-PMA textures with the same blend mode
- TMX: fix crash in `getObjects(false)` when a map contains an empty object group (Container.children lazily initialized)
Expand Down
4 changes: 0 additions & 4 deletions packages/melonjs/src/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { getUriFragment } from "../utils/utils.ts";
import CanvasRenderer from "../video/canvas/canvas_renderer.js";
import type Renderer from "./../video/renderer.js";
import { autoDetectRenderer } from "../video/utils/autodetect.js";
import { setRenderer } from "../video/video.js";
import { defaultApplicationSettings } from "./defaultApplicationSettings.ts";
import { consoleHeader } from "./header.ts";
import { onresize } from "./resize.ts";
Expand Down Expand Up @@ -277,9 +276,6 @@ export default class Application {
this.renderer = new CustomRenderer(this.settings);
}

// set the global renderer reference for renderables that depend on it
setRenderer(this.renderer);

// make this the active game instance for modules that reference the global
setDefaultGame(this);

Expand Down
4 changes: 2 additions & 2 deletions packages/melonjs/src/level/tiled/TMXTileset.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { game } from "../../application/application.ts";
import { getImage, getTMX } from "../../loader/loader.js";
import { Vector2d } from "../../math/vector2d.ts";
import timer from "../../system/timer.ts";
import { getBasename, getExtension } from "../../utils/file.ts";
import { renderer } from "../../video/video.js";
import { resolveEmbeddedImage } from "./TMXUtils.js";

/**
Expand Down Expand Up @@ -348,7 +348,7 @@ export default class TMXTileset {
}

// create a texture atlas for the given tileset
this.texture = renderer.cache.get(this.image, {
this.texture = game.renderer.cache.get(this.image, {
framewidth: this.tilewidth,
frameheight: this.tileheight,
margin: this.margin,
Expand Down
6 changes: 4 additions & 2 deletions packages/melonjs/src/renderable/imagelayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
VIEWPORT_ONRESIZE,
} from "../system/event.ts";
import * as stringUtil from "./../utils/string.ts";
import { renderer } from "./../video/video.js";
import Sprite from "./sprite.js";

/**
Expand Down Expand Up @@ -160,7 +159,10 @@ export default class ImageLayer extends Sprite {
* @ignore
*/
createPattern() {
this._pattern = renderer.createPattern(this.image, this._repeat);
const renderer = this.parentApp?.renderer ?? game.renderer;
if (renderer) {
this._pattern = renderer.createPattern(this.image, this._repeat);
}
Comment on lines 161 to +165
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createPattern() now silently skips pattern creation when no renderer is available, leaving this._pattern undefined. draw() unconditionally calls renderer.drawPattern(this._pattern, ...), which will throw (Canvas sets fillStyle = undefined, WebGL calls pattern.getUVs). Either ensure createPattern() is (re)called once a renderer is available (e.g., from onActivateEvent()), or make draw() handle a missing pattern (lazy-create or early-return) so this can't crash at render time.

Copilot uses AI. Check for mistakes.
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/melonjs/src/renderable/light2d.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { game } from "../application/application.ts";
import { ellipsePool } from "./../geometries/ellipse.ts";
import { colorPool } from "./../math/color.ts";
import pool from "../system/legacy_pool.js";
import CanvasRenderTarget from "../video/rendertarget/canvasrendertarget.js";
import Renderable from "./renderable.js";

Expand Down Expand Up @@ -205,8 +205,8 @@ export default class Light2d extends Renderable {
destroy() {
colorPool.release(this.color);
this.color = undefined;
pool.push(this.texture);
this.texture.destroy();
const renderer = this.parentApp?.renderer ?? game.renderer;
this.texture.destroy(renderer);
Comment on lines 205 to +209
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light2d.destroy() no longer returns the CanvasRenderTarget to legacy_pool (pool.push(this.texture) was removed), and as a result the existing pool import is now unused. Either remove the unused pool import, or keep the pooling behavior if reusing render targets is still desired.

Copilot uses AI. Check for mistakes.
this.texture = undefined;
Comment on lines +209 to 210
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light2d.destroy() now calls this.texture.destroy(renderer) but CanvasRenderTarget.destroy currently assumes glTextureUnit is set when renderer.gl exists. Light2d never calls texture.invalidate(renderer), so glTextureUnit is likely undefined and this path can crash in WebGL. Either ensure Light2d tracks/invalidates its CanvasRenderTarget before it’s used as a WebGL texture, or make CanvasRenderTarget.destroy resilient when glTextureUnit is undefined.

Suggested change
this.texture.destroy(renderer);
this.texture = undefined;
if (this.texture !== undefined && this.texture !== null) {
// Ensure the CanvasRenderTarget is invalidated before destruction,
// so any WebGL-related state (e.g. glTextureUnit) is correctly handled.
if (renderer && typeof this.texture.invalidate === "function") {
this.texture.invalidate(renderer);
}
this.texture.destroy(renderer);
this.texture = undefined;
}

Copilot uses AI. Check for mistakes.
ellipsePool.release(this.visibleArea);
this.visibleArea = undefined;
Expand Down
4 changes: 2 additions & 2 deletions packages/melonjs/src/renderable/sprite.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { game } from "../application/application.ts";
import { getImage } from "./../loader/loader.js";
import { Color } from "../math/color.ts";
import { vector2dPool } from "../math/vector2d.ts";
import { on } from "../system/event.ts";
import { TextureAtlas } from "./../video/texture/atlas.js";
import { renderer } from "./../video/video.js";
import Renderable from "./renderable.js";

/**
Expand Down Expand Up @@ -223,7 +223,7 @@ export default class Sprite extends Renderable {
this.current.height =
settings.frameheight =
settings.frameheight || this.image.height;
this.source = renderer.cache.get(this.image, settings);
this.source = game.renderer.cache.get(this.image, settings);
this.textureAtlas = this.source.getAtlas();
}
}
Expand Down
57 changes: 27 additions & 30 deletions packages/melonjs/src/renderable/text/text.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { game } from "../../application/application.ts";
import { Color, colorPool } from "../../math/color.ts";
import { nextPowerOfTwo } from "../../math/math.ts";
import pool from "../../system/legacy_pool.js";
import CanvasRenderTarget from "../../video/rendertarget/canvasrendertarget.js";
import { renderer as globalRenderer } from "../../video/video.js";
import Renderable from "../renderable.js";
Comment on lines +1 to 5
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pool is now imported but no longer used after delegating cleanup to canvasTexture.destroy(renderer). This will trip linting/unused-import checks and makes the file harder to maintain; remove the import or reintroduce pooling where intended.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to 5
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pool is now imported but never used in this module. Please remove the unused import to avoid dead code and keep linting/treeshaking clean.

Copilot uses AI. Check for mistakes.
import TextMetrics from "./textmetrics.js";
import setContextStyle from "./textstyle.js";
Expand Down Expand Up @@ -165,7 +164,7 @@ export default class Text extends Renderable {
// font name and type
this.setFont(settings.font, settings.size);

// aditional
// additional font styles
if (settings.bold === true) {
this.bold();
}
Expand All @@ -192,8 +191,13 @@ export default class Text extends Renderable {
* @returns {Text} this object for chaining
*/
bold() {
this.font = "bold " + this.font;
this.isDirty = true;
if (
!this.font.startsWith("bold ") &&
!this.font.startsWith("italic bold ")
) {
this.font = "bold " + this.font;
this.isDirty = true;
}
return this;
}

Expand All @@ -202,8 +206,13 @@ export default class Text extends Renderable {
* @returns {Text} this object for chaining
*/
italic() {
this.font = "italic " + this.font;
this.isDirty = true;
if (
!this.font.startsWith("italic ") &&
!this.font.startsWith("bold italic ")
) {
this.font = "italic " + this.font;
this.isDirty = true;
}
return this;
}

Expand Down Expand Up @@ -278,18 +287,14 @@ export default class Text extends Renderable {
true,
);

// update the offScreenCanvas texture if required
let width = Math.ceil(this.metrics.width);
let height = Math.ceil(this.metrics.height);

if (globalRenderer.WebGLVersion === 1) {
// round size to next Pow2
width = nextPowerOfTwo(this.metrics.width);
height = nextPowerOfTwo(this.metrics.height);
}
// round the offscreen canvas size to the next power of two
// (required for WebGL1, harmless for WebGL2/Canvas)
const width = nextPowerOfTwo(this.metrics.width);
const height = nextPowerOfTwo(this.metrics.height);

// invalidate the texture
this.canvasTexture.invalidate(globalRenderer);
const renderer = this.parentApp?.renderer ?? game.renderer;
this.canvasTexture.invalidate(renderer);

// resize the cache canvas if necessary
if (
Expand Down Expand Up @@ -330,7 +335,8 @@ export default class Text extends Renderable {
* @param {number} [y]
*/
draw(renderer, text, x = this.pos.x, y = this.pos.y) {
// "hacky patch" for backward compatibilty
// @deprecated since 10.6.0 — standalone draw without a parent container
// TODO: remove in 19.0.0
if (typeof this.ancestor === "undefined") {
// update position if changed
if (this.pos.x !== x || this.pos.y !== y) {
Expand Down Expand Up @@ -362,7 +368,7 @@ export default class Text extends Renderable {
// draw the text
renderer.drawImage(this.canvasTexture.canvas, x, y);

// for backward compatibilty
// @deprecated since 10.6.0 — TODO: remove in 19.0.0
if (typeof this.ancestor === "undefined") {
// restore previous context
renderer.restore();
Expand Down Expand Up @@ -396,17 +402,8 @@ export default class Text extends Renderable {
* @ignore
*/
destroy() {
if (typeof globalRenderer.gl !== "undefined") {
// make sure the right batcher is active
globalRenderer.setBatcher("quad");
globalRenderer.currentBatcher.deleteTexture2D(
globalRenderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
globalRenderer.cache.delete(this.canvasTexture.canvas);
pool.push(this.canvasTexture);
this.canvasTexture.destroy();
const renderer = this.parentApp?.renderer ?? game.renderer;
this.canvasTexture.destroy(renderer);
this.canvasTexture = undefined;
colorPool.release(this.fillStyle);
colorPool.release(this.strokeStyle);
Expand Down
23 changes: 17 additions & 6 deletions packages/melonjs/src/tweens/tween.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { game } from "../application/application.ts";
import { off, on, STATE_RESUME } from "../system/event.js";
import { GAME_AFTER_UPDATE, off, on, STATE_RESUME } from "../system/event.js";
import { createPool } from "../system/pool.ts";
import timer from "../system/timer.js";
import { Easing, EasingFunction } from "./easing.js";
Expand Down Expand Up @@ -50,6 +50,7 @@ export default class Tween {
_onUpdateCallback: OnUpdateCallback<object> | null;
_onCompleteCallback: OnCompleteCallback<object> | null;
_tweenTimeTracker: number;
_lastUpdate: number;
isPersistent: boolean;
updateWhenPaused: boolean;
isRenderable: boolean;
Expand Down Expand Up @@ -99,8 +100,9 @@ export default class Tween {
this._onStartCallbackFired = false;
this._onUpdateCallback = null;
this._onCompleteCallback = null;
// tweens are synchronized with the game update loop
this._tweenTimeTracker = game.lastUpdate;
// track the last update timestamp from the game loop
this._lastUpdate = globalThis.performance.now();
this._tweenTimeTracker = this._lastUpdate;

// reset flags to default value
this.isPersistent = false;
Expand All @@ -126,13 +128,20 @@ export default class Tween {
}
}

/** @ignore */
_onAfterUpdate(lastUpdate: number) {
this._lastUpdate = lastUpdate;
}

/**
* subscribe to the resume event when added
* subscribe to events when added
* @ignore
*/
onActivateEvent() {
// eslint-disable-next-line @typescript-eslint/unbound-method
on(STATE_RESUME, this._resumeCallback, this);
// eslint-disable-next-line @typescript-eslint/unbound-method
on(GAME_AFTER_UPDATE, this._onAfterUpdate, this);
}

/**
Expand All @@ -142,6 +151,8 @@ export default class Tween {
onDeactivateEvent() {
// eslint-disable-next-line @typescript-eslint/unbound-method
off(STATE_RESUME, this._resumeCallback, this);
// eslint-disable-next-line @typescript-eslint/unbound-method
off(GAME_AFTER_UPDATE, this._onAfterUpdate, this);
}

/**
Expand Down Expand Up @@ -344,8 +355,8 @@ export default class Tween {
// the original Tween implementation expect
// a timestamp and not a time delta
this._tweenTimeTracker =
game.lastUpdate > this._tweenTimeTracker
? game.lastUpdate
this._lastUpdate > this._tweenTimeTracker
? this._lastUpdate
: this._tweenTimeTracker + dt;
const time = this._tweenTimeTracker;

Expand Down
18 changes: 17 additions & 1 deletion packages/melonjs/src/video/rendertarget/canvasrendertarget.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,25 @@ class CanvasRenderTarget {
}

/**
* Destroy this canvas render target and release associated GPU resources.
* @param {CanvasRenderer|WebGLRenderer} [renderer] - the renderer to clean up WebGL resources from
* @ignore
*/
destroy() {
destroy(renderer) {
if (
renderer &&
typeof renderer.gl !== "undefined" &&
typeof this.glTextureUnit !== "undefined"
) {
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
if (renderer) {
renderer.cache.delete(this.canvas);
}
this.context = undefined;
this.canvas = undefined;
}
Expand Down
43 changes: 18 additions & 25 deletions packages/melonjs/src/video/texture/atlas.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { game } from "../../application/application.ts";
import { getImage } from "./../../loader/loader.js";
import { Vector2d } from "../../math/vector2d.ts";
import Sprite from "./../../renderable/sprite.js";
import pool from "../../system/legacy_pool.js";
import { renderer } from "./../video.js";
import { parseAseprite } from "./parser/aseprite.js";
import { parseSpriteSheet } from "./parser/spritesheet.js";
import { parseTexturePacker } from "./parser/texturepacker.js";
Expand Down Expand Up @@ -234,7 +234,7 @@ export class TextureAtlas {
// Add self to TextureCache if cache !== false
if (cache !== false) {
this.sources.forEach((source) => {
renderer.cache.set(source, this);
game.renderer.cache.set(source, this);
});
}
}
Expand Down Expand Up @@ -283,11 +283,6 @@ export class TextureAtlas {
* @returns {object} the created region
*/
addRegion(name, x, y, w, h) {
// see https://github.com/melonjs/melonJS/issues/1281
if (renderer.settings.verbose === true) {
console.warn("Adding texture region", name, "for texture", this);
}

const source = this.getTexture();
const atlas = this.getAtlas();
const dw = source.width;
Expand Down Expand Up @@ -384,24 +379,22 @@ export class TextureAtlas {
* @returns {Float32Array} the created region UVs
*/
addUVs(atlas, name, w, h) {
// ignore if using the Canvas Renderer
if (typeof renderer.gl !== "undefined") {
// Source coordinates
const s = atlas[name].offset;
const sw = atlas[name].width;
const sh = atlas[name].height;

atlas[name].uvs = new Float32Array([
s.x / w, // u0 (left)
s.y / h, // v0 (top)
(s.x + sw) / w, // u1 (right)
(s.y + sh) / h, // v1 (bottom)
]);
// Cache source coordinates
// see https://github.com/melonjs/melonJS/issues/1281
const key = s.x + "," + s.y + "," + w + "," + h;
atlas[key] = atlas[name];
}
// Source coordinates
const s = atlas[name].offset;
const sw = atlas[name].width;
const sh = atlas[name].height;

atlas[name].uvs = new Float32Array([
s.x / w, // u0 (left)
s.y / h, // v0 (top)
(s.x + sw) / w, // u1 (right)
(s.y + sh) / h, // v1 (bottom)
]);
// Cache source coordinates
// see https://github.com/melonjs/melonJS/issues/1281
const key = `${s.x},${s.y},${w},${h}`;
atlas[key] = atlas[name];

return atlas[name].uvs;
}

Expand Down
Loading
Loading