diff --git a/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl b/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl new file mode 100644 index 0000000..9e032e7 --- /dev/null +++ b/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl @@ -0,0 +1,45 @@ +precision mediump float; + +varying vec2 vUv; +uniform sampler2D uSource; +uniform float uSharpenBoost; +uniform float uClarityBoost; +uniform float uDenoiseLuma; +uniform float uDenoiseColor; +uniform float uGrainAmount; +uniform float uGrainScale; + +float pseudoNoise(float seed) { + float x = sin(seed * 12.9898) * 43758.5453; + return fract(x); +} + +void main() { + vec4 color = texture2D(uSource, vUv); + vec3 rgb = color.rgb * 255.0; + + float luma = dot(rgb, vec3(0.2126, 0.7152, 0.0722)); + + rgb.r = rgb.r + (rgb.r - luma) * uSharpenBoost * 0.6; + rgb.g = rgb.g + (rgb.g - luma) * uSharpenBoost * 0.6; + rgb.b = rgb.b + (rgb.b - luma) * uSharpenBoost * 0.6; + + float midtoneFactor = 1.0 - abs(luma / 255.0 - 0.5) * 2.0; + float clarityScale = 1.0 + uClarityBoost * midtoneFactor * 0.7; + rgb = (rgb - 128.0) * clarityScale + 128.0; + + if (uDenoiseLuma > 0.0 || uDenoiseColor > 0.0) { + rgb = rgb * (1.0 - uDenoiseLuma * 0.2) + vec3(luma) * uDenoiseLuma * 0.2; + + float average = (rgb.r + rgb.g + rgb.b) / 3.0; + rgb = rgb * (1.0 - uDenoiseColor * 0.2) + vec3(average) * uDenoiseColor * 0.2; + } + + if (uGrainAmount > 0.0) { + float grainSeed = (gl_FragCoord.y * 4096.0 + gl_FragCoord.x) / max(0.5, uGrainScale); + float grain = (pseudoNoise(grainSeed) - 0.5) * uGrainAmount * 40.0; + rgb += vec3(grain); + } + + gl_FragColor = vec4(clamp(rgb / 255.0, 0.0, 1.0), color.a); +} diff --git a/lib/image-pipeline/backend/webgl/shaders/light-adjust.frag.glsl b/lib/image-pipeline/backend/webgl/shaders/light-adjust.frag.glsl new file mode 100644 index 0000000..507492c --- /dev/null +++ b/lib/image-pipeline/backend/webgl/shaders/light-adjust.frag.glsl @@ -0,0 +1,52 @@ +precision mediump float; + +varying vec2 vUv; +uniform sampler2D uSource; +uniform float uExposureFactor; +uniform float uContrastFactor; +uniform float uBrightnessShift; +uniform float uHighlights; +uniform float uShadows; +uniform float uWhites; +uniform float uBlacks; +uniform float uVignetteAmount; +uniform float uVignetteSize; +uniform float uVignetteRoundness; + +float toByte(float value) { + return clamp(floor(value + 0.5), 0.0, 255.0); +} + +void main() { + vec4 color = texture2D(uSource, vUv); + vec3 rgb = color.rgb * 255.0; + + rgb *= uExposureFactor; + rgb = (rgb - 128.0) * uContrastFactor + 128.0 + uBrightnessShift; + + float luma = dot(rgb, vec3(0.2126, 0.7152, 0.0722)); + float highlightsBoost = (luma / 255.0) * uHighlights * 40.0; + float shadowsBoost = ((255.0 - luma) / 255.0) * uShadows * 40.0; + float whitesBoost = (luma / 255.0) * uWhites * 35.0; + float blacksBoost = ((255.0 - luma) / 255.0) * uBlacks * 35.0; + float totalBoost = highlightsBoost + shadowsBoost + whitesBoost + blacksBoost; + rgb = vec3( + toByte(rgb.r + totalBoost), + toByte(rgb.g + totalBoost), + toByte(rgb.b + totalBoost) + ); + + if (uVignetteAmount > 0.0) { + vec2 centeredUv = (vUv - vec2(0.5)) / vec2(0.5); + float radialDistance = length(centeredUv); + float softEdge = pow(1.0 - clamp(radialDistance, 0.0, 1.0), 1.0 + uVignetteRoundness); + float strength = 1.0 - uVignetteAmount * (1.0 - softEdge) * (1.5 - uVignetteSize); + rgb = vec3( + toByte(rgb.r * strength), + toByte(rgb.g * strength), + toByte(rgb.b * strength) + ); + } + + gl_FragColor = vec4(clamp(rgb / 255.0, 0.0, 1.0), color.a); +} diff --git a/lib/image-pipeline/backend/webgl/webgl-backend.ts b/lib/image-pipeline/backend/webgl/webgl-backend.ts index 929503d..368d24d 100644 --- a/lib/image-pipeline/backend/webgl/webgl-backend.ts +++ b/lib/image-pipeline/backend/webgl/webgl-backend.ts @@ -3,9 +3,15 @@ import type { BackendStepRequest, ImagePipelineBackend, } from "@/lib/image-pipeline/backend/backend-types"; +import { + normalizeDetailAdjustData, + normalizeLightAdjustData, +} from "@/lib/image-pipeline/adjustment-types"; import type { PipelineStep } from "@/lib/image-pipeline/contracts"; import colorAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/color-adjust.frag.glsl?raw"; import curvesFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/curves.frag.glsl?raw"; +import detailAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/detail-adjust.frag.glsl?raw"; +import lightAdjustFragmentShaderSource from "@/lib/image-pipeline/backend/webgl/shaders/light-adjust.frag.glsl?raw"; const VERTEX_SHADER_SOURCE = ` attribute vec2 aPosition; @@ -17,18 +23,22 @@ void main() { } `; -type SupportedPreviewStepType = "curves" | "color-adjust"; +type SupportedPreviewStepType = "curves" | "color-adjust" | "light-adjust" | "detail-adjust"; type WebglBackendContext = { gl: WebGLRenderingContext; curvesProgram: WebGLProgram; colorAdjustProgram: WebGLProgram; + lightAdjustProgram: WebGLProgram; + detailAdjustProgram: WebGLProgram; quadBuffer: WebGLBuffer; }; const SUPPORTED_PREVIEW_STEP_TYPES = new Set([ "curves", "color-adjust", + "light-adjust", + "detail-adjust", ]); function assertSupportedStep(step: PipelineStep): void { @@ -156,9 +166,127 @@ function mapColorShift(step: PipelineStep): [number, number, number] { ]; } +function applyStepUniforms( + gl: WebGLRenderingContext, + shaderProgram: WebGLProgram, + request: BackendStepRequest, +): void { + if (request.step.type === "curves") { + const gammaLocation = gl.getUniformLocation(shaderProgram, "uGamma"); + if (gammaLocation) { + gl.uniform1f(gammaLocation, mapCurvesGamma(request.step)); + } + return; + } + + if (request.step.type === "color-adjust") { + const colorShiftLocation = gl.getUniformLocation(shaderProgram, "uColorShift"); + if (colorShiftLocation) { + const [r, g, b] = mapColorShift(request.step); + gl.uniform3f(colorShiftLocation, r, g, b); + } + return; + } + + if (request.step.type === "light-adjust") { + const light = normalizeLightAdjustData(request.step.params); + const exposureFactorLocation = gl.getUniformLocation(shaderProgram, "uExposureFactor"); + if (exposureFactorLocation) { + gl.uniform1f(exposureFactorLocation, Math.pow(2, light.exposure / 2)); + } + + const contrastFactorLocation = gl.getUniformLocation(shaderProgram, "uContrastFactor"); + if (contrastFactorLocation) { + gl.uniform1f(contrastFactorLocation, 1 + light.contrast / 100); + } + + const brightnessShiftLocation = gl.getUniformLocation(shaderProgram, "uBrightnessShift"); + if (brightnessShiftLocation) { + gl.uniform1f(brightnessShiftLocation, light.brightness * 1.8); + } + + const highlightsLocation = gl.getUniformLocation(shaderProgram, "uHighlights"); + if (highlightsLocation) { + gl.uniform1f(highlightsLocation, light.highlights / 100); + } + + const shadowsLocation = gl.getUniformLocation(shaderProgram, "uShadows"); + if (shadowsLocation) { + gl.uniform1f(shadowsLocation, light.shadows / 100); + } + + const whitesLocation = gl.getUniformLocation(shaderProgram, "uWhites"); + if (whitesLocation) { + gl.uniform1f(whitesLocation, light.whites / 100); + } + + const blacksLocation = gl.getUniformLocation(shaderProgram, "uBlacks"); + if (blacksLocation) { + gl.uniform1f(blacksLocation, light.blacks / 100); + } + + const vignetteAmountLocation = gl.getUniformLocation(shaderProgram, "uVignetteAmount"); + if (vignetteAmountLocation) { + gl.uniform1f(vignetteAmountLocation, light.vignette.amount); + } + + const vignetteSizeLocation = gl.getUniformLocation(shaderProgram, "uVignetteSize"); + if (vignetteSizeLocation) { + gl.uniform1f(vignetteSizeLocation, light.vignette.size); + } + + const vignetteRoundnessLocation = gl.getUniformLocation(shaderProgram, "uVignetteRoundness"); + if (vignetteRoundnessLocation) { + gl.uniform1f(vignetteRoundnessLocation, light.vignette.roundness); + } + return; + } + + if (request.step.type === "detail-adjust") { + const detail = normalizeDetailAdjustData(request.step.params); + + const sharpenBoostLocation = gl.getUniformLocation(shaderProgram, "uSharpenBoost"); + if (sharpenBoostLocation) { + gl.uniform1f(sharpenBoostLocation, detail.sharpen.amount / 500); + } + + const clarityBoostLocation = gl.getUniformLocation(shaderProgram, "uClarityBoost"); + if (clarityBoostLocation) { + gl.uniform1f(clarityBoostLocation, detail.clarity / 100); + } + + const denoiseLumaLocation = gl.getUniformLocation(shaderProgram, "uDenoiseLuma"); + if (denoiseLumaLocation) { + gl.uniform1f(denoiseLumaLocation, detail.denoise.luminance / 100); + } + + const denoiseColorLocation = gl.getUniformLocation(shaderProgram, "uDenoiseColor"); + if (denoiseColorLocation) { + gl.uniform1f(denoiseColorLocation, detail.denoise.color / 100); + } + + const grainAmountLocation = gl.getUniformLocation(shaderProgram, "uGrainAmount"); + if (grainAmountLocation) { + gl.uniform1f(grainAmountLocation, detail.grain.amount / 100); + } + + const grainScaleLocation = gl.getUniformLocation(shaderProgram, "uGrainScale"); + if (grainScaleLocation) { + gl.uniform1f(grainScaleLocation, Math.max(0.5, detail.grain.size)); + } + } +} + function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest): void { const { gl } = context; - const shaderProgram = request.step.type === "curves" ? context.curvesProgram : context.colorAdjustProgram; + const shaderProgram = + request.step.type === "curves" + ? context.curvesProgram + : request.step.type === "color-adjust" + ? context.colorAdjustProgram + : request.step.type === "light-adjust" + ? context.lightAdjustProgram + : context.detailAdjustProgram; gl.useProgram(shaderProgram); gl.bindBuffer(gl.ARRAY_BUFFER, context.quadBuffer); @@ -239,18 +367,7 @@ function runStepOnGpu(context: WebglBackendContext, request: BackendStepRequest) gl.uniform1i(sourceLocation, 0); } - if (request.step.type === "curves") { - const gammaLocation = gl.getUniformLocation(shaderProgram, "uGamma"); - if (gammaLocation) { - gl.uniform1f(gammaLocation, mapCurvesGamma(request.step)); - } - } else { - const colorShiftLocation = gl.getUniformLocation(shaderProgram, "uColorShift"); - if (colorShiftLocation) { - const [r, g, b] = mapColorShift(request.step); - gl.uniform3f(colorShiftLocation, r, g, b); - } - } + applyStepUniforms(gl, shaderProgram, request); gl.viewport(0, 0, request.width, request.height); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); @@ -286,6 +403,8 @@ export function createWebglPreviewBackend(): ImagePipelineBackend { gl, curvesProgram: compileProgram(gl, curvesFragmentShaderSource), colorAdjustProgram: compileProgram(gl, colorAdjustFragmentShaderSource), + lightAdjustProgram: compileProgram(gl, lightAdjustFragmentShaderSource), + detailAdjustProgram: compileProgram(gl, detailAdjustFragmentShaderSource), quadBuffer: createQuadBuffer(gl), }; diff --git a/tests/image-pipeline/parity/cpu-webgl-parity.test.ts b/tests/image-pipeline/parity/cpu-webgl-parity.test.ts index c4fb4e7..7cba7e7 100644 --- a/tests/image-pipeline/parity/cpu-webgl-parity.test.ts +++ b/tests/image-pipeline/parity/cpu-webgl-parity.test.ts @@ -62,6 +62,47 @@ describe("cpu vs webgl parity", () => { ); }); + it("keeps light-adjust-only pipeline within parity tolerance", () => { + const pipelines = createParityPipelines(); + installParityWebglContextMock(); + + const metrics = evaluateCpuWebglParity(pipelines.lightAdjustOnly); + + expect(metrics.maxChannelDelta).toBeLessThanOrEqual(parityTolerances.lightAdjustOnly.maxChannelDelta); + expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual( + parityTolerances.lightAdjustOnly.histogramSimilarity, + ); + expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.lightAdjustOnly.spatialRmse); + }); + + it("keeps detail-adjust-only pipeline within parity tolerance", () => { + const pipelines = createParityPipelines(); + installParityWebglContextMock(); + + const metrics = evaluateCpuWebglParity(pipelines.detailAdjustOnly); + + expect(metrics.maxChannelDelta).toBeLessThanOrEqual(parityTolerances.detailAdjustOnly.maxChannelDelta); + expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual( + parityTolerances.detailAdjustOnly.histogramSimilarity, + ); + expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.detailAdjustOnly.spatialRmse); + }); + + it("keeps curves + color-adjust + light-adjust + detail-adjust chain within parity tolerance", () => { + const pipelines = createParityPipelines(); + installParityWebglContextMock(); + + const metrics = evaluateCpuWebglParity(pipelines.curvesColorLightDetailChain); + + expect(metrics.maxChannelDelta).toBeLessThanOrEqual( + parityTolerances.curvesColorLightDetailChain.maxChannelDelta, + ); + expect(metrics.histogramSimilarity).toBeGreaterThanOrEqual( + parityTolerances.curvesColorLightDetailChain.histogramSimilarity, + ); + expect(metrics.spatialRmse).toBeLessThanOrEqual(parityTolerances.curvesColorLightDetailChain.spatialRmse); + }); + it("keeps channel-specific curves pressure case within parity tolerance", () => { const pipelines = createParityPipelines(); installParityWebglContextMock(); diff --git a/tests/image-pipeline/parity/fixtures.ts b/tests/image-pipeline/parity/fixtures.ts index be1c4db..6d17c03 100644 --- a/tests/image-pipeline/parity/fixtures.ts +++ b/tests/image-pipeline/parity/fixtures.ts @@ -7,6 +7,9 @@ type ParityPipelineKey = | "curvesOnly" | "colorAdjustOnly" | "curvesPlusColorAdjust" + | "lightAdjustOnly" + | "detailAdjustOnly" + | "curvesColorLightDetailChain" | "curvesChannelPressure" | "colorAdjustPressure" | "curvesColorPressureChain"; @@ -61,6 +64,21 @@ export const parityTolerances: Record = { histogramSimilarity: 0.16, spatialRmse: 43.5, }, + lightAdjustOnly: { + maxChannelDelta: 52, + histogramSimilarity: 0.5, + spatialRmse: 20.0, + }, + detailAdjustOnly: { + maxChannelDelta: 2, + histogramSimilarity: 0.99, + spatialRmse: 1.2, + }, + curvesColorLightDetailChain: { + maxChannelDelta: 130, + histogramSimilarity: 0.17, + spatialRmse: 57.5, + }, curvesChannelPressure: { maxChannelDelta: 99, histogramSimilarity: 0.08, @@ -223,9 +241,59 @@ function createCurvesColorPressureChainSteps(): PipelineStep[] { ]; } +function createLightAdjustStep(): PipelineStep { + return { + nodeId: "light-adjust-parity", + type: "light-adjust", + params: { + brightness: 14, + contrast: 22, + exposure: 0.8, + highlights: -16, + shadows: 24, + whites: 8, + blacks: -10, + vignette: { + amount: 0.25, + size: 0.72, + roundness: 0.85, + }, + }, + }; +} + +function createDetailAdjustStep(): PipelineStep { + return { + nodeId: "detail-adjust-parity", + type: "detail-adjust", + params: { + sharpen: { + amount: 210, + radius: 1.4, + threshold: 6, + }, + clarity: 32, + denoise: { + luminance: 18, + color: 14, + }, + grain: { + amount: 12, + size: 1.3, + }, + }, + }; +} + +function createCurvesColorLightDetailChainSteps(): PipelineStep[] { + return [createCurvesStep(), createColorAdjustStep(), createLightAdjustStep(), createDetailAdjustStep()]; +} + export function createParityPipelines(): Record { const curvesStep = createCurvesStep(); const colorAdjustStep = createColorAdjustStep(); + const lightAdjustStep = createLightAdjustStep(); + const detailAdjustStep = createDetailAdjustStep(); const curvesChannelPressureStep = createCurvesChannelPressureStep(); const colorAdjustPressureStep = createColorAdjustPressureStep(); @@ -242,6 +310,18 @@ export function createParityPipelines(): Record, +): Uint8Array { + const output = new Uint8Array(input.length); + const centerX = width / 2; + const centerY = height / 2; + + const exposureFactor = Number(uniforms.get("uExposureFactor") ?? 1); + const contrastFactor = Number(uniforms.get("uContrastFactor") ?? 1); + const brightnessShift = Number(uniforms.get("uBrightnessShift") ?? 0); + const highlights = Number(uniforms.get("uHighlights") ?? 0); + const shadows = Number(uniforms.get("uShadows") ?? 0); + const whites = Number(uniforms.get("uWhites") ?? 0); + const blacks = Number(uniforms.get("uBlacks") ?? 0); + const vignetteAmount = Number(uniforms.get("uVignetteAmount") ?? 0); + const vignetteSize = Number(uniforms.get("uVignetteSize") ?? 0.5); + const vignetteRoundness = Number(uniforms.get("uVignetteRoundness") ?? 1); + + for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const index = (y * width + x) * 4; + + let red = input[index] ?? 0; + let green = input[index + 1] ?? 0; + let blue = input[index + 2] ?? 0; + + red *= exposureFactor; + green *= exposureFactor; + blue *= exposureFactor; + + red = (red - 128) * contrastFactor + 128 + brightnessShift; + green = (green - 128) * contrastFactor + 128 + brightnessShift; + blue = (blue - 128) * contrastFactor + 128 + brightnessShift; + + const luma = red * 0.2126 + green * 0.7152 + blue * 0.0722; + const highlightsBoost = (luma / 255) * highlights * 40; + const shadowsBoost = ((255 - luma) / 255) * shadows * 40; + const whitesBoost = (luma / 255) * whites * 35; + const blacksBoost = ((255 - luma) / 255) * blacks * 35; + const totalBoost = highlightsBoost + shadowsBoost + whitesBoost + blacksBoost; + + red += totalBoost; + green += totalBoost; + blue += totalBoost; + + if (vignetteAmount > 0) { + const dx = (x - centerX) / Math.max(1, centerX); + const dy = (y - centerY) / Math.max(1, centerY); + const radialDistance = Math.sqrt(dx * dx + dy * dy); + const softEdge = Math.pow(1 - Math.max(0, Math.min(1, radialDistance)), 1 + vignetteRoundness); + const strength = 1 - vignetteAmount * (1 - softEdge) * (1.5 - vignetteSize); + + red *= strength; + green *= strength; + blue *= strength; + } + + output[index] = toByte(red / 255); + output[index + 1] = toByte(green / 255); + output[index + 2] = toByte(blue / 255); + output[index + 3] = input[index + 3] ?? 255; + } + } + + return output; +} + +function runDetailAdjustShader( + input: Uint8Array, + uniforms: Map, +): Uint8Array { + const output = new Uint8Array(input.length); + + const sharpenBoost = Number(uniforms.get("uSharpenBoost") ?? 0); + const clarityBoost = Number(uniforms.get("uClarityBoost") ?? 0); + const denoiseLuma = Number(uniforms.get("uDenoiseLuma") ?? 0); + 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)); + + for (let index = 0; index < input.length; index += 4) { + let red = input[index] ?? 0; + let green = input[index + 1] ?? 0; + let blue = input[index + 2] ?? 0; + + const luma = red * 0.2126 + green * 0.7152 + blue * 0.0722; + + red = red + (red - luma) * sharpenBoost * 0.6; + green = green + (green - luma) * sharpenBoost * 0.6; + blue = blue + (blue - luma) * sharpenBoost * 0.6; + + const midtoneFactor = 1 - Math.abs(luma / 255 - 0.5) * 2; + const clarityScale = 1 + clarityBoost * midtoneFactor * 0.7; + + red = (red - 128) * clarityScale + 128; + green = (green - 128) * clarityScale + 128; + blue = (blue - 128) * clarityScale + 128; + + if (denoiseLuma > 0 || denoiseColor > 0) { + red = red * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2; + green = green * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2; + blue = blue * (1 - denoiseLuma * 0.2) + luma * denoiseLuma * 0.2; + + const average = (red + green + blue) / 3; + red = red * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2; + green = green * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2; + blue = blue * (1 - denoiseColor * 0.2) + average * denoiseColor * 0.2; + } + + if (grainAmount > 0) { + const grain = (pseudoNoise((index + 1) / grainScale) - 0.5) * grainAmount * 40; + red += grain; + green += grain; + blue += grain; + } + + output[index] = toByte(red / 255); + output[index + 1] = toByte(green / 255); + output[index + 2] = toByte(blue / 255); + output[index + 3] = input[index + 3] ?? 255; + } + + return output; +} + function createParityWebglContext(): WebGLRenderingContext { const glConstants = { VERTEX_SHADER: 0x8b31, @@ -392,6 +623,9 @@ function createParityWebglContext(): WebGLRenderingContext { let currentProgram: FakeProgram | null = null; let currentTexture: FakeTexture | null = null; let currentFramebuffer: FakeFramebuffer | null = null; + let sourceTexture: FakeTexture | null = null; + let drawWidth = 1; + let drawHeight = 1; const gl = { ...glConstants, @@ -453,6 +687,9 @@ function createParityWebglContext(): WebGLRenderingContext { }, bindTexture(_target: number, texture: FakeTexture | null) { currentTexture = texture; + if (texture) { + sourceTexture = texture; + } }, texParameteri() {}, texImage2D( @@ -464,7 +701,7 @@ function createParityWebglContext(): WebGLRenderingContext { _border: number, _format: number, _type: number, - pixels: ArrayBufferView | null, + pixels: ArrayBufferView | FakeTextureImageSource | null, ) { if (!currentTexture) { return; @@ -473,7 +710,13 @@ function createParityWebglContext(): WebGLRenderingContext { if (pixels) { currentTexture.width = width; currentTexture.height = height; - currentTexture.data = new Uint8Array(pixels.buffer.slice(0)); + if ("buffer" in pixels) { + const byteOffset = pixels.byteOffset ?? 0; + const byteLength = pixels.byteLength ?? pixels.buffer.byteLength; + currentTexture.data = new Uint8Array(pixels.buffer.slice(byteOffset, byteOffset + byteLength)); + } else { + currentTexture.data = new Uint8Array(pixels.data); + } return; } @@ -520,15 +763,18 @@ function createParityWebglContext(): WebGLRenderingContext { return glConstants.FRAMEBUFFER_COMPLETE; }, deleteFramebuffer() {}, - viewport() {}, + viewport(_x: number, _y: number, width: number, height: number) { + drawWidth = width; + drawHeight = height; + }, drawArrays() { - if (!currentProgram || !currentTexture || !currentFramebuffer?.attachment) { + if (!currentProgram || !sourceTexture || !currentFramebuffer?.attachment) { throw new Error("Parity WebGL mock is missing required render state."); } if (currentProgram.kind === "curves") { const gamma = Number(currentProgram.uniforms.get("uGamma") ?? 1); - currentFramebuffer.attachment.data = runCurvesShader(currentTexture.data, gamma); + currentFramebuffer.attachment.data = runCurvesShader(sourceTexture.data, gamma); return; } @@ -537,7 +783,22 @@ function createParityWebglContext(): WebGLRenderingContext { const shift: [number, number, number] = Array.isArray(colorShift) ? [colorShift[0] ?? 0, colorShift[1] ?? 0, colorShift[2] ?? 0] : [0, 0, 0]; - currentFramebuffer.attachment.data = runColorAdjustShader(currentTexture.data, shift); + currentFramebuffer.attachment.data = runColorAdjustShader(sourceTexture.data, shift); + return; + } + + if (currentProgram.kind === "light-adjust") { + currentFramebuffer.attachment.data = runLightAdjustShader( + sourceTexture.data, + drawWidth, + drawHeight, + currentProgram.uniforms, + ); + return; + } + + if (currentProgram.kind === "detail-adjust") { + currentFramebuffer.attachment.data = runDetailAdjustShader(sourceTexture.data, currentProgram.uniforms); return; } diff --git a/tests/image-pipeline/webgl-backend-poc.test.ts b/tests/image-pipeline/webgl-backend-poc.test.ts index d5ffaaa..3a2b8b7 100644 --- a/tests/image-pipeline/webgl-backend-poc.test.ts +++ b/tests/image-pipeline/webgl-backend-poc.test.ts @@ -58,7 +58,7 @@ function createColorAdjustStep(): PipelineStep { function createUnsupportedStep(): PipelineStep { return { nodeId: "light-1", - type: "light-adjust", + type: "unsupported-adjust" as PipelineStep["type"], params: { exposure: 0, },