Add analytic clip fragment processors for affine-mapped rects and rounded rects to avoid mask textures. - #1495
Conversation
…oid mask textures.
…the obsolete GLSLAARectEffect and porting its highp precision fix into the new GLSLRectEffect and GLSLRRectEffect shaders.
…sk path behavior accurately.
- Fix DEBUG_ASSERT_RESULT dropping its expression side effect under NO_LOG. - Drop the unreachable perspective note from Rect and RRect effect key comments. - Fix a stale function name and remove verifying phrasing in ClipTest comments. - Rename kScale to diagInsetScale to drop the disallowed k prefix. - Document the invariant guarding the RRect axis-aligned transform assert in simplify, with a concrete degenerate transform example. - Remove orphaned CanvasTest clip baseline keys after the test migration to ClipTest.
…a redundant simplify.
# Conflicts: # src/gpu/OpsCompositor.cpp
| Path curBoundsPath = {}; | ||
| curBoundsPath.addRect(cur.bounds); | ||
| const ClipElement curElement(curBoundsPath, false); | ||
| const ClipElement curElement(GeometryShape(curBoundsPath), Matrix::I(), false); |
There was a problem hiding this comment.
这里为把 cur.bounds 包成 ClipElement,先构造了一个 Path 再传入 GeometryShape(Path 类型),而 ClipElement 构造函数里的 simplify() 会立刻把它降级回 Rect 类型并销毁该 Path。这涉及一次 PathRef(shared_ptr)的堆分配与释放,纯属浪费。cur.bounds 本身就是 Rect,可直接用 Rect 构造,产生完全等价的元素(simplify 后同为 Rect 类型、相同 rect 及 outer/inner bounds),无需 Path:
const ClipElement curElement(GeometryShape(cur.bounds), Matrix::I(), false);该路径在每次非首个裁剪元素加入时都会执行,属热点,建议改用 Rect 构造避免分配。
| if (IsClipPixelAligned(_shape.rect())) { | ||
| // Case 1a: this is pixel-aligned so AA has no visual effect, adopt other's AA flag. | ||
| _antiAlias = other._antiAlias; | ||
| } else if (!IsClipPixelAligned(other._shape.rect())) { |
There was a problem hiding this comment.
在 UpdateElements 中走到 tryCombine 时,上游已保证不存在两个元素互相包含的场景,因此这里无需再做 100% 包含判断。
| if (transformed.has_value()) { | ||
| _matrix.setIdentity(); | ||
| if (transformed->type() == RRect::Type::Rect) { | ||
| _shape.setRect(transformed->rect()); |
There was a problem hiding this comment.
会走到 setRect。进来虽是 RRect,但经 TryAxisAlignedTransform 变换后半径可能退化归零:RRect::setRectRadii 里的 ScaleRadii/FlushToZero 会把被大值吸收的极小半径归零,四角全零后 ComputeType 将类型归一化为 Rect。例如 rect={0,0,10,10}、四角半径 {1e-6, 1e-6} 的 RRect 经 scale(1e-3, 1e-3) 变换后,半径约 1e-9,与边长 0.01 相加时被 float 吸收归零,transformed->type() 即为 RRect::Type::Rect,走此分支。
| namespace tgfx { | ||
|
|
||
| /** | ||
| * GeometryShape is a tagged union over geometric primitives: an empty shape, an axis-aligned rect, |
There was a problem hiding this comment.
an axis-aligned rect 这个不只是像素对齐的rect吧
There was a problem hiding this comment.
这里 "axis-aligned rect" 指轴对齐矩形,即矩形四条边与坐标系 x/y 轴平行,与像素对齐无关。
| for (size_t i = clip.oldestValidIndex(); i < elements.size(); ++i) { | ||
| const auto& element = elements[i]; | ||
| if (element.isValid() && element.path().isInverseFillType()) { | ||
| if (element.isValid() && element.shape().isPath() && |
There was a problem hiding this comment.
这个和path的关系是?其他场景不是也是hasInverseClip吗
There was a problem hiding this comment.
对一个 Picture 里的裁剪记录,它总是早于实际绘制指令被处理,需要更新整个 Picture 的裁剪状态是否包含未限定的裁剪区(即实际内容是否需绘制到一个没有限制的区域内)。裁剪元素类型中只有 path 可能被设为反转状态来表示未限定区域,其他图形都是有限区域,所以只判断 path。
| static constexpr float BOUNDS_TOLERANCE = 1e-3f; | ||
| static constexpr float CLIP_BOUNDS_TOLERANCE = 1e-3f; | ||
|
|
||
| inline bool IsPixelAligned(float value) { |
There was a problem hiding this comment.
当前仅用于裁剪区。该安全阈值是按裁剪区用覆盖率模拟元素通过情况计算得出的,其他场景未必安全,因此特意加 CLIP 前缀提醒开发者注意使用场景。
| return out; | ||
| } | ||
|
|
||
| PlacementPtr<FragmentProcessor> OpsCompositor::makeAnalyticFP( |
There was a problem hiding this comment.
这里返回 bool 是有意的。tryApplyAnalyticFP 内部不仅创建解析 FP,还负责把它与传入的 inputFP 合并成链(FragmentProcessor::Compose)。合并后返回的指针在成功和失败时都可能非空(失败时返回原样的 inputFP 累积链),因此外部无法仅凭指针是否为空判断是否成功创建了解析 FP,需要 bool 显式区分。把合并逻辑收敛到函数内部是为了让调用方统一处理链的累积,代价就是需要额外的成功标志。
There was a problem hiding this comment.
理解在当前设计下 bool 是必需的,也认同它与 getClipMaskFP 都在内部接管 Compose、保持了对称。不过换个角度:累积链 clipFP 本就是 applyClip 循环在维护的状态,若把 Compose 从 tryApplyAnalyticFP 收回循环里,函数退化为「只为单个元素造 FP,失败返回 nullptr」的纯转换,返回值语义单一、bool 即可去除,也更好测试。代价是 getClipMaskFP 也应同步把 Compose 收回循环以保持一致,改动涉及三处。功能无 bug,仅是职责纯度上的可选优化,供参考,是否调整由你权衡。
There was a problem hiding this comment.
想了下这个重构不做了。clip 链每个节点都是 out = in * coverage(RectEffect/RRectEffect 的 shader 输出 inputColor * coverage,mask 侧是 MulInputByChildAlpha),整链就是标量 coverage 连乘。tryApplyAnalyticFP 和 makeMaskFP「基于已有 FP 追加本阶段 FP」本身是内聚的职责,不算职责不纯。把 Compose 抽回循环会让 makeMaskFP 多一个永远传 nullptr 的死参数、Compose 顺序约束从函数内散到调用点,换来的只是去掉一个 bool,且改的是 clip 合成核心路径。无 bug、无真实痛点,收益不抵成本,保持现状。
…n extra Path allocation.
…t leave uncleared stencil regions. glClear is affected by the scissor test, so a scissor rect left enabled by a previous render pass restricted the depth/stencil clear to that region, leaving stale stencil values that corrupted the stencil-and-cover inner-stroke rendering of adjacent shapes.
为仿射变换下的矩形和圆角矩形裁剪引入解析式 coverage FragmentProcessor(RectEffect / RRectEffect),在 shader 内直接计算覆盖率,避免额外分配 mask 纹理和渲染 pass。
主要改动: