|
1 | | -//! Progressive RFX decode and encode algorithms ([MS-RDPEGFX] 2.2.4.2). |
| 1 | +//! RemoteFX Progressive codec implementation ([MS-RDPEGFX] 2.2.4.2). |
2 | 2 | //! |
3 | | -//! Provides first-pass decode (RLGR1 + progressive dequantization + sign capture) |
4 | | -//! and upgrade-pass decode (SRL/raw routing by DAS sign state, coefficient |
5 | | -//! accumulation) for the RemoteFX Progressive codec. |
| 3 | +//! This module implements the full progressive RemoteFX codec for both |
| 4 | +//! client-side decode and server-side encode. The progressive codec delivers |
| 5 | +//! screen updates in multiple passes: a coarse first pass followed by |
| 6 | +//! refinement upgrade passes that progressively improve quality. |
6 | 7 | //! |
7 | | -//! These are pure algorithmic functions operating on coefficient buffers. |
8 | | -//! Tile state management and EGFX integration belong in a higher layer. |
| 8 | +//! # Architecture |
| 9 | +//! |
| 10 | +//! ## Decode pipeline (client) |
| 11 | +//! - [`decode_first_pass`]: RLGR1 → LL3 delta decode → base dequantization → |
| 12 | +//! progressive dequantization → DAS sign capture |
| 13 | +//! - [`decode_upgrade_pass`]: SRL/raw routing by DAS sign state → coefficient |
| 14 | +//! accumulation |
| 15 | +//! |
| 16 | +//! ## Encode pipeline (server) |
| 17 | +//! - [`encode_first_pass`]: forward DWT → base quantization → progressive |
| 18 | +//! quantization → LL3 delta encode → RLGR1 |
| 19 | +//! - [`encode_upgrade_pass`]: per-band SRL + raw bit encoding for refinement |
| 20 | +//! - [`rgba_to_ycbcr`]: ITU-R BT.601 color space conversion |
| 21 | +//! |
| 22 | +//! ## State management |
| 23 | +//! - [`TileState`]: per-tile coefficient and DAS sign storage (~37 KB per tile) |
| 24 | +//! - [`SurfaceTiles`]: lazily-allocated tile grid for a surface |
| 25 | +//! - [`ProgressiveDecoder`]: high-level decoder maintaining per-context state, |
| 26 | +//! wired into the EGFX `WireToSurface2Pdu` path |
| 27 | +//! |
| 28 | +//! # Progressive quantization |
| 29 | +//! |
| 30 | +//! Progressive regions use [`ComponentCodecQuant`] (different nibble ordering |
| 31 | +//! from classic RFX `Quant`). Each quality level specifies a BitPos per band |
| 32 | +//! that controls how many bits are transmitted. Higher BitPos means fewer bits |
| 33 | +//! (coarser quality). Upgrade passes decrease BitPos, revealing more bits. |
9 | 34 |
|
10 | 35 | use ironrdp_pdu::codecs::rfx::EntropyAlgorithm; |
11 | 36 | use ironrdp_pdu::codecs::rfx::progressive::ComponentCodecQuant; |
@@ -1735,4 +1760,219 @@ mod tests { |
1735 | 1760 | assert!(srl_data.is_empty(), "no refinement bits, SRL should be empty"); |
1736 | 1761 | assert!(raw_data.is_empty(), "no refinement bits, raw should be empty"); |
1737 | 1762 | } |
| 1763 | + |
| 1764 | + // --- B12: Integration / round-trip tests --- |
| 1765 | + |
| 1766 | + #[test] |
| 1767 | + fn first_pass_encode_decode_round_trip_lossless() { |
| 1768 | + // With LOSSLESS quants (all 1s), quantization is a no-op (shift by 0). |
| 1769 | + // The only error source is DWT integer truncation (LeGall 5/3). |
| 1770 | + // |
| 1771 | + // decode_first_pass returns frequency-domain coefficients (post-dequant), |
| 1772 | + // so we apply inverse DWT to get back to spatial domain for comparison. |
| 1773 | + let original = [42i16; COEFFICIENTS_PER_COMPONENT]; |
| 1774 | + let mut encode_buf = original; |
| 1775 | + let mut output = vec![0u8; 16384]; |
| 1776 | + |
| 1777 | + let base_quant = ComponentCodecQuant::LOSSLESS; |
| 1778 | + let prog_quant = ComponentCodecQuant::LOSSLESS; |
| 1779 | + |
| 1780 | + let bytes = encode_first_pass(&mut encode_buf, &mut output, &base_quant, &prog_quant, false).unwrap(); |
| 1781 | + |
| 1782 | + let mut decoded = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1783 | + let mut sign = [0i8; COEFFICIENTS_PER_COMPONENT]; |
| 1784 | + decode_first_pass( |
| 1785 | + &output[..bytes], |
| 1786 | + &base_quant, |
| 1787 | + &prog_quant, |
| 1788 | + false, |
| 1789 | + &mut decoded, |
| 1790 | + &mut sign, |
| 1791 | + ) |
| 1792 | + .unwrap(); |
| 1793 | + |
| 1794 | + // Inverse DWT to get back to spatial domain |
| 1795 | + let mut temp = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1796 | + crate::dwt::decode(&mut decoded, &mut temp); |
| 1797 | + |
| 1798 | + let max_err = original |
| 1799 | + .iter() |
| 1800 | + .zip(decoded.iter()) |
| 1801 | + .map(|(a, b)| (i32::from(*a) - i32::from(*b)).unsigned_abs()) |
| 1802 | + .max() |
| 1803 | + .unwrap(); |
| 1804 | + |
| 1805 | + assert!(max_err <= 4, "flat data round-trip max error {max_err} exceeds 4"); |
| 1806 | + } |
| 1807 | + |
| 1808 | + #[test] |
| 1809 | + fn first_pass_encode_decode_round_trip_reduce_extrapolate() { |
| 1810 | + let original = [42i16; COEFFICIENTS_PER_COMPONENT]; |
| 1811 | + let mut encode_buf = original; |
| 1812 | + let mut output = vec![0u8; 16384]; |
| 1813 | + |
| 1814 | + let base_quant = ComponentCodecQuant::LOSSLESS; |
| 1815 | + let prog_quant = ComponentCodecQuant::LOSSLESS; |
| 1816 | + |
| 1817 | + let bytes = encode_first_pass(&mut encode_buf, &mut output, &base_quant, &prog_quant, true).unwrap(); |
| 1818 | + |
| 1819 | + let mut decoded = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1820 | + let mut sign = [0i8; COEFFICIENTS_PER_COMPONENT]; |
| 1821 | + decode_first_pass( |
| 1822 | + &output[..bytes], |
| 1823 | + &base_quant, |
| 1824 | + &prog_quant, |
| 1825 | + true, |
| 1826 | + &mut decoded, |
| 1827 | + &mut sign, |
| 1828 | + ) |
| 1829 | + .unwrap(); |
| 1830 | + |
| 1831 | + // Inverse DWT (reduce-extrapolate variant) |
| 1832 | + let mut temp = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1833 | + crate::dwt_extrapolate::decode(&mut decoded, &mut temp); |
| 1834 | + |
| 1835 | + let max_err = original |
| 1836 | + .iter() |
| 1837 | + .zip(decoded.iter()) |
| 1838 | + .map(|(a, b)| (i32::from(*a) - i32::from(*b)).unsigned_abs()) |
| 1839 | + .max() |
| 1840 | + .unwrap(); |
| 1841 | + |
| 1842 | + assert!( |
| 1843 | + max_err <= 6, |
| 1844 | + "reduce-extrapolate round-trip max error {max_err} exceeds 6" |
| 1845 | + ); |
| 1846 | + } |
| 1847 | + |
| 1848 | + #[test] |
| 1849 | + fn first_pass_encode_decode_with_quantization() { |
| 1850 | + // Test encode/decode with realistic quantization (non-lossless). |
| 1851 | + // Quantization introduces controlled error, so we just verify |
| 1852 | + // the pipeline completes and the decoded output is in a sensible range. |
| 1853 | + let mut coefficients = [42i16; COEFFICIENTS_PER_COMPONENT]; |
| 1854 | + let mut output = vec![0u8; 16384]; |
| 1855 | + |
| 1856 | + let base_quant = ComponentCodecQuant { |
| 1857 | + ll3: 6, |
| 1858 | + hl3: 6, |
| 1859 | + lh3: 6, |
| 1860 | + hh3: 6, |
| 1861 | + hl2: 7, |
| 1862 | + lh2: 7, |
| 1863 | + hh2: 7, |
| 1864 | + hl1: 8, |
| 1865 | + lh1: 8, |
| 1866 | + hh1: 8, |
| 1867 | + }; |
| 1868 | + let prog_quant = ComponentCodecQuant::LOSSLESS; |
| 1869 | + |
| 1870 | + let bytes = encode_first_pass(&mut coefficients, &mut output, &base_quant, &prog_quant, false).unwrap(); |
| 1871 | + assert!(bytes > 0, "should produce encoded output"); |
| 1872 | + |
| 1873 | + // Quantized data should compress better than lossless |
| 1874 | + let mut decoded = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1875 | + let mut sign = [0i8; COEFFICIENTS_PER_COMPONENT]; |
| 1876 | + decode_first_pass( |
| 1877 | + &output[..bytes], |
| 1878 | + &base_quant, |
| 1879 | + &prog_quant, |
| 1880 | + false, |
| 1881 | + &mut decoded, |
| 1882 | + &mut sign, |
| 1883 | + ) |
| 1884 | + .unwrap(); |
| 1885 | + |
| 1886 | + // Inverse DWT |
| 1887 | + let mut temp = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1888 | + crate::dwt::decode(&mut decoded, &mut temp); |
| 1889 | + |
| 1890 | + // With quantization, values should be approximately the original (42) |
| 1891 | + // but with significant quantization noise. Just check within +-200. |
| 1892 | + let mean_err: f64 = decoded |
| 1893 | + .iter() |
| 1894 | + .map(|v| f64::from((i32::from(*v) - 42).unsigned_abs())) |
| 1895 | + .sum::<f64>() |
| 1896 | + / 4096.0; |
| 1897 | + |
| 1898 | + assert!( |
| 1899 | + mean_err < 200.0, |
| 1900 | + "mean error {mean_err} too large for quantized flat tile" |
| 1901 | + ); |
| 1902 | + } |
| 1903 | + |
| 1904 | + #[test] |
| 1905 | + fn rgba_ycbcr_reconstruct_round_trip() { |
| 1906 | + // Test the color conversion path: RGB -> YCbCr -> DWT -> IDWT -> RGB |
| 1907 | + // should produce approximately the same pixel values |
| 1908 | + let mut pixels = vec![0u8; 64 * 64 * 4]; |
| 1909 | + for i in 0..64 * 64 { |
| 1910 | + // Smooth gradient |
| 1911 | + let row = i / 64; |
| 1912 | + let col = i % 64; |
| 1913 | + pixels[i * 4] = (row * 4) as u8; // R |
| 1914 | + pixels[i * 4 + 1] = (col * 4) as u8; // G |
| 1915 | + pixels[i * 4 + 2] = 128; // B |
| 1916 | + pixels[i * 4 + 3] = 255; // A |
| 1917 | + } |
| 1918 | + |
| 1919 | + let mut y = vec![0i16; 4096]; |
| 1920 | + let mut cb = vec![0i16; 4096]; |
| 1921 | + let mut cr = vec![0i16; 4096]; |
| 1922 | + |
| 1923 | + rgba_to_ycbcr(&pixels, &mut y, &mut cb, &mut cr); |
| 1924 | + |
| 1925 | + // Verify Y is in expected range [-128..127] and Cb/Cr in [-128..127] |
| 1926 | + for i in 0..4096 { |
| 1927 | + assert!(y[i] >= -128 && y[i] <= 127, "Y[{i}] = {} out of range", y[i]); |
| 1928 | + assert!(cb[i] >= -128 && cb[i] <= 127, "Cb[{i}] = {} out of range", cb[i]); |
| 1929 | + assert!(cr[i] >= -128 && cr[i] <= 127, "Cr[{i}] = {} out of range", cr[i]); |
| 1930 | + } |
| 1931 | + } |
| 1932 | + |
| 1933 | + #[test] |
| 1934 | + fn quantize_dequantize_ccq_round_trip() { |
| 1935 | + let quant = ComponentCodecQuant { |
| 1936 | + ll3: 4, |
| 1937 | + hl3: 4, |
| 1938 | + lh3: 4, |
| 1939 | + hh3: 5, |
| 1940 | + hl2: 5, |
| 1941 | + lh2: 5, |
| 1942 | + hh2: 6, |
| 1943 | + hl1: 6, |
| 1944 | + lh1: 6, |
| 1945 | + hh1: 7, |
| 1946 | + }; |
| 1947 | + |
| 1948 | + // Start with some known coefficient values |
| 1949 | + let original = { |
| 1950 | + let mut c = [0i16; COEFFICIENTS_PER_COMPONENT]; |
| 1951 | + for (i, v) in c.iter_mut().enumerate() { |
| 1952 | + *v = ((i * 7 % 256) as i16) - 128; |
| 1953 | + } |
| 1954 | + c |
| 1955 | + }; |
| 1956 | + |
| 1957 | + let mut coefficients = original; |
| 1958 | + |
| 1959 | + // Quantize then dequantize |
| 1960 | + quantize_component_ccq(&mut coefficients, &quant, false); |
| 1961 | + dequantize_component_ccq(&mut coefficients, &quant, false); |
| 1962 | + |
| 1963 | + // Quantization is lossy, but the round-trip should be in the right ballpark. |
| 1964 | + // Error bound per coefficient: at most 2^(quant_val-1) per quantization step |
| 1965 | + // With quant values 4-7, max error per step is 2^6 = 64 |
| 1966 | + let max_err = original |
| 1967 | + .iter() |
| 1968 | + .zip(coefficients.iter()) |
| 1969 | + .map(|(a, b)| (i32::from(*a) - i32::from(*b)).unsigned_abs()) |
| 1970 | + .max() |
| 1971 | + .unwrap(); |
| 1972 | + |
| 1973 | + assert!( |
| 1974 | + max_err <= 64, |
| 1975 | + "quantize/dequantize round-trip max error {max_err} exceeds 64" |
| 1976 | + ); |
| 1977 | + } |
1738 | 1978 | } |
0 commit comments