Add support for subpixel text rendering (#45423)

John Tur and Max Brunsfeld created

Subpixel text rendering is now implemented on Windows and Linux.

Comparison screenshots:

|Before|After|
| ------------- | ------------- |
| <img width="400"
src="https://github.com/user-attachments/assets/9d720d2c-2ec4-4adf-a83f-7c2d81d30025"
/> | <img width="400"
src="https://github.com/user-attachments/assets/8fd7dc2a-8ca0-4f71-86cd-55460f568f7a"
/> |


Release Notes:

- Added support for subpixel (ClearType-style) text rendering. This
improves the legibility of text on standard DPI displays. Subpixel
rendering is enabled by default on Windows and Linux and can be
configured using the `text_rendering_mode` setting.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

Cargo.lock                                             |  10 
Cargo.toml                                             |   6 
assets/settings/default.json                           |   9 
crates/gpui/Cargo.toml                                 |   1 
crates/gpui/build.rs                                   |   1 
crates/gpui/src/app.rs                                 |  18 +
crates/gpui/src/platform.rs                            |  27 ++
crates/gpui/src/platform/blade/blade_atlas.rs          |  11 +
crates/gpui/src/platform/blade/blade_context.rs        |   5 
crates/gpui/src/platform/blade/blade_renderer.rs       |  76 ++++++
crates/gpui/src/platform/blade/shaders.wgsl            |  74 ++++++
crates/gpui/src/platform/linux/text_system.rs          | 132 +++++++----
crates/gpui/src/platform/linux/wayland/client.rs       |   4 
crates/gpui/src/platform/linux/wayland/window.rs       |  10 
crates/gpui/src/platform/linux/x11/client.rs           |   2 
crates/gpui/src/platform/linux/x11/window.rs           |  18 +
crates/gpui/src/platform/mac/metal_atlas.rs            |   5 
crates/gpui/src/platform/mac/metal_renderer.rs         |   1 
crates/gpui/src/platform/mac/text_system.rs            |  10 
crates/gpui/src/platform/mac/window.rs                 |  11 +
crates/gpui/src/platform/test/window.rs                |   8 
crates/gpui/src/platform/windows/alpha_correction.hlsl |  17 +
crates/gpui/src/platform/windows/direct_write.rs       | 104 ++++++++
crates/gpui/src/platform/windows/directx_atlas.rs      |  14 +
crates/gpui/src/platform/windows/directx_renderer.rs   |  96 +++++++
crates/gpui/src/platform/windows/shaders.hlsl          |  21 +
crates/gpui/src/platform/windows/system_settings.rs    |  14 
crates/gpui/src/platform/windows/window.rs             |  17 +
crates/gpui/src/scene.rs                               |  66 ++++++
crates/gpui/src/text_system.rs                         |  15 +
crates/gpui/src/window.rs                              |  67 ++++-
crates/settings/src/settings_content/workspace.rs      |  29 ++
crates/settings/src/vscode_import.rs                   |   1 
crates/settings_ui/src/page_data.rs                    |  16 +
crates/settings_ui/src/settings_ui.rs                  |   1 
crates/workspace/src/workspace_settings.rs             |   2 
crates/zed/src/main.rs                                 |  12 +
37 files changed, 820 insertions(+), 111 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2187,8 +2187,7 @@ dependencies = [
 [[package]]
 name = "blade-graphics"
 version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
 dependencies = [
  "ash",
  "ash-window",
@@ -2222,8 +2221,7 @@ dependencies = [
 [[package]]
 name = "blade-macros"
 version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2233,8 +2231,7 @@ dependencies = [
 [[package]]
 name = "blade-util"
 version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c"
+source = "git+https://github.com/kvark/blade?rev=e3cf011ca18a6dfd907d1dedd93e85e21f005fe3#e3cf011ca18a6dfd907d1dedd93e85e21f005fe3"
 dependencies = [
  "blade-graphics",
  "bytemuck",
@@ -7392,6 +7389,7 @@ dependencies = [
  "stacksafe",
  "strum 0.27.2",
  "sum_tree",
+ "swash",
  "taffy",
  "thiserror 2.0.17",
  "unicode-segmentation",

Cargo.toml 🔗

@@ -473,9 +473,9 @@ backtrace = "0.3"
 base64 = "0.22"
 bincode = "1.2.1"
 bitflags = "2.6.0"
-blade-graphics = { version = "0.7.0" }
-blade-macros = { version = "0.3.0" }
-blade-util = { version = "0.3.0" }
+blade-graphics = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
+blade-macros = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
+blade-util = { git = "https://github.com/kvark/blade", rev = "e3cf011ca18a6dfd907d1dedd93e85e21f005fe3" }
 brotli = "8.0.2"
 bytes = "1.0"
 cargo_metadata = "0.19"

assets/settings/default.json 🔗

@@ -169,6 +169,15 @@
   //  2. Always quit the application
   //         "on_last_window_closed": "quit_app",
   "on_last_window_closed": "platform_default",
+  // The text rendering mode to use.
+  // May take 3 values:
+  //  1. Use platform default behavior:
+  //         "text_rendering_mode": "platform_default"
+  //  2. Use subpixel (ClearType-style) text rendering:
+  //         "text_rendering_mode": "subpixel"
+  //  3. Use grayscale text rendering:
+  //         "text_rendering_mode": "grayscale"
+  "text_rendering_mode": "platform_default",
   // Whether to show padding for zoomed panels.
   // When enabled, zoomed center panels (e.g. code editor) will have padding all around,
   // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).

crates/gpui/Cargo.toml 🔗

@@ -183,6 +183,7 @@ blade-macros = { workspace = true, optional = true }
 blade-util = { workspace = true, optional = true }
 bytemuck = { version = "1", optional = true }
 cosmic-text = { version = "0.14.0", optional = true }
+swash = { version = "0.2.6" }
 # WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
 font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
     "source-fontconfig-dlopen",

crates/gpui/build.rs 🔗

@@ -297,6 +297,7 @@ mod windows {
             "path_sprite",
             "underline",
             "monochrome_sprite",
+            "subpixel_sprite",
             "polychrome_sprite",
         ];
 

crates/gpui/src/app.rs 🔗

@@ -1,6 +1,6 @@
 use std::{
     any::{TypeId, type_name},
-    cell::{BorrowMutError, Ref, RefCell, RefMut},
+    cell::{BorrowMutError, Cell, Ref, RefCell, RefMut},
     marker::PhantomData,
     mem,
     ops::{Deref, DerefMut},
@@ -43,8 +43,8 @@ use crate::{
     PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority,
     PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
     RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId,
-    WindowInvalidator,
+    Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem, Window, WindowAppearance,
+    WindowHandle, WindowId, WindowInvalidator,
     colors::{Colors, GlobalColors},
     current_platform, hash, init_app_menus,
 };
@@ -637,6 +637,7 @@ pub struct App {
     pub(crate) inspector_element_registry: InspectorElementRegistry,
     #[cfg(any(test, feature = "test-support", debug_assertions))]
     pub(crate) name: Option<&'static str>,
+    pub(crate) text_rendering_mode: Rc<Cell<TextRenderingMode>>,
     quit_mode: QuitMode,
     quitting: bool,
 }
@@ -666,6 +667,7 @@ impl App {
                 liveness: std::sync::Arc::new(()),
                 platform: platform.clone(),
                 text_system,
+                text_rendering_mode: Rc::new(Cell::new(TextRenderingMode::default())),
                 mode: GpuiMode::Production,
                 actions: Rc::new(ActionRegistry::default()),
                 flushing_effects: false,
@@ -1088,6 +1090,16 @@ impl App {
         self.platform.read_from_clipboard()
     }
 
+    /// Sets the text rendering mode for the application.
+    pub fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) {
+        self.text_rendering_mode.set(mode);
+    }
+
+    /// Returns the current text rendering mode for the application.
+    pub fn text_rendering_mode(&self) -> TextRenderingMode {
+        self.text_rendering_mode.get()
+    }
+
     /// Writes data to the platform clipboard.
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item)

crates/gpui/src/platform.rs 🔗

@@ -499,6 +499,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn activate(&self);
     fn is_active(&self) -> bool;
     fn is_hovered(&self) -> bool;
+    fn background_appearance(&self) -> WindowBackgroundAppearance;
     fn set_title(&mut self, title: &str);
     fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
     fn minimize(&self);
@@ -518,6 +519,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn draw(&self, scene: &Scene);
     fn completed_frame(&self) {}
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+    fn is_subpixel_rendering_supported(&self) -> bool;
 
     // macOS specific methods
     fn get_title(&self) -> String {
@@ -654,6 +656,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
         raster_bounds: Bounds<DevicePixels>,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
+    fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
+    -> TextRenderingMode;
 }
 
 pub(crate) struct NoopTextSystem;
@@ -774,6 +778,14 @@ impl PlatformTextSystem for NoopTextSystem {
             len: text.len(),
         }
     }
+
+    fn recommended_rendering_mode(
+        &self,
+        _font_id: FontId,
+        _font_size: Pixels,
+    ) -> TextRenderingMode {
+        TextRenderingMode::Grayscale
+    }
 }
 
 // Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp
@@ -831,6 +843,8 @@ impl AtlasKey {
             AtlasKey::Glyph(params) => {
                 if params.is_emoji {
                     AtlasTextureKind::Polychrome
+                } else if params.subpixel_rendering {
+                    AtlasTextureKind::Subpixel
                 } else {
                     AtlasTextureKind::Monochrome
                 }
@@ -932,6 +946,7 @@ pub(crate) struct AtlasTextureId {
 pub(crate) enum AtlasTextureKind {
     Monochrome = 0,
     Polychrome = 1,
+    Subpixel = 2,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -1443,6 +1458,18 @@ pub enum WindowBackgroundAppearance {
     MicaAltBackdrop,
 }
 
+/// The text rendering mode to use for drawing glyphs.
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum TextRenderingMode {
+    /// Use the platform's default text rendering mode.
+    #[default]
+    PlatformDefault,
+    /// Use subpixel (ClearType-style) text rendering.
+    Subpixel,
+    /// Use grayscale text rendering.
+    Grayscale,
+}
+
 /// The options that can be configured for a file dialog prompt
 #[derive(Clone, Debug)]
 pub struct PathPromptOptions {

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

@@ -162,6 +162,10 @@ impl BladeAtlasState {
                 format = gpu::TextureFormat::R8Unorm;
                 usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
             }
+            AtlasTextureKind::Subpixel => {
+                format = gpu::TextureFormat::Bgra8Unorm;
+                usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
+            }
             AtlasTextureKind::Polychrome => {
                 format = gpu::TextureFormat::Bgra8Unorm;
                 usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
@@ -263,6 +267,7 @@ impl BladeAtlasState {
 #[derive(Default)]
 struct BladeAtlasStorage {
     monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
+    subpixel_textures: AtlasTextureList<BladeAtlasTexture>,
     polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
 }
 
@@ -271,6 +276,7 @@ impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
     fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
         match kind {
             crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
+            crate::AtlasTextureKind::Subpixel => &self.subpixel_textures,
             crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
         }
     }
@@ -280,6 +286,7 @@ impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage {
     fn index_mut(&mut self, kind: AtlasTextureKind) -> &mut Self::Output {
         match kind {
             crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
+            crate::AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
             crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
         }
     }
@@ -290,6 +297,7 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
     fn index(&self, id: AtlasTextureId) -> &Self::Output {
         let textures = match id.kind {
             crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
+            crate::AtlasTextureKind::Subpixel => &self.subpixel_textures,
             crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
         };
         textures[id.index as usize].as_ref().unwrap()
@@ -301,6 +309,9 @@ impl BladeAtlasStorage {
         for mut texture in self.monochrome_textures.drain().flatten() {
             texture.destroy(gpu);
         }
+        for mut texture in self.subpixel_textures.drain().flatten() {
+            texture.destroy(gpu);
+        }
         for mut texture in self.polychrome_textures.drain().flatten() {
             texture.destroy(gpu);
         }

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

@@ -34,6 +34,11 @@ impl BladeContext {
         );
         Ok(Self { gpu })
     }
+
+    #[allow(dead_code)]
+    pub fn supports_dual_source_blending(&self) -> bool {
+        self.gpu.capabilities().dual_source_blending
+    }
 }
 
 fn parse_pci_id(id: &str) -> anyhow::Result<u32> {

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

@@ -95,6 +95,16 @@ struct ShaderMonoSpritesData {
     b_mono_sprites: gpu::BufferPiece,
 }
 
+#[derive(blade_macros::ShaderData)]
+struct ShaderSubpixelSpritesData {
+    globals: GlobalParams,
+    gamma_ratios: [f32; 4],
+    subpixel_enhanced_contrast: f32,
+    t_sprite: gpu::TextureView,
+    s_sprite: gpu::Sampler,
+    b_subpixel_sprites: gpu::BufferPiece,
+}
+
 #[derive(blade_macros::ShaderData)]
 struct ShaderPolySpritesData {
     globals: GlobalParams,
@@ -134,6 +144,7 @@ struct BladePipelines {
     paths: gpu::RenderPipeline,
     underlines: gpu::RenderPipeline,
     mono_sprites: gpu::RenderPipeline,
+    subpixel_sprites: gpu::RenderPipeline,
     poly_sprites: gpu::RenderPipeline,
     surfaces: gpu::RenderPipeline,
 }
@@ -277,6 +288,31 @@ impl BladePipelines {
                 color_targets,
                 multisample_state: gpu::MultisampleState::default(),
             }),
+            subpixel_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
+                name: "subpixel-sprites",
+                data_layouts: &[&ShaderSubpixelSpritesData::layout()],
+                vertex: shader.at("vs_subpixel_sprite"),
+                vertex_fetches: &[],
+                primitive: gpu::PrimitiveState {
+                    topology: gpu::PrimitiveTopology::TriangleStrip,
+                    ..Default::default()
+                },
+                depth_stencil: None,
+                fragment: Some(shader.at("fs_subpixel_sprite")),
+                color_targets: &[gpu::ColorTargetState {
+                    format: surface_info.format,
+                    blend: Some(gpu::BlendState {
+                        color: gpu::BlendComponent {
+                            src_factor: gpu::BlendFactor::Src1,
+                            dst_factor: gpu::BlendFactor::OneMinusSrc1,
+                            operation: gpu::BlendOperation::Add,
+                        },
+                        alpha: gpu::BlendComponent::OVER,
+                    }),
+                    write_mask: gpu::ColorWrites::COLOR,
+                }],
+                multisample_state: gpu::MultisampleState::default(),
+            }),
             poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
                 name: "poly-sprites",
                 data_layouts: &[&ShaderPolySpritesData::layout()],
@@ -315,6 +351,7 @@ impl BladePipelines {
         gpu.destroy_render_pipeline(&mut self.paths);
         gpu.destroy_render_pipeline(&mut self.underlines);
         gpu.destroy_render_pipeline(&mut self.mono_sprites);
+        gpu.destroy_render_pipeline(&mut self.subpixel_sprites);
         gpu.destroy_render_pipeline(&mut self.poly_sprites);
         gpu.destroy_render_pipeline(&mut self.surfaces);
     }
@@ -672,7 +709,11 @@ impl BladeRenderer {
             gpu::RenderTargetSet {
                 colors: &[gpu::RenderTarget {
                     view: frame.texture_view(),
-                    init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack),
+                    init_op: gpu::InitOp::Clear(if self.surface_config.transparent {
+                        gpu::TextureColor::TransparentBlack
+                    } else {
+                        gpu::TextureColor::White
+                    }),
                     finish_op: gpu::FinishOp::Store,
                 }],
                 depth_stencil: None,
@@ -818,6 +859,29 @@ impl BladeRenderer {
                     );
                     encoder.draw(0, 4, 0, sprites.len() as u32);
                 }
+                PrimitiveBatch::SubpixelSprites {
+                    texture_id,
+                    sprites,
+                } => {
+                    let tex_info = self.atlas.get_texture_info(texture_id);
+                    let instance_buf =
+                        unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) };
+                    let mut encoder = pass.with(&self.pipelines.subpixel_sprites);
+                    encoder.bind(
+                        0,
+                        &ShaderSubpixelSpritesData {
+                            globals,
+                            gamma_ratios: self.rendering_parameters.gamma_ratios,
+                            subpixel_enhanced_contrast: self
+                                .rendering_parameters
+                                .subpixel_enhanced_contrast,
+                            t_sprite: tex_info.raw_view,
+                            s_sprite: self.atlas_sampler,
+                            b_subpixel_sprites: instance_buf,
+                        },
+                    );
+                    encoder.draw(0, 4, 0, sprites.len() as u32);
+                }
                 PrimitiveBatch::Surfaces(surfaces) => {
                     let mut _encoder = pass.with(&self.pipelines.surfaces);
 
@@ -1016,6 +1080,10 @@ struct RenderingParameters {
     // Allowed range: [0.0, ..), other values are clipped
     // Default: 1.0
     grayscale_enhanced_contrast: f32,
+    // Env var: ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST
+    // Allowed range: [0.0, ..), other values are clipped
+    // Default: 0.5
+    subpixel_enhanced_contrast: f32,
 }
 
 impl RenderingParameters {
@@ -1042,11 +1110,17 @@ impl RenderingParameters {
             .and_then(|v| v.parse().ok())
             .unwrap_or(1.0_f32)
             .max(0.0);
+        let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST")
+            .ok()
+            .and_then(|v| v.parse().ok())
+            .unwrap_or(0.5_f32)
+            .max(0.0);
 
         Self {
             path_sample_count,
             gamma_ratios,
             grayscale_enhanced_contrast,
+            subpixel_enhanced_contrast,
         }
     }
 }

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

@@ -1,3 +1,4 @@
+enable dual_source_blending;
 /* Functions useful for debugging:
 
 // A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
@@ -46,12 +47,22 @@ fn enhance_contrast(alpha: f32, k: f32) -> f32 {
     return alpha * (k + 1.0) / (alpha * k + 1.0);
 }
 
+fn enhance_contrast3(alpha: vec3<f32>, k: f32) -> vec3<f32> {
+    return alpha * (k + 1.0) / (alpha * k + 1.0);
+}
+
 fn apply_alpha_correction(a: f32, b: f32, g: vec4<f32>) -> 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_alpha_correction3(a: vec3<f32>, b: vec3<f32>, g: vec4<f32>) -> vec3<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<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> f32 {
     let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
     let brightness = color_brightness(color);
@@ -60,6 +71,13 @@ fn apply_contrast_and_gamma_correction(sample: f32, color: vec3<f32>, enhanced_c
     return apply_alpha_correction(contrasted, brightness, gamma_ratios);
 }
 
+fn apply_contrast_and_gamma_correction3(sample: vec3<f32>, color: vec3<f32>, enhanced_contrast_factor: f32, gamma_ratios: vec4<f32>) -> vec3<f32> {
+    let enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
+
+    let contrasted = enhance_contrast3(sample, enhanced_contrast);
+    return apply_alpha_correction3(contrasted, color, gamma_ratios);
+}
+
 struct GlobalParams {
     viewport_size: vec2<f32>,
     premultiplied_alpha: u32,
@@ -69,6 +87,7 @@ struct GlobalParams {
 var<uniform> globals: GlobalParams;
 var<uniform> gamma_ratios: vec4<f32>;
 var<uniform> grayscale_enhanced_contrast: f32;
+var<uniform> subpixel_enhanced_contrast: f32;
 var t_sprite: texture_2d<f32>;
 var s_sprite: sampler;
 
@@ -1190,7 +1209,6 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
         return vec4<f32>(0.0);
     }
 
-    // convert to srgb space as the rest of the code (output swapchain) expects that
     return blend_color(input.color, alpha_corrected);
 }
 
@@ -1297,3 +1315,57 @@ fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
 
     return ycbcr_to_RGB * y_cb_cr;
 }
+
+// --- subpixel sprites --- //
+
+struct SubpixelSprite {
+    order: u32,
+    pad: u32,
+    bounds: Bounds,
+    content_mask: Bounds,
+    color: Hsla,
+    tile: AtlasTile,
+    transformation: TransformationMatrix,
+}
+var<storage, read> b_subpixel_sprites: array<SubpixelSprite>;
+
+struct SubpixelSpriteOutput {
+    @builtin(position) position: vec4<f32>,
+    @location(0) tile_position: vec2<f32>,
+    @location(1) @interpolate(flat) color: vec4<f32>,
+    @location(3) clip_distances: vec4<f32>,
+}
+
+struct SubpixelSpriteFragmentOutput {
+    @location(0) @blend_src(0) foreground: vec4<f32>,
+    @location(0) @blend_src(1) alpha: vec4<f32>,
+}
+
+@vertex
+fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput {
+    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
+    let sprite = b_subpixel_sprites[instance_id];
+
+    var out = SubpixelSpriteOutput();
+    out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
+    out.tile_position = to_tile_position(unit_vertex, sprite.tile);
+    out.color = hsla_to_rgba(sprite.color);
+    out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
+    return out;
+}
+
+@fragment
+fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutput {
+    let sample = textureSample(t_sprite, s_sprite, input.tile_position).rgb;
+    let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios);
+
+    // Alpha clip after using the derivatives.
+    if (any(input.clip_distances < vec4<f32>(0.0))) {
+        return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
+    }
+
+    var out = SubpixelSpriteFragmentOutput();
+    out.foreground = vec4<f32>(input.color.rgb, 1.0);
+    out.alpha = vec4<f32>(input.color.a * alpha_corrected, 1.0);
+    return out;
+}

crates/gpui/src/platform/linux/text_system.rs 🔗

@@ -1,13 +1,14 @@
 use crate::{
     Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
     GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
-    SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size,
+    SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, TextRenderingMode, point,
+    size,
 };
 use anyhow::{Context as _, Ok, Result};
 use collections::HashMap;
 use cosmic_text::{
-    Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
-    FontSystem, ShapeBuffer, ShapeLine, SwashCache,
+    Attrs, AttrsList, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
+    FontSystem, ShapeBuffer, ShapeLine,
 };
 
 use itertools::Itertools;
@@ -18,6 +19,10 @@ use pathfinder_geometry::{
 };
 use smallvec::SmallVec;
 use std::{borrow::Cow, sync::Arc};
+use swash::{
+    scale::{Render, ScaleContext, Source, StrikeWith},
+    zeno::{Format, Transform, Vector},
+};
 
 pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
 
@@ -34,9 +39,9 @@ impl FontKey {
 }
 
 struct CosmicTextSystemState {
-    swash_cache: SwashCache,
     font_system: FontSystem,
     scratch: ShapeBuffer,
+    swash_scale_context: ScaleContext,
     /// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
     loaded_fonts: Vec<LoadedFont>,
     /// Caches the `FontId`s associated with a specific family to avoid iterating the font database
@@ -57,8 +62,8 @@ impl CosmicTextSystem {
 
         Self(RwLock::new(CosmicTextSystemState {
             font_system,
-            swash_cache: SwashCache::new(),
             scratch: ShapeBuffer::default(),
+            swash_scale_context: ScaleContext::new(),
             loaded_fonts: Vec::new(),
             font_ids_by_family_cache: HashMap::default(),
         }))
@@ -183,6 +188,15 @@ impl PlatformTextSystem for CosmicTextSystem {
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
         self.0.write().layout_line(text, font_size, runs)
     }
+
+    fn recommended_rendering_mode(
+        &self,
+        _font_id: FontId,
+        _font_size: Pixels,
+    ) -> TextRenderingMode {
+        // Ideally, we'd use fontconfig to read the user preference.
+        TextRenderingMode::Subpixel
+    }
 }
 
 impl CosmicTextSystemState {
@@ -273,26 +287,7 @@ impl CosmicTextSystemState {
     }
 
     fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
-        let font = &self.loaded_fonts[params.font_id.0].font;
-        let subpixel_shift = point(
-            params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
-            params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
-        );
-        let image = self
-            .swash_cache
-            .get_image(
-                &mut self.font_system,
-                CacheKey::new(
-                    font.id(),
-                    params.glyph_id.0 as u16,
-                    (params.font_size * params.scale_factor).into(),
-                    (subpixel_shift.x, subpixel_shift.y.trunc()),
-                    cosmic_text::CacheKeyFlags::empty(),
-                )
-                .0,
-            )
-            .clone()
-            .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
+        let image = self.render_glyph_image(params)?;
         Ok(Bounds {
             origin: point(image.placement.left.into(), (-image.placement.top).into()),
             size: size(image.placement.width.into(), image.placement.height.into()),
@@ -307,38 +302,75 @@ impl CosmicTextSystemState {
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
         if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
             anyhow::bail!("glyph bounds are empty");
-        } else {
-            let bitmap_size = glyph_bounds.size;
-            let font = &self.loaded_fonts[params.font_id.0].font;
-            let subpixel_shift = point(
-                params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
-                params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
-            );
-            let mut image = self
-                .swash_cache
-                .get_image(
-                    &mut self.font_system,
-                    CacheKey::new(
-                        font.id(),
-                        params.glyph_id.0 as u16,
-                        (params.font_size * params.scale_factor).into(),
-                        (subpixel_shift.x, subpixel_shift.y.trunc()),
-                        cosmic_text::CacheKeyFlags::empty(),
-                    )
-                    .0,
-                )
-                .clone()
-                .with_context(|| format!("no image for {params:?} in font {font:?}"))?;
-
-            if params.is_emoji {
+        }
+
+        let mut image = self.render_glyph_image(params)?;
+        let bitmap_size = glyph_bounds.size;
+        match image.content {
+            swash::scale::image::Content::Color | swash::scale::image::Content::SubpixelMask => {
                 // Convert from RGBA to BGRA.
                 for pixel in image.data.chunks_exact_mut(4) {
                     pixel.swap(0, 2);
                 }
+                Ok((bitmap_size, image.data))
             }
+            swash::scale::image::Content::Mask => Ok((bitmap_size, image.data)),
+        }
+    }
 
-            Ok((bitmap_size, image.data))
+    fn render_glyph_image(
+        &mut self,
+        params: &RenderGlyphParams,
+    ) -> Result<swash::scale::image::Image> {
+        let loaded_font = &self.loaded_fonts[params.font_id.0];
+        let font_ref = loaded_font.font.as_swash();
+        let pixel_size = params.font_size.0;
+
+        let subpixel_offset = Vector::new(
+            params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
+            params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
+        );
+
+        let mut scaler = self
+            .swash_scale_context
+            .builder(font_ref)
+            .size(pixel_size)
+            .hint(true)
+            .build();
+
+        let sources: &[Source] = if params.is_emoji {
+            &[
+                Source::ColorOutline(0),
+                Source::ColorBitmap(StrikeWith::BestFit),
+                Source::Outline,
+            ]
+        } else {
+            &[Source::Outline]
+        };
+
+        let mut renderer = Render::new(sources);
+        renderer.transform(Some(Transform {
+            xx: params.scale_factor,
+            xy: 0.0,
+            yx: 0.0,
+            yy: params.scale_factor,
+            x: 0.0,
+            y: 0.0,
+        }));
+
+        if params.subpixel_rendering {
+            // There seems to be a bug in Swash where the B and R values are swapped.
+            renderer
+                .format(Format::subpixel_bgra())
+                .offset(subpixel_offset);
+        } else {
+            renderer.format(Format::Alpha).offset(subpixel_offset);
         }
+
+        let glyph_id: u16 = params.glyph_id.0.try_into()?;
+        renderer
+            .render(&mut scaler, glyph_id)
+            .with_context(|| format!("unable to render glyph via swash for {params:?}"))
     }
 
     /// This is used when cosmic_text has chosen a fallback font instead of using the requested

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -204,7 +204,7 @@ pub struct Output {
 pub(crate) struct WaylandClientState {
     serial_tracker: SerialTracker,
     globals: Globals,
-    gpu_context: BladeContext,
+    pub gpu_context: BladeContext,
     wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
     wl_pointer: Option<wl_pointer::WlPointer>,
     wl_keyboard: Option<wl_keyboard::WlKeyboard>,
@@ -247,7 +247,7 @@ pub(crate) struct WaylandClientState {
     cursor: Cursor,
     pending_activation: Option<PendingActivation>,
     event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
-    common: LinuxCommon,
+    pub common: LinuxCommon,
 }
 
 pub struct DragState {

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -1220,6 +1220,16 @@ impl PlatformWindow for WaylandWindow {
         update_window(state);
     }
 
+    fn background_appearance(&self) -> WindowBackgroundAppearance {
+        self.borrow().background_appearance
+    }
+
+    fn is_subpixel_rendering_supported(&self) -> bool {
+        let client = self.borrow().client.get_client();
+        let state = client.borrow();
+        state.gpu_context.supports_dual_source_blending()
+    }
+
     fn minimize(&self) {
         if let Some(toplevel) = self.borrow().surface_state.toplevel() {
             toplevel.set_minimized();

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -177,7 +177,7 @@ pub struct X11ClientState {
     pub(crate) last_location: Point<Pixels>,
     pub(crate) current_count: usize,
 
-    gpu_context: BladeContext,
+    pub(crate) gpu_context: BladeContext,
 
     pub(crate) scale_factor: f32,
 

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -1459,6 +1459,24 @@ impl PlatformWindow for X11Window {
         state.renderer.update_transparency(transparent);
     }
 
+    fn background_appearance(&self) -> WindowBackgroundAppearance {
+        self.0.state.borrow().background_appearance
+    }
+
+    fn is_subpixel_rendering_supported(&self) -> bool {
+        self.0
+            .state
+            .borrow()
+            .client
+            .0
+            .upgrade()
+            .map(|ref_cell| {
+                let state = ref_cell.borrow();
+                state.gpu_context.supports_dual_source_blending()
+            })
+            .unwrap_or_default()
+    }
+
     fn minimize(&self) {
         let state = self.0.state.borrow();
         const WINDOW_ICONIC_STATE: u32 = 3;

crates/gpui/src/platform/mac/metal_atlas.rs 🔗

@@ -66,6 +66,7 @@ impl PlatformAtlas for MetalAtlas {
         let textures = match id.kind {
             AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
             AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
+            AtlasTextureKind::Subpixel => unreachable!(),
         };
 
         let Some(texture_slot) = textures
@@ -99,6 +100,7 @@ impl MetalAtlasState {
             let textures = match texture_kind {
                 AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
                 AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+                AtlasTextureKind::Subpixel => unreachable!(),
             };
 
             if let Some(tile) = textures
@@ -143,6 +145,7 @@ impl MetalAtlasState {
                 pixel_format = metal::MTLPixelFormat::BGRA8Unorm;
                 usage = metal::MTLTextureUsage::ShaderRead;
             }
+            AtlasTextureKind::Subpixel => unreachable!(),
         }
         texture_descriptor.set_pixel_format(pixel_format);
         texture_descriptor.set_usage(usage);
@@ -151,6 +154,7 @@ impl MetalAtlasState {
         let texture_list = match kind {
             AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
             AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+            AtlasTextureKind::Subpixel => unreachable!(),
         };
 
         let index = texture_list.free_list.pop();
@@ -181,6 +185,7 @@ impl MetalAtlasState {
         let textures = match id.kind {
             crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
             crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
+            crate::AtlasTextureKind::Subpixel => unreachable!(),
         };
         textures[id.index as usize].as_ref().unwrap()
     }

crates/gpui/src/platform/mac/text_system.rs 🔗

@@ -2,7 +2,7 @@ use crate::{
     Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
     FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
     RenderGlyphParams, Result, SUBPIXEL_VARIANTS_X, ShapedGlyph, ShapedRun, SharedString, Size,
-    point, px, size, swap_rgba_pa_to_bgra,
+    TextRenderingMode, point, px, size, swap_rgba_pa_to_bgra,
 };
 use anyhow::anyhow;
 use cocoa::appkit::CGFloat;
@@ -204,6 +204,14 @@ impl PlatformTextSystem for MacTextSystem {
     fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
         self.0.write().layout_line(text, font_size, font_runs)
     }
+
+    fn recommended_rendering_mode(
+        &self,
+        _font_id: FontId,
+        _font_size: Pixels,
+    ) -> TextRenderingMode {
+        TextRenderingMode::Grayscale
+    }
 }
 
 impl MacTextSystemState {

crates/gpui/src/platform/mac/window.rs 🔗

@@ -402,6 +402,7 @@ struct MacWindowState {
     native_window: id,
     native_view: NonNull<Object>,
     blurred_view: Option<id>,
+    background_appearance: WindowBackgroundAppearance,
     display_link: Option<DisplayLink>,
     renderer: renderer::Renderer,
     request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
@@ -706,6 +707,7 @@ impl MacWindow {
                 native_window,
                 native_view: NonNull::new_unchecked(native_view),
                 blurred_view: None,
+                background_appearance: WindowBackgroundAppearance::Opaque,
                 display_link: None,
                 renderer: renderer::new_renderer(
                     renderer_context,
@@ -1304,6 +1306,7 @@ impl PlatformWindow for MacWindow {
 
     fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         let mut this = self.0.as_ref().lock();
+        this.background_appearance = background_appearance;
 
         let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
         this.renderer.update_transparency(!opaque);
@@ -1358,6 +1361,14 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn background_appearance(&self) -> WindowBackgroundAppearance {
+        self.0.as_ref().lock().background_appearance
+    }
+
+    fn is_subpixel_rendering_supported(&self) -> bool {
+        false
+    }
+
     fn set_edited(&mut self, edited: bool) {
         unsafe {
             let window = self.0.lock().native_window;

crates/gpui/src/platform/test/window.rs 🔗

@@ -199,6 +199,14 @@ impl PlatformWindow for TestWindow {
         false
     }
 
+    fn background_appearance(&self) -> WindowBackgroundAppearance {
+        WindowBackgroundAppearance::Opaque
+    }
+
+    fn is_subpixel_rendering_supported(&self) -> bool {
+        false
+    }
+
     fn set_title(&mut self, title: &str) {
         self.0.lock().title = Some(title.to_owned());
     }

crates/gpui/src/platform/windows/alpha_correction.hlsl 🔗

@@ -17,12 +17,22 @@ float enhance_contrast(float alpha, float k) {
     return alpha * (k + 1.0f) / (alpha * k + 1.0f);
 }
 
+float3 enhance_contrast3(float3 alpha, float k) {
+    return alpha * (k + 1.0f) / (alpha * k + 1.0f);
+}
+
 float apply_alpha_correction(float a, float b, float4 g) {
     float brightness_adjustment = g.x * b + g.y;
     float correction = brightness_adjustment * a + (g.z * b + g.w);
     return a + a * (1.0f - a) * correction;
 }
 
+float3 apply_alpha_correction3(float3 a, float3 b, float4 g) {
+    float3 brightness_adjustment = g.x * b + g.y;
+    float3 correction = brightness_adjustment * a + (g.z * b + g.w);
+    return a + a * (1.0f - a) * correction;
+}
+
 float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
     float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
     float brightness = color_brightness(color);
@@ -30,3 +40,10 @@ float apply_contrast_and_gamma_correction(float sample, float3 color, float enha
     float contrasted = enhance_contrast(sample, enhanced_contrast);
     return apply_alpha_correction(contrasted, brightness, gamma_ratios);
 }
+
+float3 apply_contrast_and_gamma_correction3(float3 sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
+    float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
+
+    float3 contrasted = enhance_contrast3(sample, enhanced_contrast);
+    return apply_alpha_correction3(contrasted, color, gamma_ratios);
+}

crates/gpui/src/platform/windows/direct_write.rs 🔗

@@ -1,4 +1,8 @@
-use std::{borrow::Cow, sync::Arc};
+use std::{
+    borrow::Cow,
+    ffi::{c_uint, c_void},
+    sync::Arc,
+};
 
 use ::util::ResultExt;
 use anyhow::{Context, Result};
@@ -60,6 +64,7 @@ struct DirectWriteState {
     fonts: Vec<FontInfo>,
     font_selections: HashMap<Font, FontId>,
     font_id_by_identifier: HashMap<FontIdentifier, FontId>,
+    system_subpixel_rendering: bool,
 }
 
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
@@ -199,6 +204,7 @@ impl DirectWriteTextSystem {
                 .CreateFontCollectionFromFontSet(&custom_font_set)?
         };
         let system_ui_font_name = get_system_ui_font_name();
+        let system_subpixel_rendering = get_system_subpixel_rendering();
 
         Ok(Self(RwLock::new(DirectWriteState {
             components,
@@ -208,6 +214,7 @@ impl DirectWriteTextSystem {
             fonts: Vec::new(),
             font_selections: HashMap::default(),
             font_id_by_identifier: HashMap::default(),
+            system_subpixel_rendering,
         })))
     }
 
@@ -280,6 +287,18 @@ impl PlatformTextSystem for DirectWriteTextSystem {
                 ..Default::default()
             })
     }
+
+    fn recommended_rendering_mode(
+        &self,
+        _font_id: FontId,
+        _font_size: Pixels,
+    ) -> TextRenderingMode {
+        if self.0.read().system_subpixel_rendering {
+            TextRenderingMode::Subpixel
+        } else {
+            TextRenderingMode::Grayscale
+        }
+    }
 }
 
 impl DirectWriteState {
@@ -759,6 +778,12 @@ impl DirectWriteState {
             m => m,
         };
 
+        let antialias_mode = if params.subpixel_rendering {
+            DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE
+        } else {
+            DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE
+        };
+
         let glyph_analysis = unsafe {
             self.components.factory.CreateGlyphRunAnalysis(
                 &glyph_run,
@@ -766,7 +791,7 @@ impl DirectWriteState {
                 rendering_mode,
                 DWRITE_MEASURING_MODE_NATURAL,
                 grid_fit_mode,
-                DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
+                antialias_mode,
                 baseline_origin_x,
                 baseline_origin_y,
             )
@@ -777,7 +802,13 @@ impl DirectWriteState {
     fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
         let glyph_analysis = self.create_glyph_run_analysis(params)?;
 
-        let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
+        let texture_type = if params.subpixel_rendering {
+            DWRITE_TEXTURE_CLEARTYPE_3x1
+        } else {
+            DWRITE_TEXTURE_ALIASED_1x1
+        };
+
+        let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(texture_type)? };
 
         if bounds.right < bounds.left {
             Ok(Bounds {
@@ -839,23 +870,64 @@ impl DirectWriteState {
         params: &RenderGlyphParams,
         glyph_bounds: Bounds<DevicePixels>,
     ) -> Result<Vec<u8>> {
-        let mut bitmap_data =
-            vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
+        if !params.subpixel_rendering {
+            let mut bitmap_data =
+                vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
+
+            let glyph_analysis = self.create_glyph_run_analysis(params)?;
+            unsafe {
+                glyph_analysis.CreateAlphaTexture(
+                    DWRITE_TEXTURE_ALIASED_1x1,
+                    &RECT {
+                        left: glyph_bounds.origin.x.0,
+                        top: glyph_bounds.origin.y.0,
+                        right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0,
+                        bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0,
+                    },
+                    &mut bitmap_data,
+                )?;
+            }
+
+            return Ok(bitmap_data);
+        }
+
+        let width = glyph_bounds.size.width.0 as usize;
+        let height = glyph_bounds.size.height.0 as usize;
+        let pixel_count = width * height;
+
+        let mut bitmap_data = vec![0u8; pixel_count * 4];
 
         let glyph_analysis = self.create_glyph_run_analysis(params)?;
         unsafe {
             glyph_analysis.CreateAlphaTexture(
-                DWRITE_TEXTURE_ALIASED_1x1,
+                DWRITE_TEXTURE_CLEARTYPE_3x1,
                 &RECT {
                     left: glyph_bounds.origin.x.0,
                     top: glyph_bounds.origin.y.0,
                     right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0,
                     bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0,
                 },
-                &mut bitmap_data,
+                &mut bitmap_data[..pixel_count * 3],
             )?;
         }
 
+        // The output buffer expects RGBA data, so pad the alpha channel with zeros.
+        for pixel_ix in (0..pixel_count).rev() {
+            let src = pixel_ix * 3;
+            let dst = pixel_ix * 4;
+            (
+                bitmap_data[dst],
+                bitmap_data[dst + 1],
+                bitmap_data[dst + 2],
+                bitmap_data[dst + 3],
+            ) = (
+                bitmap_data[src],
+                bitmap_data[src + 1],
+                bitmap_data[src + 2],
+                0,
+            );
+        }
+
         Ok(bitmap_data)
     }
 
@@ -1076,6 +1148,7 @@ impl DirectWriteState {
         let crate::FontInfo {
             gamma_ratios,
             grayscale_enhanced_contrast,
+            ..
         } = DirectXRenderer::get_font_info();
 
         for layer in glyph_layers {
@@ -1820,6 +1893,23 @@ fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result<String> {
     Ok(String::from_utf16_lossy(&name_vec[..name_length]))
 }
 
+fn get_system_subpixel_rendering() -> bool {
+    let mut value = c_uint::default();
+    let result = unsafe {
+        SystemParametersInfoW(
+            SPI_GETFONTSMOOTHINGTYPE,
+            0,
+            Some((&mut value) as *mut c_uint as *mut c_void),
+            SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
+        )
+    };
+    if result.log_err().is_some() {
+        value == FE_FONTSMOOTHINGCLEARTYPE
+    } else {
+        true
+    }
+}
+
 fn get_system_ui_font_name() -> SharedString {
     unsafe {
         let mut info: LOGFONTW = std::mem::zeroed();

crates/gpui/src/platform/windows/directx_atlas.rs 🔗

@@ -21,6 +21,7 @@ struct DirectXAtlasState {
     device_context: ID3D11DeviceContext,
     monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
     polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
+    subpixel_textures: AtlasTextureList<DirectXAtlasTexture>,
     tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
 }
 
@@ -40,6 +41,7 @@ impl DirectXAtlas {
             device_context: device_context.clone(),
             monochrome_textures: Default::default(),
             polychrome_textures: Default::default(),
+            subpixel_textures: Default::default(),
             tiles_by_key: Default::default(),
         }))
     }
@@ -63,6 +65,7 @@ impl DirectXAtlas {
         lock.device_context = device_context.clone();
         lock.monochrome_textures = AtlasTextureList::default();
         lock.polychrome_textures = AtlasTextureList::default();
+        lock.subpixel_textures = AtlasTextureList::default();
         lock.tiles_by_key.clear();
     }
 }
@@ -102,6 +105,7 @@ impl PlatformAtlas for DirectXAtlas {
         let textures = match id.kind {
             AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
             AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
+            AtlasTextureKind::Subpixel => &mut lock.subpixel_textures,
         };
 
         let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
@@ -130,6 +134,7 @@ impl DirectXAtlasState {
             let textures = match texture_kind {
                 AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
                 AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+                AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
             };
 
             if let Some(tile) = textures
@@ -175,6 +180,11 @@ impl DirectXAtlasState {
                 bind_flag = D3D11_BIND_SHADER_RESOURCE;
                 bytes_per_pixel = 4;
             }
+            AtlasTextureKind::Subpixel => {
+                pixel_format = DXGI_FORMAT_R8G8B8A8_UNORM;
+                bind_flag = D3D11_BIND_SHADER_RESOURCE;
+                bytes_per_pixel = 4;
+            }
         }
         let texture_desc = D3D11_TEXTURE2D_DESC {
             Width: size.width.0 as u32,
@@ -204,6 +214,7 @@ impl DirectXAtlasState {
         let texture_list = match kind {
             AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
             AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
+            AtlasTextureKind::Subpixel => &mut self.subpixel_textures,
         };
         let index = texture_list.free_list.pop();
         let view = unsafe {
@@ -241,6 +252,9 @@ impl DirectXAtlasState {
             crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
                 .as_ref()
                 .unwrap(),
+            crate::AtlasTextureKind::Subpixel => {
+                &self.subpixel_textures[id.index as usize].as_ref().unwrap()
+            }
         }
     }
 }

crates/gpui/src/platform/windows/directx_renderer.rs 🔗

@@ -34,6 +34,7 @@ const PATH_MULTISAMPLE_COUNT: u32 = 4;
 pub(crate) struct FontInfo {
     pub gamma_ratios: [f32; 4],
     pub grayscale_enhanced_contrast: f32,
+    pub subpixel_enhanced_contrast: f32,
 }
 
 pub(crate) struct DirectXRenderer {
@@ -89,6 +90,7 @@ struct DirectXRenderPipelines {
     path_sprite_pipeline: PipelineState<PathSprite>,
     underline_pipeline: PipelineState<Underline>,
     mono_sprites: PipelineState<MonochromeSprite>,
+    subpixel_sprites: PipelineState<SubpixelSprite>,
     poly_sprites: PipelineState<PolychromeSprite>,
 }
 
@@ -181,7 +183,7 @@ impl DirectXRenderer {
         self.atlas.clone()
     }
 
-    fn pre_draw(&self) -> Result<()> {
+    fn pre_draw(&self, clear_color: &[f32; 4]) -> Result<()> {
         let resources = self.resources.as_ref().expect("resources missing");
         let device_context = &self
             .devices
@@ -195,7 +197,7 @@ impl DirectXRenderer {
                 gamma_ratios: self.font_info.gamma_ratios,
                 viewport_size: [resources.viewport.Width, resources.viewport.Height],
                 grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast,
-                _pad: 0,
+                subpixel_enhanced_contrast: self.font_info.subpixel_enhanced_contrast,
             }],
         )?;
         unsafe {
@@ -204,7 +206,7 @@ impl DirectXRenderer {
                     .render_target_view
                     .as_ref()
                     .context("missing render target view")?,
-                &[0.0; 4],
+                clear_color,
             );
             device_context
                 .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
@@ -299,13 +301,20 @@ impl DirectXRenderer {
         Ok(())
     }
 
-    pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> {
+    pub(crate) fn draw(
+        &mut self,
+        scene: &Scene,
+        background_appearance: WindowBackgroundAppearance,
+    ) -> Result<()> {
         if self.skip_draws {
             // skip drawing this frame, we just recovered from a device lost event
             // and so likely do not have the textures anymore that are required for drawing
             return Ok(());
         }
-        self.pre_draw()?;
+        self.pre_draw(&match background_appearance {
+            WindowBackgroundAppearance::Opaque => [1.0f32; 4],
+            _ => [0.0f32; 4],
+        })?;
         for batch in scene.batches() {
             match batch {
                 PrimitiveBatch::Shadows(shadows) => self.draw_shadows(shadows),
@@ -319,6 +328,10 @@ impl DirectXRenderer {
                     texture_id,
                     sprites,
                 } => self.draw_monochrome_sprites(texture_id, sprites),
+                PrimitiveBatch::SubpixelSprites {
+                    texture_id,
+                    sprites,
+                } => self.draw_subpixel_sprites(texture_id, sprites),
                 PrimitiveBatch::PolychromeSprites {
                     texture_id,
                     sprites,
@@ -327,12 +340,13 @@ impl DirectXRenderer {
             }
             .context(format!(
                 "scene too large:\
-                {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
+                {} paths, {} shadows, {} quads, {} underlines, {} mono, {} subpixel, {} poly, {} surfaces",
                 scene.paths.len(),
                 scene.shadows.len(),
                 scene.quads.len(),
                 scene.underlines.len(),
                 scene.monochrome_sprites.len(),
+                scene.subpixel_sprites.len(),
                 scene.polychrome_sprites.len(),
                 scene.surfaces.len(),
             ))?;
@@ -578,6 +592,7 @@ impl DirectXRenderer {
         }
         let devices = self.devices.as_ref().context("devices missing")?;
         let resources = self.resources.as_ref().context("resources missing")?;
+
         self.pipelines.mono_sprites.update_buffer(
             &devices.device,
             &devices.device_context,
@@ -594,6 +609,33 @@ impl DirectXRenderer {
         )
     }
 
+    fn draw_subpixel_sprites(
+        &mut self,
+        texture_id: AtlasTextureId,
+        sprites: &[SubpixelSprite],
+    ) -> Result<()> {
+        if sprites.is_empty() {
+            return Ok(());
+        }
+        let devices = self.devices.as_ref().context("devices missing")?;
+        let resources = self.resources.as_ref().context("resources missing")?;
+
+        self.pipelines.subpixel_sprites.update_buffer(
+            &devices.device,
+            &devices.device_context,
+            &sprites,
+        )?;
+        let texture_view = self.atlas.get_texture_view(texture_id);
+        self.pipelines.subpixel_sprites.draw_with_texture(
+            &devices.device_context,
+            &texture_view,
+            slice::from_ref(&resources.viewport),
+            slice::from_ref(&self.globals.global_params_buffer),
+            slice::from_ref(&self.globals.sampler),
+            sprites.len() as u32,
+        )
+    }
+
     fn draw_polychrome_sprites(
         &mut self,
         texture_id: AtlasTextureId,
@@ -667,6 +709,7 @@ impl DirectXRenderer {
             FontInfo {
                 gamma_ratios: get_gamma_correction_ratios(render_params.GetGamma()),
                 grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(),
+                subpixel_enhanced_contrast: render_params.GetEnhancedContrast(),
             }
         })
     }
@@ -789,6 +832,13 @@ impl DirectXRenderPipelines {
             512,
             create_blend_state(device)?,
         )?;
+        let subpixel_sprites = PipelineState::new(
+            device,
+            "subpixel_sprite_pipeline",
+            ShaderModule::SubpixelSprite,
+            512,
+            create_blend_state_for_subpixel_rendering(device)?,
+        )?;
         let poly_sprites = PipelineState::new(
             device,
             "polychrome_sprite_pipeline",
@@ -804,6 +854,7 @@ impl DirectXRenderPipelines {
             path_sprite_pipeline,
             underline_pipeline,
             mono_sprites,
+            subpixel_sprites,
             poly_sprites,
         })
     }
@@ -878,7 +929,7 @@ struct GlobalParams {
     gamma_ratios: [f32; 4],
     viewport_size: [f32; 2],
     grayscale_enhanced_contrast: f32,
-    _pad: u32,
+    subpixel_enhanced_contrast: f32,
 }
 
 struct PipelineState<T> {
@@ -1235,8 +1286,6 @@ fn set_rasterizer_state(device: &ID3D11Device, device_context: &ID3D11DeviceCont
 // https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_blend_desc
 #[inline]
 fn create_blend_state(device: &ID3D11Device) -> Result<ID3D11BlendState> {
-    // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display
-    // device performs the blend in linear space, which is ideal.
     let mut desc = D3D11_BLEND_DESC::default();
     desc.RenderTarget[0].BlendEnable = true.into();
     desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
@@ -1253,6 +1302,27 @@ fn create_blend_state(device: &ID3D11Device) -> Result<ID3D11BlendState> {
     }
 }
 
+#[inline]
+fn create_blend_state_for_subpixel_rendering(device: &ID3D11Device) -> Result<ID3D11BlendState> {
+    let mut desc = D3D11_BLEND_DESC::default();
+    desc.RenderTarget[0].BlendEnable = true.into();
+    desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
+    desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
+    desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC1_COLOR;
+    desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC1_COLOR;
+    // It does not make sense to draw transparent subpixel-rendered text, since it cannot be meaningfully alpha-blended onto anything else.
+    desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
+    desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
+    desc.RenderTarget[0].RenderTargetWriteMask =
+        D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8 & !D3D11_COLOR_WRITE_ENABLE_ALPHA.0 as u8;
+
+    unsafe {
+        let mut state = None;
+        device.CreateBlendState(&desc, Some(&mut state))?;
+        Ok(state.unwrap())
+    }
+}
+
 #[inline]
 fn create_blend_state_for_path_rasterization(device: &ID3D11Device) -> Result<ID3D11BlendState> {
     // If the feature level is set to greater than D3D_FEATURE_LEVEL_9_3, the display
@@ -1410,6 +1480,7 @@ pub(crate) mod shader_resources {
         PathRasterization,
         PathSprite,
         MonochromeSprite,
+        SubpixelSprite,
         PolychromeSprite,
         EmojiRasterization,
     }
@@ -1477,6 +1548,10 @@ pub(crate) mod shader_resources {
                     ShaderTarget::Vertex => MONOCHROME_SPRITE_VERTEX_BYTES,
                     ShaderTarget::Fragment => MONOCHROME_SPRITE_FRAGMENT_BYTES,
                 },
+                ShaderModule::SubpixelSprite => match target {
+                    ShaderTarget::Vertex => SUBPIXEL_SPRITE_VERTEX_BYTES,
+                    ShaderTarget::Fragment => SUBPIXEL_SPRITE_FRAGMENT_BYTES,
+                },
                 ShaderModule::PolychromeSprite => match target {
                     ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES,
                     ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES,
@@ -1561,7 +1636,7 @@ pub(crate) mod shader_resources {
 
     #[cfg(debug_assertions)]
     impl ShaderModule {
-        pub fn as_str(&self) -> &str {
+        pub fn as_str(self) -> &'static str {
             match self {
                 ShaderModule::Quad => "quad",
                 ShaderModule::Shadow => "shadow",
@@ -1569,6 +1644,7 @@ pub(crate) mod shader_resources {
                 ShaderModule::PathRasterization => "path_rasterization",
                 ShaderModule::PathSprite => "path_sprite",
                 ShaderModule::MonochromeSprite => "monochrome_sprite",
+                ShaderModule::SubpixelSprite => "subpixel_sprite",
                 ShaderModule::PolychromeSprite => "polychrome_sprite",
                 ShaderModule::EmojiRasterization => "emoji_rasterization",
             }

crates/gpui/src/platform/windows/shaders.hlsl 🔗

@@ -4,12 +4,17 @@ cbuffer GlobalParams: register(b0) {
     float4 gamma_ratios;
     float2 global_viewport_size;
     float grayscale_enhanced_contrast;
-    uint _pad;
+    float subpixel_enhanced_contrast;
 };
 
 Texture2D<float4> t_sprite: register(t0);
 SamplerState s_sprite: register(s0);
 
+struct SubpixelSpriteFragmentOutput {
+    float4 foreground : SV_Target0;
+    float4 alpha : SV_Target1;
+};
+
 struct Bounds {
     float2 origin;
     float2 size;
@@ -1119,6 +1124,20 @@ float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Targe
     return float4(input.color.rgb, input.color.a * alpha_corrected);
 }
 
+MonochromeSpriteVertexOutput subpixel_sprite_vertex(uint vertex_id: SV_VertexID, uint sprite_id: SV_InstanceID) {
+    return monochrome_sprite_vertex(vertex_id, sprite_id);
+}
+
+SubpixelSpriteFragmentOutput subpixel_sprite_fragment(MonochromeSpriteFragmentInput input) {
+    float3 sample = t_sprite.Sample(s_sprite, input.tile_position).rgb;
+    float3 alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, subpixel_enhanced_contrast, gamma_ratios);
+
+    SubpixelSpriteFragmentOutput output;
+    output.foreground = float4(input.color.rgb, 1.0f);
+    output.alpha = float4(input.color.a * alpha_corrected, 1.0f);
+    return output;
+}
+
 /*
 **
 **              Polychrome sprites

crates/gpui/src/platform/windows/system_settings.rs 🔗

@@ -7,8 +7,8 @@ use ::util::ResultExt;
 use windows::Win32::UI::{
     Shell::{ABM_GETSTATE, ABM_GETTASKBARPOS, ABS_AUTOHIDE, APPBARDATA, SHAppBarMessage},
     WindowsAndMessaging::{
-        SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
-        SystemParametersInfoW,
+        SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SPI_SETWORKAREA,
+        SYSTEM_PARAMETERS_INFO_ACTION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
     },
 };
 
@@ -39,18 +39,16 @@ impl WindowsSystemSettings {
         settings
     }
 
-    fn init(&self, display: WindowsDisplay) {
+    fn init(&mut self, display: WindowsDisplay) {
         self.mouse_wheel_settings.update();
         self.auto_hide_taskbar_position
             .set(AutoHideTaskbarPosition::new(display).log_err().flatten());
     }
 
     pub(crate) fn update(&self, display: WindowsDisplay, wparam: usize) {
-        match wparam {
-            // SPI_SETWORKAREA
-            47 => self.update_taskbar_position(display),
-            // SPI_GETWHEELSCROLLLINES, SPI_GETWHEELSCROLLCHARS
-            104 | 108 => self.update_mouse_wheel_settings(),
+        match SYSTEM_PARAMETERS_INFO_ACTION(wparam as u32) {
+            SPI_SETWORKAREA => self.update_taskbar_position(display),
+            SPI_GETWHEELSCROLLLINES | SPI_GETWHEELSCROLLCHARS => self.update_mouse_wheel_settings(),
             _ => {}
         }
     }

crates/gpui/src/platform/windows/window.rs 🔗

@@ -45,6 +45,7 @@ pub struct WindowsWindowState {
     pub fullscreen_restore_bounds: Cell<Bounds<Pixels>>,
     pub border_offset: WindowBorderOffset,
     pub appearance: Cell<WindowAppearance>,
+    pub background_appearance: Cell<WindowBackgroundAppearance>,
     pub scale_factor: Cell<f32>,
     pub restore_from_minimized: Cell<Option<Box<dyn FnMut(RequestFrameOptions)>>>,
 
@@ -135,6 +136,7 @@ impl WindowsWindowState {
             fullscreen_restore_bounds: Cell::new(fullscreen_restore_bounds),
             border_offset,
             appearance: Cell::new(appearance),
+            background_appearance: Cell::new(WindowBackgroundAppearance::Opaque),
             scale_factor: Cell::new(scale_factor),
             restore_from_minimized: Cell::new(restore_from_minimized),
             min_size,
@@ -788,6 +790,14 @@ impl PlatformWindow for WindowsWindow {
         self.state.hovered.get()
     }
 
+    fn background_appearance(&self) -> WindowBackgroundAppearance {
+        self.state.background_appearance.get()
+    }
+
+    fn is_subpixel_rendering_supported(&self) -> bool {
+        true
+    }
+
     fn set_title(&mut self, title: &str) {
         unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) }
             .inspect_err(|e| log::error!("Set title failed: {e}"))
@@ -795,6 +805,7 @@ impl PlatformWindow for WindowsWindow {
     }
 
     fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
+        self.state.background_appearance.set(background_appearance);
         let hwnd = self.0.hwnd;
 
         // using Dwm APIs for Mica and MicaAlt backdrops.
@@ -905,7 +916,11 @@ impl PlatformWindow for WindowsWindow {
     }
 
     fn draw(&self, scene: &Scene) {
-        self.state.renderer.borrow_mut().draw(scene).log_err();
+        self.state
+            .renderer
+            .borrow_mut()
+            .draw(scene, self.state.background_appearance.get())
+            .log_err();
     }
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {

crates/gpui/src/scene.rs 🔗

@@ -30,6 +30,7 @@ pub(crate) struct Scene {
     pub(crate) paths: Vec<Path<ScaledPixels>>,
     pub(crate) underlines: Vec<Underline>,
     pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
+    pub(crate) subpixel_sprites: Vec<SubpixelSprite>,
     pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
     pub(crate) surfaces: Vec<PaintSurface>,
 }
@@ -44,6 +45,7 @@ impl Scene {
         self.quads.clear();
         self.underlines.clear();
         self.monochrome_sprites.clear();
+        self.subpixel_sprites.clear();
         self.polychrome_sprites.clear();
         self.surfaces.clear();
     }
@@ -101,6 +103,10 @@ impl Scene {
                 sprite.order = order;
                 self.monochrome_sprites.push(sprite.clone());
             }
+            Primitive::SubpixelSprite(sprite) => {
+                sprite.order = order;
+                self.subpixel_sprites.push(sprite.clone());
+            }
             Primitive::PolychromeSprite(sprite) => {
                 sprite.order = order;
                 self.polychrome_sprites.push(sprite.clone());
@@ -131,6 +137,8 @@ impl Scene {
         self.underlines.sort_by_key(|underline| underline.order);
         self.monochrome_sprites
             .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
+        self.subpixel_sprites
+            .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
         self.polychrome_sprites
             .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
         self.surfaces.sort_by_key(|surface| surface.order);
@@ -160,6 +168,9 @@ impl Scene {
             monochrome_sprites: &self.monochrome_sprites,
             monochrome_sprites_start: 0,
             monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
+            subpixel_sprites: &self.subpixel_sprites,
+            subpixel_sprites_start: 0,
+            subpixel_sprites_iter: self.subpixel_sprites.iter().peekable(),
             polychrome_sprites: &self.polychrome_sprites,
             polychrome_sprites_start: 0,
             polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
@@ -185,6 +196,7 @@ pub(crate) enum PrimitiveKind {
     Path,
     Underline,
     MonochromeSprite,
+    SubpixelSprite,
     PolychromeSprite,
     Surface,
 }
@@ -202,6 +214,7 @@ pub(crate) enum Primitive {
     Path(Path<ScaledPixels>),
     Underline(Underline),
     MonochromeSprite(MonochromeSprite),
+    SubpixelSprite(SubpixelSprite),
     PolychromeSprite(PolychromeSprite),
     Surface(PaintSurface),
 }
@@ -214,6 +227,7 @@ impl Primitive {
             Primitive::Path(path) => &path.bounds,
             Primitive::Underline(underline) => &underline.bounds,
             Primitive::MonochromeSprite(sprite) => &sprite.bounds,
+            Primitive::SubpixelSprite(sprite) => &sprite.bounds,
             Primitive::PolychromeSprite(sprite) => &sprite.bounds,
             Primitive::Surface(surface) => &surface.bounds,
         }
@@ -226,6 +240,7 @@ impl Primitive {
             Primitive::Path(path) => &path.content_mask,
             Primitive::Underline(underline) => &underline.content_mask,
             Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
+            Primitive::SubpixelSprite(sprite) => &sprite.content_mask,
             Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
             Primitive::Surface(surface) => &surface.content_mask,
         }
@@ -255,6 +270,9 @@ struct BatchIterator<'a> {
     monochrome_sprites: &'a [MonochromeSprite],
     monochrome_sprites_start: usize,
     monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
+    subpixel_sprites: &'a [SubpixelSprite],
+    subpixel_sprites_start: usize,
+    subpixel_sprites_iter: Peekable<slice::Iter<'a, SubpixelSprite>>,
     polychrome_sprites: &'a [PolychromeSprite],
     polychrome_sprites_start: usize,
     polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
@@ -282,6 +300,10 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.monochrome_sprites_iter.peek().map(|s| s.order),
                 PrimitiveKind::MonochromeSprite,
             ),
+            (
+                self.subpixel_sprites_iter.peek().map(|s| s.order),
+                PrimitiveKind::SubpixelSprite,
+            ),
             (
                 self.polychrome_sprites_iter.peek().map(|s| s.order),
                 PrimitiveKind::PolychromeSprite,
@@ -383,6 +405,27 @@ impl<'a> Iterator for BatchIterator<'a> {
                     sprites: &self.monochrome_sprites[sprites_start..sprites_end],
                 })
             }
+            PrimitiveKind::SubpixelSprite => {
+                let texture_id = self.subpixel_sprites_iter.peek().unwrap().tile.texture_id;
+                let sprites_start = self.subpixel_sprites_start;
+                let mut sprites_end = sprites_start + 1;
+                self.subpixel_sprites_iter.next();
+                while self
+                    .subpixel_sprites_iter
+                    .next_if(|sprite| {
+                        (sprite.order, batch_kind) < max_order_and_kind
+                            && sprite.tile.texture_id == texture_id
+                    })
+                    .is_some()
+                {
+                    sprites_end += 1;
+                }
+                self.subpixel_sprites_start = sprites_end;
+                Some(PrimitiveBatch::SubpixelSprites {
+                    texture_id,
+                    sprites: &self.subpixel_sprites[sprites_start..sprites_end],
+                })
+            }
             PrimitiveKind::PolychromeSprite => {
                 let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id;
                 let sprites_start = self.polychrome_sprites_start;
@@ -441,6 +484,11 @@ pub(crate) enum PrimitiveBatch<'a> {
         texture_id: AtlasTextureId,
         sprites: &'a [MonochromeSprite],
     },
+    #[cfg_attr(target_os = "macos", allow(dead_code))]
+    SubpixelSprites {
+        texture_id: AtlasTextureId,
+        sprites: &'a [SubpixelSprite],
+    },
     PolychromeSprites {
         texture_id: AtlasTextureId,
         sprites: &'a [PolychromeSprite],
@@ -634,6 +682,24 @@ impl From<MonochromeSprite> for Primitive {
     }
 }
 
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub(crate) struct SubpixelSprite {
+    pub order: DrawOrder,
+    pub pad: u32, // align to 8 bytes
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub color: Hsla,
+    pub tile: AtlasTile,
+    pub transformation: TransformationMatrix,
+}
+
+impl From<SubpixelSprite> for Primitive {
+    fn from(sprite: SubpixelSprite) -> Self {
+        Primitive::SubpixelSprite(sprite)
+    }
+}
+
 #[derive(Clone, Debug)]
 #[repr(C)]
 pub(crate) struct PolychromeSprite {

crates/gpui/src/text_system.rs 🔗

@@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::{
     Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
-    StrikethroughStyle, UnderlineStyle, px,
+    StrikethroughStyle, TextRenderingMode, UnderlineStyle, px,
 };
 use anyhow::{Context as _, anyhow};
 use collections::FxHashMap;
@@ -326,6 +326,17 @@ impl TextSystem {
         self.platform_text_system
             .rasterize_glyph(params, raster_bounds)
     }
+
+    /// Returns the text rendering mode recommended by the platform for the given font and size.
+    /// The return value will never be [`TextRenderingMode::PlatformDefault`].
+    pub(crate) fn recommended_rendering_mode(
+        &self,
+        font_id: FontId,
+        font_size: Pixels,
+    ) -> TextRenderingMode {
+        self.platform_text_system
+            .recommended_rendering_mode(font_id, font_size)
+    }
 }
 
 /// The GPUI text layout subsystem.
@@ -775,6 +786,7 @@ pub(crate) struct RenderGlyphParams {
     pub(crate) subpixel_variant: Point<u8>,
     pub(crate) scale_factor: f32,
     pub(crate) is_emoji: bool,
+    pub(crate) subpixel_rendering: bool,
 }
 
 impl Eq for RenderGlyphParams {}
@@ -787,6 +799,7 @@ impl Hash for RenderGlyphParams {
         self.subpixel_variant.hash(state);
         self.scale_factor.to_bits().hash(state);
         self.is_emoji.hash(state);
+        self.subpixel_rendering.hash(state);
     }
 }
 

crates/gpui/src/window.rs 🔗

@@ -12,12 +12,12 @@ use crate::{
     PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton,
     PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
     Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
-    ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubscriberSet,
-    Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task,
-    TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
-    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
-    WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
-    transparent_black,
+    ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite,
+    SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap,
+    TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement,
+    TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
+    WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
+    point, prelude::*, px, rems, size, transparent_black,
 };
 use anyhow::{Context as _, Result, anyhow};
 use collections::{FxHashMap, FxHashSet};
@@ -838,6 +838,7 @@ pub struct Window {
     display_id: Option<DisplayId>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     text_system: Arc<WindowTextSystem>,
+    text_rendering_mode: Rc<Cell<TextRenderingMode>>,
     rem_size: Pixels,
     /// The stack of override values for the window's rem size.
     ///
@@ -1312,6 +1313,7 @@ impl Window {
             display_id,
             sprite_atlas,
             text_system,
+            text_rendering_mode: cx.text_rendering_mode.clone(),
             rem_size: px(16.),
             rem_size_override_stack: SmallVec::new(),
             viewport_size: content_size,
@@ -3130,6 +3132,7 @@ impl Window {
             x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS_X as f32).floor() as u8,
             y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS_Y as f32).floor() as u8,
         };
+        let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size);
         let params = RenderGlyphParams {
             font_id,
             glyph_id,
@@ -3137,6 +3140,7 @@ impl Window {
             subpixel_variant,
             scale_factor,
             is_emoji: false,
+            subpixel_rendering,
         };
 
         let raster_bounds = self.text_system().raster_bounds(&params)?;
@@ -3153,19 +3157,51 @@ impl Window {
                 size: tile.bounds.size.map(Into::into),
             };
             let content_mask = self.content_mask().scale(scale_factor);
-            self.next_frame.scene.insert_primitive(MonochromeSprite {
-                order: 0,
-                pad: 0,
-                bounds,
-                content_mask,
-                color: color.opacity(element_opacity),
-                tile,
-                transformation: TransformationMatrix::unit(),
-            });
+
+            if subpixel_rendering {
+                self.next_frame.scene.insert_primitive(SubpixelSprite {
+                    order: 0,
+                    pad: 0,
+                    bounds,
+                    content_mask,
+                    color: color.opacity(element_opacity),
+                    tile,
+                    transformation: TransformationMatrix::unit(),
+                });
+            } else {
+                self.next_frame.scene.insert_primitive(MonochromeSprite {
+                    order: 0,
+                    pad: 0,
+                    bounds,
+                    content_mask,
+                    color: color.opacity(element_opacity),
+                    tile,
+                    transformation: TransformationMatrix::unit(),
+                });
+            }
         }
         Ok(())
     }
 
+    fn should_use_subpixel_rendering(&self, font_id: FontId, font_size: Pixels) -> bool {
+        if self.platform_window.background_appearance() != WindowBackgroundAppearance::Opaque {
+            return false;
+        }
+
+        if !self.platform_window.is_subpixel_rendering_supported() {
+            return false;
+        }
+
+        let mode = match self.text_rendering_mode.get() {
+            TextRenderingMode::PlatformDefault => self
+                .text_system()
+                .recommended_rendering_mode(font_id, font_size),
+            mode => mode,
+        };
+
+        mode == TextRenderingMode::Subpixel
+    }
+
     /// Paints an emoji glyph into the scene for the next frame at the current z-index.
     ///
     /// The y component of the origin is the baseline of the glyph.
@@ -3193,6 +3229,7 @@ impl Window {
             subpixel_variant: Default::default(),
             scale_factor,
             is_emoji: true,
+            subpixel_rendering: false,
         };
 
         let raster_bounds = self.text_system().raster_bounds(&params)?;

crates/settings/src/settings_content/workspace.rs 🔗

@@ -15,6 +15,10 @@ use crate::{
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
     pub active_pane_modifiers: Option<ActivePaneModifiers>,
+    /// The text rendering mode to use.
+    ///
+    /// Default: platform_default
+    pub text_rendering_mode: Option<TextRenderingMode>,
     /// Layout mode for the bottom dock
     ///
     /// Default: contained
@@ -545,6 +549,31 @@ pub enum OnLastWindowClosed {
     QuitApp,
 }
 
+#[derive(
+    Copy,
+    Clone,
+    Default,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    PartialEq,
+    Eq,
+    Debug,
+    strum::VariantArray,
+    strum::VariantNames,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum TextRenderingMode {
+    /// Use platform default behavior.
+    #[default]
+    PlatformDefault,
+    /// Use subpixel (ClearType-style) text rendering.
+    Subpixel,
+    /// Use grayscale text rendering.
+    Grayscale,
+}
+
 impl OnLastWindowClosed {
     pub fn is_quit_app(&self) -> bool {
         match self {

crates/settings/src/vscode_import.rs 🔗

@@ -817,6 +817,7 @@ impl VsCodeSettings {
     fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
         WorkspaceSettingsContent {
             active_pane_modifiers: self.active_pane_modifiers(),
+            text_rendering_mode: None,
             autosave: self.read_enum("files.autoSave", |s| match s {
                 "off" => Some(AutosaveSetting::Off),
                 "afterDelay" => Some(AutosaveSetting::AfterDelay {

crates/settings_ui/src/page_data.rs 🔗

@@ -890,6 +890,22 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SectionHeader("Text Rendering"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Text Rendering Mode",
+                    description: "The text rendering mode to use.",
+                    field: Box::new(SettingField {
+                        json_path: Some("text_rendering_mode"),
+                        pick: |settings_content| {
+                            settings_content.workspace.text_rendering_mode.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.workspace.text_rendering_mode = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SectionHeader("Cursor"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Multi Cursor Modifier",

crates/settings_ui/src/settings_ui.rs 🔗

@@ -436,6 +436,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
         .add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
         .add_basic_renderer::<settings::CloseWindowWhenNoItems>(render_dropdown)
+        .add_basic_renderer::<settings::TextRenderingMode>(render_dropdown)
         .add_basic_renderer::<settings::FontFamilyName>(render_font_picker)
         .add_basic_renderer::<settings::BaseKeymapContent>(render_dropdown)
         .add_basic_renderer::<settings::MultiCursorModifier>(render_dropdown)

crates/workspace/src/workspace_settings.rs 🔗

@@ -27,6 +27,7 @@ pub struct WorkspaceSettings {
     pub max_tabs: Option<NonZeroUsize>,
     pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems,
     pub on_last_window_closed: settings::OnLastWindowClosed,
+    pub text_rendering_mode: settings::TextRenderingMode,
     pub resize_all_panels_in_dock: Vec<DockPosition>,
     pub close_on_file_delete: bool,
     pub use_system_window_tabs: bool,
@@ -96,6 +97,7 @@ impl Settings for WorkspaceSettings {
             max_tabs: workspace.max_tabs,
             when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(),
             on_last_window_closed: workspace.on_last_window_closed.unwrap(),
+            text_rendering_mode: workspace.text_rendering_mode.unwrap(),
             resize_all_panels_in_dock: workspace
                 .resize_all_panels_in_dock
                 .clone()

crates/zed/src/main.rs 🔗

@@ -678,6 +678,18 @@ fn main() {
                         .ok();
                 }
 
+                cx.set_text_rendering_mode(
+                    match WorkspaceSettings::get_global(cx).text_rendering_mode {
+                        settings::TextRenderingMode::PlatformDefault => {
+                            gpui::TextRenderingMode::PlatformDefault
+                        }
+                        settings::TextRenderingMode::Subpixel => gpui::TextRenderingMode::Subpixel,
+                        settings::TextRenderingMode::Grayscale => {
+                            gpui::TextRenderingMode::Grayscale
+                        }
+                    },
+                );
+
                 let new_host = &client::ClientSettings::get_global(cx).server_url;
                 if &http.base_url() != new_host {
                     http.set_base_url(new_host);