1 /*
2 * Copyright 2015 Google Inc.
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/image/SkImage_Lazy.h"
9
10 #include "include/core/SkBitmap.h"
11 #include "include/core/SkData.h"
12 #include "include/core/SkImageGenerator.h"
13 #include "src/core/SkBitmapCache.h"
14 #include "src/core/SkCachedData.h"
15 #include "src/core/SkImagePriv.h"
16 #include "src/core/SkNextID.h"
17
18 #if SK_SUPPORT_GPU
19 #include "include/gpu/GrDirectContext.h"
20 #include "include/gpu/GrRecordingContext.h"
21 #include "include/private/GrResourceKey.h"
22 #include "src/core/SkResourceCache.h"
23 #include "src/core/SkYUVPlanesCache.h"
24 #include "src/gpu/GrCaps.h"
25 #include "src/gpu/GrColorSpaceXform.h"
26 #include "src/gpu/GrGpuResourcePriv.h"
27 #include "src/gpu/GrPaint.h"
28 #include "src/gpu/GrProxyProvider.h"
29 #include "src/gpu/GrRecordingContextPriv.h"
30 #include "src/gpu/GrSamplerState.h"
31 #include "src/gpu/GrYUVATextureProxies.h"
32 #include "src/gpu/SkGr.h"
33 #include "src/gpu/SurfaceFillContext.h"
34 #include "src/gpu/effects/GrYUVtoRGBEffect.h"
35 #endif
36
37 // Ref-counted tuple(SkImageGenerator, SkMutex) which allows sharing one generator among N images
38 class SharedGenerator final : public SkNVRefCnt<SharedGenerator> {
39 public:
Make(std::unique_ptr<SkImageGenerator> gen)40 static sk_sp<SharedGenerator> Make(std::unique_ptr<SkImageGenerator> gen) {
41 return gen ? sk_sp<SharedGenerator>(new SharedGenerator(std::move(gen))) : nullptr;
42 }
43
44 // This is thread safe. It is a const field set in the constructor.
getInfo()45 const SkImageInfo& getInfo() { return fGenerator->getInfo(); }
46
47 private:
SharedGenerator(std::unique_ptr<SkImageGenerator> gen)48 explicit SharedGenerator(std::unique_ptr<SkImageGenerator> gen)
49 : fGenerator(std::move(gen)) {
50 SkASSERT(fGenerator);
51 }
52
53 friend class ScopedGenerator;
54 friend class SkImage_Lazy;
55
56 std::unique_ptr<SkImageGenerator> fGenerator;
57 SkMutex fMutex;
58 };
59
60 ///////////////////////////////////////////////////////////////////////////////
61
Validator(sk_sp<SharedGenerator> gen,const SkColorType * colorType,sk_sp<SkColorSpace> colorSpace)62 SkImage_Lazy::Validator::Validator(sk_sp<SharedGenerator> gen, const SkColorType* colorType,
63 sk_sp<SkColorSpace> colorSpace)
64 : fSharedGenerator(std::move(gen)) {
65 if (!fSharedGenerator) {
66 return;
67 }
68
69 // The following generator accessors are safe without acquiring the mutex (const getters).
70 // TODO: refactor to use a ScopedGenerator instead, for clarity.
71 fInfo = fSharedGenerator->fGenerator->getInfo();
72 if (fInfo.isEmpty()) {
73 fSharedGenerator.reset();
74 return;
75 }
76
77 fUniqueID = fSharedGenerator->fGenerator->uniqueID();
78
79 if (colorType && (*colorType == fInfo.colorType())) {
80 colorType = nullptr;
81 }
82
83 if (colorType || colorSpace) {
84 if (colorType) {
85 fInfo = fInfo.makeColorType(*colorType);
86 }
87 if (colorSpace) {
88 fInfo = fInfo.makeColorSpace(colorSpace);
89 }
90 fUniqueID = SkNextID::ImageID();
91 }
92 }
93
94 ///////////////////////////////////////////////////////////////////////////////
95
96 // Helper for exclusive access to a shared generator.
97 class SkImage_Lazy::ScopedGenerator {
98 public:
ScopedGenerator(const sk_sp<SharedGenerator> & gen)99 ScopedGenerator(const sk_sp<SharedGenerator>& gen)
100 : fSharedGenerator(gen)
101 , fAutoAquire(gen->fMutex) {}
102
operator ->() const103 SkImageGenerator* operator->() const {
104 fSharedGenerator->fMutex.assertHeld();
105 return fSharedGenerator->fGenerator.get();
106 }
107
operator SkImageGenerator*() const108 operator SkImageGenerator*() const {
109 fSharedGenerator->fMutex.assertHeld();
110 return fSharedGenerator->fGenerator.get();
111 }
112
113 private:
114 const sk_sp<SharedGenerator>& fSharedGenerator;
115 SkAutoMutexExclusive fAutoAquire;
116 };
117
118 ///////////////////////////////////////////////////////////////////////////////
119
SkImage_Lazy(Validator * validator)120 SkImage_Lazy::SkImage_Lazy(Validator* validator)
121 : INHERITED(validator->fInfo, validator->fUniqueID)
122 , fSharedGenerator(std::move(validator->fSharedGenerator))
123 {
124 SkASSERT(fSharedGenerator);
125 }
126
127
128 //////////////////////////////////////////////////////////////////////////////////////////////////
129
getROPixels(GrDirectContext *,SkBitmap * bitmap,SkImage::CachingHint chint) const130 bool SkImage_Lazy::getROPixels(GrDirectContext*, SkBitmap* bitmap,
131 SkImage::CachingHint chint) const {
132 auto check_output_bitmap = [bitmap]() {
133 SkASSERT(bitmap->isImmutable());
134 SkASSERT(bitmap->getPixels());
135 (void)bitmap;
136 };
137
138 auto desc = SkBitmapCacheDesc::Make(this);
139 if (SkBitmapCache::Find(desc, bitmap)) {
140 check_output_bitmap();
141 return true;
142 }
143
144 if (SkImage::kAllow_CachingHint == chint) {
145 SkPixmap pmap;
146 SkBitmapCache::RecPtr cacheRec = SkBitmapCache::Alloc(desc, this->imageInfo(), &pmap);
147 if (!cacheRec || !ScopedGenerator(fSharedGenerator)->getPixels(pmap)) {
148 return false;
149 }
150 SkBitmapCache::Add(std::move(cacheRec), bitmap);
151 this->notifyAddedToRasterCache();
152 } else {
153 if (!bitmap->tryAllocPixels(this->imageInfo()) ||
154 !ScopedGenerator(fSharedGenerator)->getPixels(bitmap->pixmap())) {
155 return false;
156 }
157 bitmap->setImmutable();
158 }
159
160 check_output_bitmap();
161 return true;
162 }
163
164 //////////////////////////////////////////////////////////////////////////////////////////////////
165
onReadPixels(GrDirectContext * dContext,const SkImageInfo & dstInfo,void * dstPixels,size_t dstRB,int srcX,int srcY,CachingHint chint) const166 bool SkImage_Lazy::onReadPixels(GrDirectContext* dContext,
167 const SkImageInfo& dstInfo,
168 void* dstPixels,
169 size_t dstRB,
170 int srcX,
171 int srcY,
172 CachingHint chint) const {
173 SkBitmap bm;
174 if (this->getROPixels(dContext, &bm, chint)) {
175 return bm.readPixels(dstInfo, dstPixels, dstRB, srcX, srcY);
176 }
177 return false;
178 }
179
onRefEncoded() const180 sk_sp<SkData> SkImage_Lazy::onRefEncoded() const {
181 // check that we aren't a subset or colortype/etc modification of the original
182 if (fSharedGenerator->fGenerator->uniqueID() == this->uniqueID()) {
183 ScopedGenerator generator(fSharedGenerator);
184 return generator->refEncodedData();
185 }
186 return nullptr;
187 }
188
onIsValid(GrRecordingContext * context) const189 bool SkImage_Lazy::onIsValid(GrRecordingContext* context) const {
190 ScopedGenerator generator(fSharedGenerator);
191 return generator->isValid(context);
192 }
193
194 ///////////////////////////////////////////////////////////////////////////////////////////////////
195
onMakeSubset(const SkIRect & subset,GrDirectContext * direct) const196 sk_sp<SkImage> SkImage_Lazy::onMakeSubset(const SkIRect& subset, GrDirectContext* direct) const {
197 // TODO: can we do this more efficiently, by telling the generator we want to
198 // "realize" a subset?
199
200 #if SK_SUPPORT_GPU
201 auto pixels = direct ? this->makeTextureImage(direct)
202 : this->makeRasterImage();
203 #else
204 auto pixels = this->makeRasterImage();
205 #endif
206 return pixels ? pixels->makeSubset(subset, direct) : nullptr;
207 }
208
onMakeColorTypeAndColorSpace(SkColorType targetCT,sk_sp<SkColorSpace> targetCS,GrDirectContext *) const209 sk_sp<SkImage> SkImage_Lazy::onMakeColorTypeAndColorSpace(SkColorType targetCT,
210 sk_sp<SkColorSpace> targetCS,
211 GrDirectContext*) const {
212 SkAutoMutexExclusive autoAquire(fOnMakeColorTypeAndSpaceMutex);
213 if (fOnMakeColorTypeAndSpaceResult &&
214 targetCT == fOnMakeColorTypeAndSpaceResult->colorType() &&
215 SkColorSpace::Equals(targetCS.get(), fOnMakeColorTypeAndSpaceResult->colorSpace())) {
216 return fOnMakeColorTypeAndSpaceResult;
217 }
218 Validator validator(fSharedGenerator, &targetCT, targetCS);
219 sk_sp<SkImage> result = validator ? sk_sp<SkImage>(new SkImage_Lazy(&validator)) : nullptr;
220 if (result) {
221 fOnMakeColorTypeAndSpaceResult = result;
222 }
223 return result;
224 }
225
onReinterpretColorSpace(sk_sp<SkColorSpace> newCS) const226 sk_sp<SkImage> SkImage_Lazy::onReinterpretColorSpace(sk_sp<SkColorSpace> newCS) const {
227 // TODO: The correct thing is to clone the generator, and modify its color space. That's hard,
228 // because we don't have a clone method, and generator is public (and derived-from by clients).
229 // So do the simple/inefficient thing here, and fallback to raster when this is called.
230
231 // We allocate the bitmap with the new color space, then generate the image using the original.
232 SkBitmap bitmap;
233 if (bitmap.tryAllocPixels(this->imageInfo().makeColorSpace(std::move(newCS)))) {
234 SkPixmap pixmap = bitmap.pixmap();
235 pixmap.setColorSpace(this->refColorSpace());
236 if (ScopedGenerator(fSharedGenerator)->getPixels(pixmap)) {
237 bitmap.setImmutable();
238 return bitmap.asImage();
239 }
240 }
241 return nullptr;
242 }
243
MakeFromGenerator(std::unique_ptr<SkImageGenerator> generator)244 sk_sp<SkImage> SkImage::MakeFromGenerator(std::unique_ptr<SkImageGenerator> generator) {
245 SkImage_Lazy::Validator
246 validator(SharedGenerator::Make(std::move(generator)), nullptr, nullptr);
247
248 return validator ? sk_make_sp<SkImage_Lazy>(&validator) : nullptr;
249 }
250
251 #if SK_SUPPORT_GPU
252
onAsView(GrRecordingContext * context,GrMipmapped mipmapped,GrImageTexGenPolicy policy) const253 std::tuple<GrSurfaceProxyView, GrColorType> SkImage_Lazy::onAsView(
254 GrRecordingContext* context,
255 GrMipmapped mipmapped,
256 GrImageTexGenPolicy policy) const {
257 GrColorType ct = this->colorTypeOfLockTextureProxy(context->priv().caps());
258 return {this->lockTextureProxyView(context, policy, mipmapped), ct};
259 }
260
onAsFragmentProcessor(GrRecordingContext * rContext,SkSamplingOptions sampling,const SkTileMode tileModes[2],const SkMatrix & m,const SkRect * subset,const SkRect * domain) const261 std::unique_ptr<GrFragmentProcessor> SkImage_Lazy::onAsFragmentProcessor(
262 GrRecordingContext* rContext,
263 SkSamplingOptions sampling,
264 const SkTileMode tileModes[2],
265 const SkMatrix& m,
266 const SkRect* subset,
267 const SkRect* domain) const {
268 // TODO: If the CPU data is extracted as planes return a FP that reconstructs the image from
269 // the planes.
270 auto mm = sampling.mipmap == SkMipmapMode::kNone ? GrMipmapped::kNo : GrMipmapped::kYes;
271 return MakeFragmentProcessorFromView(rContext,
272 std::get<0>(this->asView(rContext, mm)),
273 this->alphaType(),
274 sampling,
275 tileModes,
276 m,
277 subset,
278 domain);
279 }
280
textureProxyViewFromPlanes(GrRecordingContext * ctx,SkBudgeted budgeted) const281 GrSurfaceProxyView SkImage_Lazy::textureProxyViewFromPlanes(GrRecordingContext* ctx,
282 SkBudgeted budgeted) const {
283 SkYUVAPixmapInfo::SupportedDataTypes supportedDataTypes(*ctx);
284 SkYUVAPixmaps yuvaPixmaps;
285 sk_sp<SkCachedData> dataStorage = this->getPlanes(supportedDataTypes, &yuvaPixmaps);
286 if (!dataStorage) {
287 return {};
288 }
289
290 GrSurfaceProxyView views[SkYUVAInfo::kMaxPlanes];
291 GrColorType pixmapColorTypes[SkYUVAInfo::kMaxPlanes];
292 for (int i = 0; i < yuvaPixmaps.numPlanes(); ++i) {
293 // If the sizes of the components are not all the same we choose to create exact-match
294 // textures for the smaller ones rather than add a texture domain to the draw.
295 // TODO: revisit this decision to improve texture reuse?
296 SkBackingFit fit = yuvaPixmaps.plane(i).dimensions() == this->dimensions()
297 ? SkBackingFit::kApprox
298 : SkBackingFit::kExact;
299
300 // We grab a ref to cached yuv data. When the SkBitmap we create below goes away it will
301 // call releaseProc which will release this ref.
302 // DDL TODO: Currently we end up creating a lazy proxy that will hold onto a ref to the
303 // SkImage in its lambda. This means that we'll keep the ref on the YUV data around for the
304 // life time of the proxy and not just upload. For non-DDL draws we should look into
305 // releasing this SkImage after uploads (by deleting the lambda after instantiation).
306 auto releaseProc = [](void*, void* data) {
307 auto cachedData = static_cast<SkCachedData*>(data);
308 SkASSERT(cachedData);
309 cachedData->unref();
310 };
311 SkBitmap bitmap;
312 bitmap.installPixels(yuvaPixmaps.plane(i).info(),
313 yuvaPixmaps.plane(i).writable_addr(),
314 yuvaPixmaps.plane(i).rowBytes(),
315 releaseProc,
316 SkRef(dataStorage.get()));
317 bitmap.setImmutable();
318
319 std::tie(views[i], std::ignore) = GrMakeUncachedBitmapProxyView(ctx,
320 bitmap,
321 GrMipmapped::kNo,
322 fit);
323 if (!views[i]) {
324 return {};
325 }
326 pixmapColorTypes[i] = SkColorTypeToGrColorType(bitmap.colorType());
327 }
328
329 // TODO: investigate preallocating mip maps here
330 GrImageInfo info(SkColorTypeToGrColorType(this->colorType()),
331 kPremul_SkAlphaType,
332 /*color space*/ nullptr,
333 this->dimensions());
334
335 auto sfc = ctx->priv().makeSFC(info,
336 SkBackingFit::kExact,
337 1,
338 GrMipmapped::kNo,
339 GrProtected::kNo,
340 kTopLeft_GrSurfaceOrigin,
341 budgeted);
342 if (!sfc) {
343 return {};
344 }
345
346 GrYUVATextureProxies yuvaProxies(yuvaPixmaps.yuvaInfo(), views, pixmapColorTypes);
347 SkAssertResult(yuvaProxies.isValid());
348
349 std::unique_ptr<GrFragmentProcessor> fp = GrYUVtoRGBEffect::Make(
350 yuvaProxies,
351 GrSamplerState::Filter::kNearest,
352 *ctx->priv().caps());
353
354 // The pixels after yuv->rgb will be in the generator's color space.
355 // If onMakeColorTypeAndColorSpace has been called then this will not match this image's
356 // color space. To correct this, apply a color space conversion from the generator's color
357 // space to this image's color space.
358 SkColorSpace* srcColorSpace;
359 {
360 ScopedGenerator generator(fSharedGenerator);
361 srcColorSpace = generator->getInfo().colorSpace();
362 }
363 SkColorSpace* dstColorSpace = this->colorSpace();
364
365 // If the caller expects the pixels in a different color space than the one from the image,
366 // apply a color conversion to do this.
367 fp = GrColorSpaceXformEffect::Make(std::move(fp),
368 srcColorSpace, kOpaque_SkAlphaType,
369 dstColorSpace, kOpaque_SkAlphaType);
370 sfc->fillWithFP(std::move(fp));
371
372 return sfc->readSurfaceView();
373 }
374
getPlanes(const SkYUVAPixmapInfo::SupportedDataTypes & supportedDataTypes,SkYUVAPixmaps * yuvaPixmaps) const375 sk_sp<SkCachedData> SkImage_Lazy::getPlanes(
376 const SkYUVAPixmapInfo::SupportedDataTypes& supportedDataTypes,
377 SkYUVAPixmaps* yuvaPixmaps) const {
378 ScopedGenerator generator(fSharedGenerator);
379
380 sk_sp<SkCachedData> data(SkYUVPlanesCache::FindAndRef(generator->uniqueID(), yuvaPixmaps));
381
382 if (data) {
383 SkASSERT(yuvaPixmaps->isValid());
384 SkASSERT(yuvaPixmaps->yuvaInfo().dimensions() == this->dimensions());
385 return data;
386 }
387 SkYUVAPixmapInfo yuvaPixmapInfo;
388 if (!generator->queryYUVAInfo(supportedDataTypes, &yuvaPixmapInfo) ||
389 yuvaPixmapInfo.yuvaInfo().dimensions() != this->dimensions()) {
390 return nullptr;
391 }
392 data.reset(SkResourceCache::NewCachedData(yuvaPixmapInfo.computeTotalBytes()));
393 SkYUVAPixmaps tempPixmaps = SkYUVAPixmaps::FromExternalMemory(yuvaPixmapInfo,
394 data->writable_data());
395 SkASSERT(tempPixmaps.isValid());
396 if (!generator->getYUVAPlanes(tempPixmaps)) {
397 return nullptr;
398 }
399 // Decoding is done, cache the resulting YUV planes
400 *yuvaPixmaps = tempPixmaps;
401 SkYUVPlanesCache::Add(this->uniqueID(), data.get(), *yuvaPixmaps);
402 return data;
403 }
404
405 /*
406 * We have 4 ways to try to return a texture (in sorted order)
407 *
408 * 1. Check the cache for a pre-existing one
409 * 2. Ask the generator to natively create one
410 * 3. Ask the generator to return YUV planes, which the GPU can convert
411 * 4. Ask the generator to return RGB(A) data, which the GPU can convert
412 */
lockTextureProxyView(GrRecordingContext * rContext,GrImageTexGenPolicy texGenPolicy,GrMipmapped mipmapped) const413 GrSurfaceProxyView SkImage_Lazy::lockTextureProxyView(GrRecordingContext* rContext,
414 GrImageTexGenPolicy texGenPolicy,
415 GrMipmapped mipmapped) const {
416 // Values representing the various texture lock paths we can take. Used for logging the path
417 // taken to a histogram.
418 enum LockTexturePath {
419 kFailure_LockTexturePath,
420 kPreExisting_LockTexturePath,
421 kNative_LockTexturePath,
422 kCompressed_LockTexturePath, // Deprecated
423 kYUV_LockTexturePath,
424 kRGBA_LockTexturePath,
425 };
426
427 enum { kLockTexturePathCount = kRGBA_LockTexturePath + 1 };
428
429 GrUniqueKey key;
430 if (texGenPolicy == GrImageTexGenPolicy::kDraw) {
431 GrMakeKeyFromImageID(&key, this->uniqueID(), SkIRect::MakeSize(this->dimensions()));
432 }
433
434 const GrCaps* caps = rContext->priv().caps();
435 GrProxyProvider* proxyProvider = rContext->priv().proxyProvider();
436
437 auto installKey = [&](const GrSurfaceProxyView& view) {
438 SkASSERT(view && view.asTextureProxy());
439 if (key.isValid()) {
440 auto listener = GrMakeUniqueKeyInvalidationListener(&key, rContext->priv().contextID());
441 this->addUniqueIDListener(std::move(listener));
442 proxyProvider->assignUniqueKeyToProxy(key, view.asTextureProxy());
443 }
444 };
445
446 auto ct = this->colorTypeOfLockTextureProxy(caps);
447
448 // 1. Check the cache for a pre-existing one.
449 if (key.isValid()) {
450 auto proxy = proxyProvider->findOrCreateProxyByUniqueKey(key);
451 if (proxy) {
452 GrSwizzle swizzle = caps->getReadSwizzle(proxy->backendFormat(), ct);
453 GrSurfaceProxyView view(std::move(proxy), kTopLeft_GrSurfaceOrigin, swizzle);
454 if (mipmapped == GrMipmapped::kNo ||
455 view.asTextureProxy()->mipmapped() == GrMipmapped::kYes) {
456 return view;
457 } else {
458 // We need a mipped proxy, but we found a cached proxy that wasn't mipped. Thus we
459 // generate a new mipped surface and copy the original proxy into the base layer. We
460 // will then let the gpu generate the rest of the mips.
461 auto mippedView = GrCopyBaseMipMapToView(rContext, view);
462 if (!mippedView) {
463 // We failed to make a mipped proxy with the base copied into it. This could
464 // have been from failure to make the proxy or failure to do the copy. Thus we
465 // will fall back to just using the non mipped proxy; See skbug.com/7094.
466 return view;
467 }
468 proxyProvider->removeUniqueKeyFromProxy(view.asTextureProxy());
469 installKey(mippedView);
470 return mippedView;
471 }
472 }
473 }
474
475 // 2. Ask the generator to natively create one.
476 {
477 ScopedGenerator generator(fSharedGenerator);
478 if (auto view = generator->generateTexture(rContext,
479 this->imageInfo(),
480 {0,0},
481 mipmapped,
482 texGenPolicy)) {
483 installKey(view);
484 return view;
485 }
486 }
487
488 // 3. Ask the generator to return YUV planes, which the GPU can convert. If we will be mipping
489 // the texture we skip this step so the CPU generate non-planar MIP maps for us.
490 if (mipmapped == GrMipmapped::kNo && !rContext->priv().options().fDisableGpuYUVConversion) {
491 // TODO: Update to create the mipped surface in the textureProxyViewFromPlanes generator and
492 // draw the base layer directly into the mipped surface.
493 SkBudgeted budgeted = texGenPolicy == GrImageTexGenPolicy::kNew_Uncached_Unbudgeted
494 ? SkBudgeted::kNo
495 : SkBudgeted::kYes;
496 auto view = this->textureProxyViewFromPlanes(rContext, budgeted);
497 if (view) {
498 installKey(view);
499 return view;
500 }
501 }
502
503 // 4. Ask the generator to return a bitmap, which the GPU can convert.
504 auto hint = texGenPolicy == GrImageTexGenPolicy::kDraw ? CachingHint::kAllow_CachingHint
505 : CachingHint::kDisallow_CachingHint;
506 if (SkBitmap bitmap; this->getROPixels(nullptr, &bitmap, hint)) {
507 // We always make an uncached bitmap here because we will cache it based on passed in policy
508 // with *our* key, not a key derived from bitmap. We're just making the proxy here.
509 auto budgeted = texGenPolicy == GrImageTexGenPolicy::kNew_Uncached_Unbudgeted
510 ? SkBudgeted::kNo
511 : SkBudgeted::kYes;
512 auto view = std::get<0>(GrMakeUncachedBitmapProxyView(rContext,
513 bitmap,
514 mipmapped,
515 SkBackingFit::kExact,
516 budgeted));
517 if (view) {
518 installKey(view);
519 return view;
520 }
521 }
522
523 return {};
524 }
525
colorTypeOfLockTextureProxy(const GrCaps * caps) const526 GrColorType SkImage_Lazy::colorTypeOfLockTextureProxy(const GrCaps* caps) const {
527 GrColorType ct = SkColorTypeToGrColorType(this->colorType());
528 GrBackendFormat format = caps->getDefaultBackendFormat(ct, GrRenderable::kNo);
529 if (!format.isValid()) {
530 ct = GrColorType::kRGBA_8888;
531 }
532 return ct;
533 }
534
addUniqueIDListener(sk_sp<SkIDChangeListener> listener) const535 void SkImage_Lazy::addUniqueIDListener(sk_sp<SkIDChangeListener> listener) const {
536 fUniqueIDListeners.add(std::move(listener));
537 }
538 #endif
539