From e72021a26bf3e7e31d736eaaea89d6914149a9bf Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 25 Sep 2025 16:02:27 +0300 Subject: [PATCH] Implement perceptual gamma / contrast correction for Linux font rendering (#38862) Part of https://github.com/zed-industries/zed/issues/7992 Port of https://github.com/zed-industries/zed/pull/37167 to Linux When using Blade rendering (Linux platforms and self-compiled builds with the Blade renderer enabled), Zed reads `ZED_FONTS_GAMMA` and `ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` environment variables for the values to use for font rendering. `ZED_FONTS_GAMMA` corresponds to [getgamma](https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwriterenderingparams-getgamma) values. Allowed range [1.0, 2.2], other values are clipped. Default: 1.8 `ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` corresponds to [getgrayscaleenhancedcontrast](https://learn.microsoft.com/en-us/windows/win32/api/dwrite_1/nf-dwrite_1-idwriterenderingparams1-getgrayscaleenhancedcontrast) values. Allowed range: [0.0, ..), other values are clipped. Default: 1.0 Screenshots (left is Nightly, right is the new code): * Non-lodpi display With the defaults: image With `env ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST=7777`: image Lodpi, default settings: image Lodpi, font size 7: image Release Notes: - Implement perceptual gamma / contrast correction for Linux font rendering --------- Co-authored-by: localcc --- .../gpui/src/platform/blade/blade_renderer.rs | 120 +++++++++++++++--- crates/gpui/src/platform/blade/shaders.wgsl | 35 ++++- docs/src/linux.md | 12 ++ 3 files changed, 149 insertions(+), 18 deletions(-) diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 1f60920bcc928c97c1f2b2c06e22ed235217c87e..7796af806f047171b16e40c2e04e77f1aed41a8d 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -83,6 +83,8 @@ struct ShaderUnderlinesData { #[derive(blade_macros::ShaderData)] struct ShaderMonoSpritesData { globals: GlobalParams, + gamma_ratios: [f32; 4], + grayscale_enhanced_contrast: f32, t_sprite: gpu::TextureView, s_sprite: gpu::Sampler, b_mono_sprites: gpu::BufferPiece, @@ -334,11 +336,11 @@ pub struct BladeRenderer { atlas_sampler: gpu::Sampler, #[cfg(target_os = "macos")] core_video_texture_cache: CVMetalTextureCache, - path_sample_count: u32, path_intermediate_texture: gpu::Texture, path_intermediate_texture_view: gpu::TextureView, path_intermediate_msaa_texture: Option, path_intermediate_msaa_texture_view: Option, + rendering_parameters: RenderingParameters, } impl BladeRenderer { @@ -364,17 +366,12 @@ impl BladeRenderer { name: "main", buffer_count: 2, }); - // workaround for https://github.com/zed-industries/zed/issues/26143 - let path_sample_count = std::env::var("ZED_PATH_SAMPLE_COUNT") - .ok() - .and_then(|v| v.parse().ok()) - .or_else(|| { - [4, 2, 1] - .into_iter() - .find(|&n| (context.gpu.capabilities().sample_count_mask & n) != 0) - }) - .unwrap_or(1); - let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count); + let rendering_parameters = RenderingParameters::from_env(context); + let pipelines = BladePipelines::new( + &context.gpu, + surface.info(), + rendering_parameters.path_sample_count, + ); let instance_belt = BufferBelt::new(BufferBeltDescriptor { memory: gpu::Memory::Shared, min_chunk_size: 0x1000, @@ -401,7 +398,7 @@ impl BladeRenderer { surface.info().format, config.size.width, config.size.height, - path_sample_count, + rendering_parameters.path_sample_count, ) .unzip(); @@ -425,11 +422,11 @@ impl BladeRenderer { atlas_sampler, #[cfg(target_os = "macos")] core_video_texture_cache, - path_sample_count, path_intermediate_texture, path_intermediate_texture_view, path_intermediate_msaa_texture, path_intermediate_msaa_texture_view, + rendering_parameters, }) } @@ -506,7 +503,7 @@ impl BladeRenderer { self.surface.info().format, gpu_size.width, gpu_size.height, - self.path_sample_count, + self.rendering_parameters.path_sample_count, ) .unzip(); self.path_intermediate_msaa_texture = path_intermediate_msaa_texture; @@ -521,8 +518,11 @@ impl BladeRenderer { self.gpu .reconfigure_surface(&mut self.surface, self.surface_config); self.pipelines.destroy(&self.gpu); - self.pipelines = - BladePipelines::new(&self.gpu, self.surface.info(), self.path_sample_count); + self.pipelines = BladePipelines::new( + &self.gpu, + self.surface.info(), + self.rendering_parameters.path_sample_count, + ); } } @@ -783,6 +783,10 @@ impl BladeRenderer { 0, &ShaderMonoSpritesData { globals, + gamma_ratios: self.rendering_parameters.gamma_ratios, + grayscale_enhanced_contrast: self + .rendering_parameters + .grayscale_enhanced_contrast, t_sprite: tex_info.raw_view, s_sprite: self.atlas_sampler, b_mono_sprites: instance_buf, @@ -984,3 +988,85 @@ fn create_msaa_texture_if_needed( Some((texture_msaa, texture_view_msaa)) } + +/// A set of parameters that can be set using a corresponding environment variable. +struct RenderingParameters { + // Env var: ZED_PATH_SAMPLE_COUNT + // workaround for https://github.com/zed-industries/zed/issues/26143 + path_sample_count: u32, + + // Env var: ZED_FONTS_GAMMA + // Allowed range [1.0, 2.2], other values are clipped + // Default: 1.8 + gamma_ratios: [f32; 4], + // Env var: ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST + // Allowed range: [0.0, ..), other values are clipped + // Default: 1.0 + grayscale_enhanced_contrast: f32, +} + +impl RenderingParameters { + fn from_env(context: &BladeContext) -> Self { + use std::env; + + let path_sample_count = env::var("ZED_PATH_SAMPLE_COUNT") + .ok() + .and_then(|v| v.parse().ok()) + .or_else(|| { + [4, 2, 1] + .into_iter() + .find(|&n| (context.gpu.capabilities().sample_count_mask & n) != 0) + }) + .unwrap_or(1); + let gamma = env::var("ZED_FONTS_GAMMA") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.8_f32) + .clamp(1.0, 2.2); + let gamma_ratios = Self::get_gamma_ratios(gamma); + let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.0_f32) + .max(0.0); + + Self { + path_sample_count, + gamma_ratios, + grayscale_enhanced_contrast, + } + } + + // Gamma ratios for brightening/darkening edges for better contrast + // https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50 + fn get_gamma_ratios(gamma: f32) -> [f32; 4] { + const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [ + [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0 + [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1 + [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2 + [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3 + [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4 + [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5 + [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6 + [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7 + [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8 + [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9 + [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0 + [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1 + [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2 + ]; + + const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32; + const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32; + + let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10; + let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index]; + + [ + ratios[0] * NORM13, + ratios[1] * NORM24, + ratios[2] * NORM13, + ratios[3] * NORM24, + ] + } +} diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 95980b54fe4f25b3936d6b095219c5674211dd0a..55a85c1ec899745c6d29bb1f8edeaccd7abcf8ad 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -28,6 +28,35 @@ fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2) */ +fn color_brightness(color: vec3) -> f32 { + // REC. 601 luminance coefficients for perceived brightness + return dot(color, vec3(0.30, 0.59, 0.11)); +} + +fn light_on_dark_contrast(enhancedContrast: f32, color: vec3) -> f32 { + let brightness = color_brightness(color); + let multiplier = saturate(4.0 * (0.75 - brightness)); + return enhancedContrast * multiplier; +} + +fn enhance_contrast(alpha: f32, k: f32) -> f32 { + return alpha * (k + 1.0) / (alpha * k + 1.0); +} + +fn apply_alpha_correction(a: f32, b: f32, g: vec4) -> f32 { + let brightness_adjustment = g.x * b + g.y; + let correction = brightness_adjustment * a + (g.z * b + g.w); + return a + a * (1.0 - a) * correction; +} + +fn apply_contrast_and_gamma_correction(sample: f32, color: vec3, enhanced_contrast_factor: f32, gamma_ratios: vec4) -> f32 { + let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color); + let brightness = color_brightness(color); + + let contrasted = enhance_contrast(sample, enhanced_contrast); + return apply_alpha_correction(contrasted, brightness, gamma_ratios); +} + struct GlobalParams { viewport_size: vec2, premultiplied_alpha: u32, @@ -35,6 +64,8 @@ struct GlobalParams { } var globals: GlobalParams; +var gamma_ratios: vec4; +var grayscale_enhanced_contrast: f32; var t_sprite: texture_2d; var s_sprite: sampler; @@ -1124,11 +1155,13 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index @fragment fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios); + // Alpha clip after using the derivatives. if (any(input.clip_distances < vec4(0.0))) { return vec4(0.0); } - return blend_color(input.color, sample); + return blend_color(input.color, alpha_corrected); } // --- polychrome sprites --- // diff --git a/docs/src/linux.md b/docs/src/linux.md index a3220e11cbe1ff25ac6c5fe736de0f88c796942d..a62b7632617d3ffe90997f31adb2b552c8631b0d 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -368,3 +368,15 @@ xrandr --dpi 192 ``` Replace `192` with your desired DPI value. This affects the system globally and will be used by Zed's automatic RandR detection when `Xft.dpi` is not set. + +### Font rendering parameters + +When using Blade rendering (Linux platforms and self-compiled builds with the Blade renderer enabled), Zed reads `ZED_FONTS_GAMMA` and `ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` environment variables for the values to use for font rendering. + +`ZED_FONTS_GAMMA` corresponds to [getgamma](https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwriterenderingparams-getgamma) values. +Allowed range [1.0, 2.2], other values are clipped. +Default: 1.8 + +`ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` corresponds to [getgrayscaleenhancedcontrast](https://learn.microsoft.com/en-us/windows/win32/api/dwrite_1/nf-dwrite_1-idwriterenderingparams1-getgrayscaleenhancedcontrast) values. +Allowed range: [0.0, ..), other values are clipped. +Default: 1.0