1 /*
2 * Copyright 2024 Google LLC
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "src/gpu/graphite/ComputePathAtlas.h"
9
10 #include "include/gpu/graphite/Recorder.h"
11 #include "src/core/SkTraceEvent.h"
12 #include "src/gpu/graphite/AtlasProvider.h"
13 #include "src/gpu/graphite/Caps.h"
14 #include "src/gpu/graphite/Log.h"
15 #include "src/gpu/graphite/RasterPathUtils.h"
16 #include "src/gpu/graphite/RecorderPriv.h"
17 #include "src/gpu/graphite/RendererProvider.h"
18 #include "src/gpu/graphite/TextureProxy.h"
19 #include "src/gpu/graphite/TextureUtils.h"
20 #include "src/gpu/graphite/geom/Transform_graphite.h"
21
22 #ifdef SK_ENABLE_VELLO_SHADERS
23 #include "src/gpu/graphite/compute/DispatchGroup.h"
24 #endif
25
26 namespace skgpu::graphite {
27 namespace {
28
29 // TODO: This is the maximum target dimension that vello can handle today.
30 constexpr uint16_t kComputeAtlasDim = 4096;
31
32 // TODO: Currently we reject shapes that are smaller than a subset of a given atlas page to avoid
33 // creating too many flushes in a Recording containing many large path draws. These shapes often
34 // don't make efficient use of the available atlas texture space and the cost of sequential
35 // dispatches to render multiple atlas pages can be prohibitive.
36 constexpr size_t kBboxAreaThreshold = 1024 * 512;
37
38 // Coordinate size that is too large for vello to handle efficiently. See the discussion on
39 // https://github.com/linebender/vello/pull/542.
40 constexpr float kCoordinateThreshold = 1e10;
41
42 } // namespace
43
ComputePathAtlas(Recorder * recorder)44 ComputePathAtlas::ComputePathAtlas(Recorder* recorder)
45 : PathAtlas(recorder, kComputeAtlasDim, kComputeAtlasDim)
46 , fRectanizer(this->width(), this->height()) {}
47
initializeTextureIfNeeded()48 bool ComputePathAtlas::initializeTextureIfNeeded() {
49 if (!fTexture) {
50 SkColorType targetCT = ComputeShaderCoverageMaskTargetFormat(fRecorder->priv().caps());
51 fTexture = fRecorder->priv().atlasProvider()->getAtlasTexture(fRecorder,
52 this->width(),
53 this->height(),
54 targetCT,
55 /*identifier=*/0,
56 /*requireStorageUsage=*/true);
57 }
58 return fTexture != nullptr;
59 }
60
isSuitableForAtlasing(const Rect & transformedShapeBounds,const Rect & clipBounds) const61 bool ComputePathAtlas::isSuitableForAtlasing(const Rect& transformedShapeBounds,
62 const Rect& clipBounds) const {
63 Rect shapeBounds = transformedShapeBounds.makeRoundOut();
64 Rect maskBounds = shapeBounds.makeIntersect(clipBounds);
65 skvx::float2 maskSize = maskBounds.size();
66 float width = maskSize.x(), height = maskSize.y();
67
68 if (width > this->width() || height > this->height()) {
69 return false;
70 }
71
72 // For now we're allowing paths that are smaller than 1/32nd of the full 4096x4096 atlas size
73 // to prevent the atlas texture from filling up too often. There are several approaches we
74 // should explore to alleviate the cost of atlasing large paths.
75 if (width * height > kBboxAreaThreshold) {
76 return false;
77 }
78
79 // Reject pathological shapes that vello can't handle efficiently yet.
80 skvx::float2 unclippedSize = shapeBounds.size();
81 if (std::fabs(unclippedSize.x()) > kCoordinateThreshold ||
82 std::fabs(unclippedSize.y()) > kCoordinateThreshold) {
83 return false;
84 }
85
86 return true;
87 }
88
addRect(skvx::half2 maskSize,SkIPoint16 * outPos)89 const TextureProxy* ComputePathAtlas::addRect(skvx::half2 maskSize,
90 SkIPoint16* outPos) {
91 if (!this->initializeTextureIfNeeded()) {
92 SKGPU_LOG_E("Failed to instantiate an atlas texture");
93 return nullptr;
94 }
95
96 // An empty mask always fits, so just return the texture.
97 // TODO: This may not be needed if we can handle clipped out bounds with inverse fills
98 // another way. See PathAtlas::addShape().
99 if (!all(maskSize)) {
100 *outPos = {0, 0};
101 return fTexture.get();
102 }
103
104 if (!fRectanizer.addPaddedRect(maskSize.x(), maskSize.y(), kEntryPadding, outPos)) {
105 return nullptr;
106 }
107
108 return fTexture.get();
109 }
110
reset()111 void ComputePathAtlas::reset() {
112 fRectanizer.reset();
113
114 this->onReset();
115 }
116
117 #ifdef SK_ENABLE_VELLO_SHADERS
118
119 /**
120 * ComputePathAtlas that uses a VelloRenderer.
121 */
122 class VelloComputePathAtlas final : public ComputePathAtlas {
123 public:
VelloComputePathAtlas(Recorder * recorder)124 explicit VelloComputePathAtlas(Recorder* recorder)
125 : ComputePathAtlas(recorder)
126 , fCachedAtlasMgr(fWidth, fHeight, recorder->priv().caps()) {}
127 // Record the compute dispatches that will draw the atlas contents.
128 bool recordDispatches(Recorder*, ComputeTask::DispatchGroupList*) const override;
129
130 private:
131 const TextureProxy* onAddShape(const Shape&,
132 const Transform&,
133 const SkStrokeRec&,
134 skvx::half2 maskOrigin,
135 skvx::half2 maskSize,
136 skvx::half2* outPos) override;
onReset()137 void onReset() override {
138 fCachedAtlasMgr.onReset();
139
140 fUncachedScene.reset();
141 fUncachedOccupiedArea = { 0, 0 };
142 }
143
144 class VelloAtlasMgr : public PathAtlas::DrawAtlasMgr {
145 public:
VelloAtlasMgr(size_t width,size_t height,const Caps * caps)146 VelloAtlasMgr(size_t width, size_t height, const Caps* caps)
147 : PathAtlas::DrawAtlasMgr(width, height, width, height,
148 DrawAtlas::UseStorageTextures::kYes,
149 /*label=*/"VelloPathAtlas", caps) {}
150
151 bool recordDispatches(Recorder* recorder, ComputeTask::DispatchGroupList* dispatches) const;
152
onReset()153 void onReset() {
154 fDrawAtlas->markUsedPlotsAsFull();
155 for (int i = 0; i < PlotLocator::kMaxMultitexturePages; ++i) {
156 fScenes[i].reset();
157 fOccupiedAreas[i] = {0, 0};
158 }
159 }
160
161 protected:
162 bool onAddToAtlas(const Shape&,
163 const Transform& transform,
164 const SkStrokeRec&,
165 SkIRect shapeBounds,
166 const AtlasLocator&) override;
167
168 private:
169 VelloScene fScenes[PlotLocator::kMaxMultitexturePages];
170 SkISize fOccupiedAreas[PlotLocator::kMaxMultitexturePages] = {
171 {0, 0}, {0, 0}, {0, 0}, {0, 0}
172 };
173 };
174
175 VelloAtlasMgr fCachedAtlasMgr;
176
177 // Contains the encoded scene buffer data that serves as the input to a vello compute pass.
178 // For the uncached atlas.
179 VelloScene fUncachedScene;
180
181 // Occupied bounds of the uncached atlas
182 SkISize fUncachedOccupiedArea = { 0, 0 };
183 };
184
get_vello_aa_config(Recorder * recorder)185 static VelloAaConfig get_vello_aa_config(Recorder* recorder) {
186 // Use the analytic area AA mode unless caps say otherwise.
187 VelloAaConfig config = VelloAaConfig::kAnalyticArea;
188 #if defined(GPU_TEST_UTILS)
189 PathRendererStrategy strategy = recorder->priv().caps()->requestedPathRendererStrategy();
190 if (strategy == PathRendererStrategy::kComputeMSAA16) {
191 config = VelloAaConfig::kMSAA16;
192 } else if (strategy == PathRendererStrategy::kComputeMSAA8) {
193 config = VelloAaConfig::kMSAA8;
194 }
195 #endif
196
197 return config;
198 }
199
render_vello_scene(Recorder * recorder,sk_sp<TextureProxy> texture,const VelloScene & scene,SkISize occupiedArea,VelloAaConfig config)200 static std::unique_ptr<DispatchGroup> render_vello_scene(Recorder* recorder,
201 sk_sp<TextureProxy> texture,
202 const VelloScene& scene,
203 SkISize occupiedArea,
204 VelloAaConfig config) {
205 return recorder->priv().rendererProvider()->velloRenderer()->renderScene(
206 {(uint32_t)occupiedArea.width(),
207 (uint32_t)occupiedArea.height(),
208 SkColors::kBlack,
209 config},
210 scene,
211 std::move(texture),
212 recorder);
213 }
214
add_shape_to_scene(const Shape & shape,const Transform & transform,const SkStrokeRec & style,Rect atlasBounds,VelloScene * scene,SkISize * occupiedArea)215 static void add_shape_to_scene(const Shape& shape,
216 const Transform& transform,
217 const SkStrokeRec& style,
218 Rect atlasBounds,
219 VelloScene* scene,
220 SkISize* occupiedArea) {
221 occupiedArea->fWidth = std::max(occupiedArea->fWidth,
222 (int)atlasBounds.right() + PathAtlas::kEntryPadding);
223 occupiedArea->fHeight = std::max(occupiedArea->fHeight,
224 (int)atlasBounds.bot() + PathAtlas::kEntryPadding);
225
226 // TODO(b/283876964): Apply clips here. Initially we'll need to encode the clip stack repeatedly
227 // for each shape since the full vello renderer treats clips and their affected draws as a
228 // single shape hierarchy in the same scene coordinate space. For coverage masks we want each
229 // mask to be transformed to its atlas allocation coordinates and for the clip to be applied
230 // with a translation relative to the atlas slot.
231 //
232 // Repeatedly encoding the clip stack should be relatively cheap (depending on how deep the
233 // clips get) however it is wasteful both in terms of time and memory. If this proves to hurt
234 // performance, future work will explore building an atlas-oriented element processing stage
235 // that applies the atlas-relative translation while evaluating the stack monoid on the GPU.
236
237 // Clip the mask to the bounds of the atlas slot, which are already inset by 1px relative to
238 // the bounds that the Rectanizer assigned.
239 SkPath clipRect = SkPath::Rect(atlasBounds.asSkRect());
240 scene->pushClipLayer(clipRect, Transform::Identity());
241
242 // The atlas transform of the shape is the linear-components (scale, rotation, skew) of
243 // `localToDevice` translated by the top-left offset of `atlasBounds`.
244 Transform atlasTransform = transform.postTranslate(atlasBounds.x(), atlasBounds.y());
245 SkPath devicePath = shape.asPath();
246
247 // For stroke-and-fill, draw two masks into the same atlas slot: one for the stroke and one for
248 // the fill.
249 SkStrokeRec::Style styleType = style.getStyle();
250 if (styleType == SkStrokeRec::kStroke_Style ||
251 styleType == SkStrokeRec::kHairline_Style ||
252 styleType == SkStrokeRec::kStrokeAndFill_Style) {
253 // We need to special-case hairline strokes and strokes with sub-pixel width as Vello
254 // draws these with aliasing and the results are barely visible. Draw the stroke with a
255 // device-space width of 1 pixel and scale down the alpha by the true width to approximate
256 // the sampled area.
257 float width = style.getWidth();
258 float deviceWidth = width * atlasTransform.maxScaleFactor();
259 if (style.isHairlineStyle() || deviceWidth <= 1.0) {
260 // Both strokes get 1/2 weight scaled by the theoretical area (1 for hairlines,
261 // `deviceWidth` otherwise).
262 SkColor4f color = SkColors::kRed;
263 color.fR *= style.isHairlineStyle() ? 1.0 : deviceWidth;
264
265 // Transform the stroke's width to its local coordinate space since it'll get drawn with
266 // `atlasTransform`.
267 float transformedWidth = 1.0f / atlasTransform.maxScaleFactor();
268 SkStrokeRec adjustedStyle(style);
269 adjustedStyle.setStrokeStyle(transformedWidth);
270 scene->solidStroke(devicePath, color, adjustedStyle, atlasTransform);
271 } else {
272 scene->solidStroke(devicePath, SkColors::kRed, style, atlasTransform);
273 }
274 }
275 if (styleType == SkStrokeRec::kFill_Style || styleType == SkStrokeRec::kStrokeAndFill_Style) {
276 scene->solidFill(devicePath, SkColors::kRed, shape.fillType(), atlasTransform);
277 }
278
279 scene->popClipLayer();
280 }
281
recordDispatches(Recorder * recorder,ComputeTask::DispatchGroupList * dispatches) const282 bool VelloComputePathAtlas::recordDispatches(Recorder* recorder,
283 ComputeTask::DispatchGroupList* dispatches) const {
284 bool addedDispatches = fCachedAtlasMgr.recordDispatches(recorder, dispatches);
285
286 if (this->texture() && !fUncachedOccupiedArea.isEmpty()) {
287 SkASSERT(recorder && recorder == fRecorder);
288
289 VelloAaConfig config = get_vello_aa_config(recorder);
290 std::unique_ptr<DispatchGroup> dispatchGroup =
291 render_vello_scene(recorder,
292 sk_ref_sp(this->texture()),
293 fUncachedScene,
294 fUncachedOccupiedArea,
295 config);
296 if (dispatchGroup) {
297 TRACE_EVENT_INSTANT1("skia.gpu", TRACE_FUNC, TRACE_EVENT_SCOPE_THREAD,
298 "# dispatches", dispatchGroup->dispatches().size());
299 dispatches->emplace_back(std::move(dispatchGroup));
300 return true;
301 } else {
302 SKGPU_LOG_E("VelloComputePathAtlas:: Failed to create dispatch group.");
303 }
304 }
305
306 return addedDispatches;
307 }
308
onAddShape(const Shape & shape,const Transform & transform,const SkStrokeRec & style,skvx::half2 maskOrigin,skvx::half2 maskSize,skvx::half2 * outPos)309 const TextureProxy* VelloComputePathAtlas::onAddShape(
310 const Shape& shape,
311 const Transform& transform,
312 const SkStrokeRec& style,
313 skvx::half2 maskOrigin,
314 skvx::half2 maskSize,
315 skvx::half2* outPos) {
316
317 skgpu::UniqueKey maskKey;
318 bool hasKey = shape.hasKey();
319 if (hasKey) {
320 // Try to locate or add to cached DrawAtlas
321 const TextureProxy* proxy = fCachedAtlasMgr.findOrCreateEntry(fRecorder,
322 shape,
323 transform,
324 style,
325 maskOrigin,
326 maskSize,
327 outPos);
328 if (proxy) {
329 return proxy;
330 }
331 }
332
333 // Try to add to uncached texture
334 SkIPoint16 iPos;
335 const TextureProxy* texProxy = this->addRect(maskSize, &iPos);
336 if (!texProxy) {
337 return nullptr;
338 }
339 *outPos = skvx::half2(iPos.x(), iPos.y());
340 // If the mask is empty, just return.
341 // TODO: This may not be needed if we can handle clipped out bounds with inverse fills
342 // another way. See PathAtlas::addShape().
343 if (!all(maskSize)) {
344 return texProxy;
345 }
346
347 // TODO: The compute renderer doesn't support perspective yet. We assume that the path has been
348 // appropriately transformed in that case.
349 SkASSERT(transform.type() != Transform::Type::kPerspective);
350
351 // Restrict the render to the occupied area of the atlas, including entry padding so that the
352 // padded row/column is cleared when Vello renders.
353 Rect atlasBounds = Rect::XYWH(skvx::float2(iPos.x(), iPos.y()), skvx::cast<float>(maskSize));
354
355 add_shape_to_scene(shape, transform, style, atlasBounds,
356 &fUncachedScene, &fUncachedOccupiedArea);
357
358 return texProxy;
359 }
360
361 /////////////////////////////////////////////////////////////////////////////////////////
362
onAddToAtlas(const Shape & shape,const Transform & transform,const SkStrokeRec & style,SkIRect shapeBounds,const AtlasLocator & locator)363 bool VelloComputePathAtlas::VelloAtlasMgr::onAddToAtlas(const Shape& shape,
364 const Transform& transform,
365 const SkStrokeRec& style,
366 SkIRect shapeBounds,
367 const AtlasLocator& locator) {
368 uint32_t index = locator.pageIndex();
369 const TextureProxy* texProxy = fDrawAtlas->getProxies()[index].get();
370 if (!texProxy) {
371 return false;
372 }
373
374 // TODO: The compute renderer doesn't support perspective yet. We assume that the path has been
375 // appropriately transformed in that case.
376 SkASSERT(transform.type() != Transform::Type::kPerspective);
377
378 // Restrict the render to the occupied area of the atlas, including entry padding so that the
379 // padded row/column is cleared when Vello renders.
380 SkIPoint iPos = locator.topLeft();
381 Rect atlasBounds = Rect::XYWH(skvx::float2(iPos.x() + kEntryPadding, iPos.y() + kEntryPadding),
382 skvx::float2(shapeBounds.width(), shapeBounds.height()));
383
384 add_shape_to_scene(shape, transform, style, atlasBounds,
385 &fScenes[index], &fOccupiedAreas[index]);
386
387 return true;
388 }
389
recordDispatches(Recorder * recorder,ComputeTask::DispatchGroupList * dispatches) const390 bool VelloComputePathAtlas::VelloAtlasMgr::recordDispatches(
391 Recorder* recorder, ComputeTask::DispatchGroupList* dispatches) const {
392 SkASSERT(recorder);
393 VelloAaConfig config = get_vello_aa_config(recorder);
394
395 bool addedDispatches = false;
396 for (int i = 0; i < 4; ++i) {
397 if (!fOccupiedAreas[i].isEmpty()) {
398 std::unique_ptr<DispatchGroup> dispatchGroup =
399 render_vello_scene(recorder,
400 fDrawAtlas->getProxies()[i],
401 fScenes[i],
402 fOccupiedAreas[i],
403 config);
404 if (dispatchGroup) {
405 TRACE_EVENT_INSTANT1("skia.gpu", TRACE_FUNC, TRACE_EVENT_SCOPE_THREAD,
406 "# dispatches", dispatchGroup->dispatches().size());
407 dispatches->emplace_back(std::move(dispatchGroup));
408 addedDispatches = true;
409 } else {
410 SKGPU_LOG_E("VelloComputePathAtlas:: Failed to create dispatch group.");
411 }
412 }
413 }
414 return addedDispatches;
415 }
416
417
418 #endif // SK_ENABLE_VELLO_SHADERS
419
CreateDefault(Recorder * recorder)420 std::unique_ptr<ComputePathAtlas> ComputePathAtlas::CreateDefault(Recorder* recorder) {
421 #ifdef SK_ENABLE_VELLO_SHADERS
422 return std::make_unique<VelloComputePathAtlas>(recorder);
423 #else
424 return nullptr;
425 #endif
426 }
427
428 } // namespace skgpu::graphite
429