mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
windows: Slightly improve font rendering quality (#12015)
Upper before this PR, lower after. ![Screenshot 2024-05-20 144852](https://github.com/zed-industries/zed/assets/14981363/88995482-3a98-41be-9c2c-6b781bef6ad2) This PR manually applies a MSAA to the font atlas. Before this PR, the font may seem aliased ( espeacially on low DPI monitors ), that's because `DirectWrite` and `CoreText` ( on which currently `Zed` built the whole text system ) uses different anti-aliasing strategy. The different anti-aliasing approach used by `DirectWrite` and `CoreText`: ![Screenshot 2024-05-20 151114](https://github.com/zed-industries/zed/assets/14981363/21a2fc1e-48a2-4cff-a9d1-41602eff3658) The upper is `VSCode` font rendering result, middle `macOS`, lower this PR ( pic captured with same font face, same font size, same DPI, same physical resolution, same editor theme, and same magnification rate ). This PR brings a quality similiar to `CoreText`. What's more, from the `VSCode` image, you can see how `DirectWrite` sub-pixel anti-aliasing is performed on the edge of the glyph. Can we achieve the same rendering quality? Currently, No. `Zed` use a grayscale image to render glyph, and a sub-pixel anti-aliasing `DirectWrite` requires all RGB channels and the foreground color of the rendering glyph, which `Zed` dose not provide. So, to achieve the quality of `VSCode` font rendering, the text system of `Zed` needs much much more efforts to refactor the codes. Release Notes: - N/A
This commit is contained in:
parent
a9e3d4ec4e
commit
c0259a448d
@ -8,7 +8,6 @@ use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
use windows::{
|
||||
core::*,
|
||||
Foundation::Numerics::Matrix3x2,
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Globalization::GetUserDefaultLocaleName,
|
||||
@ -45,6 +44,12 @@ struct DirectWriteComponent {
|
||||
in_memory_loader: IDWriteInMemoryFontFileLoader,
|
||||
builder: IDWriteFontSetBuilder1,
|
||||
text_renderer: Arc<TextRendererWrapper>,
|
||||
render_context: GlyphRenderContext,
|
||||
}
|
||||
|
||||
struct GlyphRenderContext {
|
||||
params: IDWriteRenderingParams3,
|
||||
dc_target: ID2D1DeviceContext4,
|
||||
}
|
||||
|
||||
// All use of the IUnknown methods should be "thread-safe".
|
||||
@ -86,6 +91,7 @@ impl DirectWriteComponent {
|
||||
GetUserDefaultLocaleName(&mut locale_vec);
|
||||
let locale = String::from_utf16_lossy(&locale_vec);
|
||||
let text_renderer = Arc::new(TextRendererWrapper::new(&locale));
|
||||
let render_context = GlyphRenderContext::new(&factory, &d2d1_factory)?;
|
||||
|
||||
Ok(DirectWriteComponent {
|
||||
locale,
|
||||
@ -95,11 +101,47 @@ impl DirectWriteComponent {
|
||||
in_memory_loader,
|
||||
builder,
|
||||
text_renderer,
|
||||
render_context,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlyphRenderContext {
|
||||
pub fn new(factory: &IDWriteFactory5, d2d1_factory: &ID2D1Factory) -> Result<Self> {
|
||||
unsafe {
|
||||
let default_params: IDWriteRenderingParams3 =
|
||||
factory.CreateRenderingParams()?.cast()?;
|
||||
let gamma = default_params.GetGamma();
|
||||
let enhanced_contrast = default_params.GetEnhancedContrast();
|
||||
let gray_contrast = default_params.GetGrayscaleEnhancedContrast();
|
||||
let cleartype_level = default_params.GetClearTypeLevel();
|
||||
let grid_fit_mode = default_params.GetGridFitMode();
|
||||
|
||||
let params = factory.CreateCustomRenderingParams(
|
||||
gamma,
|
||||
enhanced_contrast,
|
||||
gray_contrast,
|
||||
cleartype_level,
|
||||
DWRITE_PIXEL_GEOMETRY_RGB,
|
||||
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
|
||||
grid_fit_mode,
|
||||
)?;
|
||||
let dc_target = {
|
||||
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
))?;
|
||||
let target = target.cast::<ID2D1DeviceContext4>()?;
|
||||
target.SetTextRenderingParams(¶ms);
|
||||
target
|
||||
};
|
||||
|
||||
Ok(Self { params, dc_target })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectWriteTextSystem {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
let components = DirectWriteComponent::new()?;
|
||||
@ -521,10 +563,12 @@ impl DirectWriteState {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn get_glyphrun_analysis(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
) -> windows::core::Result<IDWriteGlyphRunAnalysis> {
|
||||
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let render_target = &self.components.render_context.dc_target;
|
||||
unsafe {
|
||||
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
|
||||
render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor);
|
||||
}
|
||||
let font = &self.fonts[params.font_id.0];
|
||||
let glyph_id = [params.glyph_id.0 as u16];
|
||||
let advance = [0.0f32];
|
||||
@ -539,40 +583,29 @@ impl DirectWriteState {
|
||||
isSideways: BOOL(0),
|
||||
bidiLevel: 0,
|
||||
};
|
||||
let transform = DWRITE_MATRIX {
|
||||
m11: params.scale_factor,
|
||||
m12: 0.0,
|
||||
m21: 0.0,
|
||||
m22: params.scale_factor,
|
||||
dx: 0.0,
|
||||
dy: 0.0,
|
||||
let bounds = unsafe {
|
||||
render_target.GetGlyphRunWorldBounds(
|
||||
D2D_POINT_2F { x: 0.0, y: 0.0 },
|
||||
&glyph_run,
|
||||
DWRITE_MEASURING_MODE_NATURAL,
|
||||
)?
|
||||
};
|
||||
self.components.factory.CreateGlyphRunAnalysis(
|
||||
&glyph_run as _,
|
||||
Some(&transform as _),
|
||||
DWRITE_RENDERING_MODE1_NATURAL,
|
||||
DWRITE_MEASURING_MODE_NATURAL,
|
||||
DWRITE_GRID_FIT_MODE_DEFAULT,
|
||||
DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
|
||||
0.0,
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
unsafe {
|
||||
let glyph_run_analysis = self.get_glyphrun_analysis(params)?;
|
||||
let bounds = glyph_run_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)?;
|
||||
|
||||
if bounds.right < bounds.left {
|
||||
Ok(Bounds {
|
||||
origin: Point {
|
||||
x: DevicePixels(bounds.left),
|
||||
y: DevicePixels(bounds.top),
|
||||
},
|
||||
size: Size {
|
||||
width: DevicePixels(bounds.right - bounds.left),
|
||||
height: DevicePixels(bounds.bottom - bounds.top),
|
||||
},
|
||||
origin: point(0.into(), 0.into()),
|
||||
size: size(0.into(), 0.into()),
|
||||
})
|
||||
} else {
|
||||
Ok(Bounds {
|
||||
origin: point(
|
||||
((bounds.left * params.scale_factor).ceil() as i32).into(),
|
||||
((bounds.top * params.scale_factor).ceil() as i32).into(),
|
||||
),
|
||||
size: size(
|
||||
(((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(),
|
||||
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32).into(),
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -626,66 +659,40 @@ impl DirectWriteState {
|
||||
bitmap_size.height += DevicePixels(1);
|
||||
}
|
||||
let bitmap_size = bitmap_size;
|
||||
let transform = DWRITE_MATRIX {
|
||||
m11: params.scale_factor,
|
||||
m12: 0.0,
|
||||
m21: 0.0,
|
||||
m22: params.scale_factor,
|
||||
dx: 0.0,
|
||||
dy: 0.0,
|
||||
};
|
||||
let brush_property = D2D1_BRUSH_PROPERTIES {
|
||||
opacity: 1.0,
|
||||
transform: Matrix3x2 {
|
||||
M11: params.scale_factor,
|
||||
M12: 0.0,
|
||||
M21: 0.0,
|
||||
M22: params.scale_factor,
|
||||
M31: 0.0,
|
||||
M32: 0.0,
|
||||
},
|
||||
};
|
||||
|
||||
let total_bytes;
|
||||
let bitmap_format;
|
||||
let render_target_property;
|
||||
let bitmap_width;
|
||||
let bitmap_height;
|
||||
let bitmap_stride;
|
||||
let bitmap_dpi;
|
||||
if params.is_emoji {
|
||||
total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize * 4;
|
||||
bitmap_format = &GUID_WICPixelFormat32bppPBGRA;
|
||||
render_target_property = D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
alphaMode: D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
},
|
||||
dpiX: params.scale_factor * 96.0,
|
||||
dpiY: params.scale_factor * 96.0,
|
||||
usage: D2D1_RENDER_TARGET_USAGE_NONE,
|
||||
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
|
||||
};
|
||||
render_target_property = get_render_target_property(
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
);
|
||||
bitmap_width = bitmap_size.width.0 as u32;
|
||||
bitmap_height = bitmap_size.height.0 as u32;
|
||||
bitmap_stride = bitmap_size.width.0 as u32 * 4;
|
||||
bitmap_dpi = 96.0;
|
||||
} else {
|
||||
total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize;
|
||||
bitmap_format = &GUID_WICPixelFormat8bppAlpha;
|
||||
render_target_property = D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: DXGI_FORMAT_A8_UNORM,
|
||||
alphaMode: D2D1_ALPHA_MODE_STRAIGHT,
|
||||
},
|
||||
dpiX: params.scale_factor * 96.0,
|
||||
dpiY: params.scale_factor * 96.0,
|
||||
usage: D2D1_RENDER_TARGET_USAGE_NONE,
|
||||
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
|
||||
};
|
||||
render_target_property =
|
||||
get_render_target_property(DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_STRAIGHT);
|
||||
bitmap_width = bitmap_size.width.0 as u32 * 2;
|
||||
bitmap_height = bitmap_size.height.0 as u32 * 2;
|
||||
bitmap_stride = bitmap_size.width.0 as u32;
|
||||
bitmap_dpi = 192.0;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let bitmap = self.components.bitmap_factory.CreateBitmap(
|
||||
bitmap_size.width.0 as u32,
|
||||
bitmap_size.height.0 as u32,
|
||||
bitmap_width,
|
||||
bitmap_height,
|
||||
bitmap_format,
|
||||
WICBitmapCacheOnLoad,
|
||||
)?;
|
||||
@ -693,7 +700,7 @@ impl DirectWriteState {
|
||||
.components
|
||||
.d2d1_factory
|
||||
.CreateWicBitmapRenderTarget(&bitmap, &render_target_property)?;
|
||||
let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, Some(&brush_property))?;
|
||||
let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, None)?;
|
||||
let subpixel_shift = params
|
||||
.subpixel_variant
|
||||
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
|
||||
@ -705,7 +712,14 @@ impl DirectWriteState {
|
||||
// This `cast()` action here should never fail since we are running on Win10+, and
|
||||
// ID2D1DeviceContext4 requires Win8+
|
||||
let render_target = render_target.cast::<ID2D1DeviceContext4>().unwrap();
|
||||
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
|
||||
render_target.SetDpi(
|
||||
bitmap_dpi * params.scale_factor,
|
||||
bitmap_dpi * params.scale_factor,
|
||||
);
|
||||
render_target.SetTextRenderingParams(&self.components.render_context.params);
|
||||
render_target.BeginDraw();
|
||||
|
||||
if params.is_emoji {
|
||||
// WARN: only DWRITE_GLYPH_IMAGE_FORMATS_COLR has been tested
|
||||
let enumerator = self.components.factory.TranslateColorGlyphRun(
|
||||
@ -718,7 +732,7 @@ impl DirectWriteState {
|
||||
| DWRITE_GLYPH_IMAGE_FORMATS_JPEG
|
||||
| DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8,
|
||||
DWRITE_MEASURING_MODE_NATURAL,
|
||||
Some(&transform as _),
|
||||
None,
|
||||
0,
|
||||
)?;
|
||||
while enumerator.MoveNext().is_ok() {
|
||||
@ -766,9 +780,10 @@ impl DirectWriteState {
|
||||
);
|
||||
}
|
||||
render_target.EndDraw(None, None)?;
|
||||
|
||||
let mut raw_data = vec![0u8; total_bytes];
|
||||
bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?;
|
||||
if params.is_emoji {
|
||||
bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?;
|
||||
// Convert from BGRA with premultiplied alpha to BGRA with straight alpha.
|
||||
for pixel in raw_data.chunks_exact_mut(4) {
|
||||
let a = pixel[3] as f32 / 255.;
|
||||
@ -776,6 +791,15 @@ impl DirectWriteState {
|
||||
pixel[1] = (pixel[1] as f32 / a) as u8;
|
||||
pixel[2] = (pixel[2] as f32 / a) as u8;
|
||||
}
|
||||
} else {
|
||||
let scaler = self.components.bitmap_factory.CreateBitmapScaler()?;
|
||||
scaler.Initialize(
|
||||
&bitmap,
|
||||
bitmap_size.width.0 as u32,
|
||||
bitmap_size.height.0 as u32,
|
||||
WICBitmapInterpolationModeHighQualityCubic,
|
||||
)?;
|
||||
scaler.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?;
|
||||
}
|
||||
Ok((bitmap_size, raw_data))
|
||||
}
|
||||
@ -1243,8 +1267,8 @@ fn make_direct_write_feature(feature_name: &str, parameter: u32) -> DWRITE_FONT_
|
||||
|
||||
#[inline]
|
||||
fn make_open_type_tag(tag_name: &str) -> u32 {
|
||||
assert_eq!(tag_name.chars().count(), 4);
|
||||
let bytes = tag_name.bytes().collect_vec();
|
||||
assert_eq!(bytes.len(), 4);
|
||||
((bytes[3] as u32) << 24)
|
||||
| ((bytes[2] as u32) << 16)
|
||||
| ((bytes[1] as u32) << 8)
|
||||
@ -1327,6 +1351,24 @@ fn get_system_ui_font_name() -> SharedString {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_render_target_property(
|
||||
pixel_format: DXGI_FORMAT,
|
||||
alpha_mode: D2D1_ALPHA_MODE,
|
||||
) -> D2D1_RENDER_TARGET_PROPERTIES {
|
||||
D2D1_RENDER_TARGET_PROPERTIES {
|
||||
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
||||
pixelFormat: D2D1_PIXEL_FORMAT {
|
||||
format: pixel_format,
|
||||
alphaMode: alpha_mode,
|
||||
},
|
||||
dpiX: 96.0,
|
||||
dpiY: 96.0,
|
||||
usage: D2D1_RENDER_TARGET_USAGE_NONE,
|
||||
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US");
|
||||
const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F {
|
||||
r: 1.0,
|
||||
|
Loading…
Reference in New Issue
Block a user