diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /gfx/wr/webrender/src/texture_cache.rs | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/wr/webrender/src/texture_cache.rs')
-rw-r--r-- | gfx/wr/webrender/src/texture_cache.rs | 1707 |
1 files changed, 1707 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/texture_cache.rs b/gfx/wr/webrender/src/texture_cache.rs new file mode 100644 index 0000000000..8650f12ecf --- /dev/null +++ b/gfx/wr/webrender/src/texture_cache.rs @@ -0,0 +1,1707 @@ +/* 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 api::{DirtyRect, ExternalImageType, ImageFormat, ImageBufferKind}; +use api::{DebugFlags, ImageDescriptor}; +use api::units::*; +#[cfg(test)] +use api::{DocumentId, IdNamespace}; +use crate::device::{TextureFilter, TextureFormatPair}; +use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle}; +use crate::gpu_cache::{GpuCache, GpuCacheHandle}; +use crate::gpu_types::{ImageSource, UvRectKind}; +use crate::internal_types::{ + CacheTextureId, Swizzle, SwizzleSettings, FrameStamp, FrameId, + TextureUpdateList, TextureUpdateSource, TextureSource, + TextureCacheAllocInfo, TextureCacheUpdate, TextureCacheCategory, +}; +use crate::lru_cache::LRUCache; +use crate::profiler::{self, TransactionProfile}; +use crate::resource_cache::{CacheItem, CachedImageData}; +use crate::texture_pack::{ + AllocatorList, AllocId, AtlasAllocatorList, ShelfAllocator, ShelfAllocatorOptions, +}; +use std::cell::Cell; +use std::mem; +use std::rc::Rc; +use euclid::size2; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; + +/// Information about which shader will use the entry. +/// +/// For batching purposes, it's beneficial to group some items in their +/// own textures if we know that they are used by a specific shader. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TargetShader { + Default, + Text, +} + +/// The size of each region in shared cache texture arrays. +pub const TEXTURE_REGION_DIMENSIONS: i32 = 512; + +/// Items in the texture cache can either be standalone textures, +/// or a sub-rect inside the shared cache. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum EntryDetails { + Standalone { + /// Number of bytes this entry allocates + size_in_bytes: usize, + }, + Cache { + /// Origin within the texture layer where this item exists. + origin: DeviceIntPoint, + /// ID of the allocation specific to its allocator. + alloc_id: AllocId, + /// The allocated size in bytes for this entry. + allocated_size_in_bytes: usize, + }, +} + +impl EntryDetails { + fn describe(&self) -> DeviceIntPoint { + match *self { + EntryDetails::Standalone { .. } => DeviceIntPoint::zero(), + EntryDetails::Cache { origin, .. } => origin, + } + } +} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum AutoCacheEntryMarker {} + +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum ManualCacheEntryMarker {} + +// 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 CacheEntry { + /// Size of the requested item, in device pixels. Does not include any + /// padding for alignment that the allocator may have added to this entry's + /// allocation. + pub size: DeviceIntSize, + /// Details specific to standalone or shared items. + pub details: EntryDetails, + /// Arbitrary user data associated with this item. + pub user_data: [f32; 4], + /// 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 PictureCacheEntry). + 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 input_format: ImageFormat, + pub filter: TextureFilter, + pub swizzle: Swizzle, + /// The actual device texture ID this is part of. + pub texture_id: CacheTextureId, + /// Optional notice when the entry is evicted from the cache. + pub eviction_notice: Option<EvictionNotice>, + /// The type of UV rect this entry specifies. + pub uv_rect_kind: UvRectKind, + + pub shader: TargetShader, +} + +malloc_size_of::malloc_size_of_is_0!( + CacheEntry, + AutoCacheEntryMarker, ManualCacheEntryMarker +); + +impl CacheEntry { + // Create a new entry for a standalone texture. + fn new_standalone( + texture_id: CacheTextureId, + last_access: FrameStamp, + params: &CacheAllocParams, + swizzle: Swizzle, + size_in_bytes: usize, + ) -> Self { + CacheEntry { + size: params.descriptor.size, + user_data: params.user_data, + last_access, + details: EntryDetails::Standalone { + size_in_bytes, + }, + texture_id, + input_format: params.descriptor.format, + filter: params.filter, + swizzle, + uv_rect_handle: GpuCacheHandle::new(), + eviction_notice: None, + uv_rect_kind: params.uv_rect_kind, + shader: TargetShader::Default, + } + } + + // Update the GPU cache for this texture cache entry. + // This ensures that the UV rect, and texture layer index + // are up to date in the GPU cache for vertex shaders + // to fetch from. + 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 = self.details.describe(); + let image_source = ImageSource { + p0: origin.to_f32(), + p1: (origin + self.size).to_f32(), + user_data: self.user_data, + uv_rect_kind: self.uv_rect_kind, + }; + image_source.write_gpu_blocks(&mut request); + } + } + + fn evict(&self) { + if let Some(eviction_notice) = self.eviction_notice.as_ref() { + eviction_notice.notify(); + } + } + + fn alternative_input_format(&self) -> ImageFormat { + match self.input_format { + ImageFormat::RGBA8 => ImageFormat::BGRA8, + ImageFormat::BGRA8 => ImageFormat::RGBA8, + other => other, + } + } +} + + +/// A texture cache handle is a weak reference to a cache entry. +/// +/// If the handle has not been inserted into the cache yet, or if the entry was +/// previously inserted and then evicted, lookup of the handle will fail, and +/// the cache handle needs to re-upload this item to the texture cache (see +/// request() below). + +#[derive(MallocSizeOf,Clone,PartialEq,Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TextureCacheHandle { + /// A fresh handle. + Empty, + + /// A handle for an entry with automatic eviction. + Auto(WeakFreeListHandle<AutoCacheEntryMarker>), + + /// A handle for an entry with manual eviction. + Manual(WeakFreeListHandle<ManualCacheEntryMarker>) +} + +impl TextureCacheHandle { + pub fn invalid() -> Self { + TextureCacheHandle::Empty + } +} + +/// Describes the eviction policy for a given entry in the texture cache. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum Eviction { + /// The entry will be evicted under the normal rules (which differ between + /// standalone and shared entries). + Auto, + /// The entry will not be evicted until the policy is explicitly set to a + /// different value. + Manual, +} + +// An eviction notice is a shared condition useful for detecting +// when a TextureCacheHandle gets evicted from the TextureCache. +// It is optionally installed to the TextureCache when an update() +// is scheduled. A single notice may be shared among any number of +// TextureCacheHandle updates. The notice may then be subsequently +// checked to see if any of the updates using it have been evicted. +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct EvictionNotice { + evicted: Rc<Cell<bool>>, +} + +impl EvictionNotice { + fn notify(&self) { + self.evicted.set(true); + } + + pub fn check(&self) -> bool { + if self.evicted.get() { + self.evicted.set(false); + true + } else { + false + } + } +} + +/// The different budget types for the texture cache. Each type has its own +/// memory budget. Once the budget is exceeded, entries with automatic eviction +/// are evicted. Entries with manual eviction share the same budget but are not +/// evicted once the budget is exceeded. +/// Keeping separate budgets ensures that we don't evict entries from unrelated +/// textures if one texture gets full. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u8)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +enum BudgetType { + SharedColor8Linear, + SharedColor8Nearest, + SharedColor8Glyphs, + SharedAlpha8, + SharedAlpha8Glyphs, + SharedAlpha16, + Standalone, +} + +impl BudgetType { + pub const COUNT: usize = 7; + + pub const VALUES: [BudgetType; BudgetType::COUNT] = [ + BudgetType::SharedColor8Linear, + BudgetType::SharedColor8Nearest, + BudgetType::SharedColor8Glyphs, + BudgetType::SharedAlpha8, + BudgetType::SharedAlpha8Glyphs, + BudgetType::SharedAlpha16, + BudgetType::Standalone, + ]; + + pub const PRESSURE_COUNTERS: [usize; BudgetType::COUNT] = [ + profiler::ATLAS_COLOR8_LINEAR_PRESSURE, + profiler::ATLAS_COLOR8_NEAREST_PRESSURE, + profiler::ATLAS_COLOR8_GLYPHS_PRESSURE, + profiler::ATLAS_ALPHA8_PRESSURE, + profiler::ATLAS_ALPHA8_GLYPHS_PRESSURE, + profiler::ATLAS_ALPHA16_PRESSURE, + profiler::ATLAS_STANDALONE_PRESSURE, + ]; + + pub fn iter() -> impl Iterator<Item = BudgetType> { + BudgetType::VALUES.iter().cloned() + } +} + +/// A set of lazily allocated, fixed size, texture arrays for each format the +/// texture cache supports. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +struct SharedTextures { + color8_nearest: AllocatorList<ShelfAllocator, TextureParameters>, + alpha8_linear: AllocatorList<ShelfAllocator, TextureParameters>, + alpha8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>, + alpha16_linear: AllocatorList<ShelfAllocator, TextureParameters>, + color8_linear: AllocatorList<ShelfAllocator, TextureParameters>, + color8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>, + bytes_per_texture_of_type: [i32 ; BudgetType::COUNT], + next_compaction_idx: usize, +} + +impl SharedTextures { + /// Mints a new set of shared textures. + fn new(color_formats: TextureFormatPair<ImageFormat>, config: &TextureCacheConfig) -> Self { + let mut bytes_per_texture_of_type = [0 ; BudgetType::COUNT]; + + // Used primarily for cached shadow masks. There can be lots of + // these on some pages like francine, but most pages don't use it + // much. + // Most content tends to fit into two 512x512 textures. We are + // conservatively using 1024x1024 to fit everything in a single + // texture and avoid breaking batches, but it's worth checking + // whether it would actually lead to a lot of batch breaks in + // practice. + let alpha8_linear = AllocatorList::new( + config.alpha8_texture_size, + ShelfAllocatorOptions { + num_columns: 1, + alignment: size2(8, 8), + .. ShelfAllocatorOptions::default() + }, + TextureParameters { + formats: TextureFormatPair::from(ImageFormat::R8), + filter: TextureFilter::Linear, + }, + ); + bytes_per_texture_of_type[BudgetType::SharedAlpha8 as usize] = + config.alpha8_texture_size * config.alpha8_texture_size; + + // The cache for alpha glyphs (separate to help with batching). + let alpha8_glyphs = AllocatorList::new( + config.alpha8_glyph_texture_size, + ShelfAllocatorOptions { + num_columns: if config.alpha8_glyph_texture_size >= 1024 { 2 } else { 1 }, + alignment: size2(4, 8), + .. ShelfAllocatorOptions::default() + }, + TextureParameters { + formats: TextureFormatPair::from(ImageFormat::R8), + filter: TextureFilter::Linear, + }, + ); + bytes_per_texture_of_type[BudgetType::SharedAlpha8Glyphs as usize] = + config.alpha8_glyph_texture_size * config.alpha8_glyph_texture_size; + + // Used for experimental hdr yuv texture support, but not used in + // production Firefox. + let alpha16_linear = AllocatorList::new( + config.alpha16_texture_size, + ShelfAllocatorOptions { + num_columns: if config.alpha16_texture_size >= 1024 { 2 } else { 1 }, + alignment: size2(8, 8), + .. ShelfAllocatorOptions::default() + }, + TextureParameters { + formats: TextureFormatPair::from(ImageFormat::R16), + filter: TextureFilter::Linear, + }, + ); + bytes_per_texture_of_type[BudgetType::SharedAlpha16 as usize] = + ImageFormat::R16.bytes_per_pixel() * + config.alpha16_texture_size * config.alpha16_texture_size; + + // The primary cache for images, etc. + let color8_linear = AllocatorList::new( + config.color8_linear_texture_size, + ShelfAllocatorOptions { + num_columns: if config.color8_linear_texture_size >= 1024 { 2 } else { 1 }, + alignment: size2(16, 16), + .. ShelfAllocatorOptions::default() + }, + TextureParameters { + formats: color_formats.clone(), + filter: TextureFilter::Linear, + }, + ); + bytes_per_texture_of_type[BudgetType::SharedColor8Linear as usize] = + color_formats.internal.bytes_per_pixel() * + config.color8_linear_texture_size * config.color8_linear_texture_size; + + // The cache for subpixel-AA and bitmap glyphs (separate to help with batching). + let color8_glyphs = AllocatorList::new( + config.color8_glyph_texture_size, + ShelfAllocatorOptions { + num_columns: if config.color8_glyph_texture_size >= 1024 { 2 } else { 1 }, + alignment: size2(4, 8), + .. ShelfAllocatorOptions::default() + }, + TextureParameters { + formats: color_formats.clone(), + filter: TextureFilter::Linear, + }, + ); + bytes_per_texture_of_type[BudgetType::SharedColor8Glyphs as usize] = + color_formats.internal.bytes_per_pixel() * + config.color8_glyph_texture_size * config.color8_glyph_texture_size; + + // Used for image-rendering: crisp. This is mostly favicons, which + // are small. Some other images use it too, but those tend to be + // larger than 512x512 and thus don't use the shared cache anyway. + let color8_nearest = AllocatorList::new( + config.color8_nearest_texture_size, + ShelfAllocatorOptions::default(), + TextureParameters { + formats: color_formats.clone(), + filter: TextureFilter::Nearest, + } + ); + bytes_per_texture_of_type[BudgetType::SharedColor8Nearest as usize] = + color_formats.internal.bytes_per_pixel() * + config.color8_nearest_texture_size * config.color8_nearest_texture_size; + + Self { + alpha8_linear, + alpha8_glyphs, + alpha16_linear, + color8_linear, + color8_glyphs, + color8_nearest, + bytes_per_texture_of_type, + next_compaction_idx: 0, + } + } + + /// Clears each texture in the set, with the given set of pending updates. + fn clear(&mut self, updates: &mut TextureUpdateList) { + let texture_dealloc_cb = &mut |texture_id| { + updates.push_free(texture_id); + }; + + self.alpha8_linear.clear(texture_dealloc_cb); + self.alpha8_glyphs.clear(texture_dealloc_cb); + self.alpha16_linear.clear(texture_dealloc_cb); + self.color8_linear.clear(texture_dealloc_cb); + self.color8_nearest.clear(texture_dealloc_cb); + self.color8_glyphs.clear(texture_dealloc_cb); + } + + /// Returns a mutable borrow for the shared texture array matching the parameters. + fn select( + &mut self, external_format: ImageFormat, filter: TextureFilter, shader: TargetShader, + ) -> (&mut dyn AtlasAllocatorList<TextureParameters>, BudgetType) { + match external_format { + ImageFormat::R8 => { + assert_eq!(filter, TextureFilter::Linear); + match shader { + TargetShader::Text => { + (&mut self.alpha8_glyphs, BudgetType::SharedAlpha8Glyphs) + }, + _ => (&mut self.alpha8_linear, BudgetType::SharedAlpha8), + } + } + ImageFormat::R16 => { + assert_eq!(filter, TextureFilter::Linear); + (&mut self.alpha16_linear, BudgetType::SharedAlpha16) + } + ImageFormat::RGBA8 | + ImageFormat::BGRA8 => { + match (filter, shader) { + (TextureFilter::Linear, TargetShader::Text) => { + (&mut self.color8_glyphs, BudgetType::SharedColor8Glyphs) + }, + (TextureFilter::Linear, _) => { + (&mut self.color8_linear, BudgetType::SharedColor8Linear) + }, + (TextureFilter::Nearest, _) => { + (&mut self.color8_nearest, BudgetType::SharedColor8Nearest) + }, + _ => panic!("Unexpected filter {:?}", filter), + } + } + _ => panic!("Unexpected format {:?}", external_format), + } + } + + /// How many bytes a single texture of the given type takes up, for the + /// configured texture sizes. + fn bytes_per_shared_texture(&self, budget_type: BudgetType) -> usize { + self.bytes_per_texture_of_type[budget_type as usize] as usize + } + + fn has_multiple_textures(&self, budget_type: BudgetType) -> bool { + match budget_type { + BudgetType::SharedColor8Linear => self.color8_linear.allocated_textures() > 1, + BudgetType::SharedColor8Nearest => self.color8_nearest.allocated_textures() > 1, + BudgetType::SharedColor8Glyphs => self.color8_glyphs.allocated_textures() > 1, + BudgetType::SharedAlpha8 => self.alpha8_linear.allocated_textures() > 1, + BudgetType::SharedAlpha8Glyphs => self.alpha8_glyphs.allocated_textures() > 1, + BudgetType::SharedAlpha16 => self.alpha16_linear.allocated_textures() > 1, + BudgetType::Standalone => false, + } + } +} + +/// Container struct for the various parameters used in cache allocation. +struct CacheAllocParams { + descriptor: ImageDescriptor, + filter: TextureFilter, + user_data: [f32; 4], + uv_rect_kind: UvRectKind, + shader: TargetShader, +} + +/// Startup parameters for the texture cache. +/// +/// Texture sizes must be at least 512. +#[derive(Clone)] +pub struct TextureCacheConfig { + pub color8_linear_texture_size: i32, + pub color8_nearest_texture_size: i32, + pub color8_glyph_texture_size: i32, + pub alpha8_texture_size: i32, + pub alpha8_glyph_texture_size: i32, + pub alpha16_texture_size: i32, +} + +impl TextureCacheConfig { + pub const DEFAULT: Self = TextureCacheConfig { + color8_linear_texture_size: 2048, + color8_nearest_texture_size: 512, + color8_glyph_texture_size: 2048, + alpha8_texture_size: 1024, + alpha8_glyph_texture_size: 2048, + alpha16_texture_size: 512, + }; +} + +/// General-purpose manager for images in GPU memory. This includes images, +/// rasterized glyphs, rasterized blobs, cached render tasks, etc. +/// +/// The texture cache is owned and managed by the RenderBackend thread, and +/// produces a series of commands to manipulate the textures on the Renderer +/// thread. These commands are executed before any rendering is performed for +/// a given frame. +/// +/// Entries in the texture cache are not guaranteed to live past the end of the +/// frame in which they are requested, and may be evicted. The API supports +/// querying whether an entry is still available. +/// +/// The TextureCache is different from the GpuCache in that the former stores +/// images, whereas the latter stores data and parameters for use in the shaders. +/// This means that the texture cache can be visualized, which is a good way to +/// understand how it works. Enabling gfx.webrender.debug.texture-cache shows a +/// live view of its contents in Firefox. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TextureCache { + /// Set of texture arrays in different formats used for the shared cache. + shared_textures: SharedTextures, + + /// Maximum texture size supported by hardware. + max_texture_size: i32, + + /// Maximum texture size before it is considered preferable to break the + /// texture into tiles. + tiling_threshold: i32, + + /// Settings on using texture unit swizzling. + swizzle: Option<SwizzleSettings>, + + /// The current set of debug flags. + debug_flags: DebugFlags, + + /// The next unused virtual texture ID. Monotonically increasing. + pub next_id: CacheTextureId, + + /// A list of allocations and updates that need to be applied to the texture + /// cache in the rendering thread this frame. + #[cfg_attr(all(feature = "serde", any(feature = "capture", feature = "replay")), serde(skip))] + pub pending_updates: TextureUpdateList, + + /// The current `FrameStamp`. Used for cache eviction policies. + now: FrameStamp, + + /// Cache of texture cache handles with automatic lifetime management, evicted + /// in a least-recently-used order. + lru_cache: LRUCache<CacheEntry, AutoCacheEntryMarker>, + + /// Cache of texture cache entries with manual liftime management. + manual_entries: FreeList<CacheEntry, ManualCacheEntryMarker>, + + /// Strong handles for the manual_entries FreeList. + manual_handles: Vec<FreeListHandle<ManualCacheEntryMarker>>, + + /// Memory usage of allocated entries in all of the shared or standalone + /// textures. Includes both manually and automatically evicted entries. + bytes_allocated: [usize ; BudgetType::COUNT], +} + +impl TextureCache { + /// The maximum number of items that will be evicted per frame. This limit helps avoid jank + /// on frames where we want to evict a large number of items. Instead, we'd prefer to drop + /// the items incrementally over a number of frames, even if that means the total allocated + /// size of the cache is above the desired threshold for a small number of frames. + const MAX_EVICTIONS_PER_FRAME: usize = 32; + + pub fn new( + max_texture_size: i32, + tiling_threshold: i32, + color_formats: TextureFormatPair<ImageFormat>, + swizzle: Option<SwizzleSettings>, + config: &TextureCacheConfig, + ) -> Self { + let pending_updates = TextureUpdateList::new(); + + // Shared texture cache controls swizzling on a per-entry basis, assuming that + // the texture as a whole doesn't need to be swizzled (but only some entries do). + // It would be possible to support this, but not needed at the moment. + assert!(color_formats.internal != ImageFormat::BGRA8 || + swizzle.map_or(true, |s| s.bgra8_sampling_swizzle == Swizzle::default()) + ); + + let next_texture_id = CacheTextureId(1); + + TextureCache { + shared_textures: SharedTextures::new(color_formats, config), + max_texture_size, + tiling_threshold, + swizzle, + debug_flags: DebugFlags::empty(), + next_id: next_texture_id, + pending_updates, + now: FrameStamp::INVALID, + lru_cache: LRUCache::new(BudgetType::COUNT), + manual_entries: FreeList::new(), + manual_handles: Vec::new(), + bytes_allocated: [0 ; BudgetType::COUNT], + } + } + + /// Creates a TextureCache and sets it up with a valid `FrameStamp`, which + /// is useful for avoiding panics when instantiating the `TextureCache` + /// directly from unit test code. + #[cfg(test)] + pub fn new_for_testing( + max_texture_size: i32, + image_format: ImageFormat, + ) -> Self { + let mut cache = Self::new( + max_texture_size, + max_texture_size, + TextureFormatPair::from(image_format), + None, + &TextureCacheConfig::DEFAULT, + ); + let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1)); + now.advance(); + cache.begin_frame(now, &mut TransactionProfile::new()); + cache + } + + pub fn set_debug_flags(&mut self, flags: DebugFlags) { + self.debug_flags = flags; + } + + /// Clear all entries in the texture cache. This is a fairly drastic + /// step that should only be called very rarely. + pub fn clear_all(&mut self) { + // Evict all manual eviction handles + let manual_handles = mem::replace( + &mut self.manual_handles, + Vec::new(), + ); + for handle in manual_handles { + let entry = self.manual_entries.free(handle); + self.evict_impl(entry); + } + + // Evict all auto (LRU) cache handles + for budget_type in BudgetType::iter() { + while let Some(entry) = self.lru_cache.pop_oldest(budget_type as u8) { + entry.evict(); + self.free(&entry); + } + } + + // Free the picture and shared textures + self.shared_textures.clear(&mut self.pending_updates); + self.pending_updates.note_clear(); + } + + /// Called at the beginning of each frame. + pub fn begin_frame(&mut self, stamp: FrameStamp, profile: &mut TransactionProfile) { + debug_assert!(!self.now.is_valid()); + profile_scope!("begin_frame"); + self.now = stamp; + + // Texture cache eviction is done at the start of the frame. This ensures that + // we won't evict items that have been requested on this frame. + // It also frees up space in the cache for items allocated later in the frame + // potentially reducing texture allocations and fragmentation. + self.evict_items_from_cache_if_required(profile); + } + + pub fn end_frame(&mut self, profile: &mut TransactionProfile) { + debug_assert!(self.now.is_valid()); + + let updates = &mut self.pending_updates; // To avoid referring to self in the closure. + let callback = &mut|texture_id| { updates.push_free(texture_id); }; + + // Release of empty shared textures is done at the end of the frame. That way, if the + // eviction at the start of the frame frees up a texture, that is then subsequently + // used during the frame, we avoid doing a free/alloc for it. + self.shared_textures.alpha8_linear.release_empty_textures(callback); + self.shared_textures.alpha8_glyphs.release_empty_textures(callback); + self.shared_textures.alpha16_linear.release_empty_textures(callback); + self.shared_textures.color8_linear.release_empty_textures(callback); + self.shared_textures.color8_nearest.release_empty_textures(callback); + self.shared_textures.color8_glyphs.release_empty_textures(callback); + + for budget in BudgetType::iter() { + let threshold = self.get_eviction_threshold(budget); + let pressure = self.bytes_allocated[budget as usize] as f32 / threshold as f32; + profile.set(BudgetType::PRESSURE_COUNTERS[budget as usize], pressure); + } + + profile.set(profiler::ATLAS_A8_PIXELS, self.shared_textures.alpha8_linear.allocated_space()); + profile.set(profiler::ATLAS_A8_TEXTURES, self.shared_textures.alpha8_linear.allocated_textures()); + profile.set(profiler::ATLAS_A8_GLYPHS_PIXELS, self.shared_textures.alpha8_glyphs.allocated_space()); + profile.set(profiler::ATLAS_A8_GLYPHS_TEXTURES, self.shared_textures.alpha8_glyphs.allocated_textures()); + profile.set(profiler::ATLAS_A16_PIXELS, self.shared_textures.alpha16_linear.allocated_space()); + profile.set(profiler::ATLAS_A16_TEXTURES, self.shared_textures.alpha16_linear.allocated_textures()); + profile.set(profiler::ATLAS_RGBA8_LINEAR_PIXELS, self.shared_textures.color8_linear.allocated_space()); + profile.set(profiler::ATLAS_RGBA8_LINEAR_TEXTURES, self.shared_textures.color8_linear.allocated_textures()); + profile.set(profiler::ATLAS_RGBA8_NEAREST_PIXELS, self.shared_textures.color8_nearest.allocated_space()); + profile.set(profiler::ATLAS_RGBA8_NEAREST_TEXTURES, self.shared_textures.color8_nearest.allocated_textures()); + profile.set(profiler::ATLAS_RGBA8_GLYPHS_PIXELS, self.shared_textures.color8_glyphs.allocated_space()); + profile.set(profiler::ATLAS_RGBA8_GLYPHS_TEXTURES, self.shared_textures.color8_glyphs.allocated_textures()); + + let shared_bytes = [ + BudgetType::SharedColor8Linear, + BudgetType::SharedColor8Nearest, + BudgetType::SharedColor8Glyphs, + BudgetType::SharedAlpha8, + BudgetType::SharedAlpha8Glyphs, + BudgetType::SharedAlpha16, + ].iter().map(|b| self.bytes_allocated[*b as usize]).sum(); + + profile.set(profiler::ATLAS_ITEMS_MEM, profiler::bytes_to_mb(shared_bytes)); + + self.now = FrameStamp::INVALID; + } + + pub fn run_compaction(&mut self, gpu_cache: &mut GpuCache) { + // Use the same order as BudgetType::VALUES so that we can index self.bytes_allocated + // with the same index. + let allocator_lists = [ + &mut self.shared_textures.color8_linear, + &mut self.shared_textures.color8_nearest, + &mut self.shared_textures.color8_glyphs, + &mut self.shared_textures.alpha8_linear, + &mut self.shared_textures.alpha8_glyphs, + &mut self.shared_textures.alpha16_linear, + ]; + + // Pick a texture type on which to try to run the compaction logic this frame. + let idx = self.shared_textures.next_compaction_idx; + + // Number of moved pixels after which we stop attempting to move more items for this frame. + // The constant is up for adjustment, the main goal is to avoid causing frame spikes on + // low end GPUs. + let area_threshold = 512*512; + + let mut changes = Vec::new(); + allocator_lists[idx].try_compaction(area_threshold, &mut changes); + + if changes.is_empty() { + // Nothing to do, we'll try another texture type next frame. + self.shared_textures.next_compaction_idx = (self.shared_textures.next_compaction_idx + 1) % allocator_lists.len(); + } + + for change in changes { + let bpp = allocator_lists[idx].texture_parameters().formats.internal.bytes_per_pixel(); + + // While the area of the image does not change, the area it occupies in the texture + // atlas may (in other words the number of wasted pixels can change), so we have + // to keep track of that. + let old_bytes = (change.old_rect.area() * bpp) as usize; + let new_bytes = (change.new_rect.area() * bpp) as usize; + self.bytes_allocated[idx] -= old_bytes; + self.bytes_allocated[idx] += new_bytes; + + let entry = match change.handle { + TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(&handle).unwrap(), + TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(&handle).unwrap(), + TextureCacheHandle::Empty => { panic!("invalid handle"); } + }; + entry.texture_id = change.new_tex; + entry.details = EntryDetails::Cache { + origin: change.new_rect.min, + alloc_id: change.new_id, + allocated_size_in_bytes: new_bytes, + }; + + gpu_cache.invalidate(&entry.uv_rect_handle); + entry.uv_rect_handle = GpuCacheHandle::new(); + + let src_rect = DeviceIntRect::from_origin_and_size(change.old_rect.min, entry.size); + let dst_rect = DeviceIntRect::from_origin_and_size(change.new_rect.min, entry.size); + + self.pending_updates.push_copy(change.old_tex, &src_rect, change.new_tex, &dst_rect); + + if self.debug_flags.contains( + DebugFlags::TEXTURE_CACHE_DBG | + DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) + { + self.pending_updates.push_debug_clear( + change.old_tex, + src_rect.min, + src_rect.width(), + src_rect.height(), + ); + } + } + } + + // Request an item in the texture cache. All images that will + // be used on a frame *must* have request() called on their + // handle, to update the last used timestamp and ensure + // that resources are not flushed from the cache too early. + // + // Returns true if the image needs to be uploaded to the + // texture cache (either never uploaded, or has been + // evicted on a previous frame). + pub fn request(&mut self, handle: &TextureCacheHandle, gpu_cache: &mut GpuCache) -> bool { + let now = self.now; + let entry = match handle { + TextureCacheHandle::Empty => None, + TextureCacheHandle::Auto(handle) => { + // Call touch rather than get_opt_mut so that the LRU index + // knows that the entry has been used. + self.lru_cache.touch(handle) + }, + TextureCacheHandle::Manual(handle) => { + self.manual_entries.get_opt_mut(handle) + }, + }; + 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 + }) + } + + fn get_entry_opt(&self, handle: &TextureCacheHandle) -> Option<&CacheEntry> { + match handle { + TextureCacheHandle::Empty => None, + TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt(handle), + TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt(handle), + } + } + + fn get_entry_opt_mut(&mut self, handle: &TextureCacheHandle) -> Option<&mut CacheEntry> { + match handle { + TextureCacheHandle::Empty => None, + TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(handle), + TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(handle), + } + } + + // Returns true if the image needs to be uploaded to the + // texture cache (either never uploaded, or has been + // evicted on a previous frame). + pub fn needs_upload(&self, handle: &TextureCacheHandle) -> bool { + !self.is_allocated(handle) + } + + pub fn max_texture_size(&self) -> i32 { + self.max_texture_size + } + + pub fn tiling_threshold(&self) -> i32 { + self.tiling_threshold + } + + #[cfg(feature = "replay")] + pub fn color_formats(&self) -> TextureFormatPair<ImageFormat> { + self.shared_textures.color8_linear.texture_parameters().formats.clone() + } + + #[cfg(feature = "replay")] + pub fn swizzle_settings(&self) -> Option<SwizzleSettings> { + self.swizzle + } + + pub fn pending_updates(&mut self) -> TextureUpdateList { + mem::replace(&mut self.pending_updates, TextureUpdateList::new()) + } + + // Update the data stored by a given texture cache handle. + pub fn update( + &mut self, + handle: &mut TextureCacheHandle, + descriptor: ImageDescriptor, + filter: TextureFilter, + data: Option<CachedImageData>, + user_data: [f32; 4], + mut dirty_rect: ImageDirtyRect, + gpu_cache: &mut GpuCache, + eviction_notice: Option<&EvictionNotice>, + uv_rect_kind: UvRectKind, + eviction: Eviction, + shader: TargetShader, + ) { + debug_assert!(self.now.is_valid()); + // Determine if we need to allocate texture cache memory + // for this item. We need to reallocate if any of the following + // is true: + // - Never been in the cache + // - Has been in the cache but was evicted. + // - Exists in the cache but dimensions / format have changed. + let realloc = match self.get_entry_opt(handle) { + Some(entry) => { + entry.size != descriptor.size || (entry.input_format != descriptor.format && + entry.alternative_input_format() != descriptor.format) + } + None => { + // Not allocated, or was previously allocated but has been evicted. + true + } + }; + + if realloc { + let params = CacheAllocParams { descriptor, filter, user_data, uv_rect_kind, shader }; + self.allocate(¶ms, handle, eviction); + + // If we reallocated, we need to upload the whole item again. + dirty_rect = DirtyRect::All; + } + + let entry = self.get_entry_opt_mut(handle) + .expect("BUG: There must be an entry at this handle now"); + + // Install the new eviction notice for this update, if applicable. + entry.eviction_notice = eviction_notice.cloned(); + entry.uv_rect_kind = uv_rect_kind; + + // Invalidate the contents of the resource rect in the GPU cache. + // This ensures that the update_gpu_cache below will add + // the new information to the GPU cache. + //TODO: only invalidate if the parameters change? + gpu_cache.invalidate(&entry.uv_rect_handle); + + // Upload the resource rect and texture array layer. + entry.update_gpu_cache(gpu_cache); + + // Create an update command, which the render thread processes + // to upload the new image data into the correct location + // in GPU memory. + if let Some(data) = data { + // If the swizzling is supported, we always upload in the internal + // texture format (thus avoiding the conversion by the driver). + // Otherwise, pass the external format to the driver. + let origin = entry.details.describe(); + let texture_id = entry.texture_id; + let size = entry.size; + let use_upload_format = self.swizzle.is_none(); + let op = TextureCacheUpdate::new_update( + data, + &descriptor, + origin, + size, + use_upload_format, + &dirty_rect, + ); + self.pending_updates.push_update(texture_id, op); + } + } + + // Check if a given texture handle has a valid allocation + // in the texture cache. + pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool { + self.get_entry_opt(handle).is_some() + } + + // Return the allocated size of the texture handle's associated data, + // or otherwise indicate the handle is invalid. + pub fn get_allocated_size(&self, handle: &TextureCacheHandle) -> Option<usize> { + self.get_entry_opt(handle).map(|entry| { + (entry.input_format.bytes_per_pixel() * entry.size.area()) as usize + }) + } + + // Retrieve the details of an item in the cache. This is used + // during batch creation to provide the resource rect address + // to the shaders and texture ID to the batching logic. + // This function will assert in debug modes if the caller + // tries to get a handle that was not requested this frame. + pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem { + let (texture_id, uv_rect, swizzle, uv_rect_handle, user_data) = self.get_cache_location(handle); + CacheItem { + uv_rect_handle, + texture_id: TextureSource::TextureCache( + texture_id, + swizzle, + ), + uv_rect, + user_data, + } + } + + /// A more detailed version of get(). This allows access to the actual + /// device rect of the cache allocation. + /// + /// Returns a tuple identifying the texture, the layer, the region, + /// and its GPU handle. + pub fn get_cache_location( + &self, + handle: &TextureCacheHandle, + ) -> (CacheTextureId, DeviceIntRect, Swizzle, GpuCacheHandle, [f32; 4]) { + let entry = self + .get_entry_opt(handle) + .expect("BUG: was dropped from cache or not updated!"); + debug_assert_eq!(entry.last_access, self.now); + let origin = entry.details.describe(); + ( + entry.texture_id, + DeviceIntRect::from_origin_and_size(origin, entry.size), + entry.swizzle, + entry.uv_rect_handle, + entry.user_data, + ) + } + + /// Internal helper function to evict a strong texture cache handle + fn evict_impl( + &mut self, + entry: CacheEntry, + ) { + entry.evict(); + self.free(&entry); + } + + /// Evict a texture cache handle that was previously set to be in manual + /// eviction mode. + pub fn evict_handle(&mut self, handle: &TextureCacheHandle) { + match handle { + TextureCacheHandle::Manual(handle) => { + // Find the strong handle that matches this weak handle. If this + // ever shows up in profiles, we can make it a hash (but the number + // of manual eviction handles is typically small). + // Alternatively, we could make a more forgiving FreeList variant + // which does not differentiate between strong and weak handles. + let index = self.manual_handles.iter().position(|strong_handle| { + strong_handle.matches(handle) + }); + if let Some(index) = index { + let handle = self.manual_handles.swap_remove(index); + let entry = self.manual_entries.free(handle); + self.evict_impl(entry); + } + } + TextureCacheHandle::Auto(handle) => { + if let Some(entry) = self.lru_cache.remove(handle) { + self.evict_impl(entry); + } + } + _ => {} + } + } + + pub fn dump_color8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { + self.shared_textures.color8_linear.dump_as_svg(output) + } + + pub fn dump_color8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { + self.shared_textures.color8_glyphs.dump_as_svg(output) + } + + pub fn dump_alpha8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { + self.shared_textures.alpha8_glyphs.dump_as_svg(output) + } + + pub fn dump_alpha8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { + self.shared_textures.alpha8_linear.dump_as_svg(output) + } + + /// Get the eviction threshold, in bytes, for the given budget type. + fn get_eviction_threshold(&self, budget_type: BudgetType) -> usize { + if budget_type == BudgetType::Standalone { + // For standalone textures, the only reason to evict textures is + // to save GPU memory. Batching / draw call concerns do not apply + // to standalone textures, because unused textures don't cause + // extra draw calls. + return 8 * 1024 * 1024; + } + + // For shared textures, evicting an entry only frees up GPU memory if it + // causes one of the shared textures to become empty, so we want to avoid + // getting slightly above the capacity of a texture. + // The other concern for shared textures is batching: The entries that + // are needed in the current frame should be distributed across as few + // shared textures as possible, to minimize the number of draw calls. + // Ideally we only want one texture per type under simple workloads. + + let bytes_per_texture = self.shared_textures.bytes_per_shared_texture(budget_type); + + // Number of allocated bytes under which we don't bother with evicting anything + // from the cache. Above the threshold we consider evicting the coldest items + // depending on how cold they are. + // + // Above all else we want to make sure that even after a heavy workload, the + // shared cache settles back to a single texture atlas per type over some reasonable + // period of time. + // This is achieved by the compaction logic which will try to consolidate items that + // are spread over multiple textures into few ones, and by evicting old items + // so that the compaction logic has room to do its job. + // + // The other goal is to leave enough empty space in the texture atlases + // so that we are not too likely to have to allocate a new texture atlas on + // the next frame if we switch to a new tab or load a new page. That's why + // the following thresholds are rather low. Note that even when above the threshold, + // we only evict cold items and ramp up the eviction pressure depending on the amount + // of allocated memory (See should_continue_evicting). + let ideal_utilization = match budget_type { + BudgetType::SharedAlpha8Glyphs | BudgetType::SharedColor8Glyphs => { + // Glyphs are usually small and tightly packed so they waste very little + // space in the cache. + bytes_per_texture * 2 / 3 + } + _ => { + // Other types of images come with a variety of sizes making them more + // prone to wasting pixels and causing fragmentation issues so we put + // more pressure on them. + bytes_per_texture / 3 + } + }; + + ideal_utilization + } + + /// Returns whether to continue eviction and how cold an item need to be to be evicted. + /// + /// If the None is returned, stop evicting. + /// If the Some(n) is returned, continue evicting if the coldest item hasn't been used + /// for more than n frames. + fn should_continue_evicting( + &self, + budget_type: BudgetType, + eviction_count: usize, + ) -> Option<usize> { + + let threshold = self.get_eviction_threshold(budget_type); + let bytes_allocated = self.bytes_allocated[budget_type as usize]; + + let uses_multiple_atlases = self.shared_textures.has_multiple_textures(budget_type); + + // If current memory usage is below selected threshold, we can stop evicting items + // except when using shared texture atlases and more than one texture is in use. + // This is not very common but can happen due to fragmentation and the only way + // to get rid of that fragmentation is to continue evicting. + if bytes_allocated < threshold && !uses_multiple_atlases { + return None; + } + + // Number of frames since last use that is considered too recent for eviction, + // depending on the cache pressure. + let age_theshold = match bytes_allocated / threshold { + 0 => 400, + 1 => 200, + 2 => 100, + 3 => 50, + 4 => 25, + 5 => 10, + 6 => 5, + _ => 1, + }; + + // If current memory usage is significantly more than the threshold, keep evicting this frame + if bytes_allocated > 4 * threshold { + return Some(age_theshold); + } + + // Otherwise, only allow evicting up to a certain number of items per frame. This allows evictions + // to be spread over a number of frames, to avoid frame spikes. + if eviction_count < Self::MAX_EVICTIONS_PER_FRAME { + return Some(age_theshold) + } + + None + } + + + /// Evict old items from the shared and standalone caches, if we're over a + /// threshold memory usage value + fn evict_items_from_cache_if_required(&mut self, profile: &mut TransactionProfile) { + let previous_frame_id = self.now.frame_id() - 1; + let mut eviction_count = 0; + let mut youngest_evicted = FrameId::first(); + + for budget in BudgetType::iter() { + while let Some(age_threshold) = self.should_continue_evicting( + budget, + eviction_count, + ) { + if let Some(entry) = self.lru_cache.peek_oldest(budget as u8) { + // Only evict this item if it wasn't used in the previous frame. The reason being that if it + // was used the previous frame then it will likely be used in this frame too, and we don't + // want to be continually evicting and reuploading the item every frame. + if entry.last_access.frame_id() + age_threshold > previous_frame_id { + // Since the LRU cache is ordered by frame access, we can break out of the loop here because + // we know that all remaining items were also used in the previous frame (or more recently). + break; + } + if entry.last_access.frame_id() > youngest_evicted { + youngest_evicted = entry.last_access.frame_id(); + } + let entry = self.lru_cache.pop_oldest(budget as u8).unwrap(); + entry.evict(); + self.free(&entry); + eviction_count += 1; + } else { + // The LRU cache is empty, all remaining items use manual + // eviction. In this case, there's nothing we can do until + // the calling code manually evicts items to reduce the + // allocated cache size. + break; + } + } + } + + if eviction_count > 0 { + profile.set(profiler::TEXTURE_CACHE_EVICTION_COUNT, eviction_count); + profile.set( + profiler::TEXTURE_CACHE_YOUNGEST_EVICTION, + self.now.frame_id().as_usize() - youngest_evicted.as_usize() + ); + } + } + + // Free a cache entry from the standalone list or shared cache. + fn free(&mut self, entry: &CacheEntry) { + match entry.details { + EntryDetails::Standalone { size_in_bytes, .. } => { + self.bytes_allocated[BudgetType::Standalone as usize] -= size_in_bytes; + + // This is a standalone texture allocation. Free it directly. + self.pending_updates.push_free(entry.texture_id); + } + EntryDetails::Cache { origin, alloc_id, allocated_size_in_bytes } => { + let (allocator_list, budget_type) = self.shared_textures.select( + entry.input_format, + entry.filter, + entry.shader, + ); + + allocator_list.deallocate(entry.texture_id, alloc_id); + + self.bytes_allocated[budget_type as usize] -= allocated_size_in_bytes; + + if self.debug_flags.contains( + DebugFlags::TEXTURE_CACHE_DBG | + DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) + { + self.pending_updates.push_debug_clear( + entry.texture_id, + origin, + entry.size.width, + entry.size.height, + ); + } + } + } + } + + /// Allocate a block from the shared cache. + fn allocate_from_shared_cache( + &mut self, + params: &CacheAllocParams, + ) -> (CacheEntry, BudgetType) { + let (allocator_list, budget_type) = self.shared_textures.select( + params.descriptor.format, + params.filter, + params.shader, + ); + + // To avoid referring to self in the closure. + let next_id = &mut self.next_id; + let pending_updates = &mut self.pending_updates; + + let (texture_id, alloc_id, allocated_rect) = allocator_list.allocate( + params.descriptor.size, + &mut |size, parameters| { + let texture_id = *next_id; + next_id.0 += 1; + pending_updates.push_alloc( + texture_id, + TextureCacheAllocInfo { + target: ImageBufferKind::Texture2D, + width: size.width, + height: size.height, + format: parameters.formats.internal, + filter: parameters.filter, + is_shared_cache: true, + has_depth: false, + category: TextureCacheCategory::Atlas, + }, + ); + + texture_id + }, + ); + + let formats = &allocator_list.texture_parameters().formats; + + let swizzle = if formats.external == params.descriptor.format { + Swizzle::default() + } else { + match self.swizzle { + Some(_) => Swizzle::Bgra, + None => Swizzle::default(), + } + }; + + let bpp = formats.internal.bytes_per_pixel(); + let allocated_size_in_bytes = (allocated_rect.area() * bpp) as usize; + self.bytes_allocated[budget_type as usize] += allocated_size_in_bytes; + + (CacheEntry { + size: params.descriptor.size, + user_data: params.user_data, + last_access: self.now, + details: EntryDetails::Cache { + origin: allocated_rect.min, + alloc_id, + allocated_size_in_bytes, + }, + uv_rect_handle: GpuCacheHandle::new(), + input_format: params.descriptor.format, + filter: params.filter, + swizzle, + texture_id, + eviction_notice: None, + uv_rect_kind: params.uv_rect_kind, + shader: params.shader + }, budget_type) + } + + // Returns true if the given image descriptor *may* be + // placed in the shared texture cache. + pub fn is_allowed_in_shared_cache( + &self, + filter: TextureFilter, + descriptor: &ImageDescriptor, + ) -> bool { + let mut allowed_in_shared_cache = true; + + if matches!(descriptor.format, ImageFormat::RGBA8 | ImageFormat::BGRA8) + && filter == TextureFilter::Linear + { + // Allow the maximum that can fit in the linear color texture's two column layout. + let max = self.shared_textures.color8_linear.size() / 2; + allowed_in_shared_cache = descriptor.size.width.max(descriptor.size.height) <= max; + } else if descriptor.size.width > TEXTURE_REGION_DIMENSIONS { + allowed_in_shared_cache = false; + } + + if descriptor.size.height > TEXTURE_REGION_DIMENSIONS { + allowed_in_shared_cache = false; + } + + // TODO(gw): For now, alpha formats of the texture cache can only be linearly sampled. + // Nearest sampling gets a standalone texture. + // This is probably rare enough that it can be fixed up later. + if filter == TextureFilter::Nearest && + descriptor.format.bytes_per_pixel() <= 2 + { + allowed_in_shared_cache = false; + } + + allowed_in_shared_cache + } + + /// Allocate a render target via the pending updates sent to the renderer + pub fn alloc_render_target( + &mut self, + size: DeviceIntSize, + format: ImageFormat, + ) -> CacheTextureId { + let texture_id = self.next_id; + self.next_id.0 += 1; + + // Push a command to allocate device storage of the right size / format. + let info = TextureCacheAllocInfo { + target: ImageBufferKind::Texture2D, + width: size.width, + height: size.height, + format, + filter: TextureFilter::Linear, + is_shared_cache: false, + has_depth: false, + category: TextureCacheCategory::RenderTarget, + }; + + self.pending_updates.push_alloc(texture_id, info); + + texture_id + } + + /// Free an existing render target + pub fn free_render_target( + &mut self, + id: CacheTextureId, + ) { + self.pending_updates.push_free(id); + } + + /// Allocates a new standalone cache entry. + fn allocate_standalone_entry( + &mut self, + params: &CacheAllocParams, + ) -> (CacheEntry, BudgetType) { + let texture_id = self.next_id; + self.next_id.0 += 1; + + // Push a command to allocate device storage of the right size / format. + let info = TextureCacheAllocInfo { + target: ImageBufferKind::Texture2D, + width: params.descriptor.size.width, + height: params.descriptor.size.height, + format: params.descriptor.format, + filter: params.filter, + is_shared_cache: false, + has_depth: false, + category: TextureCacheCategory::Standalone, + }; + + let size_in_bytes = (info.width * info.height * info.format.bytes_per_pixel()) as usize; + self.bytes_allocated[BudgetType::Standalone as usize] += size_in_bytes; + + self.pending_updates.push_alloc(texture_id, info); + + // Special handing for BGRA8 textures that may need to be swizzled. + let swizzle = if params.descriptor.format == ImageFormat::BGRA8 { + self.swizzle.map(|s| s.bgra8_sampling_swizzle) + } else { + None + }; + + (CacheEntry::new_standalone( + texture_id, + self.now, + params, + swizzle.unwrap_or_default(), + size_in_bytes, + ), BudgetType::Standalone) + } + + /// Allocates a cache entry for the given parameters, and updates the + /// provided handle to point to the new entry. + fn allocate( + &mut self, + params: &CacheAllocParams, + handle: &mut TextureCacheHandle, + eviction: Eviction, + ) { + debug_assert!(self.now.is_valid()); + assert!(!params.descriptor.size.is_empty()); + + // If this image doesn't qualify to go in the shared (batching) cache, + // allocate a standalone entry. + let use_shared_cache = self.is_allowed_in_shared_cache(params.filter, ¶ms.descriptor); + let (new_cache_entry, budget_type) = if use_shared_cache { + self.allocate_from_shared_cache(params) + } else { + self.allocate_standalone_entry(params) + }; + + let details = new_cache_entry.details.clone(); + let texture_id = new_cache_entry.texture_id; + + // If the handle points to a valid cache entry, we want to replace the + // cache entry with our newly updated location. We also need to ensure + // that the storage (region or standalone) associated with the previous + // entry here gets freed. + // + // If the handle is invalid, we need to insert the data, and append the + // result to the corresponding vector. + let old_entry = match (&mut *handle, eviction) { + (TextureCacheHandle::Auto(handle), Eviction::Auto) => { + self.lru_cache.replace_or_insert(handle, budget_type as u8, new_cache_entry) + }, + (TextureCacheHandle::Manual(handle), Eviction::Manual) => { + let entry = self.manual_entries.get_opt_mut(handle) + .expect("Don't call this after evicting"); + Some(mem::replace(entry, new_cache_entry)) + }, + (TextureCacheHandle::Manual(_), Eviction::Auto) | + (TextureCacheHandle::Auto(_), Eviction::Manual) => { + panic!("Can't change eviction policy after initial allocation"); + }, + (TextureCacheHandle::Empty, Eviction::Auto) => { + let new_handle = self.lru_cache.push_new(budget_type as u8, new_cache_entry); + *handle = TextureCacheHandle::Auto(new_handle); + None + }, + (TextureCacheHandle::Empty, Eviction::Manual) => { + let manual_handle = self.manual_entries.insert(new_cache_entry); + let new_handle = manual_handle.weak(); + self.manual_handles.push(manual_handle); + *handle = TextureCacheHandle::Manual(new_handle); + None + }, + }; + if let Some(old_entry) = old_entry { + old_entry.evict(); + self.free(&old_entry); + } + + if let EntryDetails::Cache { alloc_id, .. } = details { + let allocator_list = self.shared_textures.select( + params.descriptor.format, + params.filter, + params.shader, + ).0; + + allocator_list.set_handle(texture_id, alloc_id, handle); + } + } + + pub fn shared_alpha_expected_format(&self) -> ImageFormat { + self.shared_textures.alpha8_linear.texture_parameters().formats.external + } + + pub fn shared_color_expected_format(&self) -> ImageFormat { + self.shared_textures.color8_linear.texture_parameters().formats.external + } + + + #[cfg(test)] + pub fn total_allocated_bytes_for_testing(&self) -> usize { + BudgetType::iter().map(|b| self.bytes_allocated[b as usize]).sum() + } + + pub fn report_memory(&self, ops: &mut MallocSizeOfOps) -> usize { + self.lru_cache.size_of(ops) + } +} + +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TextureParameters { + pub formats: TextureFormatPair<ImageFormat>, + pub filter: TextureFilter, +} + +impl TextureCacheUpdate { + // Constructs a TextureCacheUpdate operation to be passed to the + // rendering thread in order to do an upload to the right + // location in the texture cache. + fn new_update( + data: CachedImageData, + descriptor: &ImageDescriptor, + origin: DeviceIntPoint, + size: DeviceIntSize, + use_upload_format: bool, + dirty_rect: &ImageDirtyRect, + ) -> TextureCacheUpdate { + let source = match data { + CachedImageData::Blob => { + panic!("The vector image should have been rasterized."); + } + CachedImageData::External(ext_image) => match ext_image.image_type { + ExternalImageType::TextureHandle(_) => { + panic!("External texture handle should not go through texture_cache."); + } + ExternalImageType::Buffer => TextureUpdateSource::External { + id: ext_image.id, + channel_index: ext_image.channel_index, + }, + }, + CachedImageData::Raw(bytes) => { + let finish = descriptor.offset + + descriptor.size.width * descriptor.format.bytes_per_pixel() + + (descriptor.size.height - 1) * descriptor.compute_stride(); + assert!(bytes.len() >= finish as usize); + + TextureUpdateSource::Bytes { data: bytes } + } + }; + let format_override = if use_upload_format { + Some(descriptor.format) + } else { + None + }; + + match *dirty_rect { + DirtyRect::Partial(dirty) => { + // the dirty rectangle doesn't have to be within the area but has to intersect it, at least + let stride = descriptor.compute_stride(); + let offset = descriptor.offset + dirty.min.y * stride + dirty.min.x * descriptor.format.bytes_per_pixel(); + + TextureCacheUpdate { + rect: DeviceIntRect::from_origin_and_size( + DeviceIntPoint::new(origin.x + dirty.min.x, origin.y + dirty.min.y), + DeviceIntSize::new( + dirty.width().min(size.width - dirty.min.x), + dirty.height().min(size.height - dirty.min.y), + ), + ), + source, + stride: Some(stride), + offset, + format_override, + } + } + DirtyRect::All => { + TextureCacheUpdate { + rect: DeviceIntRect::from_origin_and_size(origin, size), + source, + stride: descriptor.stride, + offset: descriptor.offset, + format_override, + } + } + } + } +} + +#[cfg(test)] +mod test_texture_cache { + #[test] + fn check_allocation_size_balance() { + // Allocate some glyphs, observe the total allocation size, and free + // the glyphs again. Check that the total allocation size is back at the + // original value. + + use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader}; + use crate::gpu_cache::GpuCache; + use crate::device::TextureFilter; + use crate::gpu_types::UvRectKind; + use api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, DirtyRect}; + use api::units::*; + use euclid::size2; + let mut texture_cache = TextureCache::new_for_testing(2048, ImageFormat::BGRA8); + let mut gpu_cache = GpuCache::new_for_testing(); + + let sizes: &[DeviceIntSize] = &[ + size2(23, 27), + size2(15, 22), + size2(11, 5), + size2(20, 25), + size2(38, 41), + size2(11, 19), + size2(13, 21), + size2(37, 40), + size2(13, 15), + size2(14, 16), + size2(10, 9), + size2(25, 28), + ]; + + let bytes_at_start = texture_cache.total_allocated_bytes_for_testing(); + + let handles: Vec<TextureCacheHandle> = sizes.iter().map(|size| { + let mut texture_cache_handle = TextureCacheHandle::invalid(); + texture_cache.request(&texture_cache_handle, &mut gpu_cache); + texture_cache.update( + &mut texture_cache_handle, + ImageDescriptor { + size: *size, + stride: None, + format: ImageFormat::BGRA8, + flags: ImageDescriptorFlags::empty(), + offset: 0, + }, + TextureFilter::Linear, + None, + [0.0; 4], + DirtyRect::All, + &mut gpu_cache, + None, + UvRectKind::Rect, + Eviction::Manual, + TargetShader::Text, + ); + texture_cache_handle + }).collect(); + + let bytes_after_allocating = texture_cache.total_allocated_bytes_for_testing(); + assert!(bytes_after_allocating > bytes_at_start); + + for handle in handles { + texture_cache.evict_handle(&handle); + } + + let bytes_at_end = texture_cache.total_allocated_bytes_for_testing(); + assert_eq!(bytes_at_end, bytes_at_start); + } +} |