From 38c4ab7b3d030cd34960296f620cfe4d066de032 Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 20 Aug 2026 13:36:39 -0700 Subject: [PATCH] Simplify and optimize `Color32::from_rgba_unmultiplied` (#8427) So I first noticed that this function was using a large lookup table behind a OnceLock. I initially thought about just making it a const, but when looking at things further. I realized it could be made much simpler. If we just treated the numbers as fixed point we can get rid of any of the floating point calculations and especially divisions. You can see how efficiently this can compile down here: https://llvm.godbolt.org/z/K83jEjvdq You can see all three versions here: https://godbolt.org/z/nWc1as1nq * First one is basically the original essentially being called by: `ColorImage::from_rgba_unmultiplied()` * Second is the const Lookup table instead of the OnceLock and runtime generation. * Third is the fixed point implementation. At least looking at the bytes reported compiler explorer the OnceLock and the const Table results are in similar size, and the const table is surprisingly smaller when I compile to a binary object in compiler explorer. However though the oncelock is producing a lot SIMD instructions for initialization so I guess not too surprised. The fixed point math is much smaller than both. The const table is probably faster, but does bloat the binary images, and again when it's this fast to compute: https://llvm.godbolt.org/z/K83jEjvdq I am not sure the extra bytes are worth it. Next, what I did was merge `from_rgba_unmultiplied` and `from_rgba_unmultiplied_const`. Moreover with the fixed point math the `from_rgba_unmultiplied_const` is probably not necessary anymore, but it's part of the public API so I left it. Lastly, I just added a sanity test to make sure the math checks out which it does. You can even sweep the 2^16 inputs to be sure. --- crates/ecolor/src/color32.rs | 47 ++++++++++++------------------------ crates/ecolor/src/lib.rs | 11 +++++++++ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 68a8fc3d6..7a9cf72b7 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -1,4 +1,4 @@ -use crate::{Rgba, fast_round, linear_f32_from_linear_u8}; +use crate::{Rgba, fast_round, mul_frac_round}; /// This format is used for space-efficient color representation (32 bits). /// @@ -131,35 +131,10 @@ impl Color32 { /// but for transparent colors what you get back might be slightly different (rounding errors). #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - use std::sync::OnceLock; - match a { - // common-case optimization: - 0 => Self::TRANSPARENT, - - // common-case optimization: - 255 => Self::from_rgb(r, g, b), - - a => { - static LOOKUP_TABLE: OnceLock> = OnceLock::new(); - let lut = LOOKUP_TABLE.get_or_init(|| { - (0..=u16::MAX) - .map(|i| { - let [value, alpha] = i.to_ne_bytes(); - fast_round(value as f32 * linear_f32_from_linear_u8(alpha)) - }) - .collect() - }); - - let [r, g, b] = - [r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]); - Self::from_rgba_premultiplied(r, g, b, a) - } - } + Self::from_rgba_unmultiplied_const(r, g, b, a) } - /// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context. - /// - /// It is slightly slower when operating on non-const data. + /// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts. #[inline] pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self { match a { @@ -170,9 +145,9 @@ impl Color32 { 255 => Self::from_rgb(r, g, b), a => { - let r = fast_round(r as f32 * linear_f32_from_linear_u8(a)); - let g = fast_round(g as f32 * linear_f32_from_linear_u8(a)); - let b = fast_round(b as f32 * linear_f32_from_linear_u8(a)); + let r = mul_frac_round(r, a); + let g = mul_frac_round(g, a); + let b = mul_frac_round(b, a); Self::from_rgba_premultiplied(r, g, b, a) } } @@ -535,4 +510,14 @@ mod test { Color32::from_rgba_unmultiplied(255, 0, 0, 128) ); } + + #[test] + fn mul_frac_round_vs_old() { + for x in (0..=255u8).step_by(4) { + for a in (1..=255u8).step_by(4) { + let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a)); + assert_eq!(old, mul_frac_round(x, a)); + } + } + } } diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index ea7cff6f7..02ea96c51 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 { (r + 0.5) as _ // rust does a saturating cast since 1.45 } +/// Compute val * (frac/255) with no floating point or divisions. +#[inline] +const fn mul_frac_round(val: u8, frac: u8) -> u8 { + // Treat this as a simple fixed point calculation + let p = (val as u16) * (frac as u16) + 128; + ((p + (p >> 8)) >> 8) as u8 + // Logic split out a bit more. + //let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate. + //return ((p + 1 + (p >> 8)) >> 8) as u8; +} + #[test] pub fn test_srgba_conversion() { for b in 0..=255 {