From d328eb18b6dc6657b102533be266311cd6d305b5 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Tue, 12 Aug 2025 12:47:20 +0000 Subject: [PATCH 1/5] Tell the GPU that the UI texture is in sRGB format so that it is converted to linear before sampling --- desktop/src/render/graphics_state.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index 85a9615315..b902eef597 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -285,7 +285,12 @@ impl GraphicsState { fn update_bindgroup(&mut self) { let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); - let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); + + // Tell the GPU that the UI texture is in sRGB format so that it is converted to linear before sampling + let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor { + format: Some(wgpu::TextureFormat::Bgra8UnormSrgb), + ..Default::default() + }); let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &self.render_pipeline.get_bind_group_layout(0), From 0e47d7e9316a15f8daa5ede1fc8116f1d2ae129a Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 13 Aug 2025 13:49:05 +0000 Subject: [PATCH 2/5] Do blending in srgb --- desktop/src/render/composite_shader.wgsl | 56 +++++++++++++++++------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index 3232e44f96..7f373ab32a 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -44,34 +44,36 @@ var s_diffuse: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let ui = textureSample(t_ui, s_diffuse, in.tex_coords); - if (ui.a >= 0.999) { - return ui; + let ui_raw = textureSample(t_ui, s_diffuse, in.tex_coords); + if (ui_raw.a >= 0.999) { + return ui_raw; } let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; // Vello renders its values to an `RgbaUnorm` texture, but if we try to use this in the main rendering pipeline // which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate. - let overlay_raw = textureSample(t_overlays, s_diffuse, viewport_coordinate); - let overlay = vec4(srgb_to_linear(overlay_raw.rgb), overlay_raw.a); - let viewport_raw = textureSample(t_viewport, s_diffuse, viewport_coordinate); - let viewport = vec4(srgb_to_linear(viewport_raw.rgb), viewport_raw.a); + let overlay = srgb_to_linear(textureSample(t_overlays, s_diffuse, viewport_coordinate)); + let viewport = srgb_to_linear(textureSample(t_viewport, s_diffuse, viewport_coordinate)); + + // UI texture is premultiplied, we need to unpremultiply before blending + let ui = unpremultiply(ui_raw); if (overlay.a < 0.001) { - return blend(ui, viewport); + return blend_in_srgb(ui, viewport); } - let composite = blend(overlay, viewport); - return blend(ui, composite); + let composite = blend_in_srgb(overlay, viewport); + return blend_in_srgb(ui, composite); } -fn srgb_to_linear(srgb: vec3) -> vec3 { - return select( - pow((srgb + 0.055) / 1.055, vec3(2.4)), - srgb / 12.92, - srgb <= vec3(0.04045) - ); +fn blend_in_srgb( + fg: vec4, bg: vec4, +) -> vec4 { + let bg_srgb = linear_to_srgb(bg); + let fg_srgb = linear_to_srgb(fg); + let out_srgb = blend(fg_srgb, bg_srgb); + return srgb_to_linear(out_srgb); } fn blend(fg: vec4, bg: vec4) -> vec4 { @@ -79,3 +81,25 @@ fn blend(fg: vec4, bg: vec4) -> vec4 { let rgb = fg.rgb * fg.a + bg.rgb * bg.a * (1.0 - fg.a); return vec4(rgb, a); } + +fn unpremultiply(in: vec4) -> vec4 { + if (in.a > 0.0) { + return vec4((in.rgb / in.a), in.a); + } else { + return vec4(0.0); + } +} + +fn linear_to_srgb(in: vec4) -> vec4 { + let cutoff = vec3(0.0031308); + let lo = in.rgb * 12.92; + let hi = 1.055 * pow(max(in.rgb, vec3(0.0)), vec3(1.0/2.4)) - 0.055; + return vec4(select(lo, hi, in.rgb > cutoff), in.a); +} + +fn srgb_to_linear(in: vec4) -> vec4 { + let cutoff = vec3(0.04045); + let lo = in.rgb / 12.92; + let hi = pow((in.rgb + 0.055) / 1.055, vec3(2.4)); + return vec4(select(lo, hi, in.rgb > cutoff), in.a); +} From 4211002508190931cb5ae1658289cca04f309e01 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 13 Aug 2025 13:49:37 +0000 Subject: [PATCH 3/5] Revert "Tell the GPU that the UI texture is in sRGB format so that it is converted to linear before sampling" This reverts commit d328eb18b6dc6657b102533be266311cd6d305b5. --- desktop/src/render/graphics_state.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index b902eef597..85a9615315 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -285,12 +285,7 @@ impl GraphicsState { fn update_bindgroup(&mut self) { let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); - - // Tell the GPU that the UI texture is in sRGB format so that it is converted to linear before sampling - let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor { - format: Some(wgpu::TextureFormat::Bgra8UnormSrgb), - ..Default::default() - }); + let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &self.render_pipeline.get_bind_group_layout(0), From 5fed5da8a4936a8cf6f36a2a1cb7e4ced91eb0f8 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 13 Aug 2025 21:36:56 +0000 Subject: [PATCH 4/5] Remove unnecessary srgb linear conversions --- desktop/src/render/composite_shader.wgsl | 47 +++++++++--------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index 7f373ab32a..ede3800740 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -44,36 +44,25 @@ var s_diffuse: sampler; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - let ui_raw = textureSample(t_ui, s_diffuse, in.tex_coords); - if (ui_raw.a >= 0.999) { - return ui_raw; + let ui_linear = textureSample(t_ui, s_diffuse, in.tex_coords); + if (ui_linear.a >= 0.999) { + return ui_linear; } let viewport_coordinate = (in.tex_coords - constants.viewport_offset) * constants.viewport_scale; - // Vello renders its values to an `RgbaUnorm` texture, but if we try to use this in the main rendering pipeline - // which renders to an `Srgb` surface, gamma mapping is applied twice. This converts back to linear to compensate. - let overlay = srgb_to_linear(textureSample(t_overlays, s_diffuse, viewport_coordinate)); - let viewport = srgb_to_linear(textureSample(t_viewport, s_diffuse, viewport_coordinate)); + let overlay_srgb = textureSample(t_overlays, s_diffuse, viewport_coordinate); + let viewport_srgb = textureSample(t_viewport, s_diffuse, viewport_coordinate); // UI texture is premultiplied, we need to unpremultiply before blending - let ui = unpremultiply(ui_raw); + let ui_srgb = linear_to_srgb(unpremultiply(ui_linear)); - if (overlay.a < 0.001) { - return blend_in_srgb(ui, viewport); + if (overlay_srgb.a < 0.001) { + return srgb_to_linear(blend(ui_srgb, viewport_srgb)); } - let composite = blend_in_srgb(overlay, viewport); - return blend_in_srgb(ui, composite); -} - -fn blend_in_srgb( - fg: vec4, bg: vec4, -) -> vec4 { - let bg_srgb = linear_to_srgb(bg); - let fg_srgb = linear_to_srgb(fg); - let out_srgb = blend(fg_srgb, bg_srgb); - return srgb_to_linear(out_srgb); + let composite_srgb = blend(overlay_srgb, viewport_srgb); + return srgb_to_linear(blend(ui_srgb, composite_srgb)); } fn blend(fg: vec4, bg: vec4) -> vec4 { @@ -82,14 +71,6 @@ fn blend(fg: vec4, bg: vec4) -> vec4 { return vec4(rgb, a); } -fn unpremultiply(in: vec4) -> vec4 { - if (in.a > 0.0) { - return vec4((in.rgb / in.a), in.a); - } else { - return vec4(0.0); - } -} - fn linear_to_srgb(in: vec4) -> vec4 { let cutoff = vec3(0.0031308); let lo = in.rgb * 12.92; @@ -103,3 +84,11 @@ fn srgb_to_linear(in: vec4) -> vec4 { let hi = pow((in.rgb + 0.055) / 1.055, vec3(2.4)); return vec4(select(lo, hi, in.rgb > cutoff), in.a); } + +fn unpremultiply(in: vec4) -> vec4 { + if (in.a > 0.0) { + return vec4((in.rgb / in.a), in.a); + } else { + return vec4(0.0); + } +} From 16e642e7b0de0791d9d3d528a8bb8bc59e9405b5 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 14 Aug 2025 10:43:24 +0000 Subject: [PATCH 5/5] Blend overlays and viewport in linear and return early when ui is fully transparent --- desktop/src/render/composite_shader.wgsl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/desktop/src/render/composite_shader.wgsl b/desktop/src/render/composite_shader.wgsl index ede3800740..27560612d6 100644 --- a/desktop/src/render/composite_shader.wgsl +++ b/desktop/src/render/composite_shader.wgsl @@ -58,11 +58,20 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let ui_srgb = linear_to_srgb(unpremultiply(ui_linear)); if (overlay_srgb.a < 0.001) { - return srgb_to_linear(blend(ui_srgb, viewport_srgb)); + if (ui_srgb.a < 0.001) { + return srgb_to_linear(viewport_srgb); + } else { + return srgb_to_linear(blend(ui_srgb, viewport_srgb)); + } } - let composite_srgb = blend(overlay_srgb, viewport_srgb); - return srgb_to_linear(blend(ui_srgb, composite_srgb)); + let composite_linear = blend(srgb_to_linear(overlay_srgb), srgb_to_linear(viewport_srgb)); + + if (ui_srgb.a < 0.001) { + return composite_linear; + } + + return srgb_to_linear(blend(ui_srgb, linear_to_srgb(composite_linear))); } fn blend(fg: vec4, bg: vec4) -> vec4 {