Skip to content

Remove all internal global renderer imports#1344

Merged
obiot merged 17 commits intomasterfrom
refactor/remove-global-renderer
Apr 2, 2026
Merged

Remove all internal global renderer imports#1344
obiot merged 17 commits intomasterfrom
refactor/remove-global-renderer

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented Apr 1, 2026

Summary

  • Remove all internal imports of the global renderer from video.js
  • imagelayer.js uses this.parentApp.renderer (available in container tree)
  • sprite.js, TMXTileset.js, atlas.js, text.js use game.renderer
  • Removed setRenderer() function — no longer needed
  • video.renderer remains as a public API export for external consumers, set once in video.init()

Zero internal files now import the global renderer from video.js.

Test plan

  • All 1951 tests pass
  • Build clean (lint + types)
  • Verify examples with rebuilt output

🤖 Generated with Claude Code

…public API

Internal code no longer imports the global `renderer` from video.js:
- imagelayer.js: uses this.parentApp.renderer
- sprite.js: uses game.renderer
- TMXTileset.js: uses game.renderer
- atlas.js: uses game.renderer
- text.js: uses game.renderer

Removed setRenderer() — no longer needed since no internal code
imports the global. video.renderer remains as a public API export,
set once in video.init() for external consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 03:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR removes internal dependencies on the global video.renderer singleton by switching engine modules to obtain the renderer via the active game instance (or via the renderable’s application context), and by removing the now-unneeded internal setRenderer() hook.

Changes:

  • Removed setRenderer() from video.js and stopped calling it from Application.init().
  • Updated several renderables/texture utilities to use game.renderer instead of importing renderer from video.js.
  • Updated ImageLayer pattern creation to use this.parentApp.renderer.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/melonjs/src/video/video.js Removes setRenderer() and keeps video.renderer assignment in video.init().
packages/melonjs/src/video/texture/atlas.js Switches texture cache/verbose/GL checks to use game.renderer.
packages/melonjs/src/renderable/text/text.js Replaces global renderer usage with game.renderer for WebGL checks and cache/GL cleanup.
packages/melonjs/src/renderable/sprite.js Uses game.renderer.cache for spritesheet atlas retrieval instead of video.renderer.
packages/melonjs/src/renderable/imagelayer.js Uses parentApp.renderer to create the repeating pattern.
packages/melonjs/src/level/tiled/TMXTileset.js Uses game.renderer.cache to create the tileset atlas texture.
packages/melonjs/src/application/application.ts Removes the call to setRenderer() during initialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

*/
createPattern() {
this._pattern = renderer.createPattern(this.image, this._repeat);
this._pattern = this.parentApp.renderer.createPattern(
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.

ImageLayer.createPattern() now dereferences this.parentApp.renderer, but createPattern() is invoked from the repeat setter during construction (before the layer is attached to a container/world), so parentApp is still undefined and this will throw at runtime. Consider falling back to the default game renderer when parentApp is not yet available, or delay pattern creation until onActivateEvent/after attachment.

Suggested change
this._pattern = this.parentApp.renderer.createPattern(
const renderer = this.parentApp ? this.parentApp.renderer : game.renderer;
this._pattern = renderer.createPattern(

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 33
/**
* A reference to the active Canvas or WebGL active renderer renderer
* @memberof video
* @type {CanvasRenderer|WebGLRenderer}
*/
export let renderer = null;
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.

video.renderer is documented as “a reference to the active … renderer”, but after removing setRenderer() it’s only assigned inside video.init(). If a consumer initializes via new Application().init() (which emits VIDEO_INIT) without calling video.init(), video.renderer will remain null, which is a public-API behavior change and makes the JSDoc misleading. Either keep video.renderer synchronized when VIDEO_INIT fires (e.g., via an event listener) or update the documentation to clearly state when it is populated.

Copilot uses AI. Check for mistakes.
obiot and others added 2 commits April 1, 2026 11:58
Tests verify that renderables in the container tree can access
the Application instance and its renderer via parentApp:
- Direct child of world
- Nested child in container hierarchy
- Renderer and cache access via parentApp
- Undefined before being added to a container

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 04:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to 71
// set the public renderer reference
renderer = game.renderer;

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.

video.renderer is now only assigned here in video.init(). Creating a standalone new Application(...) (supported entry point) instantiates a renderer without updating this exported API, so video.renderer can remain null/stale even though a renderer exists. Either update the docs to reflect this constraint, or keep video.renderer in sync without internal imports (e.g., update it in response to the existing VIDEO_INIT event emitted by Application.init).

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
boot();
video.init(800, 600, {
parent: "screen",
scale: "auto",
renderer: video.AUTO,
});
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.

This block re-calls boot()/video.init() even though the default game instance is already initialized earlier in this file. video.init() unconditionally appends a new canvas and registers global window listeners, so re-initializing here can leak DOM nodes and duplicate handlers. Prefer reusing the existing initialized game (or guard the init with if (!game.isInitialized) / consolidate to a single beforeAll).

Suggested change
boot();
video.init(800, 600, {
parent: "screen",
scale: "auto",
renderer: video.AUTO,
});
if (!initialized) {
boot();
video.init(800, 600, {
parent: "screen",
scale: "auto",
renderer: video.AUTO,
});
}

Copilot uses AI. Check for mistakes.
obiot and others added 2 commits April 1, 2026 14:48
- ImageLayer.createPattern() falls back to game.renderer when parentApp
  is not yet available (called from constructor via repeat setter)
- Updated video.renderer JSDoc to note it's only available after
  video.init(), and to use app.renderer with Application directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Always round offscreen canvas to next power of two regardless of
  renderer type (required for WebGL1, harmless for WebGL2/Canvas)
- Use this.parentApp?.renderer ?? game.renderer for invalidate and
  destroy, preferring the Application instance when in the container tree
- Removes WebGLVersion check dependency from setText()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 06:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 395 to 405
const renderer = this.parentApp?.renderer ?? game.renderer;
if (typeof renderer.gl !== "undefined") {
// make sure the right batcher is active
globalRenderer.setBatcher("quad");
globalRenderer.currentBatcher.deleteTexture2D(
globalRenderer.currentBatcher.getTexture2D(this.glTextureUnit),
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
globalRenderer.cache.delete(this.canvasTexture.canvas);
renderer.cache.delete(this.canvasTexture.canvas);
pool.push(this.canvasTexture);
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.

destroy() assumes const renderer = this.parentApp?.renderer ?? game.renderer is always defined, but it can be undefined/null for detached renderables or during teardown. Accessing renderer.gl / renderer.cache will then throw. Please guard against a missing renderer (e.g., if (renderer && typeof renderer.gl !== "undefined") ... and only delete from renderer.cache when renderer is present), or ensure a renderer is always available for Text instances before calling these methods.

Copilot uses AI. Check for mistakes.
Comment on lines 161 to +165
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);
}
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.
obiot and others added 2 commits April 1, 2026 15:06
CanvasRenderTarget.destroy(renderer) now handles its own GPU resource
cleanup (deleting the WebGL texture and removing from cache) instead
of callers reaching into batcher internals.

Updated text.js and light2d.js to pass the renderer to destroy().
Fixes potential WebGL texture leak in light2d which wasn't cleaning
up GPU resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove invalid pool.push(canvasTexture) calls in text.js and light2d.js
  (CanvasRenderTarget was never pool-registered, so push always threw)
- Add Text tests: setText, canvas texture creation, parentApp access,
  destroy cleanup via removeChildNow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 07:12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New tests (16 added, 7→23 total for font.spec.js):
- TextMetrics: lineHeight, single/multiline measureText
- measureText: textAlign (left/center/right) x positioning
- measureText: textBaseline (top/middle/bottom) y positioning
- wordWrap: long text wrapping, short text, empty text

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +294 to +298
if (renderer && typeof renderer.gl !== "undefined") {
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
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.

CanvasRenderTarget.destroy(renderer) can throw in WebGL when this.glTextureUnit was never set (e.g. no invalidate() call): getTexture2D(undefined) returns undefined and deleteTexture2D(undefined) ends up calling gl.deleteTexture(undefined). Guard for an undefined texture unit/texture before deleting, or delete via the cached TextureAtlas (if present) instead of relying on glTextureUnit.

Suggested change
if (renderer && typeof renderer.gl !== "undefined") {
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
if (
renderer &&
typeof renderer.gl !== "undefined" &&
typeof this.glTextureUnit !== "undefined"
) {
renderer.setBatcher("quad");
const texture = renderer.currentBatcher.getTexture2D(this.glTextureUnit);
if (texture) {
renderer.currentBatcher.deleteTexture2D(texture);
}

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +302
if (renderer && typeof renderer.gl !== "undefined") {
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
if (renderer) {
renderer.cache.delete(this.canvas);
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.

CanvasRenderTarget.destroy only cleans up the GPU texture/cache when a renderer is explicitly passed. There are existing call sites that call .destroy() without arguments (e.g. particle emitter default texture), which will continue to leak cached texture units / WebGL textures. Consider either storing the owning renderer on the CanvasRenderTarget at creation time, or ensuring all internal destroy call sites pass the correct renderer consistently.

Suggested change
if (renderer && typeof renderer.gl !== "undefined") {
renderer.setBatcher("quad");
renderer.currentBatcher.deleteTexture2D(
renderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
if (renderer) {
renderer.cache.delete(this.canvas);
const targetRenderer = renderer || this.renderer;
if (targetRenderer && typeof targetRenderer.gl !== "undefined") {
targetRenderer.setBatcher("quad");
targetRenderer.currentBatcher.deleteTexture2D(
targetRenderer.currentBatcher.getTexture2D(this.glTextureUnit),
);
this.glTextureUnit = undefined;
}
if (targetRenderer) {
targetRenderer.cache.delete(this.canvas);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to 6
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";
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 +209 to 210
this.texture.destroy(renderer);
this.texture = undefined;
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.
Comment on lines 205 to +209
destroy() {
colorPool.release(this.color);
this.color = undefined;
pool.push(this.texture);
this.texture.destroy();
const renderer = this.parentApp?.renderer;
this.texture.destroy(renderer);
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.
The draw() method has a special path for when Text is used without
a parent container (ancestor === undefined). This was a backward
compatibility patch from the TextMetrics refactor in 10.6.0.
Marked as deprecated for future removal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 07:23
@obiot obiot force-pushed the refactor/remove-global-renderer branch from 06f024b to f0dd7ff Compare April 1, 2026 07:23
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to 5
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";
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.
obiot and others added 2 commits April 1, 2026 15:32
bold() and italic() now check if the style is already applied before
prepending. Previously, calling bold() twice would produce
"bold bold 10px Arial". Same for italic().

Also fixed typo: "aditional" → "additional"

Added 6 tests covering: single bold, double bold, single italic,
double italic, combined, settings + method double-call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UVs are now always computed regardless of renderer type (Canvas/WebGL).
The overhead is negligible (a few float divisions per region) and
removes a game.renderer dependency from atlas.js.

Also converted string concatenation to template literal for UV cache key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 09:44
Removes the last game.renderer.settings.verbose check in atlas.js.
Atlas now has only one game.renderer reference remaining (cache.set
in constructor).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

packages/melonjs/src/video/texture/atlas.js:402

  • TextureAtlas.addUVs now always adds an extra key into the atlas dictionary (atlas[key] = atlas[name]). Because getAnimationSettings() iterates all keys when names is undefined, these internal UV cache keys get treated as frames and inflate/alter the returned atlas/indices. To keep the public atlas entries stable, consider storing UV lookup caching separately from the atlas dictionary or filtering out cache keys when enumerating frames.

		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;
	}

	/**
	 * Create a sprite object using the first region found using the specified name

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

obiot and others added 2 commits April 1, 2026 22:39
- CanvasRenderTarget.destroy(): guard against undefined glTextureUnit
  to prevent throw when WebGL texture was never allocated
- Light2d.destroy(): fallback to game.renderer when parentApp is
  undefined (e.g. destroyed while not in container tree)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…astUpdate

Tween now listens to GAME_AFTER_UPDATE to receive the lastUpdate
timestamp, storing it as an instance property. This removes the
game.lastUpdate dependency for timing, making tweens aware of
pause/resume through the event system rather than polling a global.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 23:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

packages/melonjs/src/renderable/text/text.js:298

  • setText() now always rounds the offscreen canvas to the next power-of-two, even for Canvas and WebGL2 where POT textures aren’t required. This can significantly inflate memory/CPU for large text (e.g. 1025px → 2048px). Suggest restoring the conditional (only POT for WebGL1) using the local renderer you already compute (e.g. renderer.WebGLVersion === 1).
		// 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
		const renderer = this.parentApp?.renderer ?? game.renderer;
		this.canvasTexture.invalidate(renderer);

		// resize the cache canvas if necessary
		if (
			this.canvasTexture.width < width ||
			this.canvasTexture.height < height
		) {
			this.canvasTexture.resize(width, height);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…rings

Use startsWith() to check for bold/italic tokens at the start of the
font string instead of includes(), avoiding false matches on font
family names like "Superbold" or "Italianno".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obiot obiot merged commit ec7e28d into master Apr 2, 2026
6 checks passed
@obiot obiot deleted the refactor/remove-global-renderer branch April 2, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants