1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ppapi/proxy/ppb_image_data_proxy.h"
6
7#include <string.h>  // For memcpy
8
9#include <map>
10#include <vector>
11
12#include "base/logging.h"
13#include "base/memory/singleton.h"
14#include "base/memory/weak_ptr.h"
15#include "build/build_config.h"
16#include "ppapi/c/pp_completion_callback.h"
17#include "ppapi/c/pp_errors.h"
18#include "ppapi/c/pp_resource.h"
19#include "ppapi/proxy/enter_proxy.h"
20#include "ppapi/proxy/host_dispatcher.h"
21#include "ppapi/proxy/plugin_dispatcher.h"
22#include "ppapi/proxy/plugin_globals.h"
23#include "ppapi/proxy/plugin_resource_tracker.h"
24#include "ppapi/proxy/ppapi_messages.h"
25#include "ppapi/shared_impl/host_resource.h"
26#include "ppapi/shared_impl/proxy_lock.h"
27#include "ppapi/shared_impl/resource.h"
28#include "ppapi/shared_impl/scoped_pp_resource.h"
29#include "ppapi/thunk/enter.h"
30#include "ppapi/thunk/thunk.h"
31
32#if !defined(OS_NACL)
33#include "skia/ext/platform_canvas.h"
34#include "ui/surface/transport_dib.h"
35#endif
36
37using ppapi::thunk::PPB_ImageData_API;
38
39namespace ppapi {
40namespace proxy {
41
42namespace {
43
44// How ImageData re-use works
45// --------------------------
46//
47// When animating plugins (like video), re-creating image datas for each frame
48// and mapping the memory has a high overhead. So we try to re-use these when
49// possible.
50//
51// 1. Plugin makes an asynchronous call that transfers an ImageData to the
52//    implementation of some API.
53// 2. Plugin frees its ImageData reference. If it doesn't do this we can't
54//    re-use it.
55// 3. When the last plugin ref of an ImageData is released, we don't actually
56//    delete it. Instead we put it on a queue where we hold onto it in the
57//    plugin process for a short period of time.
58// 4. The API implementation that received the ImageData finishes using it.
59//    Without our caching system it would get deleted at this point.
60// 5. The proxy in the renderer will send NotifyUnusedImageData back to the
61//    plugin process. We check if the given resource is in the queue and mark
62//    it as usable.
63// 6. When the plugin requests a new image data, we check our queue and if there
64//    is a usable ImageData of the right size and format, we'll return it
65//    instead of making a new one. It's important that caching is only requested
66//    when the size is unlikely to change, so cache hits are high.
67//
68// Some notes:
69//
70//  - We only re-use image data when the plugin and host are rapidly exchanging
71//    them and the size is likely to remain constant. It should be clear that
72//    the plugin is promising that it's done with the image.
73//
74//  - Theoretically we could re-use them in other cases but the lifetime
75//    becomes more difficult to manage. The plugin could have used an ImageData
76//    in an arbitrary number of queued up PaintImageData calls which we would
77//    have to check.
78//
79//  - If a flush takes a long time or there are many released image datas
80//    accumulating in our queue such that some are deleted, we will have
81//    released our reference by the time the renderer notifies us of an unused
82//    image data. In this case we just give up.
83//
84//  - We maintain a per-instance cache. Some pages have many instances of
85//    Flash, for example, each of a different size. If they're all animating we
86//    want each to get its own image data re-use.
87//
88//  - We generate new resource IDs when re-use happens to try to avoid weird
89//    problems if the plugin messes up its refcounting.
90
91// Keep a cache entry for this many seconds before expiring it. We get an entry
92// back from the renderer after an ImageData is swapped out, so it means the
93// plugin has to be painting at least two frames for this time interval to
94// get caching.
95static const int kMaxAgeSeconds = 2;
96
97// ImageDataCacheEntry ---------------------------------------------------------
98
99struct ImageDataCacheEntry {
100  ImageDataCacheEntry() : added_time(), usable(false), image() {}
101  ImageDataCacheEntry(ImageData* i)
102      : added_time(base::TimeTicks::Now()),
103        usable(false),
104        image(i) {
105  }
106
107  base::TimeTicks added_time;
108
109  // Set to true when the renderer tells us that it's OK to re-use this iamge.
110  bool usable;
111
112  scoped_refptr<ImageData> image;
113};
114
115// ImageDataInstanceCache ------------------------------------------------------
116
117// Per-instance cache of image datas.
118class ImageDataInstanceCache {
119 public:
120  ImageDataInstanceCache() : next_insertion_point_(0) {}
121
122  // These functions have the same spec as the ones in ImageDataCache.
123  scoped_refptr<ImageData> Get(PPB_ImageData_Shared::ImageDataType type,
124                               int width, int height,
125                               PP_ImageDataFormat format);
126  void Add(ImageData* image_data);
127  void ImageDataUsable(ImageData* image_data);
128
129  // Expires old entries. Returns true if there are still entries in the list,
130  // false if this instance cache is now empty.
131  bool ExpireEntries();
132
133 private:
134  void IncrementInsertionPoint();
135
136  // We'll store this many ImageDatas per instance.
137  const static int kCacheSize = 2;
138
139  ImageDataCacheEntry images_[kCacheSize];
140
141  // Index into cache where the next item will go.
142  int next_insertion_point_;
143};
144
145scoped_refptr<ImageData> ImageDataInstanceCache::Get(
146    PPB_ImageData_Shared::ImageDataType type,
147    int width, int height,
148    PP_ImageDataFormat format) {
149  // Just do a brute-force search since the cache is so small.
150  for (int i = 0; i < kCacheSize; i++) {
151    if (!images_[i].usable)
152      continue;
153    if (images_[i].image->type() != type)
154      continue;
155    const PP_ImageDataDesc& desc = images_[i].image->desc();
156    if (desc.format == format &&
157        desc.size.width == width && desc.size.height == height) {
158      scoped_refptr<ImageData> ret(images_[i].image);
159      images_[i] = ImageDataCacheEntry();
160
161      // Since we just removed an item, this entry is the best place to insert
162      // a subsequent one.
163      next_insertion_point_ = i;
164      return ret;
165    }
166  }
167  return scoped_refptr<ImageData>();
168}
169
170void ImageDataInstanceCache::Add(ImageData* image_data) {
171  images_[next_insertion_point_] = ImageDataCacheEntry(image_data);
172  IncrementInsertionPoint();
173}
174
175void ImageDataInstanceCache::ImageDataUsable(ImageData* image_data) {
176  for (int i = 0; i < kCacheSize; i++) {
177    if (images_[i].image.get() == image_data) {
178      images_[i].usable = true;
179
180      // This test is important. The renderer doesn't guarantee how many image
181      // datas it has or when it notifies us when one is usable. Its possible
182      // to get into situations where it's always telling us the old one is
183      // usable, and then the older one immediately gets expired. Therefore,
184      // if the next insertion would overwrite this now-usable entry, make the
185      // next insertion overwrite some other entry to avoid the replacement.
186      if (next_insertion_point_ == i)
187        IncrementInsertionPoint();
188      return;
189    }
190  }
191}
192
193bool ImageDataInstanceCache::ExpireEntries() {
194  base::TimeTicks threshold_time =
195      base::TimeTicks::Now() - base::TimeDelta::FromSeconds(kMaxAgeSeconds);
196
197  bool has_entry = false;
198  for (int i = 0; i < kCacheSize; i++) {
199    if (images_[i].image.get()) {
200      // Entry present.
201      if (images_[i].added_time <= threshold_time) {
202        // Found an entry to expire.
203        images_[i] = ImageDataCacheEntry();
204        next_insertion_point_ = i;
205      } else {
206        // Found an entry that we're keeping.
207        has_entry = true;
208      }
209    }
210  }
211  return has_entry;
212}
213
214void ImageDataInstanceCache::IncrementInsertionPoint() {
215  // Go to the next location, wrapping around to get LRU.
216  next_insertion_point_++;
217  if (next_insertion_point_ >= kCacheSize)
218    next_insertion_point_ = 0;
219}
220
221// ImageDataCache --------------------------------------------------------------
222
223class ImageDataCache {
224 public:
225  ImageDataCache() : weak_factory_(this) {}
226  ~ImageDataCache() {}
227
228  static ImageDataCache* GetInstance();
229
230  // Retrieves an image data from the cache of the specified type, size and
231  // format if one exists. If one doesn't exist, this will return a null refptr.
232  scoped_refptr<ImageData> Get(PP_Instance instance,
233                               PPB_ImageData_Shared::ImageDataType type,
234                               int width, int height,
235                               PP_ImageDataFormat format);
236
237  // Adds the given image data to the cache. There should be no plugin
238  // references to it. This may delete an older item from the cache.
239  void Add(ImageData* image_data);
240
241  // Notification from the renderer that the given image data is usable.
242  void ImageDataUsable(ImageData* image_data);
243
244  void DidDeleteInstance(PP_Instance instance);
245
246 private:
247  friend struct LeakySingletonTraits<ImageDataCache>;
248
249  // Timer callback to expire entries for the given instance.
250  void OnTimer(PP_Instance instance);
251
252  typedef std::map<PP_Instance, ImageDataInstanceCache> CacheMap;
253  CacheMap cache_;
254
255  // This class does timer calls and we don't want to run these outside of the
256  // scope of the object. Technically, since this class is a leaked static,
257  // this will never happen and this factory is unnecessary. However, it's
258  // probably better not to make assumptions about the lifetime of this class.
259  base::WeakPtrFactory<ImageDataCache> weak_factory_;
260
261  DISALLOW_COPY_AND_ASSIGN(ImageDataCache);
262};
263
264// static
265ImageDataCache* ImageDataCache::GetInstance() {
266  return Singleton<ImageDataCache,
267                   LeakySingletonTraits<ImageDataCache> >::get();
268}
269
270scoped_refptr<ImageData> ImageDataCache::Get(
271    PP_Instance instance,
272    PPB_ImageData_Shared::ImageDataType type,
273    int width, int height,
274    PP_ImageDataFormat format) {
275  CacheMap::iterator found = cache_.find(instance);
276  if (found == cache_.end())
277    return scoped_refptr<ImageData>();
278  return found->second.Get(type, width, height, format);
279}
280
281void ImageDataCache::Add(ImageData* image_data) {
282  cache_[image_data->pp_instance()].Add(image_data);
283
284  // Schedule a timer to invalidate this entry.
285  base::MessageLoop::current()->PostDelayedTask(
286      FROM_HERE,
287      RunWhileLocked(base::Bind(&ImageDataCache::OnTimer,
288                                weak_factory_.GetWeakPtr(),
289                                image_data->pp_instance())),
290      base::TimeDelta::FromSeconds(kMaxAgeSeconds));
291}
292
293void ImageDataCache::ImageDataUsable(ImageData* image_data) {
294  CacheMap::iterator found = cache_.find(image_data->pp_instance());
295  if (found != cache_.end())
296    found->second.ImageDataUsable(image_data);
297}
298
299void ImageDataCache::DidDeleteInstance(PP_Instance instance) {
300  cache_.erase(instance);
301}
302
303void ImageDataCache::OnTimer(PP_Instance instance) {
304  CacheMap::iterator found = cache_.find(instance);
305  if (found == cache_.end())
306    return;
307  if (!found->second.ExpireEntries()) {
308    // There are no more entries for this instance, remove it from the cache.
309    cache_.erase(found);
310  }
311}
312
313}  // namespace
314
315// ImageData -------------------------------------------------------------------
316
317ImageData::ImageData(const HostResource& resource,
318                     PPB_ImageData_Shared::ImageDataType type,
319                     const PP_ImageDataDesc& desc)
320    : Resource(OBJECT_IS_PROXY, resource),
321      type_(type),
322      desc_(desc),
323      is_candidate_for_reuse_(false) {
324}
325
326ImageData::~ImageData() {
327}
328
329PPB_ImageData_API* ImageData::AsPPB_ImageData_API() {
330  return this;
331}
332
333void ImageData::LastPluginRefWasDeleted() {
334  // The plugin no longer needs this ImageData, add it to our cache if it's
335  // been used in a ReplaceContents. These are the ImageDatas that the renderer
336  // will send back ImageDataUsable messages for.
337  if (is_candidate_for_reuse_)
338    ImageDataCache::GetInstance()->Add(this);
339}
340
341void ImageData::InstanceWasDeleted() {
342  ImageDataCache::GetInstance()->DidDeleteInstance(pp_instance());
343}
344
345PP_Bool ImageData::Describe(PP_ImageDataDesc* desc) {
346  memcpy(desc, &desc_, sizeof(PP_ImageDataDesc));
347  return PP_TRUE;
348}
349
350int32_t ImageData::GetSharedMemory(int* /* handle */,
351                                   uint32_t* /* byte_count */) {
352  // Not supported in the proxy (this method is for actually implementing the
353  // proxy in the host).
354  return PP_ERROR_NOACCESS;
355}
356
357void ImageData::SetIsCandidateForReuse() {
358  is_candidate_for_reuse_ = true;
359}
360
361void ImageData::RecycleToPlugin(bool zero_contents) {
362  is_candidate_for_reuse_ = false;
363  if (zero_contents) {
364    void* data = Map();
365    memset(data, 0, desc_.stride * desc_.size.height);
366    Unmap();
367  }
368}
369
370// PlatformImageData -----------------------------------------------------------
371
372#if !defined(OS_NACL)
373PlatformImageData::PlatformImageData(const HostResource& resource,
374                                     const PP_ImageDataDesc& desc,
375                                     ImageHandle handle)
376    : ImageData(resource, PPB_ImageData_Shared::PLATFORM, desc) {
377#if defined(OS_WIN)
378  transport_dib_.reset(TransportDIB::CreateWithHandle(handle));
379#else
380  transport_dib_.reset(TransportDIB::Map(handle));
381#endif  // defined(OS_WIN)
382}
383
384PlatformImageData::~PlatformImageData() {
385}
386
387void* PlatformImageData::Map() {
388  if (!mapped_canvas_.get()) {
389    mapped_canvas_.reset(transport_dib_->GetPlatformCanvas(desc_.size.width,
390                                                           desc_.size.height));
391    if (!mapped_canvas_.get())
392      return NULL;
393  }
394  const SkBitmap& bitmap =
395      skia::GetTopDevice(*mapped_canvas_)->accessBitmap(true);
396
397  bitmap.lockPixels();
398  return bitmap.getAddr(0, 0);
399}
400
401void PlatformImageData::Unmap() {
402  // TODO(brettw) have a way to unmap a TransportDIB. Currently this isn't
403  // possible since deleting the TransportDIB also frees all the handles.
404  // We need to add a method to TransportDIB to release the handles.
405}
406
407SkCanvas* PlatformImageData::GetPlatformCanvas() {
408  return mapped_canvas_.get();
409}
410
411SkCanvas* PlatformImageData::GetCanvas() {
412  return mapped_canvas_.get();
413}
414
415// static
416ImageHandle PlatformImageData::NullHandle() {
417#if defined(OS_WIN)
418  return NULL;
419#else
420  return ImageHandle();
421#endif
422}
423
424ImageHandle PlatformImageData::HandleFromInt(int32_t i) {
425#if defined(OS_WIN)
426    return reinterpret_cast<ImageHandle>(i);
427#else
428    return ImageHandle(i, false);
429#endif
430}
431#endif  // !defined(OS_NACL)
432
433// SimpleImageData -------------------------------------------------------------
434
435SimpleImageData::SimpleImageData(const HostResource& resource,
436                                 const PP_ImageDataDesc& desc,
437                                 const base::SharedMemoryHandle& handle)
438    : ImageData(resource, PPB_ImageData_Shared::SIMPLE, desc),
439      shm_(handle, false /* read_only */),
440      size_(desc.size.width * desc.size.height * 4),
441      map_count_(0) {
442}
443
444SimpleImageData::~SimpleImageData() {
445}
446
447void* SimpleImageData::Map() {
448  if (map_count_++ == 0)
449    shm_.Map(size_);
450  return shm_.memory();
451}
452
453void SimpleImageData::Unmap() {
454  if (--map_count_ == 0)
455    shm_.Unmap();
456}
457
458SkCanvas* SimpleImageData::GetPlatformCanvas() {
459  return NULL;  // No canvas available.
460}
461
462SkCanvas* SimpleImageData::GetCanvas() {
463  return NULL;  // No canvas available.
464}
465
466// PPB_ImageData_Proxy ---------------------------------------------------------
467
468PPB_ImageData_Proxy::PPB_ImageData_Proxy(Dispatcher* dispatcher)
469    : InterfaceProxy(dispatcher) {
470}
471
472PPB_ImageData_Proxy::~PPB_ImageData_Proxy() {
473}
474
475// static
476PP_Resource PPB_ImageData_Proxy::CreateProxyResource(
477    PP_Instance instance,
478    PPB_ImageData_Shared::ImageDataType type,
479    PP_ImageDataFormat format,
480    const PP_Size& size,
481    PP_Bool init_to_zero) {
482  PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance);
483  if (!dispatcher)
484    return 0;
485
486  // Check the cache.
487  scoped_refptr<ImageData> cached_image_data =
488      ImageDataCache::GetInstance()->Get(instance, type,
489                                         size.width, size.height, format);
490  if (cached_image_data.get()) {
491    // We have one we can re-use rather than allocating a new one.
492    cached_image_data->RecycleToPlugin(PP_ToBool(init_to_zero));
493    return cached_image_data->GetReference();
494  }
495
496  HostResource result;
497  PP_ImageDataDesc desc;
498  switch (type) {
499    case PPB_ImageData_Shared::SIMPLE: {
500      ppapi::proxy::SerializedHandle image_handle_wrapper;
501      dispatcher->Send(new PpapiHostMsg_PPBImageData_CreateSimple(
502          kApiID, instance, format, size, init_to_zero,
503          &result, &desc, &image_handle_wrapper));
504      if (image_handle_wrapper.is_shmem()) {
505        base::SharedMemoryHandle image_handle = image_handle_wrapper.shmem();
506        if (!result.is_null())
507          return
508              (new SimpleImageData(result, desc, image_handle))->GetReference();
509      }
510      break;
511    }
512    case PPB_ImageData_Shared::PLATFORM: {
513#if !defined(OS_NACL)
514      ImageHandle image_handle = PlatformImageData::NullHandle();
515      dispatcher->Send(new PpapiHostMsg_PPBImageData_CreatePlatform(
516          kApiID, instance, format, size, init_to_zero,
517          &result, &desc, &image_handle));
518      if (!result.is_null())
519        return
520            (new PlatformImageData(result, desc, image_handle))->GetReference();
521#else
522      // PlatformImageData shouldn't be created in untrusted code.
523      NOTREACHED();
524#endif
525      break;
526    }
527  }
528
529  return 0;
530}
531
532bool PPB_ImageData_Proxy::OnMessageReceived(const IPC::Message& msg) {
533  bool handled = true;
534  IPC_BEGIN_MESSAGE_MAP(PPB_ImageData_Proxy, msg)
535#if !defined(OS_NACL)
536    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreatePlatform,
537                        OnHostMsgCreatePlatform)
538    IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreateSimple,
539                        OnHostMsgCreateSimple)
540#endif
541    IPC_MESSAGE_HANDLER(PpapiMsg_PPBImageData_NotifyUnusedImageData,
542                        OnPluginMsgNotifyUnusedImageData)
543
544    IPC_MESSAGE_UNHANDLED(handled = false)
545  IPC_END_MESSAGE_MAP()
546  return handled;
547}
548
549#if !defined(OS_NACL)
550// static
551PP_Resource PPB_ImageData_Proxy::CreateImageData(
552    PP_Instance instance,
553    PPB_ImageData_Shared::ImageDataType type,
554    PP_ImageDataFormat format,
555    const PP_Size& size,
556    bool init_to_zero,
557    PP_ImageDataDesc* desc,
558    IPC::PlatformFileForTransit* image_handle,
559    uint32_t* byte_count) {
560  HostDispatcher* dispatcher = HostDispatcher::GetForInstance(instance);
561  if (!dispatcher)
562    return 0;
563
564  thunk::EnterResourceCreation enter(instance);
565  if (enter.failed())
566    return 0;
567
568  PP_Bool pp_init_to_zero = init_to_zero ? PP_TRUE : PP_FALSE;
569  PP_Resource pp_resource = 0;
570  switch (type) {
571    case PPB_ImageData_Shared::SIMPLE: {
572      pp_resource = enter.functions()->CreateImageDataSimple(
573          instance, format, &size, pp_init_to_zero);
574      break;
575    }
576    case PPB_ImageData_Shared::PLATFORM: {
577      pp_resource = enter.functions()->CreateImageData(
578          instance, format, &size, pp_init_to_zero);
579      break;
580    }
581  }
582
583  if (!pp_resource)
584    return 0;
585
586  ppapi::ScopedPPResource resource(ppapi::ScopedPPResource::PassRef(),
587                                   pp_resource);
588
589  thunk::EnterResourceNoLock<PPB_ImageData_API> enter_resource(resource.get(),
590                                                               false);
591  if (enter_resource.object()->Describe(desc) != PP_TRUE) {
592    DVLOG(1) << "CreateImageData failed: could not Describe";
593    return 0;
594  }
595
596  int local_fd = 0;
597  if (enter_resource.object()->GetSharedMemory(&local_fd,
598                                               byte_count) != PP_OK) {
599    DVLOG(1) << "CreateImageData failed: could not GetSharedMemory";
600    return 0;
601  }
602
603#if defined(OS_WIN)
604  *image_handle = dispatcher->ShareHandleWithRemote(
605      reinterpret_cast<HANDLE>(static_cast<intptr_t>(local_fd)), false);
606#elif defined(OS_POSIX)
607  *image_handle = dispatcher->ShareHandleWithRemote(local_fd, false);
608#else
609  #error Not implemented.
610#endif
611
612  return resource.Release();
613}
614
615void PPB_ImageData_Proxy::OnHostMsgCreatePlatform(
616    PP_Instance instance,
617    int32_t format,
618    const PP_Size& size,
619    PP_Bool init_to_zero,
620    HostResource* result,
621    PP_ImageDataDesc* desc,
622    ImageHandle* result_image_handle) {
623  // Clear |desc| so we don't send unitialized memory to the plugin.
624  // https://crbug.com/391023.
625  *desc = PP_ImageDataDesc();
626  IPC::PlatformFileForTransit image_handle;
627  uint32_t byte_count;
628  PP_Resource resource =
629      CreateImageData(instance,
630                      PPB_ImageData_Shared::PLATFORM,
631                      static_cast<PP_ImageDataFormat>(format),
632                      size,
633                      true /* init_to_zero */,
634                      desc, &image_handle, &byte_count);
635  result->SetHostResource(instance, resource);
636  if (resource) {
637    *result_image_handle = image_handle;
638  } else {
639    *result_image_handle = PlatformImageData::NullHandle();
640  }
641}
642
643void PPB_ImageData_Proxy::OnHostMsgCreateSimple(
644    PP_Instance instance,
645    int32_t format,
646    const PP_Size& size,
647    PP_Bool init_to_zero,
648    HostResource* result,
649    PP_ImageDataDesc* desc,
650    ppapi::proxy::SerializedHandle* result_image_handle) {
651  // Clear |desc| so we don't send unitialized memory to the plugin.
652  // https://crbug.com/391023.
653  *desc = PP_ImageDataDesc();
654  IPC::PlatformFileForTransit image_handle;
655  uint32_t byte_count;
656  PP_Resource resource =
657      CreateImageData(instance,
658                      PPB_ImageData_Shared::SIMPLE,
659                      static_cast<PP_ImageDataFormat>(format),
660                      size,
661                      true /* init_to_zero */,
662                      desc, &image_handle, &byte_count);
663
664  result->SetHostResource(instance, resource);
665  if (resource) {
666    result_image_handle->set_shmem(image_handle, byte_count);
667  } else {
668    result_image_handle->set_null_shmem();
669  }
670}
671#endif  // !defined(OS_NACL)
672
673void PPB_ImageData_Proxy::OnPluginMsgNotifyUnusedImageData(
674    const HostResource& old_image_data) {
675  PluginGlobals* plugin_globals = PluginGlobals::Get();
676  if (!plugin_globals)
677    return;  // This may happen if the plugin is maliciously sending this
678             // message to the renderer.
679
680  EnterPluginFromHostResource<PPB_ImageData_API> enter(old_image_data);
681  if (enter.succeeded()) {
682    ImageData* image_data = static_cast<ImageData*>(enter.object());
683    ImageDataCache::GetInstance()->ImageDataUsable(image_data);
684  }
685
686  // The renderer sent us a reference with the message. If the image data was
687  // still cached in our process, the proxy still holds a reference so we can
688  // remove the one the renderer just sent is. If the proxy no longer holds a
689  // reference, we released everything and we should also release the one the
690  // renderer just sent us.
691  dispatcher()->Send(new PpapiHostMsg_PPBCore_ReleaseResource(
692      API_ID_PPB_CORE, old_image_data));
693}
694
695}  // namespace proxy
696}  // namespace ppapi
697