summaryrefslogtreecommitdiffstats
path: root/gfx/wr/webrender/src/texture_cache.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/webrender/src/texture_cache.rs')
-rw-r--r--gfx/wr/webrender/src/texture_cache.rs1707
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(&params, 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, &params.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);
+ }
+}