From 65cc1d602341ec96be9c37f65fb9a889ba674e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 8 Sep 2026 14:12:28 -0400 Subject: [PATCH] fix(shader): keep vertex shaders within their WebGL context (#3672) --- packages/shader-transitions/src/webgl.test.ts | 44 +++++++++++++++++++ packages/shader-transitions/src/webgl.ts | 7 +-- 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 packages/shader-transitions/src/webgl.test.ts diff --git a/packages/shader-transitions/src/webgl.test.ts b/packages/shader-transitions/src/webgl.test.ts new file mode 100644 index 0000000000..6b44fb99e8 --- /dev/null +++ b/packages/shader-transitions/src/webgl.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; +import { createProgram } from "./webgl.js"; + +/** Model WebGL's rule that shader handles belong to the context that created them. */ +function createMockContext() { + const shaders = new Set(); + const gl = { + VERTEX_SHADER: 0x8b31, + FRAGMENT_SHADER: 0x8b30, + COMPILE_STATUS: 0x8b81, + LINK_STATUS: 0x8b82, + createShader: vi.fn(() => { + const shader = {}; + shaders.add(shader); + return shader; + }), + shaderSource: vi.fn(), + compileShader: vi.fn(), + getShaderParameter: vi.fn(() => true), + createProgram: vi.fn(() => ({})), + attachShader: vi.fn((_program: WebGLProgram, shader: WebGLShader) => { + if (!shaders.has(shader)) throw new Error("Shader belongs to another WebGL context"); + }), + linkProgram: vi.fn(), + getProgramParameter: vi.fn(() => true), + }; + return gl; +} + +describe("createProgram", () => { + it("uses context-owned shaders across multiple contexts and repeated calls", () => { + const first = createMockContext(); + const second = createMockContext(); + const fragment = "precision mediump float;void main(){gl_FragColor=vec4(1.0);}"; + + for (const gl of [first, second, first, second]) { + // The test double implements only the WebGL methods used to create a program. + expect(() => createProgram(gl as unknown as WebGLRenderingContext, fragment)).not.toThrow(); + } + + expect(first.linkProgram).toHaveBeenCalledTimes(2); + expect(second.linkProgram).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/shader-transitions/src/webgl.ts b/packages/shader-transitions/src/webgl.ts index 1a2c3584e6..ed0bf931ad 100644 --- a/packages/shader-transitions/src/webgl.ts +++ b/packages/shader-transitions/src/webgl.ts @@ -23,8 +23,6 @@ export function setupQuad(gl: WebGLRenderingContext): WebGLBuffer { return buf; } -let cachedVertexShader: WebGLShader | null = null; - function compileShader(gl: WebGLRenderingContext, src: string, type: number): WebGLShader { const s = gl.createShader(type); if (!s) throw new Error("[HyperShader] Failed to create shader"); @@ -53,10 +51,7 @@ function linkProgram( } export function createProgram(gl: WebGLRenderingContext, fragSrc: string): WebGLProgram { - if (!cachedVertexShader) { - cachedVertexShader = compileShader(gl, vertSrc, gl.VERTEX_SHADER); - } - return linkProgram(gl, cachedVertexShader, fragSrc); + return createProgramWithVertex(gl, vertSrc, fragSrc); } export function createProgramWithVertex(