[base, smooth] Implement direct LCD filtering.

Applying an LCD filter to spans rather than the entire image improves
the performance of ClearType-like rendering by about 40% at 32 ppem
and much more at larger sizes.  Small rounding differences are expected.

* src/smooth/ftsmooth.c (ft_smooth_raster_lcd, ft_smooth_lcd_spans,
ft_smooth_raster_lcdv, ft_smooth_lcdv_spans, TOrigin): Implement it.
* include/freetype/internal/ftobjs.h (FT_LibraryRec): lcd_filter_func gone.
* src/base/ftlcdfil.c (ft_lcd_filter_fir): Removed.
(ft_lcd_padding): Use padding sufficient for any 5-tap filter.
(FT_Library_SetLcdFilterWeights, FT_Library_SetLcdFilter): Updated.
* docs/CHANGES: Updated.
This commit is contained in:
Alexei Podtelezhnikov 2026-01-26 12:43:28 -05:00
parent e8f0969dcf
commit 7cc8f37b9a
4 changed files with 143 additions and 160 deletions

View file

@ -5,6 +5,11 @@ CHANGES BETWEEN 2.14.1 and 2.14.2 (2026-Mmm-DD)
- Several changes related to LCD filtering are implemented to
achieve better performance and encourage sound practices.
. Instead of blanket LCD filtering over the entire bitmap, it is
now applied only to non-zero spans using direct rendering. This
speeds up the ClearType-like rendering by more than 40% at sizes
above 32 ppem.
. Setting the filter weights with FT_Face_Properties is no longer
supported. The default and light filters are optimized to work
with any face.

View file

@ -879,10 +879,6 @@ FT_BEGIN_HEADER
* lcd_weights ::
* The LCD filter weights for ClearType-style subpixel rendering.
*
* lcd_filter_func ::
* The LCD filtering callback function for for ClearType-style subpixel
* rendering.
*
* lcd_geometry ::
* This array specifies LCD subpixel geometry and controls Harmony LCD
* rendering technique, alternative to ClearType.
@ -916,7 +912,6 @@ FT_BEGIN_HEADER
#ifdef FT_CONFIG_OPTION_SUBPIXEL_RENDERING
FT_LcdFiveTapFilter lcd_weights; /* filter weights, if any */
FT_Bitmap_LcdFilterFunc lcd_filter_func; /* filtering callback */
#else
FT_Vector lcd_geometry[3]; /* RGB subpixel positions */
#endif

View file

@ -25,142 +25,24 @@
#ifdef FT_CONFIG_OPTION_SUBPIXEL_RENDERING
#define FT_SHIFTCLAMP( x ) ( x >>= 8, (FT_Byte)( x > 255 ? 255 : x ) )
/* add padding according to filter weights */
/* add padding sufficient for a 5-tap filter, */
/* which is 2/3 of a pixel */
FT_BASE_DEF( void )
ft_lcd_padding( FT_BBox* cbox,
FT_GlyphSlot slot,
FT_Render_Mode mode )
{
FT_Byte* lcd_weights;
FT_Bitmap_LcdFilterFunc lcd_filter_func;
FT_UNUSED( slot );
lcd_weights = slot->library->lcd_weights;
lcd_filter_func = slot->library->lcd_filter_func;
if ( lcd_filter_func == ft_lcd_filter_fir )
if ( mode == FT_RENDER_MODE_LCD )
{
if ( mode == FT_RENDER_MODE_LCD )
{
cbox->xMin -= lcd_weights[0] ? 43 :
lcd_weights[1] ? 22 : 0;
cbox->xMax += lcd_weights[4] ? 43 :
lcd_weights[3] ? 22 : 0;
}
else if ( mode == FT_RENDER_MODE_LCD_V )
{
cbox->yMin -= lcd_weights[0] ? 43 :
lcd_weights[1] ? 22 : 0;
cbox->yMax += lcd_weights[4] ? 43 :
lcd_weights[3] ? 22 : 0;
}
cbox->xMin -= 43;
cbox->xMax += 43;
}
}
/* FIR filter used by the default and light filters */
FT_BASE_DEF( void )
ft_lcd_filter_fir( FT_Bitmap* bitmap,
FT_LcdFiveTapFilter weights )
{
FT_UInt width = (FT_UInt)bitmap->width;
FT_UInt height = (FT_UInt)bitmap->rows;
FT_Int pitch = bitmap->pitch;
FT_Byte* origin = bitmap->buffer;
FT_Byte mode = bitmap->pixel_mode;
/* take care of bitmap flow */
if ( pitch > 0 && height > 0 )
origin += pitch * (FT_Int)( height - 1 );
/* horizontal in-place FIR filter */
if ( mode == FT_PIXEL_MODE_LCD && width >= 2 )
else if ( mode == FT_RENDER_MODE_LCD_V )
{
FT_Byte* line = origin;
/* `fir' must be at least 32 bit wide, since the sum of */
/* the values in `weights' can exceed 0xFF */
for ( ; height > 0; height--, line -= pitch )
{
FT_UInt fir[5];
FT_UInt val, xx;
val = line[0];
fir[2] = weights[2] * val;
fir[3] = weights[3] * val;
fir[4] = weights[4] * val;
val = line[1];
fir[1] = fir[2] + weights[1] * val;
fir[2] = fir[3] + weights[2] * val;
fir[3] = fir[4] + weights[3] * val;
fir[4] = weights[4] * val;
for ( xx = 2; xx < width; xx++ )
{
val = line[xx];
fir[0] = fir[1] + weights[0] * val;
fir[1] = fir[2] + weights[1] * val;
fir[2] = fir[3] + weights[2] * val;
fir[3] = fir[4] + weights[3] * val;
fir[4] = weights[4] * val;
line[xx - 2] = FT_SHIFTCLAMP( fir[0] );
}
line[xx - 2] = FT_SHIFTCLAMP( fir[1] );
line[xx - 1] = FT_SHIFTCLAMP( fir[2] );
}
}
/* vertical in-place FIR filter */
else if ( mode == FT_PIXEL_MODE_LCD_V && height >= 2 )
{
FT_Byte* column = origin;
for ( ; width > 0; width--, column++ )
{
FT_Byte* col = column;
FT_UInt fir[5];
FT_UInt val, yy;
val = col[0];
fir[2] = weights[2] * val;
fir[3] = weights[3] * val;
fir[4] = weights[4] * val;
col -= pitch;
val = col[0];
fir[1] = fir[2] + weights[1] * val;
fir[2] = fir[3] + weights[2] * val;
fir[3] = fir[4] + weights[3] * val;
fir[4] = weights[4] * val;
col -= pitch;
for ( yy = 2; yy < height; yy++, col -= pitch )
{
val = col[0];
fir[0] = fir[1] + weights[0] * val;
fir[1] = fir[2] + weights[1] * val;
fir[2] = fir[3] + weights[2] * val;
fir[3] = fir[4] + weights[3] * val;
fir[4] = weights[4] * val;
col[pitch * 2] = FT_SHIFTCLAMP( fir[0] );
}
col[pitch * 2] = FT_SHIFTCLAMP( fir[1] );
col[pitch] = FT_SHIFTCLAMP( fir[2] );
}
cbox->yMin -= 43;
cbox->yMax += 43;
}
}
@ -178,7 +60,6 @@
return FT_THROW( Invalid_Argument );
ft_memcpy( library->lcd_weights, weights, FT_LCD_FILTER_FIVE_TAPS );
library->lcd_filter_func = ft_lcd_filter_fir;
return FT_Err_Ok;
}
@ -202,21 +83,21 @@
switch ( filter )
{
case FT_LCD_FILTER_NONE:
library->lcd_filter_func = NULL;
ft_memset( library->lcd_weights,
0,
FT_LCD_FILTER_FIVE_TAPS );
break;
case FT_LCD_FILTER_DEFAULT:
ft_memcpy( library->lcd_weights,
default_weights,
FT_LCD_FILTER_FIVE_TAPS );
library->lcd_filter_func = ft_lcd_filter_fir;
break;
case FT_LCD_FILTER_LIGHT:
ft_memcpy( library->lcd_weights,
light_weights,
FT_LCD_FILTER_FIVE_TAPS );
library->lcd_filter_func = ft_lcd_filter_fir;
break;
default:

View file

@ -80,6 +80,7 @@
{
unsigned char* origin; /* pixmap origin at the bottom-left */
int pitch; /* pitch to go down one row */
unsigned char wght[5]; /* filtering weights */
} TOrigin;
@ -274,6 +275,32 @@
}
/* This function applies a horizontal filter in direct rendering mode */
static void
ft_smooth_lcd_spans( int y,
int count,
const FT_Span* spans,
void* target_ ) /* TOrigin* */
{
TOrigin* target = (TOrigin*)target_;
unsigned char* dst_line = target->origin - y * target->pitch - 2;
unsigned char* dst;
unsigned short w;
for ( ; count--; spans++ )
for ( dst = dst_line + spans->x, w = spans->len; w--; dst++ )
{
dst[0] += ( spans->coverage * target->wght[0] + 85 ) >> 8;
dst[1] += ( spans->coverage * target->wght[1] + 85 ) >> 8;
dst[2] += ( spans->coverage * target->wght[2] + 85 ) >> 8;
dst[3] += ( spans->coverage * target->wght[3] + 85 ) >> 8;
dst[4] += ( spans->coverage * target->wght[4] + 85 ) >> 8;
}
}
static FT_Error
ft_smooth_raster_lcd( FT_Renderer render,
FT_Outline* outline,
@ -285,11 +312,47 @@
FT_Vector* vec;
FT_Raster_Params params;
TOrigin target;
params.target = bitmap;
params.source = outline;
params.flags = FT_RASTER_FLAG_AA;
if ( render->root.library->lcd_weights[2] )
{
/* Reject outlines that are too wide for 16-bit FT_Span. */
/* Other limits are applied upstream with the same error code. */
if ( bitmap->width > 0x7FFF )
return FT_THROW( Raster_Overflow );
/* Set up direct rendering for instant filtering. */
params.source = outline;
params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;
params.gray_spans = ft_smooth_lcd_spans;
params.user = &target;
params.clip_box.xMin = 0;
params.clip_box.yMin = 0;
params.clip_box.xMax = bitmap->width;
params.clip_box.yMax = bitmap->rows;
if ( bitmap->pitch < 0 )
target.origin = bitmap->buffer;
else
target.origin = bitmap->buffer
+ ( bitmap->rows - 1 ) * (unsigned int)bitmap->pitch;
target.pitch = bitmap->pitch;
target.wght[0] = render->root.library->lcd_weights[0];
target.wght[1] = render->root.library->lcd_weights[1];
target.wght[2] = render->root.library->lcd_weights[2];
target.wght[3] = render->root.library->lcd_weights[3];
target.wght[4] = render->root.library->lcd_weights[4];
}
else
{
params.target = bitmap;
params.source = outline;
params.flags = FT_RASTER_FLAG_AA;
}
/* implode outline */
for ( vec = points; vec < points_end; vec++ )
@ -306,6 +369,32 @@
}
/* This function applies a vertical filter in direct rendering mode */
static void
ft_smooth_lcdv_spans( int y,
int count,
const FT_Span* spans,
void* target_ ) /* TOrigin* */
{
TOrigin* target = (TOrigin*)target_;
int pitch = target->pitch;
unsigned char* dst_line = target->origin - ( y + 2 ) * pitch;
unsigned char* dst;
unsigned short w;
for ( ; count--; spans++ )
for ( dst = dst_line + spans->x, w = spans->len; w--; dst++ )
{
dst[ 0] += ( spans->coverage * target->wght[0] + 85 ) >> 8;
dst[ pitch] += ( spans->coverage * target->wght[1] + 85 ) >> 8;
dst[2 * pitch] += ( spans->coverage * target->wght[2] + 85 ) >> 8;
dst[3 * pitch] += ( spans->coverage * target->wght[3] + 85 ) >> 8;
dst[4 * pitch] += ( spans->coverage * target->wght[4] + 85 ) >> 8;
}
}
static FT_Error
ft_smooth_raster_lcdv( FT_Renderer render,
FT_Outline* outline,
@ -317,11 +406,42 @@
FT_Vector* vec;
FT_Raster_Params params;
TOrigin target;
params.target = bitmap;
params.source = outline;
params.flags = FT_RASTER_FLAG_AA;
if ( render->root.library->lcd_weights[2] )
{
/* Set up direct rendering for instant filtering. */
params.source = outline;
params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;
params.gray_spans = ft_smooth_lcdv_spans;
params.user = &target;
params.clip_box.xMin = 0;
params.clip_box.yMin = 0;
params.clip_box.xMax = bitmap->width;
params.clip_box.yMax = bitmap->rows;
if ( bitmap->pitch < 0 )
target.origin = bitmap->buffer;
else
target.origin = bitmap->buffer
+ ( bitmap->rows - 1 ) * (unsigned int)bitmap->pitch;
target.pitch = bitmap->pitch;
target.wght[0] = render->root.library->lcd_weights[0];
target.wght[1] = render->root.library->lcd_weights[1];
target.wght[2] = render->root.library->lcd_weights[2];
target.wght[3] = render->root.library->lcd_weights[3];
target.wght[4] = render->root.library->lcd_weights[4];
}
else
{
params.target = bitmap;
params.source = outline;
params.flags = FT_RASTER_FLAG_AA;
}
/* implode outline */
for ( vec = points; vec < points_end; vec++ )
@ -521,24 +641,6 @@
error = ft_smooth_raster_lcd ( render, outline, bitmap );
else if ( mode == FT_RENDER_MODE_LCD_V )
error = ft_smooth_raster_lcdv( render, outline, bitmap );
#ifdef FT_CONFIG_OPTION_SUBPIXEL_RENDERING
/* finally apply filtering */
{
FT_Byte* lcd_weights;
FT_Bitmap_LcdFilterFunc lcd_filter_func;
lcd_weights = slot->library->lcd_weights;
lcd_filter_func = slot->library->lcd_filter_func;
if ( lcd_filter_func )
lcd_filter_func( bitmap, lcd_weights );
}
#endif /* FT_CONFIG_OPTION_SUBPIXEL_RENDERING */
}
Exit: