// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ppapi/proxy/ppb_image_data_proxy.h" #include // For memcpy #include #include #include "base/logging.h" #include "base/memory/singleton.h" #include "base/memory/weak_ptr.h" #include "build/build_config.h" #include "ppapi/c/pp_completion_callback.h" #include "ppapi/c/pp_errors.h" #include "ppapi/c/pp_resource.h" #include "ppapi/proxy/enter_proxy.h" #include "ppapi/proxy/host_dispatcher.h" #include "ppapi/proxy/plugin_dispatcher.h" #include "ppapi/proxy/plugin_globals.h" #include "ppapi/proxy/plugin_resource_tracker.h" #include "ppapi/proxy/ppapi_messages.h" #include "ppapi/shared_impl/host_resource.h" #include "ppapi/shared_impl/proxy_lock.h" #include "ppapi/shared_impl/resource.h" #include "ppapi/shared_impl/scoped_pp_resource.h" #include "ppapi/thunk/enter.h" #include "ppapi/thunk/thunk.h" #if !defined(OS_NACL) #include "skia/ext/platform_canvas.h" #include "ui/surface/transport_dib.h" #endif using ppapi::thunk::PPB_ImageData_API; namespace ppapi { namespace proxy { namespace { // How ImageData re-use works // -------------------------- // // When animating plugins (like video), re-creating image datas for each frame // and mapping the memory has a high overhead. So we try to re-use these when // possible. // // 1. Plugin makes an asynchronous call that transfers an ImageData to the // implementation of some API. // 2. Plugin frees its ImageData reference. If it doesn't do this we can't // re-use it. // 3. When the last plugin ref of an ImageData is released, we don't actually // delete it. Instead we put it on a queue where we hold onto it in the // plugin process for a short period of time. // 4. The API implementation that received the ImageData finishes using it. // Without our caching system it would get deleted at this point. // 5. The proxy in the renderer will send NotifyUnusedImageData back to the // plugin process. We check if the given resource is in the queue and mark // it as usable. // 6. When the plugin requests a new image data, we check our queue and if there // is a usable ImageData of the right size and format, we'll return it // instead of making a new one. It's important that caching is only requested // when the size is unlikely to change, so cache hits are high. // // Some notes: // // - We only re-use image data when the plugin and host are rapidly exchanging // them and the size is likely to remain constant. It should be clear that // the plugin is promising that it's done with the image. // // - Theoretically we could re-use them in other cases but the lifetime // becomes more difficult to manage. The plugin could have used an ImageData // in an arbitrary number of queued up PaintImageData calls which we would // have to check. // // - If a flush takes a long time or there are many released image datas // accumulating in our queue such that some are deleted, we will have // released our reference by the time the renderer notifies us of an unused // image data. In this case we just give up. // // - We maintain a per-instance cache. Some pages have many instances of // Flash, for example, each of a different size. If they're all animating we // want each to get its own image data re-use. // // - We generate new resource IDs when re-use happens to try to avoid weird // problems if the plugin messes up its refcounting. // Keep a cache entry for this many seconds before expiring it. We get an entry // back from the renderer after an ImageData is swapped out, so it means the // plugin has to be painting at least two frames for this time interval to // get caching. static const int kMaxAgeSeconds = 2; // ImageDataCacheEntry --------------------------------------------------------- struct ImageDataCacheEntry { ImageDataCacheEntry() : added_time(), usable(false), image() {} ImageDataCacheEntry(ImageData* i) : added_time(base::TimeTicks::Now()), usable(false), image(i) { } base::TimeTicks added_time; // Set to true when the renderer tells us that it's OK to re-use this iamge. bool usable; scoped_refptr image; }; // ImageDataInstanceCache ------------------------------------------------------ // Per-instance cache of image datas. class ImageDataInstanceCache { public: ImageDataInstanceCache() : next_insertion_point_(0) {} // These functions have the same spec as the ones in ImageDataCache. scoped_refptr Get(PPB_ImageData_Shared::ImageDataType type, int width, int height, PP_ImageDataFormat format); void Add(ImageData* image_data); void ImageDataUsable(ImageData* image_data); // Expires old entries. Returns true if there are still entries in the list, // false if this instance cache is now empty. bool ExpireEntries(); private: void IncrementInsertionPoint(); // We'll store this many ImageDatas per instance. const static int kCacheSize = 2; ImageDataCacheEntry images_[kCacheSize]; // Index into cache where the next item will go. int next_insertion_point_; }; scoped_refptr ImageDataInstanceCache::Get( PPB_ImageData_Shared::ImageDataType type, int width, int height, PP_ImageDataFormat format) { // Just do a brute-force search since the cache is so small. for (int i = 0; i < kCacheSize; i++) { if (!images_[i].usable) continue; if (images_[i].image->type() != type) continue; const PP_ImageDataDesc& desc = images_[i].image->desc(); if (desc.format == format && desc.size.width == width && desc.size.height == height) { scoped_refptr ret(images_[i].image); images_[i] = ImageDataCacheEntry(); // Since we just removed an item, this entry is the best place to insert // a subsequent one. next_insertion_point_ = i; return ret; } } return scoped_refptr(); } void ImageDataInstanceCache::Add(ImageData* image_data) { images_[next_insertion_point_] = ImageDataCacheEntry(image_data); IncrementInsertionPoint(); } void ImageDataInstanceCache::ImageDataUsable(ImageData* image_data) { for (int i = 0; i < kCacheSize; i++) { if (images_[i].image.get() == image_data) { images_[i].usable = true; // This test is important. The renderer doesn't guarantee how many image // datas it has or when it notifies us when one is usable. Its possible // to get into situations where it's always telling us the old one is // usable, and then the older one immediately gets expired. Therefore, // if the next insertion would overwrite this now-usable entry, make the // next insertion overwrite some other entry to avoid the replacement. if (next_insertion_point_ == i) IncrementInsertionPoint(); return; } } } bool ImageDataInstanceCache::ExpireEntries() { base::TimeTicks threshold_time = base::TimeTicks::Now() - base::TimeDelta::FromSeconds(kMaxAgeSeconds); bool has_entry = false; for (int i = 0; i < kCacheSize; i++) { if (images_[i].image.get()) { // Entry present. if (images_[i].added_time <= threshold_time) { // Found an entry to expire. images_[i] = ImageDataCacheEntry(); next_insertion_point_ = i; } else { // Found an entry that we're keeping. has_entry = true; } } } return has_entry; } void ImageDataInstanceCache::IncrementInsertionPoint() { // Go to the next location, wrapping around to get LRU. next_insertion_point_++; if (next_insertion_point_ >= kCacheSize) next_insertion_point_ = 0; } // ImageDataCache -------------------------------------------------------------- class ImageDataCache { public: ImageDataCache() : weak_factory_(this) {} ~ImageDataCache() {} static ImageDataCache* GetInstance(); // Retrieves an image data from the cache of the specified type, size and // format if one exists. If one doesn't exist, this will return a null refptr. scoped_refptr Get(PP_Instance instance, PPB_ImageData_Shared::ImageDataType type, int width, int height, PP_ImageDataFormat format); // Adds the given image data to the cache. There should be no plugin // references to it. This may delete an older item from the cache. void Add(ImageData* image_data); // Notification from the renderer that the given image data is usable. void ImageDataUsable(ImageData* image_data); void DidDeleteInstance(PP_Instance instance); private: friend struct LeakySingletonTraits; // Timer callback to expire entries for the given instance. void OnTimer(PP_Instance instance); typedef std::map CacheMap; CacheMap cache_; // This class does timer calls and we don't want to run these outside of the // scope of the object. Technically, since this class is a leaked static, // this will never happen and this factory is unnecessary. However, it's // probably better not to make assumptions about the lifetime of this class. base::WeakPtrFactory weak_factory_; DISALLOW_COPY_AND_ASSIGN(ImageDataCache); }; // static ImageDataCache* ImageDataCache::GetInstance() { return Singleton >::get(); } scoped_refptr ImageDataCache::Get( PP_Instance instance, PPB_ImageData_Shared::ImageDataType type, int width, int height, PP_ImageDataFormat format) { CacheMap::iterator found = cache_.find(instance); if (found == cache_.end()) return scoped_refptr(); return found->second.Get(type, width, height, format); } void ImageDataCache::Add(ImageData* image_data) { cache_[image_data->pp_instance()].Add(image_data); // Schedule a timer to invalidate this entry. base::MessageLoop::current()->PostDelayedTask( FROM_HERE, RunWhileLocked(base::Bind(&ImageDataCache::OnTimer, weak_factory_.GetWeakPtr(), image_data->pp_instance())), base::TimeDelta::FromSeconds(kMaxAgeSeconds)); } void ImageDataCache::ImageDataUsable(ImageData* image_data) { CacheMap::iterator found = cache_.find(image_data->pp_instance()); if (found != cache_.end()) found->second.ImageDataUsable(image_data); } void ImageDataCache::DidDeleteInstance(PP_Instance instance) { cache_.erase(instance); } void ImageDataCache::OnTimer(PP_Instance instance) { CacheMap::iterator found = cache_.find(instance); if (found == cache_.end()) return; if (!found->second.ExpireEntries()) { // There are no more entries for this instance, remove it from the cache. cache_.erase(found); } } } // namespace // ImageData ------------------------------------------------------------------- ImageData::ImageData(const HostResource& resource, PPB_ImageData_Shared::ImageDataType type, const PP_ImageDataDesc& desc) : Resource(OBJECT_IS_PROXY, resource), type_(type), desc_(desc), is_candidate_for_reuse_(false) { } ImageData::~ImageData() { } PPB_ImageData_API* ImageData::AsPPB_ImageData_API() { return this; } void ImageData::LastPluginRefWasDeleted() { // The plugin no longer needs this ImageData, add it to our cache if it's // been used in a ReplaceContents. These are the ImageDatas that the renderer // will send back ImageDataUsable messages for. if (is_candidate_for_reuse_) ImageDataCache::GetInstance()->Add(this); } void ImageData::InstanceWasDeleted() { ImageDataCache::GetInstance()->DidDeleteInstance(pp_instance()); } PP_Bool ImageData::Describe(PP_ImageDataDesc* desc) { memcpy(desc, &desc_, sizeof(PP_ImageDataDesc)); return PP_TRUE; } int32_t ImageData::GetSharedMemory(int* /* handle */, uint32_t* /* byte_count */) { // Not supported in the proxy (this method is for actually implementing the // proxy in the host). return PP_ERROR_NOACCESS; } void ImageData::SetIsCandidateForReuse() { is_candidate_for_reuse_ = true; } void ImageData::RecycleToPlugin(bool zero_contents) { is_candidate_for_reuse_ = false; if (zero_contents) { void* data = Map(); memset(data, 0, desc_.stride * desc_.size.height); Unmap(); } } // PlatformImageData ----------------------------------------------------------- #if !defined(OS_NACL) PlatformImageData::PlatformImageData(const HostResource& resource, const PP_ImageDataDesc& desc, ImageHandle handle) : ImageData(resource, PPB_ImageData_Shared::PLATFORM, desc) { #if defined(OS_WIN) transport_dib_.reset(TransportDIB::CreateWithHandle(handle)); #else transport_dib_.reset(TransportDIB::Map(handle)); #endif // defined(OS_WIN) } PlatformImageData::~PlatformImageData() { } void* PlatformImageData::Map() { if (!mapped_canvas_.get()) { mapped_canvas_.reset(transport_dib_->GetPlatformCanvas(desc_.size.width, desc_.size.height)); if (!mapped_canvas_.get()) return NULL; } const SkBitmap& bitmap = skia::GetTopDevice(*mapped_canvas_)->accessBitmap(true); bitmap.lockPixels(); return bitmap.getAddr(0, 0); } void PlatformImageData::Unmap() { // TODO(brettw) have a way to unmap a TransportDIB. Currently this isn't // possible since deleting the TransportDIB also frees all the handles. // We need to add a method to TransportDIB to release the handles. } SkCanvas* PlatformImageData::GetPlatformCanvas() { return mapped_canvas_.get(); } SkCanvas* PlatformImageData::GetCanvas() { return mapped_canvas_.get(); } // static ImageHandle PlatformImageData::NullHandle() { #if defined(OS_WIN) return NULL; #else return ImageHandle(); #endif } ImageHandle PlatformImageData::HandleFromInt(int32_t i) { #if defined(OS_WIN) return reinterpret_cast(i); #else return ImageHandle(i, false); #endif } #endif // !defined(OS_NACL) // SimpleImageData ------------------------------------------------------------- SimpleImageData::SimpleImageData(const HostResource& resource, const PP_ImageDataDesc& desc, const base::SharedMemoryHandle& handle) : ImageData(resource, PPB_ImageData_Shared::SIMPLE, desc), shm_(handle, false /* read_only */), size_(desc.size.width * desc.size.height * 4), map_count_(0) { } SimpleImageData::~SimpleImageData() { } void* SimpleImageData::Map() { if (map_count_++ == 0) shm_.Map(size_); return shm_.memory(); } void SimpleImageData::Unmap() { if (--map_count_ == 0) shm_.Unmap(); } SkCanvas* SimpleImageData::GetPlatformCanvas() { return NULL; // No canvas available. } SkCanvas* SimpleImageData::GetCanvas() { return NULL; // No canvas available. } // PPB_ImageData_Proxy --------------------------------------------------------- PPB_ImageData_Proxy::PPB_ImageData_Proxy(Dispatcher* dispatcher) : InterfaceProxy(dispatcher) { } PPB_ImageData_Proxy::~PPB_ImageData_Proxy() { } // static PP_Resource PPB_ImageData_Proxy::CreateProxyResource( PP_Instance instance, PPB_ImageData_Shared::ImageDataType type, PP_ImageDataFormat format, const PP_Size& size, PP_Bool init_to_zero) { PluginDispatcher* dispatcher = PluginDispatcher::GetForInstance(instance); if (!dispatcher) return 0; // Check the cache. scoped_refptr cached_image_data = ImageDataCache::GetInstance()->Get(instance, type, size.width, size.height, format); if (cached_image_data.get()) { // We have one we can re-use rather than allocating a new one. cached_image_data->RecycleToPlugin(PP_ToBool(init_to_zero)); return cached_image_data->GetReference(); } HostResource result; PP_ImageDataDesc desc; switch (type) { case PPB_ImageData_Shared::SIMPLE: { ppapi::proxy::SerializedHandle image_handle_wrapper; dispatcher->Send(new PpapiHostMsg_PPBImageData_CreateSimple( kApiID, instance, format, size, init_to_zero, &result, &desc, &image_handle_wrapper)); if (image_handle_wrapper.is_shmem()) { base::SharedMemoryHandle image_handle = image_handle_wrapper.shmem(); if (!result.is_null()) return (new SimpleImageData(result, desc, image_handle))->GetReference(); } break; } case PPB_ImageData_Shared::PLATFORM: { #if !defined(OS_NACL) ImageHandle image_handle = PlatformImageData::NullHandle(); dispatcher->Send(new PpapiHostMsg_PPBImageData_CreatePlatform( kApiID, instance, format, size, init_to_zero, &result, &desc, &image_handle)); if (!result.is_null()) return (new PlatformImageData(result, desc, image_handle))->GetReference(); #else // PlatformImageData shouldn't be created in untrusted code. NOTREACHED(); #endif break; } } return 0; } bool PPB_ImageData_Proxy::OnMessageReceived(const IPC::Message& msg) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(PPB_ImageData_Proxy, msg) #if !defined(OS_NACL) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreatePlatform, OnHostMsgCreatePlatform) IPC_MESSAGE_HANDLER(PpapiHostMsg_PPBImageData_CreateSimple, OnHostMsgCreateSimple) #endif IPC_MESSAGE_HANDLER(PpapiMsg_PPBImageData_NotifyUnusedImageData, OnPluginMsgNotifyUnusedImageData) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } #if !defined(OS_NACL) // static PP_Resource PPB_ImageData_Proxy::CreateImageData( PP_Instance instance, PPB_ImageData_Shared::ImageDataType type, PP_ImageDataFormat format, const PP_Size& size, bool init_to_zero, PP_ImageDataDesc* desc, IPC::PlatformFileForTransit* image_handle, uint32_t* byte_count) { HostDispatcher* dispatcher = HostDispatcher::GetForInstance(instance); if (!dispatcher) return 0; thunk::EnterResourceCreation enter(instance); if (enter.failed()) return 0; PP_Bool pp_init_to_zero = init_to_zero ? PP_TRUE : PP_FALSE; PP_Resource pp_resource = 0; switch (type) { case PPB_ImageData_Shared::SIMPLE: { pp_resource = enter.functions()->CreateImageDataSimple( instance, format, &size, pp_init_to_zero); break; } case PPB_ImageData_Shared::PLATFORM: { pp_resource = enter.functions()->CreateImageData( instance, format, &size, pp_init_to_zero); break; } } if (!pp_resource) return 0; ppapi::ScopedPPResource resource(ppapi::ScopedPPResource::PassRef(), pp_resource); thunk::EnterResourceNoLock enter_resource(resource.get(), false); if (enter_resource.object()->Describe(desc) != PP_TRUE) { DVLOG(1) << "CreateImageData failed: could not Describe"; return 0; } int local_fd = 0; if (enter_resource.object()->GetSharedMemory(&local_fd, byte_count) != PP_OK) { DVLOG(1) << "CreateImageData failed: could not GetSharedMemory"; return 0; } #if defined(OS_WIN) *image_handle = dispatcher->ShareHandleWithRemote( reinterpret_cast(static_cast(local_fd)), false); #elif defined(OS_POSIX) *image_handle = dispatcher->ShareHandleWithRemote(local_fd, false); #else #error Not implemented. #endif return resource.Release(); } void PPB_ImageData_Proxy::OnHostMsgCreatePlatform( PP_Instance instance, int32_t format, const PP_Size& size, PP_Bool init_to_zero, HostResource* result, PP_ImageDataDesc* desc, ImageHandle* result_image_handle) { // Clear |desc| so we don't send unitialized memory to the plugin. // https://crbug.com/391023. *desc = PP_ImageDataDesc(); IPC::PlatformFileForTransit image_handle; uint32_t byte_count; PP_Resource resource = CreateImageData(instance, PPB_ImageData_Shared::PLATFORM, static_cast(format), size, true /* init_to_zero */, desc, &image_handle, &byte_count); result->SetHostResource(instance, resource); if (resource) { *result_image_handle = image_handle; } else { *result_image_handle = PlatformImageData::NullHandle(); } } void PPB_ImageData_Proxy::OnHostMsgCreateSimple( PP_Instance instance, int32_t format, const PP_Size& size, PP_Bool init_to_zero, HostResource* result, PP_ImageDataDesc* desc, ppapi::proxy::SerializedHandle* result_image_handle) { // Clear |desc| so we don't send unitialized memory to the plugin. // https://crbug.com/391023. *desc = PP_ImageDataDesc(); IPC::PlatformFileForTransit image_handle; uint32_t byte_count; PP_Resource resource = CreateImageData(instance, PPB_ImageData_Shared::SIMPLE, static_cast(format), size, true /* init_to_zero */, desc, &image_handle, &byte_count); result->SetHostResource(instance, resource); if (resource) { result_image_handle->set_shmem(image_handle, byte_count); } else { result_image_handle->set_null_shmem(); } } #endif // !defined(OS_NACL) void PPB_ImageData_Proxy::OnPluginMsgNotifyUnusedImageData( const HostResource& old_image_data) { PluginGlobals* plugin_globals = PluginGlobals::Get(); if (!plugin_globals) return; // This may happen if the plugin is maliciously sending this // message to the renderer. EnterPluginFromHostResource enter(old_image_data); if (enter.succeeded()) { ImageData* image_data = static_cast(enter.object()); ImageDataCache::GetInstance()->ImageDataUsable(image_data); } // The renderer sent us a reference with the message. If the image data was // still cached in our process, the proxy still holds a reference so we can // remove the one the renderer just sent is. If the proxy no longer holds a // reference, we released everything and we should also release the one the // renderer just sent us. dispatcher()->Send(new PpapiHostMsg_PPBCore_ReleaseResource( API_ID_PPB_CORE, old_image_data)); } } // namespace proxy } // namespace ppapi