in src/gpu/graphite/Device.cpp [1191:1494]
void Device::drawGeometry(const Transform& localToDevice,
const Geometry& geometry,
const SkPaint& paint,
const SkStrokeRec& style,
SkEnumBitMask<DrawFlags> flags,
sk_sp<SkBlender> primitiveBlender,
bool skipColorXform) {
ASSERT_SINGLE_OWNER
if (!localToDevice.valid()) {
// If the transform is not invertible or not finite then drawing isn't well defined.
SKGPU_LOG_W("Skipping draw with non-invertible/non-finite transform.");
return;
}
// Heavy weight paint options like path effects, mask filters, and stroke-and-fill style are
// applied on the CPU by generating a new shape and recursing on drawGeometry with updated flags
if (!(flags & DrawFlags::kIgnorePathEffect) && paint.getPathEffect()) {
// Apply the path effect before anything else, which if we are applying here, means that we
// are dealing with a Shape. drawVertices (and a SkVertices geometry) should pass in
// kIgnorePathEffect per SkCanvas spec. Text geometry also should pass in kIgnorePathEffect
// because the path effect is applied per glyph by the SkStrikeSpec already.
SkASSERT(geometry.isShape());
// TODO: If asADash() returns true and the base path matches the dashing fast path, then
// that should be detected now as well. Maybe add dashPath to Device so canvas can handle it
SkStrokeRec newStyle = style;
float maxScaleFactor = localToDevice.maxScaleFactor();
if (localToDevice.type() == Transform::Type::kPerspective) {
auto bounds = geometry.bounds();
float tl = std::get<1>(localToDevice.scaleFactors({bounds.left(), bounds.top()}));
float tr = std::get<1>(localToDevice.scaleFactors({bounds.right(), bounds.top()}));
float br = std::get<1>(localToDevice.scaleFactors({bounds.right(), bounds.bot()}));
float bl = std::get<1>(localToDevice.scaleFactors({bounds.left(), bounds.bot()}));
maxScaleFactor = std::max(std::max(tl, tr), std::max(bl, br));
}
newStyle.setResScale(maxScaleFactor);
SkPath dst;
if (paint.getPathEffect()->filterPath(&dst, geometry.shape().asPath(), &newStyle,
nullptr, localToDevice)) {
dst.setIsVolatile(true);
// Recurse using the path and new style, while disabling downstream path effect handling
this->drawGeometry(localToDevice, Geometry(Shape(dst)), paint, newStyle,
flags | DrawFlags::kIgnorePathEffect, std::move(primitiveBlender),
skipColorXform);
return;
} else {
SKGPU_LOG_W("Path effect failed to apply, drawing original path.");
this->drawGeometry(localToDevice, geometry, paint, style,
flags | DrawFlags::kIgnorePathEffect, std::move(primitiveBlender),
skipColorXform);
return;
}
}
// TODO: The tessellating and atlas path renderers haven't implemented perspective yet, so
// transform to device space so we draw something approximately correct (barring local coord
// issues).
if (geometry.isShape() && localToDevice.type() == Transform::Type::kPerspective &&
!is_simple_shape(geometry.shape(), style.getStyle())) {
SkPath devicePath = geometry.shape().asPath();
devicePath.transform(localToDevice.matrix().asM33());
devicePath.setIsVolatile(true);
this->drawGeometry(Transform::Identity(), Geometry(Shape(devicePath)), paint, style, flags,
std::move(primitiveBlender), skipColorXform);
return;
}
// TODO: Manually snap pixels for rects, rrects, and lines if paint is non-AA (ideally also
// consider snapping stroke width and/or adjusting geometry for hairlines). This pixel snapping
// math should be consistent with how non-AA clip [r]rects are handled.
// If we got here, then path effects should have been handled and the style should be fill or
// stroke/hairline. Stroke-and-fill is not handled by DrawContext, but is emulated here by
// drawing twice--one stroke and one fill--using the same depth value.
SkASSERT(!SkToBool(paint.getPathEffect()) || (flags & DrawFlags::kIgnorePathEffect));
// TODO: Some renderer decisions could depend on the clip (see PathAtlas::addShape for
// one workaround) so we should figure out how to remove this circular dependency.
// We assume that we will receive a renderer, or a PathAtlas. If it's a PathAtlas,
// then we assume that the renderer chosen in PathAtlas::addShape() will have
// single-channel coverage, require AA bounds outsetting, and have a single renderStep.
auto [renderer, pathAtlas] =
this->chooseRenderer(localToDevice, geometry, style, /*requireMSAA=*/false);
if (!renderer && !pathAtlas) {
SKGPU_LOG_W("Skipping draw with no supported renderer or PathAtlas.");
return;
}
// Calculate the clipped bounds of the draw and determine the clip elements that affect the
// draw without updating the clip stack.
const bool outsetBoundsForAA = renderer ? renderer->outsetBoundsForAA() : true;
ClipStack::ElementList clipElements;
const Clip clip =
fClip.visitClipStackForDraw(localToDevice, geometry, style, outsetBoundsForAA,
fMSAASupported, &clipElements);
if (clip.isClippedOut()) {
// Clipped out, so don't record anything.
return;
}
// Figure out what dst color requirements we have, if any.
const SkBlenderBase* blender = as_BB(paint.getBlender());
const std::optional<SkBlendMode> blendMode = blender ? blender->asBlendMode()
: SkBlendMode::kSrcOver;
Coverage rendererCoverage = renderer ? renderer->coverage()
: Coverage::kSingleChannel;
if (clip.needsCoverage() && rendererCoverage == Coverage::kNone) {
// Must upgrade to single channel coverage if the clip requires coverage;
// but preserve LCD coverage if the Renderer uses that.
rendererCoverage = Coverage::kSingleChannel;
}
bool dstReadRequired =
!CanUseHardwareBlending(fRecorder->priv().caps(), blendMode, rendererCoverage);
// A primitive blender should be ignored if there is no primitive color to blend against.
// Additionally, if a renderer emits a primitive color, then a null primitive blender should
// be interpreted as SrcOver blending mode.
if (!renderer || !renderer->emitsPrimitiveColor()) {
primitiveBlender = nullptr;
} else if (!SkToBool(primitiveBlender)) {
primitiveBlender = SkBlender::Mode(SkBlendMode::kSrcOver);
}
PaintParams shading{paint,
std::move(primitiveBlender),
clip.nonMSAAClip(),
sk_ref_sp(clip.shader()),
dstReadRequired,
skipColorXform};
const bool dependsOnDst = paint_depends_on_dst(shading) ||
clip.shader() || !clip.nonMSAAClip().isEmpty();
// Some shapes and styles combine multiple draws so the total render step count is split between
// the main renderer and possibly a secondaryRenderer.
SkStrokeRec::Style styleType = style.getStyle();
const Renderer* secondaryRenderer = nullptr;
Rect innerFillBounds = Rect::InfiniteInverted();
if (renderer) {
if (styleType == SkStrokeRec::kStrokeAndFill_Style) {
// `renderer` covers the fill, `secondaryRenderer` covers the stroke
secondaryRenderer = fRecorder->priv().rendererProvider()->tessellatedStrokes();
} else if (style.isFillStyle() && renderer->useNonAAInnerFill() && !dependsOnDst) {
// `renderer` opts into drawing a non-AA inner fill
innerFillBounds = get_inner_bounds(geometry, localToDevice);
if (!innerFillBounds.isEmptyNegativeOrNaN()) {
secondaryRenderer = fRecorder->priv().rendererProvider()->nonAABounds();
}
}
}
const int numNewRenderSteps = (renderer ? renderer->numRenderSteps() : 1) +
(secondaryRenderer ? secondaryRenderer->numRenderSteps() : 0);
// Decide if we have any reason to flush pending work. We want to flush before updating the clip
// state or making any permanent changes to a path atlas, since otherwise clip operations and/or
// atlas entries for the current draw will be flushed.
const bool requiresMSAA = (renderer && renderer->requiresMSAA()) ||
(secondaryRenderer && secondaryRenderer->requiresMSAA());
DstReadStrategy dstReadStrategy = dstReadRequired ? fDC->dstReadStrategy(requiresMSAA)
: DstReadStrategy::kNoneRequired;
const bool needsFlush =
this->needsFlushBeforeDraw(numNewRenderSteps, dstReadStrategy, requiresMSAA);
if (needsFlush) {
if (pathAtlas != nullptr) {
// We need to flush work for all devices associated with the current Recorder.
// Otherwise we may end up with outstanding draws that depend on past atlas state.
fRecorder->priv().flushTrackedDevices();
} else {
this->flushPendingWorkToRecorder();
}
}
// If an atlas path renderer was chosen we need to insert the shape into the atlas and schedule
// it to be drawn.
std::optional<PathAtlas::MaskAndOrigin> atlasMask; // only used if `pathAtlas != nullptr`
if (pathAtlas != nullptr) {
std::tie(renderer, atlasMask) = pathAtlas->addShape(clip.transformedShapeBounds(),
geometry.shape(),
localToDevice,
style);
// If there was no space in the atlas and we haven't flushed already, then flush pending
// work to clear up space in the atlas. If we had already flushed once (which would have
// cleared the atlas) then the atlas is too small for this shape.
if (!atlasMask && !needsFlush) {
// We need to flush work for all devices associated with the current Recorder.
// Otherwise we may end up with outstanding draws that depend on past atlas state.
fRecorder->priv().flushTrackedDevices();
// Try inserting the shape again.
std::tie(renderer, atlasMask) = pathAtlas->addShape(clip.transformedShapeBounds(),
geometry.shape(),
localToDevice,
style);
}
if (!atlasMask) {
SKGPU_LOG_E("Failed to add shape to atlas!");
// TODO(b/285195175): This can happen if the atlas is not large enough or a compatible
// atlas texture cannot be created. Handle the first case in `chooseRenderer` and make
// sure that the atlas path renderer is not chosen if the path is larger than the atlas
// texture.
return;
}
// Since addShape() was successful we should have a valid Renderer now.
SkASSERT(renderer && renderer->numRenderSteps() == 1 && !renderer->emitsPrimitiveColor());
}
#if defined(SK_DEBUG)
// Renderers and their component RenderSteps have flexibility in defining their
// DepthStencilSettings. However, the clipping and ordering managed between Device and ClipStack
// requires that only GREATER or GEQUAL depth tests are used for draws recorded through the
// client-facing, painters-order-oriented API. We assert here vs. in Renderer's constructor to
// allow internal-oriented Renderers that are never selected for a "regular" draw call to have
// more flexibility in their settings.
SkASSERT(renderer);
for (const RenderStep* step : renderer->steps()) {
auto dss = step->depthStencilSettings();
SkASSERT((!step->performsShading() || dss.fDepthTestEnabled) &&
(!dss.fDepthTestEnabled ||
dss.fDepthCompareOp == CompareOp::kGreater ||
dss.fDepthCompareOp == CompareOp::kGEqual));
}
#endif
// Update the clip stack after issuing a flush (if it was needed). A draw will be recorded after
// this point.
DrawOrder order(fCurrentDepth.next());
CompressedPaintersOrder clipOrder = fClip.updateClipStateForDraw(
clip, clipElements, fColorDepthBoundsManager.get(), order.depth());
// A draw's order always depends on the clips that must be drawn before it
order.dependsOnPaintersOrder(clipOrder);
// If a draw is not opaque, it must be drawn after the most recent draw it intersects with in
// order to blend correctly.
if (rendererCoverage != Coverage::kNone || dependsOnDst) {
CompressedPaintersOrder prevDraw =
fColorDepthBoundsManager->getMostRecentDraw(clip.drawBounds());
order.dependsOnPaintersOrder(prevDraw);
}
// Now that the base paint order and draw bounds are finalized, if the Renderer relies on the
// stencil attachment, we compute a secondary sorting field to allow disjoint draws to reorder
// the RenderSteps across draws instead of in sequence for each draw.
if (renderer->depthStencilFlags() & DepthStencilFlags::kStencil) {
DisjointStencilIndex setIndex = fDisjointStencilSet->add(order.paintOrder(),
clip.drawBounds());
order.dependsOnStencil(setIndex);
}
// TODO(b/330864257): This is an extra traversal of all paint effects, that can be avoided when
// the paint key itself is determined inside this function.
shading.notifyImagesInUse(fRecorder, fDC.get());
// If an atlas path renderer was chosen, then record a single CoverageMaskShape draw.
// The shape will be scheduled to be rendered or uploaded into the atlas during the
// next invocation of flushPendingWorkToRecorder().
if (pathAtlas != nullptr) {
// Record the draw as a fill since stroking is handled by the atlas render/upload.
SkASSERT(atlasMask.has_value());
auto [mask, origin] = *atlasMask;
fDC->recordDraw(renderer, Transform::Translate(origin.fX, origin.fY), Geometry(mask),
clip, order, &shading, nullptr);
} else {
if (styleType == SkStrokeRec::kStroke_Style ||
styleType == SkStrokeRec::kHairline_Style ||
styleType == SkStrokeRec::kStrokeAndFill_Style) {
// For stroke-and-fill, 'renderer' is used for the fill and we always use the
// TessellatedStrokes renderer; for stroke and hairline, 'renderer' is used.
StrokeStyle stroke(style.getWidth(), style.getMiter(), style.getJoin(), style.getCap());
fDC->recordDraw(styleType == SkStrokeRec::kStrokeAndFill_Style
? fRecorder->priv().rendererProvider()->tessellatedStrokes()
: renderer,
localToDevice, geometry, clip, order, &shading, &stroke);
}
if (styleType == SkStrokeRec::kFill_Style ||
styleType == SkStrokeRec::kStrokeAndFill_Style) {
// Possibly record an additional draw using the non-AA bounds renderer to fill the
// interior with a renderer that can disable blending entirely.
if (!innerFillBounds.isEmptyNegativeOrNaN()) {
SkASSERT(!dependsOnDst && renderer->useNonAAInnerFill());
DrawOrder orderWithoutCoverage{order.depth()};
orderWithoutCoverage.dependsOnPaintersOrder(clipOrder);
fDC->recordDraw(fRecorder->priv().rendererProvider()->nonAABounds(),
localToDevice, Geometry(Shape(innerFillBounds)),
clip, orderWithoutCoverage, &shading, nullptr);
// Force the coverage draw to come after the non-AA draw in order to benefit from
// early depth testing.
order.dependsOnPaintersOrder(orderWithoutCoverage.paintOrder());
}
fDC->recordDraw(renderer, localToDevice, geometry, clip, order, &shading, nullptr);
}
}
// Post-draw book keeping (bounds manager, depth tracking, etc.)
fColorDepthBoundsManager->recordDraw(clip.drawBounds(), order.paintOrder());
fCurrentDepth = order.depth();
// TODO(b/238758897): When we enable layer elision that depends on draws not overlapping, we
// can use the `getMostRecentDraw()` query to determine that, although that will mean querying
// even if the draw does not depend on dst (so should be only be used when the Device is an
// elision candidate).
}