From fff060d6383a382b7692ef78d0030d0032a97302 Mon Sep 17 00:00:00 2001 From: Tyler Critchlow Date: Sat, 12 Jul 2025 19:16:33 -0400 Subject: [PATCH 1/6] Preconvert colors to sRGB --- Cargo.toml | 11 + crates/bevy_ui_render/src/gradient.rs | 36 ++- crates/bevy_ui_render/src/gradient.wgsl | 307 ++++++++++-------------- examples/stress_tests/many_gradients.rs | 145 +++++++++++ 4 files changed, 321 insertions(+), 178 deletions(-) create mode 100644 examples/stress_tests/many_gradients.rs diff --git a/Cargo.toml b/Cargo.toml index f047040bdc9f7..373003861661c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3068,6 +3068,17 @@ description = "Test rendering of many UI elements" category = "Stress Tests" wasm = true +[[example]] +name = "many_gradients" +path = "examples/stress_tests/many_gradients.rs" +doc-scrape-examples = true + +[package.metadata.example.many_gradients] +name = "Many Gradients" +description = "Stress test for gradient rendering performance" +category = "Stress Tests" +wasm = true + [[example]] name = "many_cameras_lights" path = "examples/stress_tests/many_cameras_lights.rs" diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 12cfcbeb68cb8..f8770f72ffd80 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -7,7 +7,7 @@ use core::{ use super::shader_flags::BORDER_ALL; use crate::*; use bevy_asset::*; -use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_color::{ColorToComponents, Hsla, Hsva, LinearRgba, Oklaba, Oklcha, Srgba}; use bevy_ecs::{ prelude::Component, system::{ @@ -654,6 +654,36 @@ struct UiGradientVertex { hint: f32, } +fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] { + match space { + InterpolationColorSpace::OkLab => { + let oklaba: Oklaba = color.into(); + [oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha] + } + InterpolationColorSpace::OkLch | InterpolationColorSpace::OkLchLong => { + let oklcha: Oklcha = color.into(); + [oklcha.lightness, oklcha.chroma, oklcha.hue.to_radians(), oklcha.alpha] + } + InterpolationColorSpace::Srgb => { + let srgba: Srgba = color.into(); + [srgba.red, srgba.green, srgba.blue, srgba.alpha] + } + InterpolationColorSpace::LinearRgb => { + color.to_f32_array() + } + InterpolationColorSpace::Hsl | InterpolationColorSpace::HslLong => { + let hsla: Hsla = color.into(); + // Normalize hue to 0..1 range for shader + [hsla.hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha] + } + InterpolationColorSpace::Hsv | InterpolationColorSpace::HsvLong => { + let hsva: Hsva = color.into(); + // Normalize hue to 0..1 range for shader + [hsva.hue / 360.0, hsva.saturation, hsva.value, hsva.alpha] + } + } +} + pub fn prepare_gradient( mut commands: Commands, render_device: Res, @@ -804,8 +834,8 @@ pub fn prepare_gradient( continue; } } - let start_color = start_stop.0.to_f32_array(); - let end_color = end_stop.0.to_f32_array(); + let start_color = convert_color_to_space(start_stop.0, gradient.color_space); + let end_color = convert_color_to_space(end_stop.0, gradient.color_space); let mut stop_flags = flags; if 0. < start_stop.1 && (stop_index == gradient.stops_range.start || segment_count == 0) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54bc35eb146b8..4c640efffc3df 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -114,26 +114,6 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { } } -// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space. -fn mix_linear_rgba_in_srgba_space(a: vec4, b: vec4, t: f32) -> vec4 { - let a_srgb = pow(a.rgb, vec3(1. / 2.2)); - let b_srgb = pow(b.rgb, vec3(1. / 2.2)); - let mixed_srgb = mix(a_srgb, b_srgb, t); - return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); -} - -fn linear_rgba_to_oklaba(c: vec4) -> vec4 { - let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.); - let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.); - let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.); - return vec4( - 0.21045426 * l + 0.7936178 * m - 0.004072047 * s, - 1.9779985 * l - 2.4285922 * m + 0.4505937 * s, - 0.025904037 * l + 0.78277177 * m - 0.80867577 * s, - c.a - ); -} - fn oklaba_to_linear_rgba(c: vec4) -> vec4 { let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; @@ -149,33 +129,6 @@ fn oklaba_to_linear_rgba(c: vec4) -> vec4 { ); } -fn mix_linear_rgba_in_oklaba_space(a: vec4, b: vec4, t: f32) -> vec4 { - return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t)); -} - -fn linear_rgba_to_hsla(c: vec4) -> vec4 { - let max = max(max(c.r, c.g), c.b); - let min = min(min(c.r, c.g), c.b); - let l = (max + min) * 0.5; - if max == min { - return vec4(0., 0., l, c.a); - } else { - let delta = max - min; - let s = delta / (1. - abs(2. * l - 1.)); - var h = 0.; - if max == c.r { - h = ((c.g - c.b) / delta) % 6.; - } else if max == c.g { - h = ((c.b - c.r) / delta) + 2.; - } else { - h = ((c.r - c.g) / delta) + 4.; - } - h = h / 6.; - return vec4(h, s, l, c.a); - } -} - - fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { let h = hsl.x; let s = hsl.y; @@ -203,30 +156,6 @@ fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { return vec4(r + m, g + m, b + m, hsl.a); } -fn linear_rgba_to_hsva(c: vec4) -> vec4 { - let maxc = max(max(c.r, c.g), c.b); - let minc = min(min(c.r, c.g), c.b); - let delta = maxc - minc; - var h: f32 = 0.0; - var s: f32 = 0.0; - let v: f32 = maxc; - if delta != 0.0 { - s = delta / maxc; - if maxc == c.r { - h = ((c.g - c.b) / delta) % 6.0; - } else if maxc == c.g { - h = ((c.b - c.r) / delta) + 2.0; - } else { - h = ((c.r - c.g) / delta) + 4.0; - } - h = h / 6.0; - if h < 0.0 { - h = h + 1.0; - } - } - return vec4(h, s, v, c.a); -} - fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { let h = hsva.x * 6.0; let s = hsva.y; @@ -253,14 +182,6 @@ fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { return vec4(r + m, g + m, b + m, hsva.a); } -/// hue is left in radians and not converted to degrees -fn linear_rgba_to_oklcha(c: vec4) -> vec4 { - let o = linear_rgba_to_oklaba(c); - let chroma = sqrt(o.y * o.y + o.z * o.z); - let hue = atan2(o.z, o.y); - return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a); -} - fn oklcha_to_linear_rgba(c: vec4) -> vec4 { let a = c.y * cos(c.z); let b = c.y * sin(c.z); @@ -271,90 +192,6 @@ fn rem_euclid(a: f32, b: f32) -> f32 { return ((a % b) + b) % b; } -fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { - let diff = rem_euclid(b - a + PI, TAU) - PI; - return rem_euclid(a + diff * t, TAU); -} - -fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { - let diff = rem_euclid(b - a + PI, TAU) - PI; - return rem_euclid(a + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU); -} - -fn mix_oklcha(a: vec4, b: vec4, t: f32) -> vec4 { - let ah = select(a.z, b.z, a.y == 0.); - let bh = select(b.z, a.z, b.y == 0.); - return vec4( - mix(a.xy, b.xy, t), - lerp_hue(ah, bh, t), - mix(a.a, b.a, t) - ); -} - -fn mix_oklcha_long(a: vec4, b: vec4, t: f32) -> vec4 { - let ah = select(a.z, b.z, a.y == 0.); - let bh = select(b.z, a.z, b.y == 0.); - return vec4( - mix(a.xy, b.xy, t), - lerp_hue_long(ah, bh, t), - mix(a.w, b.w, t) - ); -} - -fn mix_linear_rgba_in_oklcha_space(a: vec4, b: vec4, t: f32) -> vec4 { - return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t)); -} - -fn mix_linear_rgba_in_oklcha_space_long(a: vec4, b: vec4, t: f32) -> vec4 { - return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t)); -} - -fn mix_linear_rgba_in_hsva_space(a: vec4, b: vec4, t: f32) -> vec4 { - let ha = linear_rgba_to_hsva(a); - let hb = linear_rgba_to_hsva(b); - var h: f32; - if ha.y == 0. { - h = hb.x; - } else if hb.y == 0. { - h = ha.x; - } else { - h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; - } - let s = mix(ha.y, hb.y, t); - let v = mix(ha.z, hb.z, t); - let a_alpha = mix(ha.a, hb.a, t); - return hsva_to_linear_rgba(vec4(h, s, v, a_alpha)); -} - -fn mix_linear_rgba_in_hsva_space_long(a: vec4, b: vec4, t: f32) -> vec4 { - let ha = linear_rgba_to_hsva(a); - let hb = linear_rgba_to_hsva(b); - let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU; - let s = mix(ha.y, hb.y, t); - let v = mix(ha.z, hb.z, t); - let a_alpha = mix(ha.a, hb.a, t); - return hsva_to_linear_rgba(vec4(h, s, v, a_alpha)); -} - -fn mix_linear_rgba_in_hsla_space(a: vec4, b: vec4, t: f32) -> vec4 { - let ha = linear_rgba_to_hsla(a); - let hb = linear_rgba_to_hsla(b); - let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; - let s = mix(ha.y, hb.y, t); - let l = mix(ha.z, hb.z, t); - let a_alpha = mix(ha.a, hb.a, t); - return hsla_to_linear_rgba(vec4(h, s, l, a_alpha)); -} - -fn mix_linear_rgba_in_hsla_space_long(a: vec4, b: vec4, t: f32) -> vec4 { - let ha = linear_rgba_to_hsla(a); - let hb = linear_rgba_to_hsla(b); - let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU; - let s = mix(ha.y, hb.y, t); - let l = mix(ha.z, hb.z, t); - let a_alpha = mix(ha.a, hb.a, t); - return hsla_to_linear_rgba(vec4(h, s, l, a_alpha)); -} // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. // The distance in gradient space is then used to interpolate between the start and end colors. @@ -386,6 +223,126 @@ fn conic_distance( return (((angle - start) % TAU) + TAU) % TAU; } +fn mix_oklcha(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.z - a.z; + var adjusted_hue = a.z; + if abs(hue_diff) > PI { + if hue_diff > 0.0 { + adjusted_hue = a.z + (hue_diff - TAU) * t; + } else { + adjusted_hue = a.z + (hue_diff + TAU) * t; + } + } else { + adjusted_hue = a.z + hue_diff * t; + } + return vec4( + mix(a.x, b.x, t), + mix(a.y, b.y, t), + rem_euclid(adjusted_hue, TAU), + mix(a.w, b.w, t) + ); +} + +fn mix_oklcha_long(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.z - a.z; + var adjusted_hue = a.z; + if abs(hue_diff) < PI { + if hue_diff >= 0.0 { + adjusted_hue = a.z + (hue_diff - TAU) * t; + } else { + adjusted_hue = a.z + (hue_diff + TAU) * t; + } + } else { + adjusted_hue = a.z + hue_diff * t; + } + return vec4( + mix(a.x, b.x, t), + mix(a.y, b.y, t), + rem_euclid(adjusted_hue, TAU), + mix(a.w, b.w, t) + ); +} + +fn mix_hsla(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.x - a.x; + var adjusted_hue = a.x; + if abs(hue_diff) > 0.5 { + if hue_diff > 0.0 { + adjusted_hue = a.x + (hue_diff - 1.0) * t; + } else { + adjusted_hue = a.x + (hue_diff + 1.0) * t; + } + } else { + adjusted_hue = a.x + hue_diff * t; + } + return vec4( + fract(adjusted_hue), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_hsla_long(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.x - a.x; + var adjusted_hue = a.x; + if abs(hue_diff) < 0.5 { + if hue_diff >= 0.0 { + adjusted_hue = a.x + (hue_diff - 1.0) * t; + } else { + adjusted_hue = a.x + (hue_diff + 1.0) * t; + } + } else { + adjusted_hue = a.x + hue_diff * t; + } + return vec4( + fract(adjusted_hue), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_hsva(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.x - a.x; + var adjusted_hue = a.x; + if abs(hue_diff) > 0.5 { + if hue_diff > 0.0 { + adjusted_hue = a.x + (hue_diff - 1.0) * t; + } else { + adjusted_hue = a.x + (hue_diff + 1.0) * t; + } + } else { + adjusted_hue = a.x + hue_diff * t; + } + return vec4( + fract(adjusted_hue), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_hsva_long(a: vec4, b: vec4, t: f32) -> vec4 { + let hue_diff = b.x - a.x; + var adjusted_hue = a.x; + if abs(hue_diff) < 0.5 { + if hue_diff >= 0.0 { + adjusted_hue = a.x + (hue_diff - 1.0) * t; + } else { + adjusted_hue = a.x + (hue_diff + 1.0) * t; + } + } else { + adjusted_hue = a.x + hue_diff * t; + } + return vec4( + fract(adjusted_hue), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + fn interpolate_gradient( distance: f32, start_color: vec4, @@ -426,23 +383,23 @@ fn interpolate_gradient( } else { t = 0.5 * (1 + (t - hint) / (1.0 - hint)); } - -#ifdef IN_SRGB - return mix_linear_rgba_in_srgba_space(start_color, end_color, t); -#else ifdef IN_OKLAB - return mix_linear_rgba_in_oklaba_space(start_color, end_color, t); -#else ifdef IN_OKLCH - return mix_linear_rgba_in_oklcha_space(start_color, end_color, t); +#ifdef IN_OKLCH + return oklcha_to_linear_rgba(mix_oklcha(start_color, end_color, t)); #else ifdef IN_OKLCH_LONG - return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t); + return oklcha_to_linear_rgba(mix_oklcha_long(start_color, end_color, t)); #else ifdef IN_HSV - return mix_linear_rgba_in_hsva_space(start_color, end_color, t); + return hsva_to_linear_rgba(mix_hsva(start_color, end_color, t)); #else ifdef IN_HSV_LONG - return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t); + return hsva_to_linear_rgba(mix_hsva_long(start_color, end_color, t)); #else ifdef IN_HSL - return mix_linear_rgba_in_hsla_space(start_color, end_color, t); + return hsla_to_linear_rgba(mix_hsla(start_color, end_color, t)); #else ifdef IN_HSL_LONG - return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t); + return hsla_to_linear_rgba(mix_hsla_long(start_color, end_color, t)); +#else ifdef IN_OKLAB + return oklaba_to_linear_rgba(mix(start_color, end_color, t)); +#else ifdef IN_SRGB + let mixed_srgb = mix(start_color, end_color, t); + return vec4(pow(mixed_srgb.rgb, vec3(2.2)), mixed_srgb.a); #else return mix(start_color, end_color, t); #endif diff --git a/examples/stress_tests/many_gradients.rs b/examples/stress_tests/many_gradients.rs new file mode 100644 index 0000000000000..27d8c546f975e --- /dev/null +++ b/examples/stress_tests/many_gradients.rs @@ -0,0 +1,145 @@ +//! Stress test demonstrating gradient performance improvements. +//! +//! This example creates many UI nodes with gradients to measure the performance +//! impact of pre-converting colors to the target color space on the CPU. + +use argh::FromArgs; +use bevy::{ + color::palettes::css::*, + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + ui::{BackgroundGradient, LinearGradient, ColorStop, Gradient, RepeatedGridTrack, Display, InterpolationColorSpace}, + window::{PresentMode, WindowResolution}, +}; + +const COLS: usize = 30; + +#[derive(FromArgs, Resource, Debug)] +/// Gradient stress test +struct Args { + /// how many gradients per group (default: 900) + #[argh(option, default = "900")] + gradient_count: usize, + + /// whether to animate gradients by changing colors + #[argh(switch)] + animate: bool, + + /// use sRGB interpolation + #[argh(switch)] + srgb: bool, + + /// use HSL interpolation + #[argh(switch)] + hsl: bool, +} + +fn main() { + let args: Args = argh::from_env(); + let total_gradients = args.gradient_count; + + println!("Gradient stress test with {} gradients", total_gradients); + println!("Color space: {}", if args.srgb { + "sRGB" + } else if args.hsl { + "HSL" + } else { + "OkLab (default)" + }); + + App::new() + .add_plugins(( + LogDiagnosticsPlugin::default(), + FrameTimeDiagnosticsPlugin::default(), + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Gradient Stress Test".to_string(), + resolution: WindowResolution::new(1920.0, 1080.0), + present_mode: PresentMode::AutoNoVsync, + ..default() + }), + ..default() + }), + )) + .insert_resource(args) + .add_systems(Startup, setup) + .add_systems(Update, animate_gradients) + .run(); +} + +fn setup(mut commands: Commands, args: Res) { + commands.spawn(Camera2d); + + let rows_to_spawn = (args.gradient_count + COLS - 1) / COLS; + + // Create a grid of gradients + commands + .spawn(Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + display: Display::Grid, + grid_template_columns: RepeatedGridTrack::flex(COLS as u16, 1.0), + grid_template_rows: RepeatedGridTrack::flex(rows_to_spawn as u16, 1.0), + ..default() + }) + .with_children(|parent| { + for i in 0..args.gradient_count { + let angle = (i as f32 * 10.0) % 360.0; + + let mut gradient = LinearGradient::new(angle, vec![ + ColorStop::new(RED, Val::Percent(0.0)), + ColorStop::new(BLUE, Val::Percent(100.0)), + ]); + + gradient.color_space = if args.srgb { + InterpolationColorSpace::Srgb + } else if args.hsl { + InterpolationColorSpace::Hsl + } else { + InterpolationColorSpace::OkLab + }; + + parent.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + ..default() + }, + BackgroundGradient(vec![Gradient::Linear(gradient)]), + GradientNode { index: i }, + )); + } + }); +} + +#[derive(Component)] +struct GradientNode { + index: usize, +} + +fn animate_gradients( + mut gradients: Query<(&mut BackgroundGradient, &GradientNode)>, + args: Res, + time: Res