Skip to content

Commit 71b1434

Browse files
bors[bot]okaneco
andauthored
Merge #184
184: Optimize into_component for float_to_uint, u8 to f32/f64 r=Ogeon a=okaneco Optimize component conversion of f32/f64 to uint. Add `IntoComponent` implementation for u8 to f32/f64. The rounding mode for components from floating point types to unsigned integers now rounds half to nearest even. Converting a NaN to uint now returns uint::MAX, previously it returned 0. Uint to float conversion behavior is not changed. Co-authored-by: okaneco <[email protected]>
2 parents 464c823 + 69d3399 commit 71b1434

File tree

2 files changed

+183
-11
lines changed

2 files changed

+183
-11
lines changed

palette/src/component.rs

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,31 @@ impl<T: Component> IntoComponent<T> for T {
9090
}
9191
}
9292

93+
// C23 = 2^23, in f32
94+
// C52 = 2^52, in f64
95+
const C23: u32 = 0x4b00_0000;
96+
const C52: u64 = 0x4330_0000_0000_0000;
97+
98+
// Float to uint conversion with rounding to nearest even number. Formula
99+
// follows the form (x_f32 + C23_f32) - C23_u32, where x is the component. From
100+
// Hacker's Delight, p. 378-380.
101+
// Works on the range of [-0.25, 2^23] for f32, [-0.25, 2^52] for f64.
102+
//
103+
// Special cases:
104+
// NaN -> uint::MAX
105+
// inf -> uint::MAX
106+
// -inf -> 0
107+
// Greater than 2^23 for f64, 2^52 for f64 -> uint::MAX
93108
macro_rules! convert_float_to_uint {
94109
($float: ident; direct ($($direct_target: ident),+); $(via $temporary: ident ($($target: ident),+);)*) => {
95110
$(
96111
impl IntoComponent<$direct_target> for $float {
97112
#[inline]
98113
fn into_component(self) -> $direct_target {
99114
let max = $direct_target::max_intensity() as $float;
100-
let scaled = self * max;
101-
clamp(scaled.round(), 0.0, max) as $direct_target
115+
let scaled = (self * max).min(max);
116+
let f = scaled + f32::from_bits(C23);
117+
(f.to_bits().saturating_sub(C23)) as $direct_target
102118
}
103119
}
104120
)+
@@ -109,15 +125,61 @@ macro_rules! convert_float_to_uint {
109125
#[inline]
110126
fn into_component(self) -> $target {
111127
let max = $target::max_intensity() as $temporary;
112-
let scaled = self as $temporary * max;
113-
clamp(scaled.round(), 0.0, max) as $target
128+
let scaled = (self as $temporary * max).min(max);
129+
let f = scaled + f64::from_bits(C52);
130+
(f.to_bits().saturating_sub(C52)) as $target
114131
}
115132
}
116133
)+
117134
)*
118135
};
119136
}
120137

138+
// Double to uint conversion with rounding to nearest even number. Formula
139+
// follows the form (x_f64 + C52_f64) - C52_u64, where x is the component.
140+
macro_rules! convert_double_to_uint {
141+
($double: ident; direct ($($direct_target: ident),+);) => {
142+
$(
143+
impl IntoComponent<$direct_target> for $double {
144+
#[inline]
145+
fn into_component(self) -> $direct_target {
146+
let max = $direct_target::max_intensity() as $double;
147+
let scaled = (self * max).min(max);
148+
let f = scaled + f64::from_bits(C52);
149+
(f.to_bits().saturating_sub(C52)) as $direct_target
150+
}
151+
}
152+
)+
153+
};
154+
}
155+
156+
// Uint to float conversion with the formula (x_u32 + C23_u32) - C23_f32, where
157+
// x is the component. We convert the component to f32 then multiply it by the
158+
// reciprocal of the float representation max value for u8.
159+
// Works on the range of [0, 2^23] for f32, [0, 2^52 - 1] for f64.
160+
impl IntoComponent<f32> for u8 {
161+
#[inline]
162+
fn into_component(self) -> f32 {
163+
let comp_u = self as u32 + C23;
164+
let comp_f = f32::from_bits(comp_u) - f32::from_bits(C23);
165+
let max_u = core::u8::MAX as u32 + C23;
166+
let max_f = (f32::from_bits(max_u) - f32::from_bits(C23)).recip();
167+
comp_f * max_f
168+
}
169+
}
170+
171+
// Uint to f64 conversion with the formula (x_u64 + C23_u64) - C23_f64.
172+
impl IntoComponent<f64> for u8 {
173+
#[inline]
174+
fn into_component(self) -> f64 {
175+
let comp_u = self as u64 + C52;
176+
let comp_f = f64::from_bits(comp_u) - f64::from_bits(C52);
177+
let max_u = core::u8::MAX as u64 + C52;
178+
let max_f = (f64::from_bits(max_u) - f64::from_bits(C52)).recip();
179+
comp_f * max_f
180+
}
181+
}
182+
121183
macro_rules! convert_uint_to_float {
122184
($uint: ident; $(via $temporary: ident ($($target: ident),+);)*) => {
123185
$(
@@ -167,9 +229,8 @@ impl IntoComponent<f32> for f64 {
167229
self as f32
168230
}
169231
}
170-
convert_float_to_uint!(f64; direct (u8, u16, u32, u64, u128););
232+
convert_double_to_uint!(f64; direct (u8, u16, u32, u64, u128););
171233

172-
convert_uint_to_float!(u8; via f32 (f32); via f64 (f64););
173234
convert_uint_to_uint!(u8; via f32 (u16); via f64 (u32, u64, u128););
174235

175236
convert_uint_to_float!(u16; via f32 (f32); via f64 (f64););
@@ -183,3 +244,119 @@ convert_uint_to_uint!(u64; via f64 (u8, u16, u32, u128););
183244

184245
convert_uint_to_float!(u128; via f64 (f32, f64););
185246
convert_uint_to_uint!(u128; via f64 (u8, u16, u32, u64););
247+
248+
#[cfg(test)]
249+
mod test {
250+
use crate::IntoComponent;
251+
use approx::assert_relative_eq;
252+
253+
#[test]
254+
fn float_to_uint() {
255+
let data = vec![
256+
-800.0,
257+
-0.3,
258+
0.0,
259+
0.005,
260+
0.024983,
261+
0.01,
262+
0.15,
263+
0.3,
264+
0.5,
265+
0.6,
266+
0.7,
267+
0.8,
268+
0.8444,
269+
0.9,
270+
0.955,
271+
0.999,
272+
1.0,
273+
1.4,
274+
f32::from_bits(0x4b44_0000),
275+
std::f32::MAX,
276+
std::f32::MIN,
277+
std::f32::NAN,
278+
std::f32::INFINITY,
279+
std::f32::NEG_INFINITY,
280+
];
281+
282+
let expected = vec![
283+
0u8, 0, 0, 1, 6, 3, 38, 76, 128, 153, 178, 204, 215, 230, 244, 255, 255, 255, 255, 255,
284+
0, 255, 255, 0,
285+
];
286+
287+
for (d, e) in data.into_iter().zip(expected) {
288+
assert_eq!(IntoComponent::<u8>::into_component(d), e);
289+
}
290+
}
291+
292+
#[test]
293+
fn double_to_uint() {
294+
let data = vec![
295+
-800.0,
296+
-0.3,
297+
0.0,
298+
0.005,
299+
0.024983,
300+
0.01,
301+
0.15,
302+
0.3,
303+
0.5,
304+
0.6,
305+
0.7,
306+
0.8,
307+
0.8444,
308+
0.9,
309+
0.955,
310+
0.999,
311+
1.0,
312+
1.4,
313+
f64::from_bits(0x4334_0000_0000_0000),
314+
std::f64::MAX,
315+
std::f64::MIN,
316+
std::f64::NAN,
317+
std::f64::INFINITY,
318+
std::f64::NEG_INFINITY,
319+
];
320+
321+
let expected = vec![
322+
0u8, 0, 0, 1, 6, 3, 38, 76, 128, 153, 178, 204, 215, 230, 244, 255, 255, 255, 255, 255,
323+
0, 255, 255, 0,
324+
];
325+
326+
for (d, e) in data.into_iter().zip(expected) {
327+
assert_eq!(IntoComponent::<u8>::into_component(d), e);
328+
}
329+
}
330+
331+
#[test]
332+
fn uint_to_float() {
333+
fn into_component_old(n: u8) -> f32 {
334+
let max = core::u8::MAX as f32;
335+
let scaled = n as f32 / max;
336+
scaled as f32
337+
}
338+
339+
for n in (0..=255).step_by(5) {
340+
assert_relative_eq!(
341+
IntoComponent::<f32>::into_component(n),
342+
into_component_old(n)
343+
)
344+
}
345+
}
346+
347+
#[test]
348+
fn uint_to_double() {
349+
fn into_component_old(n: u8) -> f64 {
350+
let max = core::u8::MAX as f64;
351+
let scaled = n as f64 / max;
352+
scaled as f64
353+
}
354+
355+
for n in (0..=255).step_by(5) {
356+
assert_relative_eq!(
357+
IntoComponent::<f64>::into_component(n),
358+
into_component_old(n)
359+
)
360+
}
361+
}
362+
}

palette/src/rgb/packed.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ use crate::Pixel;
3939
///
4040
/// let rgba = Srgba::from(0xFF80007F);
4141
/// assert_eq!(Srgba::new(0xFF, 0x80, 0x00, 0x7F), rgba);
42-
///
43-
/// // The second assert is essentially how library components are converted
44-
/// let float = 0.5f32 * 255.0;
45-
/// assert_eq!(0x7F, float as u32);
46-
/// assert_eq!(0x80, float.round() as u32);
4742
/// ```
4843
///
4944
/// When an `Rgb` type is packed, the alpha value will be `0xFF` in the

0 commit comments

Comments
 (0)