From 8e08ccdabd9772acbc508ab17d5ad7276c3d4718 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Thu, 16 Jul 2026 20:37:39 +0800 Subject: [PATCH] Improve JPEG IDCT parity for Bing real-web References isu issue 280; follow-up isu issue 387. --- .isu/issues.json | 15 +- crates/image/src/jpeg.rs | 296 +++++++++++++++++---------------------- 2 files changed, 140 insertions(+), 171 deletions(-) diff --git a/.isu/issues.json b/.isu/issues.json index cde1347..37bf877 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -1,5 +1,5 @@ { - "next_id": 387, + "next_id": 388, "issues": [ { "id": 1, @@ -4725,6 +4725,19 @@ "author": "piefev", "state": "open", "created_at": "2026-07-16T12:13:28Z" + }, + { + "id": 387, + "repo": "we", + "title": "Bing parity remains above threshold after accurate JPEG IDCT", + "body": "Parent: isu issue 280\n\nThe JPEG IDCT fix reduces one large source of pixel drift in the Bing real-web snapshot, but the scenario still cannot pass within the committed Chromium-golden threshold.\n\nRepro:\n`cargo run -p we-e2e -- --scenario crates/e2e/scenarios/real-web/bing.com.we --out-dir crates/e2e/artifacts`\n\nObserved after the IDCT fix against the committed goldens:\n- desktop: `4.85% match (1168923/1228500 px differ, tol=4, max_diff=0.1000%)`\n- mobile: `5.17% match (312148/329160 px differ, tol=4, max_diff=0.1000%)`\n\nSame-cache Chromium comparison improved but remains far above threshold:\n- desktop: about `72.36%` match versus `crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png`\n- mobile: about `69.70%` match versus `crates/e2e/artifacts/real-web/bing.com/mobile.scenario-cache.chromium.png`\n\nThe committed goldens are also stale relative to the offline Sichuan Tea fixture (see isu issue 305), so there are two remaining blockers: rebaseline drift and genuine same-cache rendering differences.\n\nArtifacts from this investigation:\n- `crates/e2e/artifacts/real-web/bing.com/desktop.png`\n- `crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.chromium.png`\n- `crates/e2e/artifacts/real-web/bing.com/desktop.scenario-cache.after-idct.diff.png`\n- `crates/e2e/artifacts/real-web/bing.com/mobile.png`\n- `crates/e2e/artifacts/real-web/bing.com/mobile.scenario-cache.chromium.png`\n\nAcceptance: after the stale-golden rebaseline, close this when `bing.com.we` passes against same-cache Chromium within the harness threshold and issue 280 can remove the xfail.", + "labels": [ + "real-web" + ], + "assigned": [], + "author": "piefev", + "state": "open", + "created_at": "2026-07-16T12:37:21Z" } ] } diff --git a/crates/image/src/jpeg.rs b/crates/image/src/jpeg.rs index b7beb15..b46820f 100644 --- a/crates/image/src/jpeg.rs +++ b/crates/image/src/jpeg.rs @@ -551,186 +551,125 @@ fn unzigzag(zigzag: &[i32; 64]) -> [i32; 64] { natural } -/// 1D IDCT on 8 values using the Loeffler/Ligtenberg/Moschytz algorithm. -/// Fixed-point with 12 bits of fractional precision. -/// -/// Constants are scaled: C_k = cos(k*pi/16) * 2^12, rounded. -const FIX_0_298: i32 = 2446; // cos(7*pi/16) * 4096 -const FIX_0_390: i32 = 3196; // sqrt(2) * (cos(6*pi/16) - cos(2*pi/16)) * 2048 -- see below -const FIX_0_541: i32 = 4433; // sqrt(2) * cos(6*pi/16) * 4096 -const FIX_0_765: i32 = 6270; // sqrt(2) * cos(2*pi/16) - sqrt(2) * cos(6*pi/16) -const FIX_1_175: i32 = 9633; // sqrt(2) * cos(pi/8) -- used in stage 1 butterfly -const FIX_1_501: i32 = 12299; // sqrt(2) * (cos(pi/16) - cos(7*pi/16)) -const FIX_1_847: i32 = 15137; // sqrt(2) * cos(3*pi/16) -const FIX_1_961: i32 = 16069; // sqrt(2) * (cos(3*pi/16) + cos(5*pi/16)) -- negative -const FIX_2_053: i32 = 16819; // sqrt(2) * (cos(pi/16) + cos(7*pi/16)) -const FIX_2_562: i32 = 20995; // sqrt(2) * (cos(3*pi/16) - cos(5*pi/16)) -- negative -const FIX_3_072: i32 = 25172; // sqrt(2) * (cos(pi/16) + cos(3*pi/16)) - -const CONST_BITS: i32 = 13; -const PASS1_BITS: i32 = 2; +const IDCT_SCALE: [f32; 8] = [ + std::f32::consts::FRAC_1_SQRT_2, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, +]; + +const IDCT_COS: [[f32; 8]; 8] = [ + [ + 1.0, + 0.98078525, + 0.9238795, + 0.8314696, + std::f32::consts::FRAC_1_SQRT_2, + 0.55557024, + 0.38268343, + 0.19509032, + ], + [ + 1.0, + 0.8314696, + 0.38268343, + -0.19509032, + -std::f32::consts::FRAC_1_SQRT_2, + -0.98078525, + -0.9238795, + -0.55557024, + ], + [ + 1.0, + 0.55557024, + -0.38268343, + -0.98078525, + -std::f32::consts::FRAC_1_SQRT_2, + 0.19509032, + 0.9238795, + 0.8314696, + ], + [ + 1.0, + 0.19509032, + -0.9238795, + -0.55557024, + std::f32::consts::FRAC_1_SQRT_2, + 0.8314696, + -0.38268343, + -0.98078525, + ], + [ + 1.0, + -0.19509032, + -0.9238795, + 0.55557024, + std::f32::consts::FRAC_1_SQRT_2, + -0.8314696, + -0.38268343, + 0.98078525, + ], + [ + 1.0, + -0.55557024, + -0.38268343, + 0.98078525, + -std::f32::consts::FRAC_1_SQRT_2, + -0.19509032, + 0.9238795, + -0.8314696, + ], + [ + 1.0, + -0.8314696, + 0.38268343, + 0.19509032, + -std::f32::consts::FRAC_1_SQRT_2, + 0.98078525, + -0.9238795, + 0.55557024, + ], + [ + 1.0, + -0.98078525, + 0.9238795, + -0.8314696, + std::f32::consts::FRAC_1_SQRT_2, + -0.55557024, + 0.38268343, + -0.19509032, + ], +]; /// Perform the 2D IDCT and produce 64 pixel values (clamped to 0..255). /// Input is in natural 8x8 row-major order, dequantized. fn idct_block(coeffs: &[i32; 64]) -> [u8; 64] { - let mut workspace = [0i32; 64]; - - // Pass 1: process columns from input, store into workspace. - for col in 0..8 { - // If all AC terms are zero, short-circuit. - if coeffs[col + 8] == 0 - && coeffs[col + 16] == 0 - && coeffs[col + 24] == 0 - && coeffs[col + 32] == 0 - && coeffs[col + 40] == 0 - && coeffs[col + 48] == 0 - && coeffs[col + 56] == 0 - { - let dcval = coeffs[col] << PASS1_BITS; - for row in 0..8 { - workspace[row * 8 + col] = dcval; + // The reference transform keeps JPEG output within browser-parity tolerances; + // the previous fixed-point shortcut accumulated visible color drift. + let mut tmp = [[0f32; 8]; 8]; + for y in 0..8 { + for u in 0..8 { + let mut sum = 0.0; + for v in 0..8 { + sum += IDCT_SCALE[v] * coeffs[v * 8 + u] as f32 * IDCT_COS[y][v]; } - continue; + tmp[y][u] = sum; } + } - // Even part: use the Loeffler method - let z2 = coeffs[col + 16]; - let z3 = coeffs[col + 48]; - - let z1 = (z2 + z3) * FIX_0_541; - let tmp2 = z1 + z3 * (-FIX_1_847); - let tmp3 = z1 + z2 * FIX_0_765; - - let z2 = coeffs[col]; - let z3 = coeffs[col + 32]; - - let tmp0 = (z2 + z3) << CONST_BITS; - let tmp1 = (z2 - z3) << CONST_BITS; - - let tmp10 = tmp0 + tmp3; - let tmp13 = tmp0 - tmp3; - let tmp11 = tmp1 + tmp2; - let tmp12 = tmp1 - tmp2; - - // Odd part - let tmp0 = coeffs[col + 56]; - let tmp1 = coeffs[col + 40]; - let tmp2 = coeffs[col + 24]; - let tmp3 = coeffs[col + 8]; - - let z1 = tmp0 + tmp3; - let z2 = tmp1 + tmp2; - let z3 = tmp0 + tmp2; - let z4 = tmp1 + tmp3; - let z5 = (z3 + z4) * FIX_1_175; - - let tmp0 = tmp0 * FIX_0_298; - let tmp1 = tmp1 * FIX_2_053; - let tmp2 = tmp2 * FIX_3_072; - let tmp3 = tmp3 * FIX_1_501; - let z1 = z1 * (-FIX_0_390); - let z2 = z2 * (-FIX_2_562); - let z3 = z3 * (-FIX_1_961); - let z4 = z4 * (-FIX_0_298); - - let z3 = z3 + z5; - let z4 = z4 + z5; - - let tmp0 = tmp0 + z1 + z3; - let tmp1 = tmp1 + z2 + z4; - let tmp2 = tmp2 + z2 + z3; - let tmp3 = tmp3 + z1 + z4; - - let shift = CONST_BITS - PASS1_BITS; - workspace[col] = (tmp10 + tmp3 + (1 << (shift - 1))) >> shift; - workspace[col + 56] = (tmp10 - tmp3 + (1 << (shift - 1))) >> shift; - workspace[col + 8] = (tmp11 + tmp2 + (1 << (shift - 1))) >> shift; - workspace[col + 48] = (tmp11 - tmp2 + (1 << (shift - 1))) >> shift; - workspace[col + 16] = (tmp12 + tmp1 + (1 << (shift - 1))) >> shift; - workspace[col + 40] = (tmp12 - tmp1 + (1 << (shift - 1))) >> shift; - workspace[col + 24] = (tmp13 + tmp0 + (1 << (shift - 1))) >> shift; - workspace[col + 32] = (tmp13 - tmp0 + (1 << (shift - 1))) >> shift; - } - - // Pass 2: process rows from workspace, produce output. let mut output = [0u8; 64]; - for row in 0..8 { - let base = row * 8; - - // Short-circuit for all-zero AC - if workspace[base + 1] == 0 - && workspace[base + 2] == 0 - && workspace[base + 3] == 0 - && workspace[base + 4] == 0 - && workspace[base + 5] == 0 - && workspace[base + 6] == 0 - && workspace[base + 7] == 0 - { - let dcval = clamp_to_u8( - ((workspace[base] + (1 << (PASS1_BITS + 2))) >> (PASS1_BITS + 3)) + 128, - ); - for col in 0..8 { - output[base + col] = dcval; + for y in 0..8 { + for x in 0..8 { + let mut sum = 0.0; + for u in 0..8 { + sum += IDCT_SCALE[u] * tmp[y][u] * IDCT_COS[x][u]; } - continue; + output[y * 8 + x] = clamp_to_u8((sum * 0.25).round() as i32 + 128); } - - let z2 = workspace[base + 2]; - let z3 = workspace[base + 6]; - - let z1 = (z2 + z3) * FIX_0_541; - let tmp2 = z1 + z3 * (-FIX_1_847); - let tmp3 = z1 + z2 * FIX_0_765; - - let z2 = workspace[base]; - let z3 = workspace[base + 4]; - - let tmp0 = (z2 + z3) << CONST_BITS; - let tmp1 = (z2 - z3) << CONST_BITS; - - let tmp10 = tmp0 + tmp3; - let tmp13 = tmp0 - tmp3; - let tmp11 = tmp1 + tmp2; - let tmp12 = tmp1 - tmp2; - - let tmp0 = workspace[base + 7]; - let tmp1 = workspace[base + 5]; - let tmp2 = workspace[base + 3]; - let tmp3 = workspace[base + 1]; - - let z1 = tmp0 + tmp3; - let z2 = tmp1 + tmp2; - let z3 = tmp0 + tmp2; - let z4 = tmp1 + tmp3; - let z5 = (z3 + z4) * FIX_1_175; - - let tmp0 = tmp0 * FIX_0_298; - let tmp1 = tmp1 * FIX_2_053; - let tmp2 = tmp2 * FIX_3_072; - let tmp3 = tmp3 * FIX_1_501; - let z1 = z1 * (-FIX_0_390); - let z2 = z2 * (-FIX_2_562); - let z3 = z3 * (-FIX_1_961); - let z4 = z4 * (-FIX_0_298); - - let z3 = z3 + z5; - let z4 = z4 + z5; - - let tmp0 = tmp0 + z1 + z3; - let tmp1 = tmp1 + z2 + z4; - let tmp2 = tmp2 + z2 + z3; - let tmp3 = tmp3 + z1 + z4; - - let shift = CONST_BITS + PASS1_BITS + 3; - let round = 1 << (shift - 1); - output[base] = clamp_to_u8(((tmp10 + tmp3 + round) >> shift) + 128); - output[base + 7] = clamp_to_u8(((tmp10 - tmp3 + round) >> shift) + 128); - output[base + 1] = clamp_to_u8(((tmp11 + tmp2 + round) >> shift) + 128); - output[base + 6] = clamp_to_u8(((tmp11 - tmp2 + round) >> shift) + 128); - output[base + 2] = clamp_to_u8(((tmp12 + tmp1 + round) >> shift) + 128); - output[base + 5] = clamp_to_u8(((tmp12 - tmp1 + round) >> shift) + 128); - output[base + 3] = clamp_to_u8(((tmp13 + tmp0 + round) >> shift) + 128); - output[base + 4] = clamp_to_u8(((tmp13 - tmp0 + round) >> shift) + 128); } output @@ -1240,6 +1179,23 @@ mod tests { } } + #[test] + fn idct_mixed_frequencies_matches_reference() { + let coeffs = [ + 120, -35, 12, 0, 5, -3, 0, 0, 22, -18, 9, 4, 0, 0, 0, 0, -16, 7, 5, -2, 0, 0, 0, 0, 3, + -5, 0, 0, 0, 0, 0, 0, 8, 0, -3, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + ]; + let expected = [ + 141, 139, 137, 139, 144, 149, 154, 160, 141, 138, 136, 138, 143, 148, 153, 159, 140, + 139, 137, 139, 144, 148, 151, 156, 141, 141, 141, 145, 149, 151, 153, 156, 139, 140, + 141, 145, 149, 151, 152, 155, 136, 136, 137, 139, 142, 143, 145, 148, 138, 138, 137, + 138, 138, 137, 139, 143, 137, 138, 138, 137, 136, 134, 135, 139, + ]; + + assert_eq!(idct_block(&coeffs), expected); + } + // -- YCbCr to RGB -- #[test] -- 2.51.2