Skip to content

Preconvert colors before sending to shader #20074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

tylercritchlow
Copy link

Objective

Solution

  • Modified prepare_gradient in gradient.rs to convert colors from LinearRgba to Srgba on the CPU before sending to the GPU
  • Updated the gradient shader to remove per-pixel color space conversions since colors are now pre-converted
  • Added documentation to clarify that vertex colors are in sRGB space

This optimization reduces the number of power operations per pixel from 3 to 1:

  • Before: Convert start color to sRGB, convert end color to sRGB, mix, convert back to linear (3 pow operations per pixel)
  • After: Colors pre-converted on CPU, mix in sRGB space, convert back to linear (1 pow operation per pixel)

Testing

  • Verified that the UI gradient examples (cargo run --example gradients) compile and render correctly
  • The visual output should remain identical while performance improves, especially for large gradient areas
  • Changes maintain the same color interpolation behavior (mixing in sRGB space)

To test:

  1. Run cargo run --example gradients or cargo run --example stacked_gradients
  2. Verify gradients render correctly

@alice-i-cecile alice-i-cecile requested a review from ickshonpe July 10, 2025 02:50
@alice-i-cecile
Copy link
Member

Previously attempted in #20073

@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times C-Code-Quality A section of code that is hard to understand or change S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 10, 2025
@alice-i-cecile alice-i-cecile added the A-UI Graphical user interfaces, styles, layouts, and widgets label Jul 10, 2025
@alice-i-cecile alice-i-cecile added this to the 0.17 milestone Jul 10, 2025
Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation is fine. The snag is that interpolation in eight more colour spaces has been added since the initial gradients PR (not sure why there aren't merge conflicts). So this is a bit more involved and needs a match statement or something to select a preconversion method. Also the hues should be normalized for interpolation in cylindrical color spaces.

}

// Only color interpolation in SRGB space is supported atm.
return mix_linear_rgb_in_srgb_space(start_color, end_color, t);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now:

#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);
#else ifdef IN_OKLCH_LONG
   return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
#else ifdef IN_HSV
   return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
#else ifdef IN_HSV_LONG
   return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
#else ifdef IN_HSL
   return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
#else ifdef IN_HSL_LONG
   return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
#else
   return mix(start_color, end_color, t);
#endif

@ickshonpe
Copy link
Contributor

ickshonpe commented Jul 10, 2025

It'd be nice to have some performance profiling data too, even though this seems like it will be an obvious improvement.

@tylercritchlow
Copy link
Author

tylercritchlow commented Jul 12, 2025

It'd be nice to have some performance profiling data too, even though this seems like it will be an obvious improvement.

Is there examples of how to do this? That is the performance profiling.

@ickshonpe
Copy link
Contributor

ickshonpe commented Jul 12, 2025

It'd be nice to have some performance profiling data too, even though this seems like it will be an obvious improvement.

Is there examples of how to do this? That is the performance profiling.

I normally use tracy, there's a section on using it with bevy here:
https://github.com/bevyengine/bevy/blob/main/docs/profiling.md

But for this it would be fine just to copy from the benchmark examples in examples/stress_tests which use the FrameTimeDiagnosticsPlugin and LogDiagnosticsPlugin plugins to report the FPS.

@tylercritchlow
Copy link
Author

tylercritchlow commented Jul 12, 2025

Performance Results

Tested with 900 gradients using a new stress test example on MacBook Pro M4:

Color Space FPS Comparison

Color Space Baseline FPS Optimized FPS
sRGB 99.64 fps 97.26 fps
HSL 95.61 fps 97.30 fps
OkLab 91.27 fps 97.06 fps

With Animation Enabled (OkLab)

Mode FPS
Baseline 93.99 fps
Optimized 98.11 fps

Not much difference, but idk if a bigger improvement is expected.

@tylercritchlow tylercritchlow requested a review from ickshonpe July 12, 2025 23:18
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

1 similar comment
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

@alice-i-cecile
Copy link
Member

Not much difference, but idk if a bigger improvement is expected.

Yeah, I would expect this to be slight.

@tylercritchlow tylercritchlow changed the title Preconvert to srgb Preconvert colors before sending to shader Jul 14, 2025
Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good, the complexity is reduced and the performance gains are actually quite significant.

Just needs some changes to fix the fill modes.

mix(a.w, b.w, t)
);
}

fn interpolate_gradient(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interpolation gets skipped when the FILL_START/END flags are set so start_color and end_color are still in the interpolation color space and need to be converted to linear rgba before they are returned.

@@ -426,23 +383,23 @@ fn interpolate_gradient(
} else {
t = 0.5 * (1 + (t - hint) / (1.0 - hint));
}

Copy link
Contributor

@ickshonpe ickshonpe Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed version of interpolate_gradient I wrote to test it after I thought I'd figured out what was wrong with the fill modes:

fn interpolate_gradient(
    distance: f32,
    start_color: vec4<f32>,
    start_distance: f32,
    end_color: vec4<f32>,
    end_distance: f32,
    hint: f32,
    flags: u32,
) -> vec4<f32> {
    if start_distance == end_distance {
        if distance <= start_distance && enabled(flags, FILL_START) {
            return convert_to_linear_rgba(start_color);
        }
        if start_distance <= distance && enabled(flags, FILL_END) {
            return convert_to_linear_rgba(end_color);
        }
        return vec4(0.);
    }

    var t = (distance - start_distance) / (end_distance - start_distance);

    if t < 0.0 {
        if enabled(flags, FILL_START) {
            return convert_to_linear_rgba(start_color);
        }
        return vec4(0.0);
    }

    if 1. < t {
        if enabled(flags, FILL_END) {
            return convert_to_linear_rgba(end_color);
        }
        return vec4(0.0);
    }

    if t < hint {
        t = 0.5 * t / hint;
    } else {
        t = 0.5 * (1 + (t - hint) / (1.0 - hint));
    }

    return convert_to_linear_rgba(mix_colors(start_color, end_color, t));
}

// Mix the colors, choosing the appropriate interpolation method for the given color space
fn mix_colors(
    start_color: vec4<f32>,
    end_color: vec4<f32>,
    t: f32,
) -> vec4<f32> {
#ifdef IN_OKLCH
    return mix_oklcha(start_color, end_color, t);
#else ifdef IN_OKLCH_LONG
    return mix_oklcha_long(start_color, end_color, t);
#else ifdef IN_HSV
    return mix_hsva(start_color, end_color, t);
#else ifdef IN_HSV_LONG
    return mix_hsva_long(start_color, end_color, t);
#else ifdef IN_HSL
    return mix_hsla(start_color, end_color, t);
#else ifdef IN_HSL_LONG
    return mix_hsla_long(start_color, end_color, t);
#else
    // Just lerp in linear RGBA, OkLab and SRGBA spaces
    return mix(start_color, end_color, t);
#endif
}

// Convert a color from the interpolation color space to linear rgba
fn convert_to_linear_rgba(
    color: vec4<f32>,
) -> vec4<f32> {
#ifdef IN_OKLCH
    return oklcha_to_linear_rgba(color);
#else ifdef IN_OKLCH_LONG
    return oklcha_to_linear_rgba(color);
#else ifdef IN_HSV
    return hsva_to_linear_rgba(color);
#else ifdef IN_HSV_LONG
    return hsva_to_linear_rgba(color);
#else ifdef IN_HSL
    return hsla_to_linear_rgba(color);
#else ifdef IN_HSL_LONG
    return hsla_to_linear_rgba(color);
#else ifdef IN_OKLAB
    return oklaba_to_linear_rgba(color);
#else ifdef IN_SRGB
    return vec4(pow(color.rgb, vec3(2.2)), color.a);
#else
    // Color is already in linear rgba space
    return color;
#endif
}

Comment on lines 90 to 91
ColorStop::new(RED, Val::Percent(0.0)),
ColorStop::new(BLUE, Val::Percent(100.0)),
Copy link
Contributor

@ickshonpe ickshonpe Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs more colour stops to capture the full benefits, otherwise the gains are masked by the overhead from layout. With this gradient:

                        ColorStop::auto(RED),
                        ColorStop::auto(BLUE),
                        ColorStop::auto(GREEN),
                        ColorStop::auto(YELLOW),
                        ColorStop::auto(ORANGE),
                        ColorStop::auto(LIME),
                        ColorStop::auto(DARK_CYAN),

I get ~30% better FPS compared to main.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 14, 2025
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HSL and HSV conversions are off a bit. If you look at the gradients example the bottom set of striped gradients uses hard stops so there is no interpolation (the conic excepted) so the gradients should look the same for each colour space, but in HSL and HSV you can see that the colours are lighter:

In Oklaba

oklaba

In Hsla

in_hsla

Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think the problem with the HSL conversions is in main too, except that when there is a hard stop the colour is already linear RGBA so it isn't converted. Everything else is okay, once the CI is happy I think this is good to merge.

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20074

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Jul 15, 2025

Ping me when CI is green and I'll do a review and merge this :) The Dependencies failure is not your fault.

@ickshonpe ickshonpe added the M-Deliberate-Rendering-Change An intentional change to how tests and examples are rendered label Jul 15, 2025
@tylercritchlow
Copy link
Author

@alice-i-cecile CI is green

@ickshonpe ickshonpe added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen A-UI Graphical user interfaces, styles, layouts, and widgets C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times M-Deliberate-Rendering-Change An intentional change to how tests and examples are rendered S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Preconvert colours before sending them to the UI gradients shader
3 participants