From f3e17b7c8f3be5ef627a38a03eaac5ee1f6b81a2 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Fri, 5 Jun 2026 09:59:56 +0200 Subject: [PATCH 1/3] fix(canvas): handle null and undefined images during rendering to prevent crashes --- src/core/renderers/canvas/CanvasRenderer.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/core/renderers/canvas/CanvasRenderer.ts b/src/core/renderers/canvas/CanvasRenderer.ts index 0a9cd74..b0ac326 100644 --- a/src/core/renderers/canvas/CanvasRenderer.ts +++ b/src/core/renderers/canvas/CanvasRenderer.ts @@ -146,9 +146,20 @@ export class CanvasRenderer extends CoreRenderer { image = (texture.ctxTexture as CanvasTexture).getImage(tintColor); } - // getImage returns null when the underlying image was freed mid-load; - // skip drawing this frame rather than crashing. - if (image === null) { + // The texture can disappear while an async load/fetch is racing (e.g. 404); + // skip drawing this frame instead of dereferencing an invalid image object. + if (image === null || image === undefined) { + return; + } + + const imageWidth = image.width; + const imageHeight = image.height; + if ( + typeof imageWidth !== 'number' || + typeof imageHeight !== 'number' || + imageWidth <= 0 || + imageHeight <= 0 + ) { return; } @@ -156,8 +167,8 @@ export class CanvasRenderer extends CoreRenderer { const txCoords = node.textureCoords; if (txCoords) { - const ix = image.width; - const iy = image.height; + const ix = imageWidth; + const iy = imageHeight; let sx = txCoords.x1 * ix; let sy = txCoords.y1 * iy; From fe7d3444fd1bf425fdc5551e750a1dfd89a29db3 Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Fri, 5 Jun 2026 10:00:40 +0200 Subject: [PATCH 2/3] test(canvas): add test for skipping drawing with undefined canvas texture image --- .../renderers/canvas/CanvasRenderer.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/core/renderers/canvas/CanvasRenderer.test.ts diff --git a/src/core/renderers/canvas/CanvasRenderer.test.ts b/src/core/renderers/canvas/CanvasRenderer.test.ts new file mode 100644 index 0000000..95c69e1 --- /dev/null +++ b/src/core/renderers/canvas/CanvasRenderer.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest'; +import { CanvasRenderer } from './CanvasRenderer.js'; +import { TextureType } from '../../textures/Texture.js'; + +describe('CanvasRenderer.renderContext', () => { + it('skips drawing when canvas texture image is undefined', () => { + const drawImage = vi.fn(); + + const renderer = Object.create(CanvasRenderer.prototype) as CanvasRenderer; + (renderer as any).context = { + drawImage, + globalAlpha: 1, + }; + + const node = { + premultipliedColorTl: 0xffffffff, + globalTransform: { tx: 10, ty: 20 }, + props: { w: 100, h: 50 }, + worldAlpha: 1, + textureCoords: { x1: 0, y1: 0, x2: 1, y2: 1 }, + } as any; + + const texture = { + type: TextureType.image, + ctxTexture: { + getImage: () => undefined, + }, + } as any; + + expect(() => renderer.renderContext(node, texture)).not.toThrow(); + expect(drawImage).not.toHaveBeenCalled(); + expect((renderer as any).context.globalAlpha).toBe(1); + }); +}); From eb2ac8dda7c8ad24bd4bdfc5b0c35312a2a0f1cd Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Fri, 5 Jun 2026 10:31:42 +0200 Subject: [PATCH 3/3] fix(canvas): throw error for null texture data --- .../renderers/canvas/CanvasTexture.test.ts | 31 +++++++++++++++++++ src/core/renderers/canvas/CanvasTexture.ts | 6 +++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/core/renderers/canvas/CanvasTexture.test.ts diff --git a/src/core/renderers/canvas/CanvasTexture.test.ts b/src/core/renderers/canvas/CanvasTexture.test.ts new file mode 100644 index 0000000..5e869a3 --- /dev/null +++ b/src/core/renderers/canvas/CanvasTexture.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { CanvasTexture } from './CanvasTexture.js'; + +describe('CanvasTexture.load', () => { + it('fails gracefully when textureData.data is null', async () => { + const textureSource = { + textureData: { data: null }, + state: 'initial', + dimensions: null, + setState(nextState: string) { + this.state = nextState; + }, + freeTextureData() { + // no-op + }, + } as any; + + const memManager = { + setTextureMemUse() { + // no-op + }, + } as any; + + const ctxTexture = new CanvasTexture(memManager, textureSource); + + await expect(ctxTexture.load()).rejects.toThrow( + 'CanvasTexture: Texture data is null', + ); + expect(textureSource.state).toBe('failed'); + }); +}); diff --git a/src/core/renderers/canvas/CanvasTexture.ts b/src/core/renderers/canvas/CanvasTexture.ts index 72a5c0f..a7ff911 100644 --- a/src/core/renderers/canvas/CanvasTexture.ts +++ b/src/core/renderers/canvas/CanvasTexture.ts @@ -131,8 +131,12 @@ export class CanvasTexture extends CoreContextTexture { } private async onLoadRequest( - data: NonNullable['data']>, + data: NonNullable['data'], ): Promise { + if (data === null) { + throw new Error('CanvasTexture: Texture data is null'); + } + // CompressedData objects (KTX, PVR, ASTC) carry GPU-format mipmap buffers // that cannot be decoded by Canvas2D. Reject explicitly rather than falling // through silently and leaving this.image unassigned.