diff --git a/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl b/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl index 9e032e7..b17e6dc 100644 --- a/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl +++ b/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl @@ -8,6 +8,7 @@ uniform float uDenoiseLuma; uniform float uDenoiseColor; uniform float uGrainAmount; uniform float uGrainScale; +uniform float uImageWidth; float pseudoNoise(float seed) { float x = sin(seed * 12.9898) * 43758.5453; @@ -36,7 +37,10 @@ void main() { } if (uGrainAmount > 0.0) { - float grainSeed = (gl_FragCoord.y * 4096.0 + gl_FragCoord.x) / max(0.5, uGrainScale); + float pixelX = floor(gl_FragCoord.x); + float pixelY = floor(gl_FragCoord.y); + float pixelIndex = ((pixelY * max(1.0, uImageWidth)) + pixelX) * 4.0; + float grainSeed = (pixelIndex + 1.0) / max(0.5, uGrainScale); float grain = (pseudoNoise(grainSeed) - 0.5) * uGrainAmount * 40.0; rgb += vec3(grain); } diff --git a/lib/image-pipeline/backend/webgl/webgl-backend.ts b/lib/image-pipeline/backend/webgl/webgl-backend.ts index 368d24d..02765b5 100644 --- a/lib/image-pipeline/backend/webgl/webgl-backend.ts +++ b/lib/image-pipeline/backend/webgl/webgl-backend.ts @@ -274,6 +274,11 @@ function applyStepUniforms( if (grainScaleLocation) { gl.uniform1f(grainScaleLocation, Math.max(0.5, detail.grain.size)); } + + const imageWidthLocation = gl.getUniformLocation(shaderProgram, "uImageWidth"); + if (imageWidthLocation) { + gl.uniform1f(imageWidthLocation, request.width); + } } } diff --git a/tests/image-pipeline/parity/fixtures.ts b/tests/image-pipeline/parity/fixtures.ts index 6d17c03..e305a16 100644 --- a/tests/image-pipeline/parity/fixtures.ts +++ b/tests/image-pipeline/parity/fixtures.ts @@ -539,6 +539,7 @@ function runLightAdjustShader( function runDetailAdjustShader( input: Uint8Array, + width: number, uniforms: Map, ): Uint8Array { const output = new Uint8Array(input.length); @@ -549,6 +550,7 @@ function runDetailAdjustShader( const denoiseColor = Number(uniforms.get("uDenoiseColor") ?? 0); const grainAmount = Number(uniforms.get("uGrainAmount") ?? 0); const grainScale = Math.max(0.5, Number(uniforms.get("uGrainScale") ?? 1)); + const imageWidth = Math.max(1, Number(uniforms.get("uImageWidth") ?? width)); for (let index = 0; index < input.length; index += 4) { let red = input[index] ?? 0; @@ -580,7 +582,11 @@ function runDetailAdjustShader( } if (grainAmount > 0) { - const grain = (pseudoNoise((index + 1) / grainScale) - 0.5) * grainAmount * 40; + const pixel = index / 4; + const x = pixel % imageWidth; + const y = Math.floor(pixel / imageWidth); + const pixelIndex = (y * imageWidth + x) * 4; + const grain = (pseudoNoise((pixelIndex + 1) / grainScale) - 0.5) * grainAmount * 40; red += grain; green += grain; blue += grain; @@ -798,7 +804,11 @@ function createParityWebglContext(): WebGLRenderingContext { } if (currentProgram.kind === "detail-adjust") { - currentFramebuffer.attachment.data = runDetailAdjustShader(sourceTexture.data, currentProgram.uniforms); + currentFramebuffer.attachment.data = runDetailAdjustShader( + sourceTexture.data, + drawWidth, + currentProgram.uniforms, + ); return; } diff --git a/tests/image-pipeline/webgl-backend-poc.test.ts b/tests/image-pipeline/webgl-backend-poc.test.ts index 3a2b8b7..c982f6f 100644 --- a/tests/image-pipeline/webgl-backend-poc.test.ts +++ b/tests/image-pipeline/webgl-backend-poc.test.ts @@ -65,6 +65,19 @@ function createUnsupportedStep(): PipelineStep { }; } +function createDetailAdjustStep(): PipelineStep { + return { + nodeId: "detail-1", + type: "detail-adjust", + params: { + sharpen: { amount: 20, radius: 1.4, detail: 10, masking: 0 }, + clarity: 15, + denoise: { luminance: 12, color: 18, detail: 50 }, + grain: { amount: 35, size: 2.5, roughness: 50 }, + }, + }; +} + describe("webgl backend poc", () => { beforeEach(() => { vi.resetModules(); @@ -425,6 +438,29 @@ describe("webgl backend poc", () => { expect(getContextSpy).toHaveBeenCalledWith("webgl2", expect.any(Object)); }); + it("passes image width uniform for detail-adjust grain parity", async () => { + const fakeGl = createFakeWebglContext({ + readbackPixels: new Uint8Array([11, 22, 33, 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(7 * 3 * 4), + step: createDetailAdjustStep(), + width: 7, + height: 3, + }); + + expect(fakeGl.uniform1f).toHaveBeenCalledWith(expect.anything(), 7); + }); + 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");