blade: Switch to linear color space (#11534)

Dzmitry Malyshau created

Release Notes:

- N/A

## What

Addresses a long-standing issue of doing the blending operations in sRGB
space. Currently, the input HSL colors are provided in sRGB space and
converted to linear in the vertex shader. Conversion back to sRGB, which
is required on most platforms today, happens at the very end of the
pipeline when writing into sRGB render target.

Note-1: in the future we may consider doing HSL -> sRGB -> Linear
transform on CPU before feeding into shaders. However, I don't expect
any significant difference here given that we are likely bound by fill
rate and pixel shaders, anyway.

Note-2: the graphics stack is programmed to detect if the platform
supports presenting in linear color space and avoids converting to sRGB
at the end in this case. However, on my Z13 laptop this isn't supported
by the RADV driver.

Closes #7684 
Closes #11462
@jansol please confirm if you can!

## Comparison

Screenshot of the Glazier theme before the change:

![glazier-old](https://github.com/zed-industries/zed/assets/107301/6a9552e1-0819-4a4e-8121-8d62ec012bf4)
Same theme after the change:

![glazier-new](https://github.com/zed-industries/zed/assets/107301/4e61c422-4a4b-4c4b-84a3-55680626d681)

Change summary

Cargo.lock                                       |  4 +-
Cargo.toml                                       |  4 +-
crates/gpui/src/platform/blade/blade_atlas.rs    |  2 
crates/gpui/src/platform/blade/blade_renderer.rs |  4 --
crates/gpui/src/platform/blade/shaders.wgsl      | 21 ++++++++++++++---
5 files changed, 23 insertions(+), 12 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1491,7 +1491,7 @@ dependencies = [
 [[package]]
 name = "blade-graphics"
 version = "0.4.0"
-source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
+source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
 dependencies = [
  "ash",
  "ash-window",
@@ -1521,7 +1521,7 @@ dependencies = [
 [[package]]
 name = "blade-macros"
 version = "0.2.1"
-source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
+source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
 dependencies = [
  "proc-macro2",
  "quote",

Cargo.toml 🔗

@@ -256,8 +256,8 @@ async-recursion = "1.0.0"
 async-tar = "0.4.2"
 async-trait = "0.1"
 bitflags = "2.4.2"
-blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
-blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
+blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
+blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
 cap-std = "3.0"
 chrono = { version = "0.4", features = ["serde"] }
 clap = { version = "4.4", features = ["derive"] }

crates/gpui/src/platform/blade/blade_atlas.rs 🔗

@@ -162,7 +162,7 @@ impl BladeAtlasState {
                 usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
             }
             AtlasTextureKind::Polychrome => {
-                format = gpu::TextureFormat::Bgra8Unorm;
+                format = gpu::TextureFormat::Bgra8UnormSrgb;
                 usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
             }
             AtlasTextureKind::Path => {

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -360,9 +360,7 @@ impl BladeRenderer {
             size: config.size,
             usage: gpu::TextureUsage::TARGET,
             display_sync: gpu::DisplaySync::Recent,
-            //Note: this matches the original logic of the Metal backend,
-            // but ultimaterly we need to switch to `Linear`.
-            color_space: gpu::ColorSpace::Srgb,
+            color_space: gpu::ColorSpace::Linear,
             allow_exclusive_full_screen: false,
             transparent: config.transparent,
         };

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -88,6 +88,14 @@ fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds:
     return distance_from_clip_rect_impl(position, clip_bounds);
 }
 
+// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
+fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
+    let cutoff = srgb < vec3<f32>(0.04045);
+    let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
+    let lower = srgb / vec3<f32>(12.92);
+    return select(higher, lower, cutoff);
+}
+
 fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
     let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
     let s = hsla.s;
@@ -97,8 +105,7 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
     let c = (1.0 - abs(2.0 * l - 1.0)) * s;
     let x = c * (1.0 - abs(h % 2.0 - 1.0));
     let m = l - c / 2.0;
-
-    var color = vec4<f32>(m, m, m, a);
+    var color = vec3<f32>(m);
 
     if (h >= 0.0 && h < 1.0) {
         color.r += c;
@@ -120,7 +127,12 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
         color.b += x;
     }
 
-    return color;
+    // Input colors are assumed to be in sRGB space,
+    // but blending and rendering needs to happen in linear space.
+    // The output will be converted to sRGB by either the target
+    // texture format or the swapchain color space.
+    let linear = srgb_to_linear(color);
+    return vec4<f32>(linear, a);
 }
 
 fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
@@ -181,7 +193,8 @@ fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
 // target alpha compositing mode.
 fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
     let alpha = color.a * alpha_factor;
-    return select(vec4<f32>(color.rgb, alpha), vec4<f32>(color.rgb, 1.0) * alpha, globals.premultiplied_alpha != 0u);
+    let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
+    return vec4<f32>(color.rgb * multiplier, alpha);
 }
 
 // --- quads --- //