Skip to content

Commit e9d0a53

Browse files
committed
Polyfill rayon in WASM
1 parent 5666d5d commit e9d0a53

File tree

8 files changed

+120
-13
lines changed

8 files changed

+120
-13
lines changed

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ name = "dssim"
1919
path = "src/main.rs"
2020

2121
[dependencies]
22-
dssim-core = { path = "./dssim-core", version = "3.1" }
22+
dssim-core = { path = "./dssim-core", version = "3.2", default-features = false }
2323
imgref = "1.9.1"
2424
getopts = "0.2.21"
25-
rayon = "1.5.1"
25+
rayon = { version = "1.5.1", optional = true }
2626
rgb = "0.8.31"
2727
lodepng = "3.5.2"
2828
load_image = { version = "2.16.1", features = ["lcms2-static"] }
2929

3030
[features]
31+
default = ["threads", "dssim-core/default"]
32+
threads = ["rayon"]
3133
avif = ["load_image/avif"]
3234
webp = ["load_image/webp"]
3335
webp-static = ["load_image/webp-static"]

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,11 @@ DSSIM is dual-licensed under [AGPL](LICENSE) or [commercial](https://supso.org/p
8888
* The lightness component of SSIM is ignored when comparing color channels.
8989
* SSIM score is pooled using mean absolute deviation. You can get per-pixel SSIM from the API to implement custom pooling.
9090

91+
92+
## Compiling for WASM
93+
94+
For compatibility with single-threaded WASM runtimes, disable the `threads` Cargo feature. It's enabled by default, so to disable it, disable default features:
95+
96+
```toml
97+
dssim-core = { version = "3.2", default-features = false }
98+
```

dssim-core/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ edition = "2018"
1717
crate-type = ["lib", "staticlib"]
1818

1919
[dependencies]
20-
rayon = "1.5.0"
2120
imgref = "1.9.1"
2221
itertools = "0.10.3"
22+
rayon = { version = "1.5.1", optional = true }
2323
rgb = "0.8.31"
2424

2525
[dev-dependencies]
2626
lodepng = "3.6.0"
2727

28+
[features]
29+
default = ["threads"]
30+
threads = ["rayon"]
31+
2832
[package.metadata.docs.rs]
2933
targets = ["x86_64-unknown-linux-gnu"]

dssim-core/src/dssim.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub use crate::tolab::ToLABBitmap;
2626
pub use crate::val::Dssim as Val;
2727
use imgref::*;
2828
use itertools::multizip;
29+
#[cfg(not(feature = "threads"))]
30+
use crate::lieon as rayon;
2931
use rayon::prelude::*;
3032
use rgb::{RGB, RGBA};
3133
use std::borrow::Borrow;
@@ -293,8 +295,8 @@ impl Dssim {
293295
/// `Val` is a fancy wrapper for `f64`
294296
pub fn compare<M: Borrow<DssimImage<f32>>>(&self, original_image: &DssimImage<f32>, modified_image: M) -> (Val, Vec<SsimMap>) {
295297
let modified_image = modified_image.borrow();
296-
let res: Vec<_> = self.scale_weights.par_iter().cloned().zip(
297-
modified_image.scale.par_iter().zip(original_image.scale.par_iter())
298+
let res: Vec<_> = self.scale_weights.as_slice().par_iter().cloned().zip(
299+
modified_image.scale.as_slice().par_iter().zip(original_image.scale.as_slice().par_iter())
298300
).enumerate().map(|(n, (weight, (modified_image_scale, original_image_scale)))| {
299301
let scale_width = original_image_scale.chan[0].width;
300302
let scale_height = original_image_scale.chan[0].height;
@@ -395,9 +397,9 @@ impl Dssim {
395397
debug_assert_eq!(img1_img2_blur.len(), original.mu.len());
396398
debug_assert_eq!(img1_img2_blur.len(), original.img_sq_blur.len());
397399

398-
let mu_iter = original.mu.par_iter().cloned().zip_eq(modified.mu.par_iter().cloned());
399-
let sq_iter = original.img_sq_blur.par_iter().cloned().zip_eq(modified.img_sq_blur.par_iter().cloned());
400-
img1_img2_blur.par_iter().cloned().zip_eq(mu_iter).zip_eq(sq_iter).zip_eq(map_out.par_iter_mut())
400+
let mu_iter = original.mu.as_slice().par_iter().cloned().zip_eq(modified.mu.as_slice().par_iter().cloned());
401+
let sq_iter = original.img_sq_blur.as_slice().par_iter().cloned().zip_eq(modified.img_sq_blur.as_slice().par_iter().cloned());
402+
img1_img2_blur.par_iter().cloned().zip_eq(mu_iter).zip_eq(sq_iter).zip_eq(map_out.as_mut_slice().par_iter_mut())
401403
.for_each(|(((img1_img2_blur, (mu1, mu2)), (img1_sq_blur, img2_sq_blur)), map_out)| {
402404
let mu1mu1 = mu1 * mu1;
403405
let mu1mu2 = mu1 * mu2;

dssim-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ mod image;
1414
mod linear;
1515
mod tolab;
1616
mod val;
17+
#[cfg(not(feature = "threads"))]
18+
mod lieon;
1719

1820
pub use crate::dssim::*;
1921
pub use crate::image::*;

dssim-core/src/lieon.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! Shim for single-threaded rayon replacement
2+
3+
pub mod prelude {
4+
pub use super::*;
5+
}
6+
7+
pub trait ParSliceLie<T> {
8+
fn par_chunks(&self, n: usize) -> std::slice::Chunks<'_, T>;
9+
}
10+
11+
pub trait ParSliceMutLie<T> {
12+
fn par_chunks_mut(&mut self, n: usize) -> std::slice::ChunksMut<'_, T>;
13+
}
14+
15+
pub trait ParIntoIterLie<T> {
16+
type IntoIter;
17+
fn into_par_iter(self) -> Self::IntoIter;
18+
}
19+
20+
pub trait ParIterLie<T> {
21+
type Iter;
22+
fn par_iter(&self) -> Self::Iter;
23+
}
24+
25+
pub trait ParIterMutLie<'a, T> {
26+
type Iter;
27+
fn par_iter_mut(&'a mut self) -> Self::Iter;
28+
}
29+
30+
pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
31+
let a = a();
32+
let b = b();
33+
(a, b)
34+
}
35+
36+
impl<'a, T> ParSliceLie<T> for &'a [T] {
37+
fn par_chunks(&self, n: usize) -> std::slice::Chunks<'_, T> {
38+
self.chunks(n)
39+
}
40+
}
41+
42+
impl<'a, T> ParSliceLie<T> for &'a mut [T] {
43+
fn par_chunks(&self, n: usize) -> std::slice::Chunks<'_, T> {
44+
self.chunks(n)
45+
}
46+
}
47+
48+
impl<'a, T> ParSliceMutLie<T> for &'a mut [T] {
49+
fn par_chunks_mut(&mut self, n: usize) -> std::slice::ChunksMut<'_, T> {
50+
self.chunks_mut(n)
51+
}
52+
}
53+
54+
impl<'a, T> ParIterLie<T> for &'a [T] {
55+
type Iter = std::slice::Iter<'a, T>;
56+
57+
fn par_iter(&self) -> Self::Iter {
58+
self.iter()
59+
}
60+
}
61+
62+
impl<'a, T> ParIterMutLie<'a, T> for &'a mut [T] {
63+
type Iter = std::slice::IterMut<'a, T>;
64+
65+
fn par_iter_mut(&'a mut self) -> Self::Iter {
66+
self.iter_mut()
67+
}
68+
}
69+
70+
71+
impl<T> ParIntoIterLie<T> for Vec<T> {
72+
type IntoIter = std::vec::IntoIter<T>;
73+
fn into_par_iter(self) -> Self::IntoIter {
74+
self.into_iter()
75+
}
76+
}

dssim-core/src/tolab.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use crate::image::ToRGB;
55
use crate::image::RGBAPLU;
66
use crate::image::RGBLU;
77
use imgref::*;
8+
#[cfg(not(feature = "threads"))]
9+
use crate::lieon as rayon;
810
use rayon::prelude::*;
911

1012
const D65x: f64 = 0.9505;
@@ -69,7 +71,7 @@ impl ToLABBitmap for GBitmap {
6971
unsafe { out.set_len(size) };
7072

7173
// For output width == stride
72-
out.par_chunks_mut(width).enumerate().for_each(|(y, out_row)| {
74+
out.as_mut_slice().par_chunks_mut(width).enumerate().for_each(|(y, out_row)| {
7375
let start = y * self.stride();
7476
let in_row = &self.buf()[start..start + width];
7577
let out_row = &mut out_row[0..width];
@@ -102,8 +104,8 @@ fn rgb_to_lab<T: Copy + Sync + Send + 'static, F>(img: ImgRef<'_, T>, cb: F) ->
102104
unsafe { out_b.set_len(size) };
103105

104106
// For output width == stride
105-
out_l.par_chunks_mut(width).zip(
106-
out_a.par_chunks_mut(width).zip(out_b.par_chunks_mut(width))
107+
out_l.as_mut_slice().par_chunks_mut(width).zip(
108+
out_a.as_mut_slice().par_chunks_mut(width).zip(out_b.as_mut_slice().par_chunks_mut(width))
107109
).enumerate()
108110
.for_each(|(y, (l_row, (a_row, b_row)))| {
109111
let start = y * stride;

src/main.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
#![allow(clippy::manual_range_contains)]
2020
use getopts::Options;
21+
#[cfg(feature = "threads")]
2122
use rayon::prelude::*;
2223
use std::env;
2324
use std::path::PathBuf;
@@ -70,7 +71,12 @@ fn main() {
7071

7172
let mut attr = dssim::Dssim::new();
7273

73-
let files = files.par_iter().map(|file| -> Result<_, String> {
74+
#[cfg(feature = "threads")]
75+
let files_iter = files.par_iter();
76+
#[cfg(not(feature = "threads"))]
77+
let files_iter = files.iter();
78+
79+
let files = files_iter.map(|file| -> Result<_, String> {
7480
let image = dssim::load_image(&attr, &file).map_err(|e| format!("Can't load {}, because: {}", file.display(), e))?;
7581
Ok((file, image))
7682
}).collect::<Result<Vec<_>,_>>();
@@ -102,7 +108,12 @@ fn main() {
102108
println!("{:.8}\t{}", dssim, file2.display());
103109

104110
if map_output_file.is_some() {
105-
ssim_maps.par_iter().enumerate().for_each(|(n, map_meta)| {
111+
#[cfg(feature = "threads")]
112+
let ssim_maps_iter = ssim_maps.par_iter();
113+
#[cfg(not(feature = "threads"))]
114+
let ssim_maps_iter = ssim_maps.iter();
115+
116+
ssim_maps_iter.enumerate().for_each(|(n, map_meta)| {
106117
let avgssim = map_meta.ssim as f32;
107118
let out: Vec<_> = map_meta.map.pixels().map(|ssim|{
108119
let max = 1_f32 - ssim;

0 commit comments

Comments
 (0)