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
34 changes: 34 additions & 0 deletions src/core/renderers/canvas/CanvasRenderer.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
21 changes: 16 additions & 5 deletions src/core/renderers/canvas/CanvasRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,29 @@ 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;
}

this.context.globalAlpha = tintColor.a ?? node.worldAlpha;

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;
Expand Down
31 changes: 31 additions & 0 deletions src/core/renderers/canvas/CanvasTexture.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
6 changes: 5 additions & 1 deletion src/core/renderers/canvas/CanvasTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,12 @@ export class CanvasTexture extends CoreContextTexture {
}

private async onLoadRequest(
data: NonNullable<NonNullable<Texture['textureData']>['data']>,
data: NonNullable<Texture['textureData']>['data'],
): Promise<Dimensions> {
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.
Expand Down
Loading