Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions crates/ironrdp-graphics/src/progressive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,14 @@ pub fn rgba_to_ycbcr(pixels: &[u8], y_out: &mut [i16], cb_out: &mut [i16], cr_ou
let cb = (-11059 * r - 21709 * g + 32768 * b + 32768) >> 16;
let cr = (32768 * r - 27439 * g - 5329 * b + 32768) >> 16;

y_out[i] = clamp_i16(y);
cb_out[i] = clamp_i16(cb);
cr_out[i] = clamp_i16(cr);
// Add the 5-bit fixed-point headroom the decoder removes with `>> 5` in
// `reconstruct_to_rgba`. The base dequant retains +5 bits (matching
// FreeRDP's `/* -6 + 5 = -1 */`); scaling up here keeps the
// encode->decode round-trip an identity. Range after `<< 5` is
// [-4096, 4064], well within i16.
y_out[i] = clamp_i16(y << 5);
cb_out[i] = clamp_i16(cb << 5);
cr_out[i] = clamp_i16(cr << 5);
}
}

Expand Down Expand Up @@ -830,9 +835,17 @@ impl TileState {

// YCbCr to RGBA conversion
for i in 0..64 * 64 {
let y = i32::from(y_buf[i]) + 128;
let cb = i32::from(cb_buf[i]);
let cr = i32::from(cr_buf[i]);
// The base dequantization (`<< (quant - 1)`) retains +5 bits of
// fixed-point headroom by design (the DWT itself is unity-gain --
// see tests/dwt_gain.rs), so the spatial coefficients arrive scaled
// up by 2^5. FreeRDP folds this descale into its colour primitive
// (yCbCrToRGB_16s8u_P3AC4R shifts by `divisor + 5`); we apply the
// matching `>> 5` here. Without it every value overflows the ±128
// range and clamps to 0/255 -- luma loses mid-tones and chroma
// collapses to pure primaries (the posterised image).
let y = (i32::from(y_buf[i]) >> 5) + 128;
let cb = i32::from(cb_buf[i]) >> 5;
let cr = i32::from(cr_buf[i]) >> 5;

// ITU-R BT.601 YCbCr to RGB conversion
let r = y + ((cr * 91881 + 32768) >> 16);
Expand Down Expand Up @@ -1689,12 +1702,11 @@ mod tests {
rgba_to_ycbcr(&pixels, &mut y, &mut cb, &mut cr);

// Pure white: R=G=B=255
// Y = (19595*255 + 38470*255 + 7471*255 + 32768) >> 16 - 128
// = (65536*255 + 32768) >> 16 - 128 = 255 - 128 = 127
// Cb and Cr should be ~0 (achromatic)
assert!((y[0] - 127).abs() <= 1, "Y for white: got {}", y[0]);
assert!(cb[0].abs() <= 1, "Cb for white: got {}", cb[0]);
assert!(cr[0].abs() <= 1, "Cr for white: got {}", cr[0]);
// Y = ((65536*255 + 32768) >> 16 - 128) << 5 = 127 << 5 = 4064
// Cb and Cr should be ~0 (achromatic), unaffected by the << 5 scale
assert!((y[0] - 4064).abs() <= 32, "Y for white: got {}", y[0]);
assert!(cb[0].abs() <= 32, "Cb for white: got {}", cb[0]);
assert!(cr[0].abs() <= 32, "Cr for white: got {}", cr[0]);
}

#[test]
Expand All @@ -1706,8 +1718,8 @@ mod tests {

rgba_to_ycbcr(&pixels, &mut y, &mut cb, &mut cr);

// Pure black: Y = -128, Cb = 0, Cr = 0
assert_eq!(y[0], -128);
// Pure black: Y = -128 << 5 = -4096, Cb = 0, Cr = 0
assert_eq!(y[0], -4096);
assert_eq!(cb[0], 0);
assert_eq!(cr[0], 0);
}
Expand Down
49 changes: 49 additions & 0 deletions crates/ironrdp-graphics/tests/dwt_gain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Regression lock for the progressive colour descale: proves the RFX
//! progressive DWT is unity-gain, so the `>> 5` descale in `reconstruct_to_rgba`
//! compensates the fixed-point headroom retained by the base dequant
//! (`<< (quant - 1)`), not any scale introduced by the transform.
//!
//! Reasoning: the wire carries forward-DWT coefficients. If a flat pixel tile of
//! value P forward-transforms to an LL/DC coefficient of ~P, wire coeffs are at
//! pixel scale and the inverse DWT must return pixel scale (no descale needed).
//! If it transformed to ~32*P, the transform would be 2^5 non-normalized. It
//! does not: the ~32x seen on real streams comes from dequant headroom.

// This integration-test binary only exercises `ironrdp_graphics`; the crate's
// other dev-dependencies are for its unit tests.
#![allow(unused_crate_dependencies)]

use ironrdp_graphics::dwt;

const N: usize = 4096; // 64x64

#[test]
fn forward_dc_gain_and_roundtrip() {
let p: i16 = 100;

// Flat tile, all pixels = P.
let mut buf = [p; N];
let mut tmp = [0i16; N];
dwt::encode(&mut buf, &mut tmp);

let peak = buf.iter().map(|c| c.unsigned_abs()).max().unwrap_or(0);

// Round-trip must be identity.
let mut rt = [p; N];
let mut tmp2 = [0i16; N];
dwt::encode(&mut rt, &mut tmp2);
dwt::decode(&mut rt, &mut tmp2);
let (rmin, rmax) = rt
.iter()
.fold((i16::MAX, i16::MIN), |(lo, hi), &v| (lo.min(v), hi.max(v)));

// Guard: unity-gain. No forward coefficient exceeds the input magnitude P
// (a 2^5 transform would push the peak to ~32*P = ~3200), and the round-trip
// is identity within integer-lifting rounding.
assert!(
i32::from(peak) <= i32::from(p) + 2,
"DWT is not unity-gain: peak|coeff|={peak} for P={p}"
);
assert!((i32::from(rmin) - i32::from(p)).abs() <= 2, "round-trip low drift");
assert!((i32::from(rmax) - i32::from(p)).abs() <= 2, "round-trip high drift");
}
Loading