From 195a812ba216ba31718d2eec0ab3cca9b52f6433 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Apr 2026 22:09:14 +0200 Subject: [PATCH] fix(image-pipeline): correct webgl source binding and context init --- .../backend/webgl/webgl-backend.ts | 11 ++- .../image-pipeline/webgl-backend-poc.test.ts | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/lib/image-pipeline/backend/webgl/webgl-backend.ts b/lib/image-pipeline/backend/webgl/webgl-backend.ts index 23d4683..929503d 100644 --- a/lib/image-pipeline/backend/webgl/webgl-backend.ts +++ b/lib/image-pipeline/backend/webgl/webgl-backend.ts @@ -42,12 +42,14 @@ function assertSupportedStep(step: PipelineStep): void { function createGlContext(): WebGLRenderingContext { if (typeof document !== "undefined") { const canvas = document.createElement("canvas"); - const context = canvas.getContext("webgl", { + const contextOptions: WebGLContextAttributes = { alpha: true, antialias: false, premultipliedAlpha: false, preserveDrawingBuffer: true, - }); + }; + const context = + canvas.getContext("webgl2", contextOptions) ?? canvas.getContext("webgl", contextOptions); if (context) { return context; } @@ -55,7 +57,7 @@ function createGlContext(): WebGLRenderingContext { if (typeof OffscreenCanvas !== "undefined") { const canvas = new OffscreenCanvas(1, 1); - const context = canvas.getContext("webgl"); + const context = canvas.getContext("webgl2") ?? canvas.getContext("webgl"); if (context) { return context; } @@ -229,6 +231,9 @@ function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest) throw new Error("WebGL framebuffer is incomplete."); } + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, sourceTexture); + const sourceLocation = gl.getUniformLocation(shaderProgram, "uSource"); if (sourceLocation) { gl.uniform1i(sourceLocation, 0); diff --git a/tests/image-pipeline/webgl-backend-poc.test.ts b/tests/image-pipeline/webgl-backend-poc.test.ts index 0eef116..77b9689 100644 --- a/tests/image-pipeline/webgl-backend-poc.test.ts +++ b/tests/image-pipeline/webgl-backend-poc.test.ts @@ -339,6 +339,84 @@ describe("webgl backend poc", () => { expect(fakeGl.readPixels).toHaveBeenCalledTimes(1); }); + it("keeps source texture bound on sampler unit when drawing", async () => { + const fakeGl = createFakeWebglContext({ + readbackPixels: new Uint8Array([1, 2, 3, 255]), + }); + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation((contextId) => { + if (contextId === "webgl") { + return fakeGl; + } + return null; + }); + + const { createWebglPreviewBackend } = await import("@/lib/image-pipeline/backend/webgl/webgl-backend"); + const backend = createWebglPreviewBackend(); + backend.runPreviewStep({ + pixels: new Uint8ClampedArray([9, 9, 9, 255]), + step: createCurvesStep(), + width: 1, + height: 1, + }); + + const sourceTexture = (fakeGl.createTexture as any).mock.results[0]?.value; + const outputTexture = (fakeGl.createTexture as any).mock.results[1]?.value; + expect(sourceTexture).toBeTruthy(); + expect(outputTexture).toBeTruthy(); + + expect(fakeGl.framebufferTexture2D).toHaveBeenCalledWith( + fakeGl.FRAMEBUFFER, + fakeGl.COLOR_ATTACHMENT0, + fakeGl.TEXTURE_2D, + outputTexture, + 0, + ); + + const bindTextureCalls = (fakeGl.bindTexture as any).mock.calls as Array<[number, unknown]>; + const bindTextureOrder = (fakeGl.bindTexture as any).mock.invocationCallOrder as number[]; + const drawOrder = (fakeGl.drawArrays as any).mock.invocationCallOrder[0] as number; + const lastBindBeforeDrawIndex = bindTextureOrder + .map((callOrder, index) => ({ callOrder, index })) + .filter(({ callOrder, index }) => callOrder < drawOrder && bindTextureCalls[index]?.[0] === fakeGl.TEXTURE_2D) + .at(-1)?.index; + + expect(lastBindBeforeDrawIndex).toBeTypeOf("number"); + expect(bindTextureCalls[lastBindBeforeDrawIndex as number]?.[1]).toBe(sourceTexture); + expect(bindTextureCalls[lastBindBeforeDrawIndex as number]?.[1]).not.toBe(outputTexture); + }); + + it("initializes backend with webgl2-only context availability", async () => { + const fakeGl = createFakeWebglContext({ + readbackPixels: new Uint8Array([11, 22, 33, 255]), + }); + const getContextSpy = vi + .spyOn(HTMLCanvasElement.prototype, "getContext") + .mockImplementation((contextId) => { + if (contextId === "webgl2") { + return fakeGl; + } + if (contextId === "webgl") { + return null; + } + return null; + }); + + const { createWebglPreviewBackend } = await import("@/lib/image-pipeline/backend/webgl/webgl-backend"); + const backend = createWebglPreviewBackend(); + const pixels = new Uint8ClampedArray([200, 100, 50, 255]); + + backend.runPreviewStep({ + pixels, + step: createCurvesStep(), + width: 1, + height: 1, + }); + + expect(Array.from(pixels)).toEqual([11, 22, 33, 255]); + expect(fakeGl.drawArrays).toHaveBeenCalledTimes(1); + expect(getContextSpy).toHaveBeenCalledWith("webgl2", expect.any(Object)); + }); + it("downgrades compile/link failures to cpu with runtime_error reason", async () => { const { createBackendRouter } = await import("@/lib/image-pipeline/backend/backend-router"); const { createWebglPreviewBackend } = await import("@/lib/image-pipeline/backend/webgl/webgl-backend");