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);