Remove all internal global renderer imports#1344
Conversation
…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>
There was a problem hiding this comment.
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()fromvideo.jsand stopped calling it fromApplication.init(). - Updated several renderables/texture utilities to use
game.rendererinstead of importingrendererfromvideo.js. - Updated
ImageLayerpattern creation to usethis.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( |
There was a problem hiding this comment.
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.
| this._pattern = this.parentApp.renderer.createPattern( | |
| const renderer = this.parentApp ? this.parentApp.renderer : game.renderer; | |
| this._pattern = renderer.createPattern( |
| /** | ||
| * A reference to the active Canvas or WebGL active renderer renderer | ||
| * @memberof video | ||
| * @type {CanvasRenderer|WebGLRenderer} | ||
| */ | ||
| export let renderer = null; |
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
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.
| // set the public renderer reference | ||
| renderer = game.renderer; | ||
|
|
There was a problem hiding this comment.
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).
| boot(); | ||
| video.init(800, 600, { | ||
| parent: "screen", | ||
| scale: "auto", | ||
| renderer: video.AUTO, | ||
| }); |
There was a problem hiding this comment.
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).
| 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, | |
| }); | |
| } |
- 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>
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
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>
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>
There was a problem hiding this comment.
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.
| if (renderer && typeof renderer.gl !== "undefined") { | ||
| renderer.setBatcher("quad"); | ||
| renderer.currentBatcher.deleteTexture2D( | ||
| renderer.currentBatcher.getTexture2D(this.glTextureUnit), | ||
| ); |
There was a problem hiding this comment.
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.
| 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); | |
| } |
| 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); |
There was a problem hiding this comment.
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.
| 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); |
| 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"; |
There was a problem hiding this comment.
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.
| this.texture.destroy(renderer); | ||
| this.texture = undefined; |
There was a problem hiding this comment.
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.
| 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; | |
| } |
| destroy() { | ||
| colorPool.release(this.color); | ||
| this.color = undefined; | ||
| pool.push(this.texture); | ||
| this.texture.destroy(); | ||
| const renderer = this.parentApp?.renderer; | ||
| this.texture.destroy(renderer); |
There was a problem hiding this comment.
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.
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>
06f024b to
f0dd7ff
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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.
| 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"; |
There was a problem hiding this comment.
pool is now imported but never used in this module. Please remove the unused import to avoid dead code and keep linting/treeshaking clean.
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>
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>
There was a problem hiding this comment.
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.
- 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>
There was a problem hiding this comment.
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 localrendereryou 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>
Summary
rendererfromvideo.jsthis.parentApp.renderer(available in container tree)game.renderersetRenderer()function — no longer neededvideo.rendererremains as a public API export for external consumers, set once invideo.init()Zero internal files now import the global
rendererfromvideo.js.Test plan
🤖 Generated with Claude Code