From 47d2694dcceae02ff9ffd5d7d174389dde6a7bf3 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 6 Jan 2026 03:46:31 -0500 Subject: [PATCH] Add support for subpixel text rendering (#45423) Subpixel text rendering is now implemented on Windows and Linux. Comparison screenshots: |Before|After| | ------------- | ------------- | | | | 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 --- 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 ++ .../gpui/src/platform/blade/blade_context.rs | 5 + .../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 +++++++++++------- .../gpui/src/platform/linux/wayland/client.rs | 4 +- .../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 + .../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 ++ .../platform/windows/alpha_correction.hlsl | 17 +++ .../gpui/src/platform/windows/direct_write.rs | 104 +++++++++++++- .../src/platform/windows/directx_atlas.rs | 14 ++ .../src/platform/windows/directx_renderer.rs | 96 +++++++++++-- crates/gpui/src/platform/windows/shaders.hlsl | 21 ++- .../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 +++++++-- .../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(-) diff --git a/Cargo.lock b/Cargo.lock index 4250684b141030991cd5582e6349719748c0769f..add19f89af3869fe4fbd203087c9ef45c644df28 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 54f256abac03c50d5aa0ca0d1c5dd7d433319ddf..69d02faeb16e743cc50a4a13a0e14b5f521624a2 100644 --- a/Cargo.toml +++ b/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" diff --git a/assets/settings/default.json b/assets/settings/default.json index e0316f2090ac1374e2a2d200c92bc39a93de77f8..25e0b1a6db889f3e774c3edcdcd12c19d8115490 100644 --- a/assets/settings/default.json +++ b/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). diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 40376f476b6d80f6b5170840f295a71acdfebb7d..bfde988490e7b89c54267481403dc71d8979abf0 100644 --- a/crates/gpui/Cargo.toml +++ b/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", diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index c7ae7ac9f239f2f6ce3880f9329f2ba92b2174f3..67032a9afdf7c2a234da80b940732783efcd966a 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -297,6 +297,7 @@ mod windows { "path_sprite", "underline", "monochrome_sprite", + "subpixel_sprite", "polychrome_sprite", ]; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d34b53c42201875b39964dab023737b8c58396db..c019bda14cb211a0521e8a3513b642a0b2a369ce 100644 --- a/crates/gpui/src/app.rs +++ b/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>, 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) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index bd4342fb5e750a60338f7024d357a552853a0ece..78b09db8a960e1831d4718b7427b035bbdbef3c3 100644 --- a/crates/gpui/src/platform.rs +++ b/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; + 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, ) -> Result<(Size, Vec)>; 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 { diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index 9b9299df9958e71713269312c12610fe176798ed..3a02564ead6e11f64dba20d1c31db0cc5af8f358 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/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, + subpixel_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, } @@ -271,6 +276,7 @@ impl ops::Index 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 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 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); } diff --git a/crates/gpui/src/platform/blade/blade_context.rs b/crates/gpui/src/platform/blade/blade_context.rs index 12c68a1e70188d3ed2ab425b5abc1bac0dfe3a19..5a5382c9c44e64bddac1a457191ecb6c98ffbff7 100644 --- a/crates/gpui/src/platform/blade/blade_context.rs +++ b/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 { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index cbd596900f92441f0ecf727b896108bd7bf3aa8a..feef6e4c70d8c251afd7b50cfca330bb8fbf1fcb 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/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, } } } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 2981b1446c6d5a2c6bd670e6a040b6a830a8e1d9..8e9921f5d758550856b7e7c2b0ea6ee3c5baa011 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/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, k: f32) -> vec3 { + 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_alpha_correction3(a: vec3, b: vec3, g: vec4) -> vec3 { + 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); @@ -60,6 +71,13 @@ fn apply_contrast_and_gamma_correction(sample: f32, color: vec3, enhanced_c return apply_alpha_correction(contrasted, brightness, gamma_ratios); } +fn apply_contrast_and_gamma_correction3(sample: vec3, color: vec3, enhanced_contrast_factor: f32, gamma_ratios: vec4) -> vec3 { + 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, premultiplied_alpha: u32, @@ -69,6 +87,7 @@ struct GlobalParams { var globals: GlobalParams; var gamma_ratios: vec4; var grayscale_enhanced_contrast: f32; +var subpixel_enhanced_contrast: f32; var t_sprite: texture_2d; var s_sprite: sampler; @@ -1190,7 +1209,6 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4 { return vec4(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 { 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 b_subpixel_sprites: array; + +struct SubpixelSpriteOutput { + @builtin(position) position: vec4, + @location(0) tile_position: vec2, + @location(1) @interpolate(flat) color: vec4, + @location(3) clip_distances: vec4, +} + +struct SubpixelSpriteFragmentOutput { + @location(0) @blend_src(0) foreground: vec4, + @location(0) @blend_src(1) alpha: vec4, +} + +@vertex +fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> SubpixelSpriteOutput { + let unit_vertex = vec2(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(0.0))) { + return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); + } + + var out = SubpixelSpriteFragmentOutput(); + out.foreground = vec4(input.color.rgb, 1.0); + out.alpha = vec4(input.color.a * alpha_corrected, 1.0); + return out; +} diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs index 958d509d5317aea32815eee2850e3a196d6586ed..1fcd7d4faa18677c4d0d5c45f3181913a999e156 100644 --- a/crates/gpui/src/platform/linux/text_system.rs +++ b/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); @@ -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, /// 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> { - 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, Vec)> { 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 { + 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 diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index b6bfbec0679f9413fceef2bb37e7bd304371707e..df169b082203f622128de6ed9351b11bb0824972 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/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_keyboard: Option, @@ -247,7 +247,7 @@ pub(crate) struct WaylandClientState { cursor: Cursor, pending_activation: Option, event_loop: Option>, - common: LinuxCommon, + pub common: LinuxCommon, } pub struct DragState { diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 6b4dad3b3917d025a80594b5ece63c26bbadde69..7adaf055d94bdd241ca6e8db82720191e337bcd0 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/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(); diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 0de5ff02f7d0895da05dfa480bff2e19abff40db..b69cad8431dffb18834ae8b7ef27c3abd976984f 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -177,7 +177,7 @@ pub struct X11ClientState { pub(crate) last_location: Point, pub(crate) current_count: usize, - gpu_context: BladeContext, + pub(crate) gpu_context: BladeContext, pub(crate) scale_factor: f32, diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 46d1cbd3253618cae5a7df9c27195ea8921954eb..ee29f0d103d808b4db064969b992d2af75c1a187 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/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; diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 8282530c5efdc13ca95a1f04c0f6ef1a23c8366c..ad2e355bed440dd27ebf0e784a4b5cf9c83f111a 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/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() } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 2ccd14d493a16e66186c57cbaced63782db62446..07e7cdd2763226e92948e7b02b2c09383a0aa52f 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -630,6 +630,7 @@ impl MetalRenderer { viewport_size, command_encoder, ), + PrimitiveBatch::SubpixelSprites { .. } => unreachable!(), }; if !ok { command_encoder.end_encoding(); diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 8595582f4ad7e078f7cfb0140e249feb0a9740dc..c72271f24fe4ea3750e7aa1d18cadf786cac741e 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/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 { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index b51c491f68cd879305f27eb2cf0523f1ab81ea39..29dcf9ba4239ecdd436c79ac45bd2aa937b4b176 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -402,6 +402,7 @@ struct MacWindowState { native_window: id, native_view: NonNull, blurred_view: Option, + background_appearance: WindowBackgroundAppearance, display_link: Option, renderer: renderer::Renderer, request_frame_callback: Option>, @@ -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; diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 9e87f4504ddd61e34b645ea69ea394c4940f9d55..bec52ccb0b3924d6e5233f4834f68f5c108d6735 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/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()); } diff --git a/crates/gpui/src/platform/windows/alpha_correction.hlsl b/crates/gpui/src/platform/windows/alpha_correction.hlsl index b0a9ca2e6b60a515ad2c1f9d95cd3e19079d326c..5a34a4ebf28f5ea1a4f70581e10a23b5b12f01a9 100644 --- a/crates/gpui/src/platform/windows/alpha_correction.hlsl +++ b/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); +} diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index 22b8e6231aa0812b63a899cfa61c94a258700079..8ac65e8fb9b935ffeda3d8f4349e0b73c0b204e4 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/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, font_selections: HashMap, font_id_by_identifier: HashMap, + 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> { 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, ) -> Result> { - 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 { 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(); diff --git a/crates/gpui/src/platform/windows/directx_atlas.rs b/crates/gpui/src/platform/windows/directx_atlas.rs index 9deae392d1a5ef18a6af644031f047780fa23f70..901e0649633652b6ccac7d20d0a7ecffc1e33366 100644 --- a/crates/gpui/src/platform/windows/directx_atlas.rs +++ b/crates/gpui/src/platform/windows/directx_atlas.rs @@ -21,6 +21,7 @@ struct DirectXAtlasState { device_context: ID3D11DeviceContext, monochrome_textures: AtlasTextureList, polychrome_textures: AtlasTextureList, + subpixel_textures: AtlasTextureList, tiles_by_key: FxHashMap, } @@ -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() + } } } } diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index 608ac2c3b065c598547be8b79f8d7fae8070ff48..443ea1424313d843293f0648449192bbea0b0234 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/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, underline_pipeline: PipelineState, mono_sprites: PipelineState, + subpixel_sprites: PipelineState, poly_sprites: PipelineState, } @@ -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 { @@ -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 { - // 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 { } } +#[inline] +fn create_blend_state_for_subpixel_rendering(device: &ID3D11Device) -> Result { + 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 { // 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", } diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index d6168eea09b4c7da705f5ecfc3e4002222f3149d..0b1543a286c957f47a92417a00ad5fb957df25ae 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/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 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 diff --git a/crates/gpui/src/platform/windows/system_settings.rs b/crates/gpui/src/platform/windows/system_settings.rs index f5ef5ce31ec23b69d1f009792c693e248d404b8e..0a002bab28e07d10f4e554f006d85f40c4aff19c 100644 --- a/crates/gpui/src/platform/windows/system_settings.rs +++ b/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(), _ => {} } } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index c7df193ad988ef289ad8ac4f96bca63b4ff30b9a..837474a9da3217f62a299a90a166e71065afa2ac 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -45,6 +45,7 @@ pub struct WindowsWindowState { pub fullscreen_restore_bounds: Cell>, pub border_offset: WindowBorderOffset, pub appearance: Cell, + pub background_appearance: Cell, pub scale_factor: Cell, pub restore_from_minimized: Cell>>, @@ -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 { diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 758d06e5972e36b15fe0b8ce9d7d3193f8d3844d..eb95be1d83b9e10e83eae129b64b51e10d667c2d 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -30,6 +30,7 @@ pub(crate) struct Scene { pub(crate) paths: Vec>, pub(crate) underlines: Vec, pub(crate) monochrome_sprites: Vec, + pub(crate) subpixel_sprites: Vec, pub(crate) polychrome_sprites: Vec, pub(crate) surfaces: Vec, } @@ -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), 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>, + subpixel_sprites: &'a [SubpixelSprite], + subpixel_sprites_start: usize, + subpixel_sprites_iter: Peekable>, polychrome_sprites: &'a [PolychromeSprite], polychrome_sprites_start: usize, polychrome_sprites_iter: Peekable>, @@ -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 for Primitive { } } +#[derive(Clone, Debug)] +#[repr(C)] +pub(crate) struct SubpixelSprite { + pub order: DrawOrder, + pub pad: u32, // align to 8 bytes + pub bounds: Bounds, + pub content_mask: ContentMask, + pub color: Hsla, + pub tile: AtlasTile, + pub transformation: TransformationMatrix, +} + +impl From for Primitive { + fn from(sprite: SubpixelSprite) -> Self { + Primitive::SubpixelSprite(sprite) + } +} + #[derive(Clone, Debug)] #[repr(C)] pub(crate) struct PolychromeSprite { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 070e434dc992af4ca5b28f6e55aa0aa3cb9e5790..d769dd3517471b5340c72ff25ceb8dc662ebf142 100644 --- a/crates/gpui/src/text_system.rs +++ b/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, 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); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index d9ac3bb0481716312666495e328ca45a261a739e..20a46622387505049fc72a9ec1ee955e38e18002 100644 --- a/crates/gpui/src/window.rs +++ b/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, sprite_atlas: Arc, text_system: Arc, + text_rendering_mode: Rc>, 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(¶ms)?; @@ -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(¶ms)?; diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 832f6ec409c8594c55beab1fd6f327c1215f8bdc..73c94561e0dfe14d6a3275b4eaf67dd52ce0d658 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/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, + /// The text rendering mode to use. + /// + /// Default: platform_default + pub text_rendering_mode: Option, /// 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 { diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index f251faca4d5a0b06faaba9f7bec4ba620b43b72e..4e7f9abaf1a350ca4ec2b8678027a7479ab3d07e 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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 { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 5fd19427c60b91afd06aacbcce4455780d4a120b..7b451b0769b26688491ec929d4d2b06d5f1f2c55 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -890,6 +890,22 @@ pub(crate) fn settings_data(cx: &App) -> Vec { 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", diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 4d66e3713b10d9b0522025273f92cedb0ad22c69..2069d512dbc987c4f1840673dff4b1e916c154f9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -436,6 +436,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_font_picker) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 4ce0394fe5fdc74754c1147138cb33c67e076d88..ed915e4a96a446c326437c6817ae471bc87b1dca 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -27,6 +27,7 @@ pub struct WorkspaceSettings { pub max_tabs: Option, 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, 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() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f632b36f2f8b98b8f7de40596e02ebca5a08a7ec..431951eb0e08133c039ac2c11cd6bef5e466de0f 100644 --- a/crates/zed/src/main.rs +++ b/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);