/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use std::mem; use smallvec::SmallVec; use api::{ImageFormat, ImageBufferKind, DebugFlags}; use api::units::*; use crate::device::TextureFilter; use crate::internal_types::{ CacheTextureId, TextureUpdateList, Swizzle, TextureCacheAllocInfo, TextureCacheCategory, TextureSource, FrameStamp, FrameId, }; use crate::profiler::{self, TransactionProfile}; use crate::gpu_types::{ImageSource, UvRectKind}; use crate::gpu_cache::{GpuCache, GpuCacheHandle}; use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle}; #[derive(Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub enum PictureCacheEntryMarker {} malloc_size_of::malloc_size_of_is_0!(PictureCacheEntryMarker); pub type PictureCacheTextureHandle = WeakFreeListHandle; use std::cmp; // Stores information related to a single entry in the texture // cache. This is stored for each item whether it's in the shared // cache or a standalone texture. #[derive(Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct PictureCacheEntry { /// Size of the requested tile. pub size: DeviceIntSize, /// The last frame this item was requested for rendering. // TODO(gw): This stamp is only used for picture cache tiles, and some checks // in the glyph cache eviction code. We could probably remove it // entirely in future (or move to EntryDetails::Picture). pub last_access: FrameStamp, /// Handle to the resource rect in the GPU cache. pub uv_rect_handle: GpuCacheHandle, /// Image format of the data that the entry expects. pub filter: TextureFilter, /// The actual device texture ID this is part of. pub texture_id: CacheTextureId, } impl PictureCacheEntry { fn update_gpu_cache(&mut self, gpu_cache: &mut GpuCache) { if let Some(mut request) = gpu_cache.request(&mut self.uv_rect_handle) { let origin = DeviceIntPoint::zero(); let image_source = ImageSource { p0: origin.to_f32(), p1: (origin + self.size).to_f32(), uv_rect_kind: UvRectKind::Rect, user_data: [0.0; 4], }; image_source.write_gpu_blocks(&mut request); } } } /// The textures used to hold picture cache tiles. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] struct PictureTexture { texture_id: CacheTextureId, size: DeviceIntSize, is_allocated: bool, last_frame_used: FrameId, } /// The textures used to hold picture cache tiles. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct PictureTextures { /// Current list of textures in the pool textures: Vec, /// Default tile size for content tiles default_tile_size: DeviceIntSize, /// Number of currently allocated textures in the pool allocated_texture_count: usize, /// Texture filter to use for picture cache textures filter: TextureFilter, debug_flags: DebugFlags, /// Cache of picture cache entries. cache_entries: FreeList, /// Strong handles for the picture_cache_entries FreeList. cache_handles: Vec>, now: FrameStamp, } impl PictureTextures { pub fn new( default_tile_size: DeviceIntSize, filter: TextureFilter, ) -> Self { PictureTextures { textures: Vec::new(), default_tile_size, allocated_texture_count: 0, filter, debug_flags: DebugFlags::empty(), cache_entries: FreeList::new(), cache_handles: Vec::new(), now: FrameStamp::INVALID, } } pub fn begin_frame(&mut self, stamp: FrameStamp, pending_updates: &mut TextureUpdateList) { self.now = stamp; // Expire picture cache tiles that haven't been referenced in the last frame. // The picture cache code manually keeps tiles alive by calling `request` on // them if it wants to retain a tile that is currently not visible. self.expire_old_tiles(pending_updates); } pub fn default_tile_size(&self) -> DeviceIntSize { self.default_tile_size } pub fn update( &mut self, tile_size: DeviceIntSize, handle: &mut Option, gpu_cache: &mut GpuCache, next_texture_id: &mut CacheTextureId, pending_updates: &mut TextureUpdateList, ) { debug_assert!(self.now.is_valid()); debug_assert!(tile_size.width > 0 && tile_size.height > 0); let need_alloc = match handle { None => true, Some(handle) => { // Check if the entry has been evicted. !self.entry_exists(&handle) }, }; if need_alloc { let new_handle = self.get_or_allocate_tile( tile_size, next_texture_id, pending_updates, ); *handle = Some(new_handle); } if let Some(handle) = handle { // Upload the resource rect and texture array layer. self.cache_entries .get_opt_mut(handle) .expect("BUG: handle must be valid now") .update_gpu_cache(gpu_cache); } else { panic!("The handle should be valid picture cache handle now") } } pub fn get_or_allocate_tile( &mut self, tile_size: DeviceIntSize, next_texture_id: &mut CacheTextureId, pending_updates: &mut TextureUpdateList, ) -> PictureCacheTextureHandle { let mut texture_id = None; self.allocated_texture_count += 1; for texture in &mut self.textures { if texture.size == tile_size && !texture.is_allocated { // Found a target that's not currently in use which matches. Update // the last_frame_used for GC purposes. texture.is_allocated = true; texture.last_frame_used = FrameId::INVALID; texture_id = Some(texture.texture_id); break; } } // Need to create a new render target and add it to the pool let texture_id = texture_id.unwrap_or_else(|| { let texture_id = *next_texture_id; next_texture_id.0 += 1; // Push a command to allocate device storage of the right size / format. let info = TextureCacheAllocInfo { target: ImageBufferKind::Texture2D, width: tile_size.width, height: tile_size.height, format: ImageFormat::RGBA8, filter: self.filter, is_shared_cache: false, has_depth: true, category: TextureCacheCategory::PictureTile, }; pending_updates.push_alloc(texture_id, info); self.textures.push(PictureTexture { texture_id, is_allocated: true, size: tile_size, last_frame_used: FrameId::INVALID, }); texture_id }); let cache_entry = PictureCacheEntry { size: tile_size, last_access: self.now, uv_rect_handle: GpuCacheHandle::new(), filter: self.filter, texture_id, }; // Add the cache entry to the picture_textures.cache_entries FreeList. let strong_handle = self.cache_entries.insert(cache_entry); let new_handle = strong_handle.weak(); self.cache_handles.push(strong_handle); new_handle } pub fn free_tile( &mut self, id: CacheTextureId, current_frame_id: FrameId, pending_updates: &mut TextureUpdateList, ) { self.allocated_texture_count -= 1; let texture = self.textures .iter_mut() .find(|t| t.texture_id == id) .expect("bug: invalid texture id"); assert!(texture.is_allocated); texture.is_allocated = false; assert_eq!(texture.last_frame_used, FrameId::INVALID); texture.last_frame_used = current_frame_id; if self.debug_flags.contains( DebugFlags::TEXTURE_CACHE_DBG | DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) { pending_updates.push_debug_clear( id, DeviceIntPoint::zero(), texture.size.width, texture.size.height, ); } } pub fn request(&mut self, handle: &PictureCacheTextureHandle, gpu_cache: &mut GpuCache) -> bool { let entry = self.cache_entries.get_opt_mut(handle); let now = self.now; entry.map_or(true, |entry| { // If an image is requested that is already in the cache, // refresh the GPU cache data associated with this item. entry.last_access = now; entry.update_gpu_cache(gpu_cache); false }) } pub fn get_texture_source(&self, handle: &PictureCacheTextureHandle) -> TextureSource { let entry = self.cache_entries.get_opt(handle) .expect("BUG: was dropped from cache or not updated!"); debug_assert_eq!(entry.last_access, self.now); TextureSource::TextureCache(entry.texture_id, Swizzle::default()) } /// Expire picture cache tiles that haven't been referenced in the last frame. /// The picture cache code manually keeps tiles alive by calling `request` on /// them if it wants to retain a tile that is currently not visible. pub fn expire_old_tiles(&mut self, pending_updates: &mut TextureUpdateList) { for i in (0 .. self.cache_handles.len()).rev() { let evict = { let entry = self.cache_entries.get( &self.cache_handles[i] ); // This function is called at the beginning of the frame, // so we don't yet know which picture cache tiles will be // requested this frame. Therefore only evict picture cache // tiles which weren't requested in the *previous* frame. entry.last_access.frame_id() < self.now.frame_id() - 1 }; if evict { let handle = self.cache_handles.swap_remove(i); let entry = self.cache_entries.free(handle); self.free_tile(entry.texture_id, self.now.frame_id(), pending_updates); } } } pub fn clear(&mut self, pending_updates: &mut TextureUpdateList) { for handle in mem::take(&mut self.cache_handles) { let entry = self.cache_entries.free(handle); self.free_tile(entry.texture_id, self.now.frame_id(), pending_updates); } for texture in self.textures.drain(..) { pending_updates.push_free(texture.texture_id); } } pub fn update_profile(&self, profile: &mut TransactionProfile) { profile.set(profiler::PICTURE_TILES, self.textures.len()); } /// Simple garbage collect of picture cache tiles pub fn gc( &mut self, pending_updates: &mut TextureUpdateList, ) { // Allow the picture cache pool to keep 25% of the current allocated tile count // as free textures to be reused. This ensures the allowed tile count is appropriate // based on current window size. let free_texture_count = self.textures.len() - self.allocated_texture_count; let allowed_retained_count = (self.allocated_texture_count as f32 * 0.25).ceil() as usize; let do_gc = free_texture_count > allowed_retained_count; if do_gc { // Sort the current pool by age, so that we remove oldest textures first self.textures.sort_unstable_by_key(|t| cmp::Reverse(t.last_frame_used)); // We can't just use retain() because `PictureTexture` requires manual cleanup. let mut allocated_targets = SmallVec::<[PictureTexture; 32]>::new(); let mut retained_targets = SmallVec::<[PictureTexture; 32]>::new(); for target in self.textures.drain(..) { if target.is_allocated { // Allocated targets can't be collected allocated_targets.push(target); } else if retained_targets.len() < allowed_retained_count { // Retain the most recently used targets up to the allowed count retained_targets.push(target); } else { // The rest of the targets get freed assert_ne!(target.last_frame_used, FrameId::INVALID); pending_updates.push_free(target.texture_id); } } self.textures.extend(retained_targets); self.textures.extend(allocated_targets); } } pub fn entry_exists(&self, handle: &PictureCacheTextureHandle) -> bool { self.cache_entries.get_opt(handle).is_some() } pub fn set_debug_flags(&mut self, flags: DebugFlags) { self.debug_flags = flags; } #[cfg(feature = "replay")] pub fn filter(&self) -> TextureFilter { self.filter } }