View.getForceSDFT() 到字形光栅化完整链路分析

概述

View.setForceSDFT(true) 是 MIUI 自定义的渲染优化 API(Feature Tag: SF_BugFixUpstream_3),用于在 scale 动画期间强制 Skia 使用 SDF(Signed Distance Field)文字渲染模式,从而消除亚像素偏移导致的 glyph atlas cache miss。

本文档详细追踪从 Java View.getForceSDFT() 到 Skia 底层字形光栅化决策的完整代码链路。


两条并行路径

setForceSDFT 通过两条独立路径将标记传递到 Skia:

  • 路径 A(Paint 路径):per-View draw 级别,通过 TextView.onDraw()Paint.setForceSDFT() 传递
  • 路径 B(Canvas/Pipeline 路径):RenderThread 全局级别,通过 ViewRootImplCanvasContextSkCanvas 传递

两条路径在 Skia 的 TextBlobRedrawCoordinator::drawForMaskType() 汇合。


路径 A:Paint 路径(per-View draw 级别)

1. View 层设置与继承

文件frameworks/base/core/java/android/view/View.java

// [View.java:20494-20513] MIUI ADD: SF_BugFixUpstream_3
private boolean mPreForceSDFT = false;  // 从父级继承
private boolean mForceSDFT = false;     // 自身设置
 
public void setForceSDFT(boolean forceSDFT) {
    mForceSDFT = true;
}
 
public boolean getForceSDFT() {
    return mForceSDFT || mPreForceSDFT;  // 自身 OR 父级
}

2. 子 View draw 时继承父级标记

文件frameworks/base/core/java/android/view/View.java

// [View.java:27024-27026] 在 draw(Canvas, ViewGroup, long) 中
// MIUI ADD: SF_BugFixUpstream_3
if (parent != null) {
    mPreForceSDFT = parent.getForceSDFT();  // ★ 子 View 从父级继承标记
}
// END SF_BugFixUpstream_3

设计意义:对父 View(如 KeyguardBottomAreaView)调用一次 setForceSDFT(true),其所有子 View(包括 KeyguardIndicationTextView)在 draw 时自动继承,无需逐个设置。

3. TextView.onDraw 传递到 Paint

文件frameworks/base/core/java/android/widget/TextView.java

// [TextView.java:9418] MIUI ADD: SF_BugFixUpstream_3
mTextPaint.setForceSDFT(getForceSDFT());
// END SF_BugFixUpstream_3

4. Paint JNI 到 SkPaint

文件frameworks/base/graphics/java/android/graphics/Paint.java

// [Paint.java:2082-2083]
public void setForceSDFT(boolean forceSDFT) {
    nSetForceSDFT(mNativePaint, forceSDFT);
}

文件frameworks/base/libs/hwui/jni/Paint.cpp

// [Paint.cpp:1196-1198]
static void setForceSDFT(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jboolean forceSDFT) {
    auto* paint = reinterpret_cast<Paint*>(paintHandle);
    paint->setForceSDFT(forceSDFT);
}

文件skia/include/core/SkPaint.h

// [SkPaint.h:344-345]
bool getForceSDFT() const { return fForceSDFT != 0; }
void setForceSDFT(bool force) { fForceSDFT = force ? 1 : 0; }

路径 B:Canvas/Pipeline 路径(RenderThread 全局级别)

1. ViewRootImpl → HardwareRenderer

文件frameworks/base/core/java/android/view/ViewRootImpl.java

// [ViewRootImpl.java:561-566] MIUI ADD: SF_BugFixUpstream_3
public void setForceSDFT(boolean forceSDFT) {
    if (mAttachInfo.mThreadedRenderer != null) {
        mAttachInfo.mThreadedRenderer.setForceSDFT(forceSDFT);
    }
}

2. JNI → RenderProxy → CanvasContext

文件frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cpp

// [android_graphics_HardwareRenderer.cpp:1305-1307]
static void android_view_ThreadedRenderer_setForceSDFT(JNIEnv* env, jobject clazz,
                                                       jlong proxyPtr, jboolean forceSDFT) {
    reinterpret_cast<RenderProxy*>(proxyPtr)->setForceSDFT(forceSDFT);
}

文件frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

// [RenderProxy.cpp:804-805]
void RenderProxy::setForceSDFT(bool forceSDFT) {
    mRenderThread.queue().post([this, forceSDFT]() { mContext->setForceSDFT(forceSDFT); });
}

文件frameworks/base/libs/hwui/renderthread/CanvasContext.h

// [CanvasContext.h:305]
void setForceSDFT(bool forceSDFT) { mForceSDFT = forceSDFT; }

3. 每帧绘制前注入到 Pipeline 和 Canvas

文件frameworks/base/libs/hwui/renderthread/CanvasContext.cpp

// [CanvasContext.cpp:1349]
mRenderPipeline->setForceSDFT(mForceSDFT);

文件frameworks/base/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp

// [SkiaVulkanPipeline.cpp:108] MIUI ADD: SF_BugFixUpstream_3
backBuffer->getCanvas()->setForceSDFT(getForceSDFT());

文件frameworks/base/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp

// [SkiaOpenGLPipeline.cpp:177] MIUI ADD: SF_BugFixUpstream_3
surface->getCanvas()->setForceSDFT(getForceSDFT());

文件skia/include/core/SkCanvas.h

// [SkCanvas.h:2204-2205]
void setForceSDFT(bool forceSDFT) { fForceSDFT = forceSDFT; }
bool getForceSDFT() { return fForceSDFT; }

路径汇合点:TextBlobRedrawCoordinator

文件skia/src/text/gpu/TextBlobRedrawCoordinator.cpp

// [TextBlobRedrawCoordinator.cpp:45-53]
void TextBlobRedrawCoordinator::drawForMaskType(...) {
    // MIUI MOD: SF_BugFixUpstream_3
    SkPaint skPaint(paint);
    if (canvas->getForceSDFT()) {                    // ← 路径 B 的标记
        skPaint.setForceSDFT(canvas->getForceSDFT()); // 注入到 Paint
    }
    sk_sp<TextBlob> blob = this->findOrCreateBlob(viewMatrix, glyphRunList, skPaint, ...);
    // END SF_BugFixUpstream_3
 
    blob->draw(canvas, glyphRunList.origin(), paint, atlasDelegate);
}

作用:确保即使路径 A 没有生效(例如非 TextView 的文字绘制),路径 B 仍然能将 forceSDFT 标记注入到 Skia 的文字渲染决策中。


核心决策点:SubRunControl::isSDFT()

文件skia/src/text/gpu/SubRunControl.cpp

// [SubRunControl.cpp:70-88]
#if !defined(SK_DISABLE_SDF_TEXT)
bool SubRunControl::isSDFT(SkScalar approximateDeviceTextSize, const SkPaint& paint,
                           const SkMatrix& matrix) const {
    // MIUI ADD: SF_BugFixUpstream_3
    const bool forceSDFT = matrix.hasScale() && paint.getForceSDFT();  // ★ 核心条件
    // END SF_BugFixUpstream_3
 
    const bool wideStroke = paint.getStyle() == SkPaint::kStroke_Style &&
            paint.getStrokeWidth() > 0;
 
    return fAbleToUseSDFT &&
           paint.getMaskFilter() == nullptr &&
           (paint.getStyle() == SkPaint::kFill_Style || wideStroke) &&
           0 < approximateDeviceTextSize &&
           (fAbleToUsePerspectiveSDFT || !matrix.hasPerspective()) &&
           // MIUI MOD: SF_BugFixUpstream_3
           // 原始 AOSP:(fMinDistanceFieldFontSize <= approximateDeviceTextSize || matrix.hasPerspective()) &&
           //            approximateDeviceTextSize <= fMaxDistanceFieldFontSize;
           (fMinDistanceFieldFontSize <= approximateDeviceTextSize || forceSDFT || matrix.hasPerspective()) &&
           (approximateDeviceTextSize <= fMaxDistanceFieldFontSize || forceSDFT);
           // END SF_BugFixUpstream_3
}
#endif

MIUI 修改的核心效果

条件AOSP 原始MIUI 修改后
字号 < minDistanceFieldFontSize不使用 SDFforceSDFT=true 时强制使用 SDF
字号 > maxDistanceFieldFontSize不使用 SDFforceSDFT=true 时强制使用 SDF
forceSDFT 生效条件-matrix.hasScale() && paint.getForceSDFT()

为什么要求 matrix.hasScale():只在有 scale 变换时才强制 SDF,因为只有 scale 变化才会导致亚像素偏移的 cache miss 问题。无 scale 时传统光栅化效果更好(更清晰)。


SDF vs DirectMask:GlyphID 构造差异

这是 forceSDFT 最终影响字形缓存命中的根本原因。

DirectMask 路径(默认,不使用 SDF)

文件skia/src/text/gpu/SubRunContainer.cpp

// [SubRunContainer.cpp:1238-1239] prepare_for_direct_mask_drawing()
const SkPoint mappedPos = positionMatrixWithRounding.mapPoint(pos);
const SkPackedGlyphID packedID{glyphID, mappedPos, mask};
//                              ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
//              包含 mappedPos 的亚像素信息!

SkPackedGlyphID 构造函数skia/src/core/SkGlyph.h:90-91):

SkPackedGlyphID(SkGlyphID glyphID, SkPoint pt, SkIPoint mask)
    : fID{PackIDSkPoint(glyphID, pt, mask)} { }
// fID = (subPixelX << kSubPixelX) | (subPixelY << kSubPixelY) | (glyphID << kGlyphID)
//        ↑ 2 bit                     ↑ 2 bit                     ↑ 16 bit

结果glyph_key = (glyphID, subpixelX, subpixelY) — 亚像素位置参与 key 计算。

SDFT 路径(forceSDFT=true 时)

文件skia/src/text/gpu/SubRunContainer.cpp

// [SubRunContainer.cpp:1184] prepare_for_SDFT_drawing()
const SkPackedGlyphID packedID{glyphID};
//                              ↑↑↑↑↑↑↑↑
//              只有 glyphID!无亚像素信息!

SkPackedGlyphID 构造函数skia/src/core/SkGlyph.h:81-82):

constexpr explicit SkPackedGlyphID(SkGlyphID glyphID)
        : fID{(uint32_t)glyphID << kGlyphID} { }
// fID = glyphID << 8,subPixelX = 0, subPixelY = 0

结果glyph_key = (glyphID) — 无亚像素位置,scale 变化不影响 key。

StrikeSpec 差异

SDFT 路径创建 strike 时使用 Identity Matrix

文件skia/src/text/gpu/SubRunContainer.cpp

// [SubRunContainer.cpp:1395-1396] make_sdft_strike_spec()
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(dfFont, dfPaint, surfaceProps, flags,
                                                 SkMatrix::I());  // ← 固定 Identity!

意义:strike 的 key 不包含 view 的 scale matrix,因此 scale 从 1.0 → 0.999636 → 0.999272 变化时,所有帧使用同一个 strike cache。


为什么 SDF 不需要亚像素位置

┌─────────────────────────────────────────────────────────────┐
│ 传统 LCD/Grayscale 光栅化                                     │
│                                                              │
│ 字形在特定亚像素位置预渲染为位图:                               │
│   每个像素的覆盖率(antialiasing)取决于字形边缘与像素网格的关系   │
│                                                              │
│   位置 A (subpixel=0.27):  ██░░  ← 边缘刚好在像素内          │
│   位置 B (subpixel=0.33):  █▓░░  ← 边缘跨越像素             │
│                                                              │
│   → 不同位置 = 不同 antialiasing 结果 = 不同的位图            │
│   → 必须分别缓存 → key 必须包含亚像素位置                      │
│   → scale 变化 → 位置变化 → key 不同 → CACHE MISS            │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ SDF (Signed Distance Field) 渲染                              │
│                                                              │
│ 字形存储为「到字形边缘的有符号距离值」:                          │
│   每个像素存储 distance(正=外部,负=内部)                      │
│                                                              │
│   同一个 SDF 纹理:  [3][2][1][0][-1][-2]                     │
│                                                              │
│   位置 A: GPU shader 在阈值 0 处切割 → 生成清晰边缘           │
│   位置 B: GPU shader 在阈值 0 处切割 → 同样清晰(亚像素插值)  │
│                                                              │
│   → 位置无关:任何坐标用同一个 SDF 纹理                        │
│   → 只需缓存一份 → key 只需 glyphID                           │
│   → scale 变化 → 位置变化 → key 不变 → CACHE HIT             │
└─────────────────────────────────────────────────────────────┘

完整链路图

┌─────────────────────────────────────────────────────────────────────┐
│  Java 层                                                             │
│                                                                      │
│  NotificationUtil.setForceSDFT2(view, true)  [反射调用]              │
│    → View.setForceSDFT(true)                  [View.java:20502]      │
│        │                                                             │
│        ├─── mForceSDFT = true  ────────────────── 路径 A ───────┐   │
│        │                                                         │   │
│        │  子 View draw() 时:                                     │   │
│        │    mPreForceSDFT = parent.getForceSDFT()                │   │
│        │                           [View.java:27025]             │   │
│        │    ↓                                                    │   │
│        │  TextView.onDraw():                                     │   │
│        │    mTextPaint.setForceSDFT(getForceSDFT())              │   │
│        │                           [TextView.java:9418]          │   │
│        │    ↓ [JNI]                                              │   │
│        │  SkPaint.fForceSDFT = 1   [SkPaint.h:345]              │   │
│        │                                                         │   │
│        └─── ViewRootImpl.setForceSDFT(true) ── 路径 B ───────┐  │   │
│               [ViewRootImpl.java:564]                         │  │   │
│             ↓                                                 │  │   │
│           HardwareRenderer.nSetForceSDFT()                    │  │   │
│             ↓ [JNI → RenderThread queue]                      │  │   │
│           RenderProxy.setForceSDFT(true)                      │  │   │
│             [RenderProxy.cpp:804]                             │  │   │
│             ↓                                                 │  │   │
│           CanvasContext.mForceSDFT = true                     │  │   │
│             [CanvasContext.h:305]                             │  │   │
└──────────────────────────────────────────────────────────────┼──┼───┘
                                                               │  │
┌──────────────────────────────────────────────────────────────▼──▼───┐
│  Native 层 (RenderThread, 每帧执行)                                   │
│                                                                      │
│  CanvasContext::draw()                                               │
│    → mRenderPipeline->setForceSDFT(mForceSDFT)                      │
│                           [CanvasContext.cpp:1349]          路径 B   │
│    → SkiaVulkanPipeline::draw()                                      │
│        backBuffer->getCanvas()->setForceSDFT(true)                   │
│                           [SkiaVulkanPipeline.cpp:108]               │
│                                                                      │
│  ════════════════════════════════════════════════════════════════     │
│  Skia 文字渲染入口                                                    │
│  ════════════════════════════════════════════════════════════════     │
│                                                                      │
│  TextBlobRedrawCoordinator::drawForMaskType()                        │
│    [TextBlobRedrawCoordinator.cpp:48-52]                             │
│    → if (canvas->getForceSDFT()) {        ← 路径 B 汇入             │
│          skPaint.setForceSDFT(true);                                 │
│      }                                                               │
│    → findOrCreateBlob(matrix, glyphRunList, skPaint, ...)            │
│                                           ↑ paint 已带 forceSDFT    │
│                                             (路径 A 或 路径 B)       │
│                                                                      │
│  ════════════════════════════════════════════════════════════════     │
│  ★ 核心决策点 ★                                                      │
│  ════════════════════════════════════════════════════════════════     │
│                                                                      │
│  SubRunContainer::MakeInAlloc()            [SubRunContainer.cpp:1471]│
│    → subRunControl->isSDFT(size, runPaint, positionMatrix)           │
│                                                                      │
│  SubRunControl::isSDFT()                   [SubRunControl.cpp:74]    │
│    → forceSDFT = matrix.hasScale() && paint.getForceSDFT()           │
│    → return ... &&                                                   │
│        (minSize <= size || forceSDFT || perspective) &&  ← ★绕过★   │
│        (size <= maxSize || forceSDFT);                   ← ★绕过★   │
│                                                                      │
│  ════════════════ isSDFT() == true ═══════════════════════════════   │
│                                                                      │
│  make_sdft_strike_spec()                   [SubRunContainer.cpp:1395]│
│    → StrikeSpec::MakeMask(..., SkMatrix::I())                        │
│                                ↑ Identity! 不受 view scale 影响      │
│                                                                      │
│  prepare_for_SDFT_drawing()                [SubRunContainer.cpp:1184]│
│    → packedID = SkPackedGlyphID{glyphID}                             │
│                                ↑ ★ 无亚像素位置!★                   │
│    → strike->digestFor(kSDFT, packedID)                              │
│                                                                      │
│  AtlasTextOp::onPrepareDraws()                                       │
│    → hasGlyph(maskFormat, glyph.entry())                             │
│    → key 不含亚像素 → scale 变化不影响 → ★ CACHE HIT ★              │
│    → 耗时: ~3μs (仅更新 draw token)                                  │
│                                                                      │
│  ════════════════ isSDFT() == false (默认) ═══════════════════════   │
│                                                                      │
│  prepare_for_direct_mask_drawing()         [SubRunContainer.cpp:1239]│
│    → mappedPos = positionMatrix.mapPoint(pos)                        │
│    → packedID = SkPackedGlyphID{glyphID, mappedPos, mask}            │
│                                ↑ ★ 包含亚像素位置!★                 │
│                                                                      │
│  AtlasTextOp::onPrepareDraws()                                       │
│    → hasGlyph(maskFormat, glyph.entry())                             │
│    → key 含亚像素 → scale 变化 → 位置不同 → ★ CACHE MISS ★          │
│    → 重新光栅化 → 耗时: ~600μs                                       │
└─────────────────────────────────────────────────────────────────────┘

关键源码行号速查表

层级文件行号作用
Java ViewView.java20502setForceSDFT() 设置 mForceSDFT
Java ViewView.java20510-20511getForceSDFT() 返回 mForceSDFT || mPreForceSDFT
Java ViewView.java27025子 View draw 时继承 mPreForceSDFT = parent.getForceSDFT()
Java TextViewTextView.java9418mTextPaint.setForceSDFT(getForceSDFT())
Java PaintPaint.java2082-2083nSetForceSDFT(mNativePaint, forceSDFT)
Java ViewRootImplViewRootImpl.java561-564mAttachInfo.mThreadedRenderer.setForceSDFT()
JNI PaintPaint.cpp1196-1198paint->setForceSDFT(forceSDFT)
JNI Rendererandroid_graphics_HardwareRenderer.cpp1305-1307RenderProxy.setForceSDFT()
Native RenderProxyRenderProxy.cpp804-805post to RenderThread → CanvasContext
Native CanvasContextCanvasContext.h305mForceSDFT = forceSDFT
Native CanvasContextCanvasContext.cpp1349mRenderPipeline->setForceSDFT(mForceSDFT)
Native PipelineSkiaVulkanPipeline.cpp108canvas->setForceSDFT(getForceSDFT())
Native PipelineSkiaOpenGLPipeline.cpp177surface->getCanvas()->setForceSDFT(getForceSDFT())
Skia SkPaintSkPaint.h344-345fForceSDFT 标记位
Skia SkCanvasSkCanvas.h2204-2205fForceSDFT 标记位
Skia CoordinatorTextBlobRedrawCoordinator.cpp48-49canvas→paint 标记传递(路径 B 汇入)
★ Skia 决策SubRunControl.cpp74forceSDFT = matrix.hasScale() && paint.getForceSDFT()
★ Skia 决策SubRunControl.cpp86-87绕过 min/maxDistanceFieldFontSize 限制
★ SDFT keySubRunContainer.cpp1184SkPackedGlyphID{glyphID} — 无亚像素位置
DirectMask keySubRunContainer.cpp1239SkPackedGlyphID{glyphID, mappedPos, mask} — 含亚像素
SDFT StrikeSpecSubRunContainer.cpp1395-1396SkMatrix::I() — 不受 view scale 影响
SkPackedGlyphIDSkGlyph.h81-82仅 glyphID 构造:fID = glyphID << kGlyphID
SkPackedGlyphIDSkGlyph.h90-91含位置构造:fID = PackIDSkPoint(glyphID, pt, mask)

条件约束

forceSDFT 生效需要满足:

  1. matrix.hasScale() — 当前变换矩阵包含 scale 分量(否则不需要 SDF 优化)
  2. paint.getForceSDFT() — 标记已通过路径 A 或 B 传递到 paint
  3. fAbleToUseSDFT — GPU 支持 SDF 渲染(现代设备均支持)
  4. paint.getMaskFilter() == nullptr — 无自定义 mask filter
  5. paint.getStyle() == kFill_Style 或 wide stroke — 填充或宽描边样式
  6. approximateDeviceTextSize > 0 — 文字可见

不生效的情况

  • matrix.hasScale() == false(纯平移/旋转,无缩放)→ 不需要 SDF,传统光栅化即可
  • 文字使用了自定义 MaskFilter → 不兼容 SDF
  • DDRE 模式启用时(strikeDeviceInfo.mUseDDRE == true)→ 走另一套渲染路径

性能对比

指标DirectMask(默认)SDFT(forceSDFT=true)
glyph_key 组成(glyphID, subpixelX, subpixelY)(glyphID)
scale 变化时 cacheMISS(每帧不同亚像素位)HIT(key 不变)
单次 onPrepare 耗时~600μs(重新光栅化)~3μs(仅 token 更新)
文字清晰度最优(亚像素精确光栅化)略低(SDF 插值,SDFT2 已改善)
适用场景静态/无 scale 动画scale 动画期间
内存开销每个亚像素位一份缓存仅一份 SDF 纹理

使用示例

SystemUI 中的使用(已有)

文件NotificationUtil.java:314

public static void setForceSDFT2(View view, boolean isForceSDFT) {
    if (view == null) return;
    try {
        Class viewCl = Class.forName("android.view.View");
        Method setForceSDFT = viewCl.getMethod("setForceSDFT", boolean.class);
        setForceSDFT.invoke(view, isForceSDFT);
    } catch (Exception e) {
        Log.d(TAG, "ViewRootImpl setForceSDFT2 Method not found!");
    }
}

调用点MiuiNotificationPanelAnimControllerImpl.kt 中的 ViewAnimatorListener

override fun beginAnimator() {
    NotificationUtil.setForceSDFT2(view, true)   // 动画开始:启用 SDF
}
 
override fun completeAnimator() {
    NotificationUtil.setForceSDFT2(view, false)  // 动画结束:恢复普通
}

适用场景

任何在 scale 动画期间包含文字的 View 树 均可使用:

// 动画开始前
targetView.setForceSDFT(true)  // 或通过反射
 
// 动画结束后
targetView.setForceSDFT(false)

由于继承机制(mPreForceSDFT = parent.getForceSDFT()),只需对动画的根 View 设置一次,子树中所有 TextView 自动生效。