From 2c764e50bc182c78bb08f83022a065635ab91239 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Fri, 22 Sep 2023 15:44:02 +0000 Subject: [PATCH 1/5] Using `std::simd` to speed-up `unfilter` for `Paeth` / `Three` bpp. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Results of running microbenchmarks on author's machine: ``` $ bench --bench=unfilter --features=benchmarks,unstable -- --baseline=my_baseline filter=Paeth/bpp=3 ... unfilter/filter=Paeth/bpp=3 time: [21.337 µs 21.379 µs 21.429 µs] thrpt: [546.86 MiB/s 548.14 MiB/s 549.22 MiB/s] change: time: [-42.023% -41.825% -41.619%] (p = 0.00 < 0.05) thrpt: [+71.288% +71.895% +72.482%] Performance has improved. ``` --- benches/unfilter.rs | 4 +- src/filter.rs | 119 ++++++++++++++++++++++++++++++++++++++------ src/lib.rs | 1 + 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/benches/unfilter.rs b/benches/unfilter.rs index 2f6e1f2f..4ff5daa8 100644 --- a/benches/unfilter.rs +++ b/benches/unfilter.rs @@ -2,9 +2,9 @@ //! //! ``` //! $ alias bench="rustup run nightly cargo bench" -//! $ bench --bench=unfilter --features=benchmarks -- --save-baseline my_baseline +//! $ bench --bench=unfilter --features=benchmarks,unstable -- --save-baseline my_baseline //! ... tweak something, say the Sub filter ... -//! $ bench --bench=unfilter --features=benchmarks -- filter=Sub --baseline my_baseline +//! $ bench --bench=unfilter --features=benchmarks,unstable -- filter=Sub --baseline my_baseline //! ``` use criterion::{criterion_group, criterion_main, Criterion, Throughput}; diff --git a/src/filter.rs b/src/filter.rs index b561e4e9..3b6ac6d4 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -2,6 +2,87 @@ use core::convert::TryInto; use crate::common::BytesPerPixel; +/// SIMD helpers for `fn unfilter` +/// +/// TODO(https://github.com/rust-lang/rust/issues/86656): Stop gating this module behind the +/// "unstable" feature of the `png` crate. This should be possible once the "portable_simd" +/// feature of Rust gets stabilized. +#[cfg(feature = "unstable")] +mod simd { + use std::simd::{i16x4, u8x4, SimdInt, SimdOrd, SimdPartialEq, SimdUint}; + + /// This is an equivalent of the `PaethPredictor` function from + /// [the spec](http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html#Filter-type-4-Paeth) + /// except that it simultaenously calculates the predictor for all SIMD lanes. + /// Mapping between parameter names and pixel positions can be found in + /// [a diagram here](https://www.w3.org/TR/png/#filter-byte-positions). + /// + /// Examples of how different pixel types may be represented as multiple SIMD lanes: + /// - RGBA => 4 lanes of `i16x4` contain R, G, B, A + /// - RGB => 4 lanes of `i16x4` contain R, G, B, and a ignored 4th value + /// + /// The SIMD algorithm below is based on [`libpng`](https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L261-L280). + fn paeth_predictor(a: i16x4, b: i16x4, c: i16x4) -> i16x4 { + let pa = b - c; // (p-a) == (a+b-c - a) == (b-c) + let pb = a - c; // (p-b) == (a+b-c - b) == (a-c) + let pc = pa + pb; // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) + + let pa = pa.abs(); + let pb = pb.abs(); + let pc = pc.abs(); + + let smallest = pc.simd_min(pa.simd_min(pb)); + + // Paeth algorithm breaks ties favoring a over b over c, so we execute the following + // lane-wise selection: + // + // if smalest == pa + // then select a + // else select (if smallest == pb then select b else select c) + smallest + .simd_eq(pa) + .select(a, smallest.simd_eq(pb).select(b, c)) + } + + fn load3(src: &[u8]) -> u8x4 { + u8x4::from_array([src[0], src[1], src[2], 0]) + } + + fn store3(src: u8x4, dest: &mut [u8]) { + dest[0..3].copy_from_slice(&src.to_array()[0..3]) + } + + /// Undoes `FilterType::Paeth` for `BytesPerPixel::Three`. + pub fn unfilter_paeth3(prev_row: &[u8], curr_row: &mut [u8]) { + debug_assert_eq!(prev_row.len(), curr_row.len()); + debug_assert_eq!(prev_row.len() % 3, 0); + + // Paeth tries to predict pixel x using the pixel to the left of it, a, + // and two pixels from the previous row, b and c: + // + // prev_row: c b + // curr_row: a x + // + // The first pixel has no left context, and so uses an Up filter, p = b. + // This works naturally with our main loop's p = a+b-c if we force a and c + // to zero. + let mut a = i16x4::default(); + let mut c = i16x4::default(); + + for (prev, curr) in prev_row.chunks_exact(3).zip(curr_row.chunks_exact_mut(3)) { + let b = load3(prev).cast::(); + let mut x = load3(curr); + + let predictor = paeth_predictor(a, b, c); + x += predictor.cast::(); + store3(x, curr); + + c = b; + a = x.cast::(); + } + } +} + /// The byte level filter applied to scanlines to prepare them for compression. /// /// Compression in general benefits from repetitive data. The filter is a content-aware method of @@ -401,21 +482,31 @@ pub(crate) fn unfilter( } } BytesPerPixel::Three => { - let mut a_bpp = [0; 3]; - let mut c_bpp = [0; 3]; - for (chunk, b_bpp) in current.chunks_exact_mut(3).zip(previous.chunks_exact(3)) + #[cfg(feature = "unstable")] + simd::unfilter_paeth3(previous, current); + + #[cfg(not(feature = "unstable"))] { - let new_chunk = [ - chunk[0] - .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), - chunk[1] - .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), - chunk[2] - .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), - ]; - *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; - a_bpp = new_chunk; - c_bpp = b_bpp.try_into().unwrap(); + let mut a_bpp = [0; 3]; + let mut c_bpp = [0; 3]; + for (chunk, b_bpp) in + current.chunks_exact_mut(3).zip(previous.chunks_exact(3)) + { + let new_chunk = [ + chunk[0].wrapping_add(filter_paeth_decode( + a_bpp[0], b_bpp[0], c_bpp[0], + )), + chunk[1].wrapping_add(filter_paeth_decode( + a_bpp[1], b_bpp[1], c_bpp[1], + )), + chunk[2].wrapping_add(filter_paeth_decode( + a_bpp[2], b_bpp[2], c_bpp[2], + )), + ]; + *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; + a_bpp = new_chunk; + c_bpp = b_bpp.try_into().unwrap(); + } } } BytesPerPixel::Four => { diff --git a/src/lib.rs b/src/lib.rs index 1bcfdb99..e71d4c5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ //! ``` //! +#![cfg_attr(feature = "unstable", feature(portable_simd))] #![forbid(unsafe_code)] #[macro_use] From 2fca2aa44d6fe4e40a90df13b76bb79ce7660367 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Wed, 27 Sep 2023 18:21:30 +0000 Subject: [PATCH 2/5] Extending `std::simd` coverage to `Paeth` / `Six` bpp. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Results of running microbenchmarks on author's machine: ``` $ bench --bench=unfilter --features=unstable,benchmarks -- --baseline=my_baseline Paeth/bpp=6 ... unfilter/filter=Paeth/bpp=6 time: [22.346 µs 22.356 µs 22.367 µs] thrpt: [1.0233 GiB/s 1.0238 GiB/s 1.0242 GiB/s] change: time: [-24.033% -23.941% -23.852%] (p = 0.00 < 0.05) thrpt: [+31.323% +31.476% +31.637%] Performance has improved. ``` --- src/filter.rs | 105 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 3b6ac6d4..918da9be 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -9,7 +9,10 @@ use crate::common::BytesPerPixel; /// feature of Rust gets stabilized. #[cfg(feature = "unstable")] mod simd { - use std::simd::{i16x4, u8x4, SimdInt, SimdOrd, SimdPartialEq, SimdUint}; + use std::simd::{ + i16x4, i16x8, u8x4, u8x8, LaneCount, Simd, SimdInt, SimdOrd, SimdPartialEq, SimdUint, + SupportedLaneCount, + }; /// This is an equivalent of the `PaethPredictor` function from /// [the spec](http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html#Filter-type-4-Paeth) @@ -22,7 +25,14 @@ mod simd { /// - RGB => 4 lanes of `i16x4` contain R, G, B, and a ignored 4th value /// /// The SIMD algorithm below is based on [`libpng`](https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L261-L280). - fn paeth_predictor(a: i16x4, b: i16x4, c: i16x4) -> i16x4 { + fn paeth_predictor( + a: Simd, + b: Simd, + c: Simd, + ) -> Simd + where + LaneCount: SupportedLaneCount, + { let pa = b - c; // (p-a) == (a+b-c - a) == (b-c) let pb = a - c; // (p-b) == (a+b-c - b) == (a-c) let pc = pa + pb; // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) @@ -81,6 +91,44 @@ mod simd { a = x.cast::(); } } + + fn load6(src: &[u8]) -> u8x8 { + u8x8::from_array([src[0], src[1], src[2], src[3], src[4], src[5], 0, 0]) + } + + fn store6(src: u8x8, dest: &mut [u8]) { + dest[0..6].copy_from_slice(&src.to_array()[0..6]) + } + + /// Undoes `FilterType::Paeth` for `BytesPerPixel::Six`. + pub fn unfilter_paeth6(prev_row: &[u8], curr_row: &mut [u8]) { + debug_assert_eq!(prev_row.len(), curr_row.len()); + debug_assert_eq!(prev_row.len() % 6, 0); + + // Paeth tries to predict pixel x using the pixel to the left of it, a, + // and two pixels from the previous row, b and c: + // + // prev_row: c b + // curr_row: a x + // + // The first pixel has no left context, and so uses an Up filter, p = b. + // This works naturally with our main loop's p = a+b-c if we force a and c + // to zero. + let mut a = i16x8::default(); + let mut c = i16x8::default(); + + for (prev, curr) in prev_row.chunks_exact(6).zip(curr_row.chunks_exact_mut(6)) { + let b = load6(prev).cast::(); + let mut x = load6(curr); + + let predictor = paeth_predictor(a, b, c); + x += predictor.cast::(); + store6(x, curr); + + c = b; + a = x.cast::(); + } + } } /// The byte level filter applied to scanlines to prepare them for compression. @@ -530,27 +578,40 @@ pub(crate) fn unfilter( } } BytesPerPixel::Six => { - let mut a_bpp = [0; 6]; - let mut c_bpp = [0; 6]; - for (chunk, b_bpp) in current.chunks_exact_mut(6).zip(previous.chunks_exact(6)) + #[cfg(feature = "unstable")] + simd::unfilter_paeth6(previous, current); + + #[cfg(not(feature = "unstable"))] { - let new_chunk = [ - chunk[0] - .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), - chunk[1] - .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), - chunk[2] - .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), - chunk[3] - .wrapping_add(filter_paeth_decode(a_bpp[3], b_bpp[3], c_bpp[3])), - chunk[4] - .wrapping_add(filter_paeth_decode(a_bpp[4], b_bpp[4], c_bpp[4])), - chunk[5] - .wrapping_add(filter_paeth_decode(a_bpp[5], b_bpp[5], c_bpp[5])), - ]; - *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; - a_bpp = new_chunk; - c_bpp = b_bpp.try_into().unwrap(); + let mut a_bpp = [0; 6]; + let mut c_bpp = [0; 6]; + for (chunk, b_bpp) in + current.chunks_exact_mut(6).zip(previous.chunks_exact(6)) + { + let new_chunk = [ + chunk[0].wrapping_add(filter_paeth_decode( + a_bpp[0], b_bpp[0], c_bpp[0], + )), + chunk[1].wrapping_add(filter_paeth_decode( + a_bpp[1], b_bpp[1], c_bpp[1], + )), + chunk[2].wrapping_add(filter_paeth_decode( + a_bpp[2], b_bpp[2], c_bpp[2], + )), + chunk[3].wrapping_add(filter_paeth_decode( + a_bpp[3], b_bpp[3], c_bpp[3], + )), + chunk[4].wrapping_add(filter_paeth_decode( + a_bpp[4], b_bpp[4], c_bpp[4], + )), + chunk[5].wrapping_add(filter_paeth_decode( + a_bpp[5], b_bpp[5], c_bpp[5], + )), + ]; + *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; + a_bpp = new_chunk; + c_bpp = b_bpp.try_into().unwrap(); + } } } BytesPerPixel::Eight => { From 63222f62220d5965d3c350ee8e79ae8b53bcc0bb Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Wed, 27 Sep 2023 18:51:03 +0000 Subject: [PATCH 3/5] Extract a separate `struct PaethState` and `fn paeth_step`. This refactoring is desirable because: * It removes a little bit of duplication between `unfilter_paeth3` and `unfilter_paeth6` * It helps in a follow-up CL, where we need to use `paeth_step` from more places. --- src/filter.rs | 81 ++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 918da9be..7457f043 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -10,8 +10,7 @@ use crate::common::BytesPerPixel; #[cfg(feature = "unstable")] mod simd { use std::simd::{ - i16x4, i16x8, u8x4, u8x8, LaneCount, Simd, SimdInt, SimdOrd, SimdPartialEq, SimdUint, - SupportedLaneCount, + u8x4, u8x8, LaneCount, Simd, SimdInt, SimdOrd, SimdPartialEq, SimdUint, SupportedLaneCount, }; /// This is an equivalent of the `PaethPredictor` function from @@ -54,6 +53,40 @@ mod simd { .select(a, smallest.simd_eq(pb).select(b, c)) } + /// Memory of previous pixels (as needed to unfilter `FilterType::Paeth`). + /// See also https://www.w3.org/TR/png/#filter-byte-positions + #[derive(Default)] + struct PaethState + where + LaneCount: SupportedLaneCount, + { + /// Previous pixel in the previous row. + c: Simd, + + /// Previous pixel in the current row. + a: Simd, + } + + /// Mutates `x` as needed to unfilter `FilterType::Paeth`. + /// + /// `b` is the current pixel in the previous row. `x` is the current pixel in the current row. + /// See also https://www.w3.org/TR/png/#filter-byte-positions + fn paeth_step(state: &mut PaethState, b: Simd, x: &mut Simd) + where + LaneCount: SupportedLaneCount, + { + // Storing the inputs. + let b = b.cast::(); + + // Calculating the new value of the current pixel. + let predictor = paeth_predictor(state.a, b, state.c); + *x += predictor.cast::(); + + // Preparing for the next step. + state.c = b; + state.a = x.cast::(); + } + fn load3(src: &[u8]) -> u8x4 { u8x4::from_array([src[0], src[1], src[2], 0]) } @@ -67,28 +100,12 @@ mod simd { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 3, 0); - // Paeth tries to predict pixel x using the pixel to the left of it, a, - // and two pixels from the previous row, b and c: - // - // prev_row: c b - // curr_row: a x - // - // The first pixel has no left context, and so uses an Up filter, p = b. - // This works naturally with our main loop's p = a+b-c if we force a and c - // to zero. - let mut a = i16x4::default(); - let mut c = i16x4::default(); - + let mut state = PaethState::<4>::default(); for (prev, curr) in prev_row.chunks_exact(3).zip(curr_row.chunks_exact_mut(3)) { - let b = load3(prev).cast::(); + let b = load3(prev); let mut x = load3(curr); - - let predictor = paeth_predictor(a, b, c); - x += predictor.cast::(); + paeth_step(&mut state, b, &mut x); store3(x, curr); - - c = b; - a = x.cast::(); } } @@ -105,28 +122,12 @@ mod simd { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 6, 0); - // Paeth tries to predict pixel x using the pixel to the left of it, a, - // and two pixels from the previous row, b and c: - // - // prev_row: c b - // curr_row: a x - // - // The first pixel has no left context, and so uses an Up filter, p = b. - // This works naturally with our main loop's p = a+b-c if we force a and c - // to zero. - let mut a = i16x8::default(); - let mut c = i16x8::default(); - + let mut state = PaethState::<8>::default(); for (prev, curr) in prev_row.chunks_exact(6).zip(curr_row.chunks_exact_mut(6)) { - let b = load6(prev).cast::(); + let b = load6(prev); let mut x = load6(curr); - - let predictor = paeth_predictor(a, b, c); - x += predictor.cast::(); + paeth_step(&mut state, b, &mut x); store6(x, curr); - - c = b; - a = x.cast::(); } } } From 22295a56c70e8912cb0a61d2a041a84eef508df1 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Wed, 27 Sep 2023 18:52:31 +0000 Subject: [PATCH 4/5] `simd::unfilter_paethN`: Load 4 (or 8) bytes at a time (faster than 3 or 6). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This CL loads RGB data using 4-bytes-wide loads (and RRGGBB data using 8-byte-wide loads), because: * This is faster as measured by the microbenchmarks below * It doesn't change the behavior - before and after these changes we were ignoring the 4th SIMD lane when processing RGB data (after this change the 4th SIMD lane will contain data from the next pixel, before this change it contained a 0 value) * This is safe as long as we have more than 4 bytes of remaining input data (we have to fall back to a 3-bytes-wide load for the last pixel). Results of running microbenchmarks on the author's machine: ``` $ bench --bench=unfilter --features=unstable,benchmarks -- --baseline=simd1 Paeth/bpp=[36] ... unfilter/filter=Paeth/bpp=3 time: [18.755 µs 18.761 µs 18.767 µs] thrpt: [624.44 MiB/s 624.65 MiB/s 624.83 MiB/s] change: time: [-16.148% -15.964% -15.751%] (p = 0.00 < 0.05) thrpt: [+18.696% +18.997% +19.258%] Performance has improved. ... unfilter/filter=Paeth/bpp=6 time: [18.991 µs 19.000 µs 19.009 µs] thrpt: [1.2041 GiB/s 1.2047 GiB/s 1.2052 GiB/s] change: time: [-15.161% -15.074% -14.987%] (p = 0.00 < 0.05) thrpt: [+17.629% +17.750% +17.871%] Performance has improved. ``` --- src/filter.rs | 54 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 7457f043..22663add 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -96,17 +96,34 @@ mod simd { } /// Undoes `FilterType::Paeth` for `BytesPerPixel::Three`. - pub fn unfilter_paeth3(prev_row: &[u8], curr_row: &mut [u8]) { + pub fn unfilter_paeth3(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 3, 0); let mut state = PaethState::<4>::default(); - for (prev, curr) in prev_row.chunks_exact(3).zip(curr_row.chunks_exact_mut(3)) { - let b = load3(prev); - let mut x = load3(curr); + while prev_row.len() >= 4 { + // `u8x4` requires working with `[u8;4]`, but we can just load and ignore the first + // byte from the next triple. This optimization technique mimics the algorithm found + // in + // https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L130-L131 + let b = u8x4::from_slice(prev_row); + let mut x = u8x4::from_slice(curr_row); + paeth_step(&mut state, b, &mut x); - store3(x, curr); + + // We can speculate that writing 4 bytes might be more efficient (just as with using + // `u8x4::from_slice` above), but we can't use that here, because we can't clobber the + // first byte of the next pixel in the `curr_row`. + store3(x, curr_row); + + prev_row = &prev_row[3..]; + curr_row = &mut curr_row[3..]; } + // Can't use `u8x4::from_slice` for the last `[u8;3]`. + let b = load3(prev_row); + let mut x = load3(curr_row); + paeth_step(&mut state, b, &mut x); + store3(x, curr_row); } fn load6(src: &[u8]) -> u8x8 { @@ -118,17 +135,34 @@ mod simd { } /// Undoes `FilterType::Paeth` for `BytesPerPixel::Six`. - pub fn unfilter_paeth6(prev_row: &[u8], curr_row: &mut [u8]) { + pub fn unfilter_paeth6(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 6, 0); let mut state = PaethState::<8>::default(); - for (prev, curr) in prev_row.chunks_exact(6).zip(curr_row.chunks_exact_mut(6)) { - let b = load6(prev); - let mut x = load6(curr); + while prev_row.len() >= 8 { + // `u8x8` requires working with `[u8;8]`, but we can just load and ignore the first two + // bytes from the next pixel. This optimization technique mimics the algorithm found + // in + // https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L130-L131 + let b = u8x8::from_slice(prev_row); + let mut x = u8x8::from_slice(curr_row); + paeth_step(&mut state, b, &mut x); - store6(x, curr); + + // We can speculate that writing 8 bytes might be more efficient (just as with using + // `u8x8::from_slice` above), but we can't use that here, because we can't clobber the + // first bytes of the next pixel in the `curr_row`. + store6(x, curr_row); + + prev_row = &prev_row[6..]; + curr_row = &mut curr_row[6..]; } + // Can't use `u8x8::from_slice` for the last `[u8;6]`. + let b = load6(prev_row); + let mut x = load6(curr_row); + paeth_step(&mut state, b, &mut x); + store6(x, curr_row); } } From c9a73271773eb3273eee72797e1bab37e9a90551 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Wed, 1 Nov 2023 20:35:11 +0000 Subject: [PATCH 5/5] Tweak CI to avoid testing the `unstable` feature with stable `rustc` --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e75cf13a..581b46df 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -55,7 +55,7 @@ jobs: - run: rustup default stable - name: test run: > - cargo test -v --all-targets --all-features + cargo test -v --all-targets rustfmt: runs-on: ubuntu-latest steps: