blob: a791747f43ac63a0349768f5d689872e544e6a6d [file] [log] [blame]
/*
* Copyright 2023 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/PathAtlas.h"
#include "include/gpu/graphite/Recorder.h"
#include "src/core/SkIPoint16.h"
#include "src/gpu/graphite/Caps.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/RendererProvider.h"
#include "src/gpu/graphite/TextureProxy.h"
#include "src/gpu/graphite/geom/Rect.h"
#include "src/gpu/graphite/geom/Shape.h"
#include "src/gpu/graphite/geom/Transform_graphite.h"
#ifdef SK_ENABLE_VELLO_SHADERS
#include "src/gpu/graphite/compute/DispatchGroup.h"
#endif
namespace skgpu::graphite {
PathAtlas::PathAtlas(uint32_t width, uint32_t height) : fRectanizer(width, height) {}
PathAtlas::~PathAtlas() = default;
bool PathAtlas::addShape(Recorder* recorder,
const Rect& maskBounds,
const Shape& shape,
const Transform& localToDevice,
const SkStrokeRec& style,
Rect* out) {
SkASSERT(out);
SkASSERT(!maskBounds.isEmptyNegativeOrNaN());
if (!fTexture) {
// TODO(chromium:1856): Dawn does not support the "storage binding" usage for the R8Unorm
// texture format. This means that we will have to use RGBA8 until Dawn provides an optional
// feature.
fTexture = TextureProxy::MakeStorage(
recorder->priv().caps(),
SkISize::Make(int32_t(this->width()), int32_t(this->height())),
kAlpha_8_SkColorType,
skgpu::Budgeted::kYes);
if (!fTexture) {
return false;
}
}
// Add a 2 pixel-wide border around the shape bounds when allocating the atlas slot. The outer
// border acts as a buffer between atlas entries and the pixels contain 0. The inner border is
// included in the mask and provides additional coverage pixels for analytic AA.
// TODO(b/273924867) Should the inner outset get applied in drawGeometry/applyClipToDraw and
// included implicitly?
Rect bounds = maskBounds.makeOutset(2);
skvx::float2 size = bounds.size();
SkIPoint16 pos;
if (!fRectanizer.addRect(size.x(), size.y(), &pos)) {
return false;
}
*out = Rect::XYWH(skvx::float2(pos.x(), pos.y()), size);
this->onAddShape(shape, localToDevice, *out, maskBounds.x(), maskBounds.y(), style);
return true;
}
void PathAtlas::reset() {
fRectanizer.reset();
this->onReset();
}
#ifdef SK_ENABLE_VELLO_SHADERS
namespace {
// TODO: select atlas size dynamically? Take ContextOptions::fMaxTextureAtlasSize into account?
// TODO: This is the maximum target dimension that vello can handle today
constexpr uint32_t kComputeAtlasDim = 4096;
} // namespace
ComputePathAtlas::ComputePathAtlas() : PathAtlas(kComputeAtlasDim, kComputeAtlasDim) {}
std::unique_ptr<DispatchGroup> ComputePathAtlas::recordDispatches(Recorder* recorder) const {
if (!this->texture()) {
return nullptr;
}
SkASSERT(recorder);
return recorder->priv().rendererProvider()->velloRenderer()->renderScene(
{fOccuppiedWidth, fOccuppiedHeight, SkColors::kBlack},
fScene,
sk_ref_sp(this->texture()),
recorder);
}
void ComputePathAtlas::onAddShape(const Shape& shape,
const Transform& localToDevice,
const Rect& atlasBounds,
float deviceOffsetX,
float deviceOffsetY,
const SkStrokeRec& style) {
// TODO: The compute renderer doesn't support perspective yet. We assume that the path has been
// appropriately transformed in that case.
SkASSERT(localToDevice.type() != Transform::Type::kProjection);
// Restrict the render to the occupied area of the atlas.
fOccuppiedWidth = std::max(fOccuppiedWidth, (uint32_t)atlasBounds.right());
fOccuppiedHeight = std::max(fOccuppiedHeight, (uint32_t)atlasBounds.bot());
// TODO(b/283876964): Apply clips here. Initially we'll need to encode the clip stack repeatedly
// for each shape since the full vello renderer treats clips and their affected draws as a
// single shape hierarchy in the same scene coordinate space. For coverage masks we want each
// mask to be transformed to its atlas allocation coordinates and for the clip to be applied
// with a translation relative to the atlas slot.
//
// Repeatedly encoding the clip stack should be relatively cheap (depending on how deep the
// clips get) however it is wasteful both in terms of time and memory. If this proves to hurt
// performance, future work will explore building an atlas-oriented element processing stage
// that applies the atlas-relative translation while evaluating the stack monoid on the GPU.
// Clip the mask to the bounds of the atlas slot. When the rectangle gets turned into a path,
// its bottom and right edges are included in the clip, however semantically those pixels are
// outside the atlas region (the implementation of Rect::size() implies that the bottom-right
// bounds are exclusive). For the clip shape we inset the bottom and right edges by one pixel to
// avoid filling into neighboring regions.
Rect clipBounds(atlasBounds.topLeft() + 1, atlasBounds.botRight() - 1);
SkPath clipRect = SkPath::Rect(clipBounds.asSkRect());
fScene.pushClipLayer(clipRect, Transform::Identity());
// The atlas transform of the shape is the linear-components (scale, rotation, skew) of
// `localToDevice` translated by the top-left offset of `atlasBounds`, accounting for the 2
// pixel-wide border we added earlier, so that the shape is correctly centered.
SkM44 atlasMatrix = localToDevice.matrix();
atlasMatrix.postTranslate(atlasBounds.x() + 2 - deviceOffsetX,
atlasBounds.y() + 2 - deviceOffsetY);
Transform atlasTransform(atlasMatrix);
SkPath devicePath = shape.asPath();
// For stroke-and-fill, draw two masks into the same atlas slot: one for the stroke and one for
// the fill.
SkStrokeRec::Style styleType = style.getStyle();
if (styleType == SkStrokeRec::kStroke_Style ||
styleType == SkStrokeRec::kHairline_Style ||
styleType == SkStrokeRec::kStrokeAndFill_Style) {
// We need to special-case hairline strokes and strokes with sub-pixel width as Vello
// draws these with aliasing and the results are barely visible. Draw the stroke with a
// device-space width of 1 pixel and scale down the alpha by the true width to approximate
// the sampled area.
float width = style.getWidth();
float deviceWidth = width * atlasTransform.maxScaleFactor();
if (style.isHairlineStyle() || deviceWidth <= 1.0) {
// Both strokes get 1/2 weight scaled by the theoretical area (1 for hairlines,
// `deviceWidth` otherwise).
SkColor4f color = SkColors::kRed;
color.fR *= style.isHairlineStyle() ? 1.0 : deviceWidth;
// Transform the stroke's width to its local coordinate space since it'll get drawn with
// `atlasTransform`.
float transformedWidth = 1.0f / atlasTransform.maxScaleFactor();
fScene.solidStroke(devicePath, color, transformedWidth, atlasTransform);
} else {
fScene.solidStroke(devicePath, SkColors::kRed, style.getWidth(), atlasTransform);
}
}
if (styleType == SkStrokeRec::kFill_Style || styleType == SkStrokeRec::kStrokeAndFill_Style) {
fScene.solidFill(devicePath, SkColors::kRed, shape.fillType(), atlasTransform);
}
fScene.popClipLayer();
}
#endif // SK_ENABLE_VELLO_SHADERS
} // namespace skgpu::graphite