FreeType: LCD subpixel

This commit is contained in:
rtw1x1
2026-02-04 12:30:14 +00:00
parent ba514d2e9a
commit 87cfa1540f
4 changed files with 72 additions and 21 deletions

View File

@@ -1,6 +1,10 @@
#include "StdAfx.h"
#include "FontManager.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_LCD_FILTER_H
#include <algorithm>
#include <cctype>
#include <sys/stat.h>
@@ -54,6 +58,9 @@ bool CFontManager::Initialize()
m_bInitialized = true;
// Enable LCD subpixel filter to reduce color fringing
FT_Library_SetLcdFilter(m_ftLibrary, FT_LCD_FILTER_DEFAULT);
// Register default font name -> file mappings
// Korean system fonts
m_fontPathMap["gulim"] = "gulim.ttc";

View File

@@ -277,16 +277,16 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI
return NULL;
}
if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_DEFAULT) != 0)
if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_TARGET_LCD) != 0)
return NULL;
if (FT_Render_Glyph(m_ftFace->glyph, FT_RENDER_MODE_NORMAL) != 0)
if (FT_Render_Glyph(m_ftFace->glyph, FT_RENDER_MODE_LCD) != 0)
return NULL;
FT_GlyphSlot slot = m_ftFace->glyph;
FT_Bitmap& bitmap = slot->bitmap;
int glyphBitmapWidth = bitmap.width;
int glyphBitmapWidth = bitmap.width / 3; // LCD bitmap is 3x wider (R,G,B per pixel)
int glyphBitmapHeight = bitmap.rows;
int bearingX = slot->bitmap_left;
int bearingY = slot->bitmap_top;
@@ -347,7 +347,7 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI
}
}
// Copy grayscale FreeType bitmap into atlas buffer with gamma correction
// Copy LCD subpixel FreeType bitmap into atlas buffer (R,G,B per-channel coverage)
for (int row = 0; row < glyphBitmapHeight; ++row)
{
int atlasY = m_y + yOffset + row;
@@ -359,11 +359,15 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI
for (int col = 0; col < glyphBitmapWidth; ++col)
{
unsigned char alpha = srcRow[col];
if (alpha)
unsigned char r = srcRow[col * 3 + 0];
unsigned char g = srcRow[col * 3 + 1];
unsigned char b = srcRow[col * 3 + 2];
if (r | g | b)
{
alpha = s_alphaGammaLUT.table[alpha];
dstRow[col] = ((DWORD)alpha << 24) | 0x00FFFFFF;
unsigned char a = (r > g) ? r : g;
if (b > a) a = b;
// A8R8G8B8: A=max_coverage, R=r_cov, G=g_cov, B=b_cov
dstRow[col] = ((DWORD)a << 24) | ((DWORD)r << 16) | ((DWORD)g << 8) | (DWORD)b;
}
}
}
@@ -383,7 +387,7 @@ CGraphicFontTexture::TCharacterInfomation* CGraphicFontTexture::UpdateCharacterI
rNewCharInfo.advance = advance;
rNewCharInfo.bearingX = (float)bearingX;
m_x += cellWidth;
m_x += cellWidth + 1; // +1 horizontal padding to prevent bilinear bleed
if (m_step < cellHeight)
m_step = cellHeight;

View File

@@ -559,8 +559,10 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
break;
}
static std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>> s_vtxBatches;
s_vtxBatches.clear();
static std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>> s_outlineBatches;
static std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>> s_mainBatches;
s_outlineBatches.clear();
s_mainBatches.clear();
STATEMANAGER.SaveRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
STATEMANAGER.SaveRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
@@ -577,6 +579,10 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
// LCD subpixel rendering: mask alpha writes to prevent corruption during two-pass blending
STATEMANAGER.SaveRenderState(D3DRS_COLORWRITEENABLE,
D3DCOLORWRITEENABLE_RED | D3DCOLORWRITEENABLE_GREEN | D3DCOLORWRITEENABLE_BLUE);
{
const float fFontHalfWeight=1.0f;
@@ -647,7 +653,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
fFontEy = fFontSy + fFontHeight;
pFontTexture->SelectTexture(pCurCharInfo->index);
std::vector<SVertex>& vtxBatch = s_vtxBatches[pFontTexture->GetD3DTexture()];
std::vector<SVertex>& vtxBatch = s_outlineBatches[pFontTexture->GetD3DTexture()];
akVertex[0].u=pCurCharInfo->left;
akVertex[0].v=pCurCharInfo->top;
@@ -755,7 +761,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
fFontEy = fFontSy + fFontHeight;
pFontTexture->SelectTexture(pCurCharInfo->index);
std::vector<SVertex>& vtxBatch = s_vtxBatches[pFontTexture->GetD3DTexture()];
std::vector<SVertex>& vtxBatch = s_mainBatches[pFontTexture->GetD3DTexture()];
akVertex[0].x=fFontSx;
akVertex[0].y=fFontSy;
@@ -859,13 +865,46 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
}
}
for (const auto& [pTexture, vtxBatch] : s_vtxBatches) {
if (vtxBatch.empty())
continue;
// LCD subpixel two-pass rendering: correct per-channel alpha blending
auto DrawBatchLCD = [](const std::unordered_map<LPDIRECT3DTEXTURE9, std::vector<SVertex>>& batches, bool skipPass2) {
for (const auto& [pTexture, vtxBatch] : batches) {
if (vtxBatch.empty())
continue;
STATEMANAGER.SetTexture(0, pTexture);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST, vtxBatch.size() / 3, vtxBatch.data(), sizeof(SVertex));
}
STATEMANAGER.SetTexture(0, pTexture);
// Pass 1: dest.rgb *= (1 - coverage.rgb)
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCCOLOR);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST,
vtxBatch.size() / 3, vtxBatch.data(), sizeof(SVertex));
if (!skipPass2) {
// Pass 2: dest.rgb += textColor.rgb * coverage.rgb
STATEMANAGER.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
STATEMANAGER.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
STATEMANAGER.SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST,
vtxBatch.size() / 3, vtxBatch.data(), sizeof(SVertex));
}
}
};
// Draw outline batches first (skip Pass 2 for black outlines — MODULATE with black = 0)
bool outlineIsBlack = ((m_dwOutLineColor & 0x00FFFFFF) == 0);
DrawBatchLCD(s_outlineBatches, outlineIsBlack);
// Draw main text batches (always both passes)
DrawBatchLCD(s_mainBatches, false);
if (m_isCursor)
{
@@ -973,6 +1012,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect)
}
}
STATEMANAGER.RestoreRenderState(D3DRS_COLORWRITEENABLE);
STATEMANAGER.RestoreRenderState(D3DRS_SRCBLEND);
STATEMANAGER.RestoreRenderState(D3DRS_DESTBLEND);

View File

@@ -91,7 +91,7 @@ void CTextBar::GetTextExtent(const char* c_szText, SIZE* p_size)
}
if (FT_Load_Glyph(m_ftFace, glyphIndex, FT_LOAD_DEFAULT) == 0)
totalAdvance += (int)ceilf((float)(m_ftFace->glyph->advance.x) / 64.0f);
totalAdvance += (int)ceilf((float)(m_ftFace->glyph->advance.x) / 64.0f) + 1; // +1px letter spacing
prevIndex = glyphIndex;
}
@@ -174,7 +174,7 @@ void CTextBar::TextOut(int ix, int iy, const char * c_szText)
}
}
penX += (int)ceilf((float)(slot->advance.x) / 64.0f);
penX += (int)ceilf((float)(slot->advance.x) / 64.0f) + 1; // +1px letter spacing
prevIndex = glyphIndex;
}