From a833fb118f1976cf377ed054bfd2d485558df2da Mon Sep 17 00:00:00 2001 From: CPritch Date: Sun, 11 Oct 2026 16:06:18 +0100 Subject: [PATCH] fix: encode sRGB in the shader when the surface format isn't sRGB Some GLES screens offer no sRGB format, so the renderer drew to an Rgba8Unorm surface, which stores what it's given: linear colour, shown too dark. A pipeline constant, ENCODE_SRGB, now has the fragment shader encode its output for the unorm formats without the sRGB suffix. Float formats hold linear colour and are left as they were. On an Android emulator whose GLES screen has no sRGB format, a photo drew at a mean brightness of 163 against 193 on Vulkan; with this it draws at 193 on both. --- CHANGELOG.md | 1 + crates/render/src/renderer.rs | 30 +++++++++++++++++++++++++++++- crates/render/src/shader.wgsl | 22 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cbded..99b68c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixed - Mali's Vulkan driver no longer crashes compiling the shader. It couldn't cope with textures passed to functions. +- Wallpapers no longer draw too dark on screens that offer no sRGB format, as some GLES ones don't. The shader now encodes the colour itself for an `Rgba8Unorm`, `Bgra8Unorm` or `Rgb10a2Unorm` surface. ## 0.4.3 - 2026-10-09 diff --git a/crates/render/src/renderer.rs b/crates/render/src/renderer.rs index 97a3f8f..5f482a1 100644 --- a/crates/render/src/renderer.rs +++ b/crates/render/src/renderer.rs @@ -1099,6 +1099,12 @@ impl Renderer { source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), }); + // A screen without an sRGB format gets colour the shader encodes itself. + let encode_srgb = if needs_srgb_encoding(format) { + 1.0 + } else { + 0.0 + }; let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("shiftpaper_rp"), layout: Some(&pipeline_layout), @@ -1116,7 +1122,10 @@ impl Renderer { blend: None, write_mask: wgpu::ColorWrites::ALL, })], - compilation_options: Default::default(), + compilation_options: wgpu::PipelineCompilationOptions { + constants: &[("ENCODE_SRGB", encode_srgb)], + ..Default::default() + }, }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -1443,10 +1452,29 @@ fn create_placeholder_depth(device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu: tex } +/// Whether `format` stores colour sRGB-encoded without the GPU encoding it +/// on write, so the shader has to: an 8 or 10 bit unorm format without the +/// sRGB suffix. Float formats hold linear colour, so they're left alone. +fn needs_srgb_encoding(format: wgpu::TextureFormat) -> bool { + use wgpu::TextureFormat::*; + matches!(format, Rgba8Unorm | Bgra8Unorm | Rgb10a2Unorm) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn only_unorm_formats_without_srgb_are_encoded_by_the_shader() { + use wgpu::TextureFormat::*; + for format in [Rgba8Unorm, Bgra8Unorm, Rgb10a2Unorm] { + assert!(needs_srgb_encoding(format), "{format:?}"); + } + for format in [Rgba8UnormSrgb, Bgra8UnormSrgb, Rgba16Float] { + assert!(!needs_srgb_encoding(format), "{format:?}"); + } + } + fn assert_close(actual: [f32; 2], expected: [f32; 2]) { assert!( (actual[0] - expected[0]).abs() < 1e-6 && (actual[1] - expected[1]).abs() < 1e-6, diff --git a/crates/render/src/shader.wgsl b/crates/render/src/shader.wgsl index 52254c9..e645815 100644 --- a/crates/render/src/shader.wgsl +++ b/crates/render/src/shader.wgsl @@ -479,8 +479,30 @@ fn mix_oklab(a: vec4, b: vec4, t: f32) -> vec4 { return vec4(clamp(from_oklab(lab), vec3(0.0), vec3(1.0)), mix(a.a, b.a, t)); } +// Whether the screen's format stores colour sRGB-encoded without saying so, +// like Rgba8Unorm, so the shader encodes it rather than the GPU. Set by the +// renderer from the format it draws in. A screen offering no sRGB format, +// as some GLES ones don't, would otherwise show linear colour, too dark. +override ENCODE_SRGB: bool = false; + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let color = shade(in); + if ENCODE_SRGB { + return vec4(linear_to_srgb(color.rgb), color.a); + } + return color; +} + +// Linear colour sRGB-encoded, as an sRGB format's write does. +fn linear_to_srgb(c: vec3) -> vec3 { + let low = c * 12.92; + let high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055; + return select(high, low, c <= vec3(0.0031308)); +} + +// The colour of one pixel of the wallpaper, in linear sRGB. +fn shade(in: VertexOutput) -> vec4 { let uv = crop(in.uv, u.uv_scale, u.uv_center); // Depth comes from the same image position as the colour. var depth = load_depth(false, uv);