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:
张小白 2024-05-27 07:41:55 +08:00 committed by GitHub
parent a9e3d4ec4e
commit c0259a448d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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(&params);
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,