Source code

Revision control

Copy as Markdown

Other Tools

/* 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<PictureCacheEntryMarker>;
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<PictureTexture>,
/// 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<PictureCacheEntry, PictureCacheEntryMarker>,
/// Strong handles for the picture_cache_entries FreeList.
cache_handles: Vec<FreeListHandle<PictureCacheEntryMarker>>,
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<PictureCacheTextureHandle>,
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
}
}