2 * Copyright 2021 Google LLC
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
8 #include "src/gpu/graphite/DrawPass.h"
10 #include "include/gpu/graphite/GraphiteTypes.h"
11 #include "include/gpu/graphite/Recorder.h"
12 #include "src/gpu/graphite/Buffer.h"
13 #include "src/gpu/graphite/ContextPriv.h"
14 #include "src/gpu/graphite/ContextUtils.h"
15 #include "src/gpu/graphite/DrawBufferManager.h"
16 #include "src/gpu/graphite/DrawContext.h"
17 #include "src/gpu/graphite/DrawList.h"
18 #include "src/gpu/graphite/DrawWriter.h"
19 #include "src/gpu/graphite/GlobalCache.h"
20 #include "src/gpu/graphite/GraphicsPipeline.h"
21 #include "src/gpu/graphite/GraphicsPipelineDesc.h"
22 #include "src/gpu/graphite/PipelineDataCache.h"
23 #include "src/gpu/graphite/RecorderPriv.h"
24 #include "src/gpu/graphite/Renderer.h"
25 #include "src/gpu/graphite/ResourceProvider.h"
26 #include "src/gpu/graphite/Sampler.h"
27 #include "src/gpu/graphite/Texture.h"
28 #include "src/gpu/graphite/TextureProxy.h"
29 #include "src/gpu/graphite/UniformManager.h"
30 #include "src/gpu/graphite/geom/BoundsManager.h"
32 #include "src/core/SkMathPriv.h"
33 #include "src/core/SkPaintParamsKey.h"
34 #include "src/core/SkPipelineData.h"
35 #include "src/core/SkTBlockList.h"
38 #include <unordered_map>
40 namespace skgpu::graphite {
42 // Helper to manage packed fields within a uint64_t
43 template <uint64_t Bits, uint64_t Offset>
45 static constexpr uint64_t kMask = ((uint64_t) 1 << Bits) - 1;
46 static constexpr uint64_t kOffset = Offset;
47 static constexpr uint64_t kBits = Bits;
49 static uint32_t get(uint64_t v) { return static_cast<uint32_t>((v >> kOffset) & kMask); }
50 static uint64_t set(uint32_t v) { return (v & kMask) << kOffset; }
54 * Each Draw in a DrawList might be processed by multiple RenderSteps (determined by the Draw's
55 * Renderer), which can be sorted independently. Each (step, draw) pair produces its own SortKey.
57 * The goal of sorting draws for the DrawPass is to minimize pipeline transitions and dynamic binds
58 * within a pipeline, while still respecting the overall painter's order. This decreases the number
59 * of low-level draw commands in a command buffer and increases the size of those, allowing the GPU
60 * to operate more efficiently and have fewer bubbles within its own instruction stream.
62 * The Draw's CompresssedPaintersOrder and DisjointStencilINdex represent the most significant bits
63 * of the key, and are shared by all SortKeys produced by the same draw. Next, the pipeline
64 * description is encoded in two steps:
65 * 1. The index of the RenderStep packed in the high bits to ensure each step for a draw is
67 * 2. An index into a cache of pipeline descriptions is used to encode the identity of the
68 * pipeline (SortKeys that differ in the bits from #1 necessarily would have different
69 * descriptions, but then the specific ordering of the RenderSteps isn't enforced).
70 * Last, the SortKey encodes an index into the set of uniform bindings accumulated for a DrawPass.
71 * This allows the SortKey to cluster draw steps that have both a compatible pipeline and do not
72 * require rebinding uniform data or other state (e.g. scissor). Since the uniform data index and
73 * the pipeline description index are packed into indices and not actual pointers, a given SortKey
74 * is only valid for the a specific DrawList->DrawPass conversion.
76 class DrawPass::SortKey {
78 SortKey(const DrawList::Draw* draw,
80 uint32_t pipelineIndex,
81 UniformDataCache::Index geomUniformIndex,
82 UniformDataCache::Index shadingUniformIndex,
83 TextureDataCache::Index textureDataIndex)
84 : fPipelineKey(ColorDepthOrderField::set(draw->fGeometry.order().paintOrder().bits()) |
85 StencilIndexField::set(draw->fGeometry.order().stencilIndex().bits()) |
86 RenderStepField::set(static_cast<uint32_t>(renderStep)) |
87 PipelineField::set(pipelineIndex))
88 , fUniformKey(GeometryUniformField::set(geomUniformIndex.asUInt()) |
89 ShadingUniformField::set(shadingUniformIndex.asUInt()) |
90 TextureBindingsField::set(textureDataIndex.asUInt()))
92 SkASSERT(renderStep <= draw->fRenderer.numRenderSteps());
95 bool operator<(const SortKey& k) const {
96 return fPipelineKey < k.fPipelineKey ||
97 (fPipelineKey == k.fPipelineKey && fUniformKey < k.fUniformKey);
100 const RenderStep& renderStep() const {
101 return *fDraw->fRenderer.steps()[RenderStepField::get(fPipelineKey)];
104 const DrawList::Draw* draw() const { return fDraw; }
106 uint32_t pipeline() const { return PipelineField::get(fPipelineKey); }
107 UniformDataCache::Index geometryUniforms() const {
108 return UniformDataCache::Index(GeometryUniformField::get(fUniformKey));
110 UniformDataCache::Index shadingUniforms() const {
111 return UniformDataCache::Index(ShadingUniformField::get(fUniformKey));
113 TextureDataCache::Index textureBindings() const {
114 return TextureDataCache::Index(TextureBindingsField::get(fUniformKey));
118 // Fields are ordered from most-significant to least when sorting by 128-bit value.
119 // NOTE: We don't use bit fields because field ordering is implementation defined and we need
120 // to sort consistently.
121 using ColorDepthOrderField = Bitfield<16, 48>; // sizeof(CompressedPaintersOrder)
122 using StencilIndexField = Bitfield<16, 32>; // sizeof(DisjointStencilIndex)
123 using RenderStepField = Bitfield<2, 30>; // bits >= log2(Renderer::kMaxRenderSteps)
124 using PipelineField = Bitfield<30, 0>; // bits >= log2(max steps*DrawList::kMaxDraws)
125 uint64_t fPipelineKey;
127 using GeometryUniformField = Bitfield<22, 42>; // bits >= log2(max steps * max draw count)
128 using ShadingUniformField = Bitfield<21, 21>; // ""
129 using TextureBindingsField = Bitfield<21, 0>; // ""
130 uint64_t fUniformKey;
132 // Backpointer to the draw that produced the sort key
133 const DrawList::Draw* fDraw;
135 static_assert(ColorDepthOrderField::kBits >= sizeof(CompressedPaintersOrder));
136 static_assert(StencilIndexField::kBits >= sizeof(DisjointStencilIndex));
137 static_assert(RenderStepField::kBits >= SkNextLog2_portable(Renderer::kMaxRenderSteps));
138 static_assert(PipelineField::kBits >=
139 SkNextLog2_portable(Renderer::kMaxRenderSteps * DrawList::kMaxDraws));
140 static_assert(GeometryUniformField::kBits >=
141 SkNextLog2_portable(Renderer::kMaxRenderSteps * DrawList::kMaxDraws));
142 static_assert(ShadingUniformField::kBits >=
143 SkNextLog2_portable(Renderer::kMaxRenderSteps * DrawList::kMaxDraws));
144 static_assert(TextureBindingsField::kBits >=
145 SkNextLog2_portable(Renderer::kMaxRenderSteps * DrawList::kMaxDraws));
148 class DrawPass::Drawer final : public DrawDispatcher {
150 Drawer(DrawPass* drawPass) : fPass(drawPass) {}
151 ~Drawer() override = default;
153 void bindDrawBuffers(BindBufferInfo vertexAttribs,
154 BindBufferInfo instanceAttribs,
155 BindBufferInfo indices) override {
156 fPass->fCommands.emplace_back(BindDrawBuffers{vertexAttribs, instanceAttribs, indices});
159 void draw(PrimitiveType type, unsigned int baseVertex, unsigned int vertexCount) override {
160 fPass->fCommands.emplace_back(Draw{type, baseVertex, vertexCount});
163 void drawIndexed(PrimitiveType type, unsigned int baseIndex,
164 unsigned int indexCount, unsigned int baseVertex) override {
165 fPass->fCommands.emplace_back(DrawIndexed{type, baseIndex, indexCount, baseVertex});
168 void drawInstanced(PrimitiveType type,
169 unsigned int baseVertex, unsigned int vertexCount,
170 unsigned int baseInstance, unsigned int instanceCount) override {
171 fPass->fCommands.emplace_back(DrawInstanced{type, baseVertex, vertexCount,
172 baseInstance, instanceCount});
175 void drawIndexedInstanced(PrimitiveType type,
176 unsigned int baseIndex, unsigned int indexCount,
177 unsigned int baseVertex, unsigned int baseInstance,
178 unsigned int instanceCount) override {
179 fPass->fCommands.emplace_back(DrawIndexedInstanced{type, baseIndex, indexCount, baseVertex,
180 baseInstance, instanceCount});
187 ///////////////////////////////////////////////////////////////////////////////////////////////////
191 class UniformBindingCache {
193 UniformBindingCache(DrawBufferManager* bufferMgr, UniformDataCache* uniformDataCache)
194 : fBufferMgr(bufferMgr)
195 , fUniformDataCache(uniformDataCache) {
198 UniformDataCache::Index addUniforms(UniformDataCache::Index uIndex) {
199 if (!uIndex.isValid()) {
203 const SkUniformDataBlock* udb = fUniformDataCache->lookup(uIndex);
206 if (fBindings.find(uIndex.asUInt()) == fBindings.end()) {
207 // First time encountering this data, so upload to the GPU
208 SkASSERT(udb->size());
209 auto[writer, bufferInfo] = fBufferMgr->getUniformWriter(udb->size());
210 writer.write(udb->data(), udb->size());
212 fBindings.insert({uIndex.asUInt(), bufferInfo});
218 BindBufferInfo getBinding(UniformDataCache::Index uniformDataIndex) {
219 auto lookup = fBindings.find(uniformDataIndex.asUInt());
220 SkASSERT(lookup != fBindings.end());
221 return lookup->second;
225 DrawBufferManager* fBufferMgr;
226 UniformDataCache* fUniformDataCache;
228 std::unordered_map<uint32_t, BindBufferInfo> fBindings;
231 // std::unordered_map implementation for GraphicsPipelineDesc* that de-reference the pointers.
233 size_t operator()(const GraphicsPipelineDesc* desc) const noexcept {
234 return GraphicsPipelineDesc::Hash()(*desc);
239 bool operator()(const GraphicsPipelineDesc* a,
240 const GraphicsPipelineDesc* b) const noexcept {
245 } // anonymous namespace
247 DrawPass::DrawPass(sk_sp<TextureProxy> target,
248 std::pair<LoadOp, StoreOp> ops,
249 std::array<float, 4> clearColor,
251 : fCommands(std::max(1, renderStepCount / 4), SkBlockAllocator::GrowthPolicy::kFibonacci)
252 , fTarget(std::move(target))
253 , fBounds(SkIRect::MakeEmpty())
255 , fClearColor(clearColor) {
256 // TODO: Tune this estimate and the above "itemPerBlock" value for the command buffer sequence
257 // After merging, etc. one pipeline per recorded draw+step combo is likely unnecessary.
258 fPipelineDescs.reserve(renderStepCount);
259 fCommands.reserve(renderStepCount);
262 DrawPass::~DrawPass() = default;
264 std::unique_ptr<DrawPass> DrawPass::Make(Recorder* recorder,
265 std::unique_ptr<DrawList> draws,
266 sk_sp<TextureProxy> target,
267 std::pair<LoadOp, StoreOp> ops,
268 std::array<float, 4> clearColor,
269 const BoundsManager* occlusionCuller) {
270 // NOTE: This assert is here to ensure SortKey is as tightly packed as possible. Any change to
271 // its size should be done with care and good reason. The performance of sorting the keys is
272 // heavily tied to the total size.
274 // At 24 bytes (current), sorting is about 30% slower than if SortKey could be packed into just
275 // 16 bytes. There are several ways this could be done if necessary:
276 // - Restricting the max draw count to 16k (14-bits) and only using a single index to refer to
277 // the uniform data => 8 bytes of key, 8 bytes of pointer.
278 // - Restrict the max draw count to 32k (15-bits), use a single uniform index, and steal the
279 // 4 low bits from the Draw* pointer since it's 16 byte aligned.
280 // - Compact the Draw* to an index into the original collection, although that has extra
281 // indirection and does not work as well with SkTBlockList.
282 // In pseudo tests, manipulating the pointer or having to mask out indices was about 15% slower
283 // than an 8 byte key and unmodified pointer.
284 static_assert(sizeof(DrawPass::SortKey) == 16 + sizeof(void*));
286 // The DrawList is converted directly into the DrawPass' data structures, but once the DrawPass
287 // is returned from Make(), it is considered immutable.
288 std::unique_ptr<DrawPass> drawPass(new DrawPass(std::move(target), ops, clearColor,
289 draws->renderStepCount()));
291 Rect passBounds = Rect::InfiniteInverted();
293 DrawBufferManager* bufferMgr = recorder->priv().drawBufferManager();
295 // We don't expect the uniforms from the renderSteps to reappear multiple times across a
296 // recorder's lifetime so we only de-dupe them w/in a given DrawPass.
297 UniformDataCache geometryUniformDataCache;
298 UniformBindingCache geometryUniformBindings(bufferMgr, &geometryUniformDataCache);
299 UniformBindingCache shadingUniformBindings(bufferMgr, recorder->priv().uniformDataCache());
300 TextureDataCache* textureDataCache = recorder->priv().textureDataCache();
302 std::unordered_map<const GraphicsPipelineDesc*, uint32_t, Hash, Eq> pipelineDescToIndex;
304 std::vector<SortKey> keys;
305 keys.reserve(draws->renderStepCount()); // will not exceed but may use less with occluded draws
307 SkShaderCodeDictionary* dict = recorder->priv().resourceProvider()->shaderCodeDictionary();
308 SkPaintParamsKeyBuilder builder(dict, SkBackend::kGraphite);
309 SkPipelineDataGatherer gatherer(Layout::kMetal); // TODO: get the layout from the recorder
311 for (const DrawList::Draw& draw : draws->fDraws.items()) {
312 if (occlusionCuller && occlusionCuller->isOccluded(draw.fGeometry.clip().drawBounds(),
313 draw.fGeometry.order().depth())) {
317 // If we have two different descriptors, such that the uniforms from the PaintParams can be
318 // bound independently of those used by the rest of the RenderStep, then we can upload now
319 // and remember the location for re-use on any RenderStep that does shading.
320 SkUniquePaintParamsID shaderID;
321 UniformDataCache::Index shadingUniformIndex;
322 TextureDataCache::Index textureBindingIndex;
323 if (draw.fPaintParams.has_value()) {
324 UniformDataCache::Index uniformDataIndex;
325 std::tie(shaderID, uniformDataIndex, textureBindingIndex) =
326 ExtractPaintData(recorder, &gatherer, &builder,
327 draw.fGeometry.transform().inverse(),
328 draw.fPaintParams.value());
329 shadingUniformIndex = shadingUniformBindings.addUniforms(uniformDataIndex);
332 for (int stepIndex = 0; stepIndex < draw.fRenderer.numRenderSteps(); ++stepIndex) {
333 const RenderStep* const step = draw.fRenderer.steps()[stepIndex];
334 const bool performsShading = draw.fPaintParams.has_value() && step->performsShading();
336 SkUniquePaintParamsID stepShaderID;
337 UniformDataCache::Index stepShadingUniformIndex;
338 TextureDataCache::Index stepTextureBindingIndex;
339 if (performsShading) {
340 stepShaderID = shaderID;
341 stepShadingUniformIndex = shadingUniformIndex;
342 stepTextureBindingIndex = textureBindingIndex;
343 } // else depth-only draw or stencil-only step of renderer so no shading is needed
345 UniformDataCache::Index geometryUniformIndex;
346 if (step->numUniforms() > 0) {
347 UniformDataCache::Index uniformDataIndex;
348 uniformDataIndex = ExtractRenderStepData(&geometryUniformDataCache,
352 geometryUniformIndex = geometryUniformBindings.addUniforms(uniformDataIndex);
355 GraphicsPipelineDesc desc;
356 desc.setProgram(step, stepShaderID);
357 uint32_t pipelineIndex = 0;
358 auto pipelineLookup = pipelineDescToIndex.find(&desc);
359 if (pipelineLookup == pipelineDescToIndex.end()) {
360 // Assign new index to first appearance of this pipeline description
361 pipelineIndex = SkTo<uint32_t>(drawPass->fPipelineDescs.count());
362 const GraphicsPipelineDesc& finalDesc = drawPass->fPipelineDescs.push_back(desc);
363 pipelineDescToIndex.insert({&finalDesc, pipelineIndex});
365 // Reuse the existing pipeline description for better batching after sorting
366 pipelineIndex = pipelineLookup->second;
369 keys.push_back({&draw, stepIndex, pipelineIndex,
370 geometryUniformIndex,
371 stepShadingUniformIndex,
372 stepTextureBindingIndex});
375 passBounds.join(draw.fGeometry.clip().drawBounds());
376 drawPass->fDepthStencilFlags |= draw.fRenderer.depthStencilFlags();
377 drawPass->fRequiresMSAA |= draw.fRenderer.requiresMSAA();
380 // TODO: Explore sorting algorithms; in all likelihood this will be mostly sorted already, so
381 // algorithms that approach O(n) in that condition may be favorable. Alternatively, could
382 // explore radix sort that is always O(n). Brief testing suggested std::sort was faster than
383 // std::stable_sort and SkTQSort on my [ml]'s Windows desktop. Also worth considering in-place
384 // vs. algorithms that require an extra O(n) storage.
385 // TODO: It's not strictly necessary, but would a stable sort be useful or just end up hiding
386 // bugs in the DrawOrder determination code?
387 std::sort(keys.begin(), keys.end());
389 // Used to record vertex/instance data, buffer binds, and draw calls
390 Drawer drawer(drawPass.get());
391 DrawWriter drawWriter(&drawer, bufferMgr);
393 // Used to track when a new pipeline or dynamic state needs recording between draw steps.
394 // Setting to # render steps ensures the very first time through the loop will bind a pipeline.
395 uint32_t lastPipeline = draws->renderStepCount();
396 UniformDataCache::Index lastShadingUniforms;
397 TextureDataCache::Index lastTextureBindings;
398 UniformDataCache::Index lastGeometryUniforms;
399 SkIRect lastScissor = SkIRect::MakeSize(drawPass->fTarget->dimensions());
401 for (const SortKey& key : keys) {
402 const DrawList::Draw& draw = *key.draw();
403 const RenderStep& renderStep = key.renderStep();
405 const bool geometryUniformChange = key.geometryUniforms().isValid() &&
406 key.geometryUniforms() != lastGeometryUniforms;
407 const bool shadingUniformChange = key.shadingUniforms().isValid() &&
408 key.shadingUniforms() != lastShadingUniforms;
409 const bool textureBindingsChange = key.textureBindings().isValid() &&
410 key.textureBindings() != lastTextureBindings;
412 const bool pipelineChange = key.pipeline() != lastPipeline;
413 const bool stateChange = geometryUniformChange ||
414 shadingUniformChange ||
415 textureBindingsChange ||
416 draw.fGeometry.clip().scissor() != lastScissor;
418 // Update DrawWriter *before* we actually change any state so that accumulated draws from
419 // the previous state use the proper state.
420 if (pipelineChange) {
421 drawWriter.newPipelineState(renderStep.primitiveType(),
422 renderStep.vertexStride(),
423 renderStep.instanceStride());
424 } else if (stateChange) {
425 drawWriter.newDynamicState();
428 // Make state changes before accumulating new draw data
429 if (pipelineChange) {
430 drawPass->fCommands.emplace_back(BindGraphicsPipeline{key.pipeline()});
431 lastPipeline = key.pipeline();
434 if (geometryUniformChange) {
435 auto binding = geometryUniformBindings.getBinding(key.geometryUniforms());
436 drawPass->fCommands.emplace_back(
437 BindUniformBuffer{binding, UniformSlot::kRenderStep});
438 lastGeometryUniforms = key.geometryUniforms();
440 if (shadingUniformChange) {
441 auto binding = shadingUniformBindings.getBinding(key.shadingUniforms());
442 drawPass->fCommands.emplace_back(
443 BindUniformBuffer{binding, UniformSlot::kPaint});
444 lastShadingUniforms = key.shadingUniforms();
446 if (textureBindingsChange) {
447 auto textureDataBlock = textureDataCache->lookup(key.textureBindings());
448 drawPass->fCommands.emplace_back(BindTexturesAndSamplers{textureDataBlock});
449 lastTextureBindings = key.textureBindings();
451 if (draw.fGeometry.clip().scissor() != lastScissor) {
452 drawPass->fCommands.emplace_back(SetScissor{draw.fGeometry.clip().scissor()});
453 lastScissor = draw.fGeometry.clip().scissor();
457 renderStep.writeVertices(&drawWriter, draw.fGeometry);
459 // Finish recording draw calls for any collected data at the end of the loop
462 passBounds.roundOut();
463 drawPass->fBounds = SkIRect::MakeLTRB((int) passBounds.left(), (int) passBounds.top(),
464 (int) passBounds.right(), (int) passBounds.bot());
468 bool DrawPass::addCommands(ResourceProvider* resourceProvider,
469 CommandBuffer* buffer,
470 const RenderPassDesc& renderPassDesc) const {
471 // TODO: Validate RenderPass state against DrawPass's target and requirements?
472 // Generate actual GraphicsPipeline objects combining the target-level properties and each of
473 // the GraphicsPipelineDesc's referenced in this DrawPass.
475 // Use a vector instead of SkTBlockList for the full pipelines so that random access is fast.
476 std::vector<sk_sp<GraphicsPipeline>> fullPipelines;
477 fullPipelines.reserve(fPipelineDescs.count());
478 for (const GraphicsPipelineDesc& pipelineDesc : fPipelineDescs.items()) {
479 fullPipelines.push_back(resourceProvider->findOrCreateGraphicsPipeline(pipelineDesc,
483 // Set viewport to the entire texture for now (eventually, we may have logically smaller bounds
484 // within an approx-sized texture). It is assumed that this also configures the sk_rtAdjust
485 // intrinsic for programs (however the backend chooses to do so).
486 buffer->setViewport(0, 0, fTarget->dimensions().width(), fTarget->dimensions().height());
488 for (const Command& c : fCommands.items()) {
490 case CommandType::kBindGraphicsPipeline: {
491 auto& d = c.fBindGraphicsPipeline;
492 buffer->bindGraphicsPipeline(fullPipelines[d.fPipelineIndex]);
494 case CommandType::kBindUniformBuffer: {
495 auto& d = c.fBindUniformBuffer;
496 buffer->bindUniformBuffer(d.fSlot, sk_ref_sp(d.fInfo.fBuffer), d.fInfo.fOffset);
498 case CommandType::kBindTexturesAndSamplers: {
499 auto& d = c.fBindTexturesAndSamplers;
501 for (int i = 0; i < d.fTextureBlock->numTextures(); ++i) {
502 const auto &texture = d.fTextureBlock->texture(i);
503 if (!texture.fProxy->texture()) {
507 sk_sp<Sampler> sampler = resourceProvider->findOrCreateCompatibleSampler(
508 texture.fSamplingOptions, texture.fTileModes[0], texture.fTileModes[1]);
511 buffer->bindTextureAndSampler(texture.fProxy->refTexture(),
517 case CommandType::kBindDrawBuffers: {
518 auto& d = c.fBindDrawBuffers;
519 buffer->bindDrawBuffers(d.fVertices, d.fInstances, d.fIndices);
521 case CommandType::kDraw: {
523 buffer->draw(d.fType, d.fBaseVertex, d.fVertexCount);
525 case CommandType::kDrawIndexed: {
526 auto& d = c.fDrawIndexed;
527 buffer->drawIndexed(d.fType, d.fBaseIndex, d.fIndexCount, d.fBaseVertex);
529 case CommandType::kDrawInstanced: {
530 auto& d = c.fDrawInstanced;
531 buffer->drawInstanced(d.fType, d.fBaseVertex, d.fVertexCount,
532 d.fBaseInstance, d.fInstanceCount);
534 case CommandType::kDrawIndexedInstanced: {
535 auto& d = c.fDrawIndexedInstanced;
536 buffer->drawIndexedInstanced(d.fType, d.fBaseIndex, d.fIndexCount, d.fBaseVertex,
537 d.fBaseInstance, d.fInstanceCount);
539 case CommandType::kSetScissor: {
540 auto& d = c.fSetScissor;
541 buffer->setScissor(d.fScissor.fLeft, d.fScissor.fTop,
542 d.fScissor.width(), d.fScissor.height());
551 } // namespace skgpu::graphite