texture_cache.rs (69759B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 use api::{DirtyRect, ExternalImageType, ImageFormat, ImageBufferKind}; 6 use api::{DebugFlags, ImageDescriptor, TextureCacheCategory}; 7 use api::units::*; 8 #[cfg(test)] 9 use api::{DocumentId, IdNamespace}; 10 use crate::device::{TextureFilter, TextureFormatPair}; 11 use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle}; 12 use crate::gpu_types::{ImageSource, UvRectKind}; 13 use crate::internal_types::{ 14 CacheTextureId, Swizzle, SwizzleSettings, FrameStamp, FrameId, 15 TextureUpdateList, TextureUpdateSource, TextureSource, 16 TextureCacheAllocInfo, TextureCacheUpdate, 17 }; 18 use crate::lru_cache::LRUCache; 19 use crate::profiler::{self, TransactionProfile}; 20 use crate::renderer::{GpuBufferBuilderF, GpuBufferHandle}; 21 use crate::resource_cache::{CacheItem, CachedImageData}; 22 use crate::texture_pack::{ 23 AllocatorList, AllocId, AtlasAllocatorList, ShelfAllocator, ShelfAllocatorOptions, 24 }; 25 use std::cell::Cell; 26 use std::mem; 27 use std::rc::Rc; 28 use euclid::size2; 29 use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; 30 31 /// Information about which shader will use the entry. 32 /// 33 /// For batching purposes, it's beneficial to group some items in their 34 /// own textures if we know that they are used by a specific shader. 35 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 36 #[cfg_attr(feature = "capture", derive(Serialize))] 37 #[cfg_attr(feature = "replay", derive(Deserialize))] 38 pub enum TargetShader { 39 Default, 40 Text, 41 } 42 43 /// The size of each region in shared cache texture arrays. 44 pub const TEXTURE_REGION_DIMENSIONS: i32 = 512; 45 46 /// Items in the texture cache can either be standalone textures, 47 /// or a sub-rect inside the shared cache. 48 #[derive(Clone, Debug)] 49 #[cfg_attr(feature = "capture", derive(Serialize))] 50 #[cfg_attr(feature = "replay", derive(Deserialize))] 51 pub enum EntryDetails { 52 Standalone { 53 /// Number of bytes this entry allocates 54 size_in_bytes: usize, 55 }, 56 Cache { 57 /// Origin within the texture layer where this item exists. 58 origin: DeviceIntPoint, 59 /// ID of the allocation specific to its allocator. 60 alloc_id: AllocId, 61 /// The allocated size in bytes for this entry. 62 allocated_size_in_bytes: usize, 63 }, 64 } 65 66 impl EntryDetails { 67 fn describe(&self) -> DeviceIntPoint { 68 match *self { 69 EntryDetails::Standalone { .. } => DeviceIntPoint::zero(), 70 EntryDetails::Cache { origin, .. } => origin, 71 } 72 } 73 } 74 75 #[derive(Debug, PartialEq)] 76 #[cfg_attr(feature = "capture", derive(Serialize))] 77 #[cfg_attr(feature = "replay", derive(Deserialize))] 78 pub enum AutoCacheEntryMarker {} 79 80 #[derive(Debug, PartialEq)] 81 #[cfg_attr(feature = "capture", derive(Serialize))] 82 #[cfg_attr(feature = "replay", derive(Deserialize))] 83 pub enum ManualCacheEntryMarker {} 84 85 // Stores information related to a single entry in the texture 86 // cache. This is stored for each item whether it's in the shared 87 // cache or a standalone texture. 88 #[derive(Debug)] 89 #[cfg_attr(feature = "capture", derive(Serialize))] 90 #[cfg_attr(feature = "replay", derive(Deserialize))] 91 pub struct CacheEntry { 92 /// Size of the requested item, in device pixels. Does not include any 93 /// padding for alignment that the allocator may have added to this entry's 94 /// allocation. 95 pub size: DeviceIntSize, 96 /// Details specific to standalone or shared items. 97 pub details: EntryDetails, 98 /// Arbitrary user data associated with this item. 99 pub user_data: [f32; 4], 100 /// The last frame this item was requested for rendering. 101 pub last_access: FrameStamp, 102 /// Address of the resource rect in the GPU cache. 103 /// 104 /// The handle is stored in the cache entry to avoid duplicates when an 105 /// item is used multiple times per frame, but greate care must be taken 106 /// to not reuse a handle that was created in a previous frame. 107 /// TODO: For now the validity of the handle can be checked by comparing 108 /// last_access with the current FrameStamp, but this is error prone. 109 pub uv_rect_handle: GpuBufferHandle, 110 /// Image format of the data that the entry expects. 111 pub input_format: ImageFormat, 112 pub filter: TextureFilter, 113 pub swizzle: Swizzle, 114 /// The actual device texture ID this is part of. 115 pub texture_id: CacheTextureId, 116 /// Optional notice when the entry is evicted from the cache. 117 pub eviction_notice: Option<EvictionNotice>, 118 /// The type of UV rect this entry specifies. 119 pub uv_rect_kind: UvRectKind, 120 121 pub shader: TargetShader, 122 } 123 124 malloc_size_of::malloc_size_of_is_0!( 125 CacheEntry, 126 AutoCacheEntryMarker, ManualCacheEntryMarker 127 ); 128 129 impl CacheEntry { 130 // Create a new entry for a standalone texture. 131 fn new_standalone( 132 texture_id: CacheTextureId, 133 last_access: FrameStamp, 134 params: &CacheAllocParams, 135 swizzle: Swizzle, 136 size_in_bytes: usize, 137 ) -> Self { 138 CacheEntry { 139 size: params.descriptor.size, 140 user_data: params.user_data, 141 last_access, 142 details: EntryDetails::Standalone { 143 size_in_bytes, 144 }, 145 texture_id, 146 input_format: params.descriptor.format, 147 filter: params.filter, 148 swizzle, 149 uv_rect_handle: GpuBufferHandle::INVALID, 150 eviction_notice: None, 151 uv_rect_kind: params.uv_rect_kind, 152 shader: TargetShader::Default, 153 } 154 } 155 156 // Update the GPU cache for this texture cache entry. 157 // This ensures that the UV rect, and texture layer index 158 // are up to date in the GPU cache for vertex shaders 159 // to fetch from. 160 fn write_gpu_blocks(&mut self, gpu_buffer: &mut GpuBufferBuilderF) { 161 let origin = self.details.describe(); 162 let image_source = ImageSource { 163 p0: origin.to_f32(), 164 p1: (origin + self.size).to_f32(), 165 user_data: self.user_data, 166 uv_rect_kind: self.uv_rect_kind, 167 }; 168 self.uv_rect_handle = image_source.write_gpu_blocks(gpu_buffer); 169 } 170 171 fn evict(&self) { 172 if let Some(eviction_notice) = self.eviction_notice.as_ref() { 173 eviction_notice.notify(); 174 } 175 } 176 177 fn alternative_input_format(&self) -> ImageFormat { 178 match self.input_format { 179 ImageFormat::RGBA8 => ImageFormat::BGRA8, 180 ImageFormat::BGRA8 => ImageFormat::RGBA8, 181 other => other, 182 } 183 } 184 } 185 186 187 /// A texture cache handle is a weak reference to a cache entry. 188 /// 189 /// If the handle has not been inserted into the cache yet, or if the entry was 190 /// previously inserted and then evicted, lookup of the handle will fail, and 191 /// the cache handle needs to re-upload this item to the texture cache (see 192 /// request() below). 193 194 #[derive(MallocSizeOf,Clone,PartialEq,Debug)] 195 #[cfg_attr(feature = "capture", derive(Serialize))] 196 #[cfg_attr(feature = "replay", derive(Deserialize))] 197 pub enum TextureCacheHandle { 198 /// A fresh handle. 199 Empty, 200 201 /// A handle for an entry with automatic eviction. 202 Auto(WeakFreeListHandle<AutoCacheEntryMarker>), 203 204 /// A handle for an entry with manual eviction. 205 Manual(WeakFreeListHandle<ManualCacheEntryMarker>) 206 } 207 208 impl TextureCacheHandle { 209 pub fn invalid() -> Self { 210 TextureCacheHandle::Empty 211 } 212 } 213 214 /// Describes the eviction policy for a given entry in the texture cache. 215 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 216 #[cfg_attr(feature = "capture", derive(Serialize))] 217 #[cfg_attr(feature = "replay", derive(Deserialize))] 218 pub enum Eviction { 219 /// The entry will be evicted under the normal rules (which differ between 220 /// standalone and shared entries). 221 Auto, 222 /// The entry will not be evicted until the policy is explicitly set to a 223 /// different value. 224 Manual, 225 } 226 227 // An eviction notice is a shared condition useful for detecting 228 // when a TextureCacheHandle gets evicted from the TextureCache. 229 // It is optionally installed to the TextureCache when an update() 230 // is scheduled. A single notice may be shared among any number of 231 // TextureCacheHandle updates. The notice may then be subsequently 232 // checked to see if any of the updates using it have been evicted. 233 #[derive(Clone, Debug, Default)] 234 #[cfg_attr(feature = "capture", derive(Serialize))] 235 #[cfg_attr(feature = "replay", derive(Deserialize))] 236 pub struct EvictionNotice { 237 evicted: Rc<Cell<bool>>, 238 } 239 240 impl EvictionNotice { 241 fn notify(&self) { 242 self.evicted.set(true); 243 } 244 245 pub fn check(&self) -> bool { 246 if self.evicted.get() { 247 self.evicted.set(false); 248 true 249 } else { 250 false 251 } 252 } 253 } 254 255 /// The different budget types for the texture cache. Each type has its own 256 /// memory budget. Once the budget is exceeded, entries with automatic eviction 257 /// are evicted. Entries with manual eviction share the same budget but are not 258 /// evicted once the budget is exceeded. 259 /// Keeping separate budgets ensures that we don't evict entries from unrelated 260 /// textures if one texture gets full. 261 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 262 #[repr(u8)] 263 #[cfg_attr(feature = "capture", derive(Serialize))] 264 #[cfg_attr(feature = "replay", derive(Deserialize))] 265 enum BudgetType { 266 SharedColor8Linear, 267 SharedColor8Nearest, 268 SharedColor8Glyphs, 269 SharedAlpha8, 270 SharedAlpha8Glyphs, 271 SharedAlpha16, 272 Standalone, 273 } 274 275 impl BudgetType { 276 pub const COUNT: usize = 7; 277 278 pub const VALUES: [BudgetType; BudgetType::COUNT] = [ 279 BudgetType::SharedColor8Linear, 280 BudgetType::SharedColor8Nearest, 281 BudgetType::SharedColor8Glyphs, 282 BudgetType::SharedAlpha8, 283 BudgetType::SharedAlpha8Glyphs, 284 BudgetType::SharedAlpha16, 285 BudgetType::Standalone, 286 ]; 287 288 pub const PRESSURE_COUNTERS: [usize; BudgetType::COUNT] = [ 289 profiler::ATLAS_COLOR8_LINEAR_PRESSURE, 290 profiler::ATLAS_COLOR8_NEAREST_PRESSURE, 291 profiler::ATLAS_COLOR8_GLYPHS_PRESSURE, 292 profiler::ATLAS_ALPHA8_PRESSURE, 293 profiler::ATLAS_ALPHA8_GLYPHS_PRESSURE, 294 profiler::ATLAS_ALPHA16_PRESSURE, 295 profiler::ATLAS_STANDALONE_PRESSURE, 296 ]; 297 298 pub fn iter() -> impl Iterator<Item = BudgetType> { 299 BudgetType::VALUES.iter().cloned() 300 } 301 } 302 303 /// A set of lazily allocated, fixed size, texture arrays for each format the 304 /// texture cache supports. 305 #[cfg_attr(feature = "capture", derive(Serialize))] 306 #[cfg_attr(feature = "replay", derive(Deserialize))] 307 struct SharedTextures { 308 color8_nearest: AllocatorList<ShelfAllocator, TextureParameters>, 309 alpha8_linear: AllocatorList<ShelfAllocator, TextureParameters>, 310 alpha8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>, 311 alpha16_linear: AllocatorList<ShelfAllocator, TextureParameters>, 312 color8_linear: AllocatorList<ShelfAllocator, TextureParameters>, 313 color8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>, 314 bytes_per_texture_of_type: [i32 ; BudgetType::COUNT], 315 next_compaction_idx: usize, 316 } 317 318 impl SharedTextures { 319 /// Mints a new set of shared textures. 320 fn new(color_formats: TextureFormatPair<ImageFormat>, config: &TextureCacheConfig) -> Self { 321 let mut bytes_per_texture_of_type = [0 ; BudgetType::COUNT]; 322 323 // Used primarily for cached shadow masks. There can be lots of 324 // these on some pages like francine, but most pages don't use it 325 // much. 326 // Most content tends to fit into two 512x512 textures. We are 327 // conservatively using 1024x1024 to fit everything in a single 328 // texture and avoid breaking batches, but it's worth checking 329 // whether it would actually lead to a lot of batch breaks in 330 // practice. 331 let alpha8_linear = AllocatorList::new( 332 config.alpha8_texture_size, 333 ShelfAllocatorOptions { 334 num_columns: 1, 335 alignment: size2(8, 8), 336 .. ShelfAllocatorOptions::default() 337 }, 338 TextureParameters { 339 formats: TextureFormatPair::from(ImageFormat::R8), 340 filter: TextureFilter::Linear, 341 }, 342 ); 343 bytes_per_texture_of_type[BudgetType::SharedAlpha8 as usize] = 344 config.alpha8_texture_size * config.alpha8_texture_size; 345 346 // The cache for alpha glyphs (separate to help with batching). 347 let alpha8_glyphs = AllocatorList::new( 348 config.alpha8_glyph_texture_size, 349 ShelfAllocatorOptions { 350 num_columns: if config.alpha8_glyph_texture_size >= 1024 { 2 } else { 1 }, 351 alignment: size2(4, 8), 352 .. ShelfAllocatorOptions::default() 353 }, 354 TextureParameters { 355 formats: TextureFormatPair::from(ImageFormat::R8), 356 filter: TextureFilter::Linear, 357 }, 358 ); 359 bytes_per_texture_of_type[BudgetType::SharedAlpha8Glyphs as usize] = 360 config.alpha8_glyph_texture_size * config.alpha8_glyph_texture_size; 361 362 // Used for experimental hdr yuv texture support, but not used in 363 // production Firefox. 364 let alpha16_linear = AllocatorList::new( 365 config.alpha16_texture_size, 366 ShelfAllocatorOptions { 367 num_columns: if config.alpha16_texture_size >= 1024 { 2 } else { 1 }, 368 alignment: size2(8, 8), 369 .. ShelfAllocatorOptions::default() 370 }, 371 TextureParameters { 372 formats: TextureFormatPair::from(ImageFormat::R16), 373 filter: TextureFilter::Linear, 374 }, 375 ); 376 bytes_per_texture_of_type[BudgetType::SharedAlpha16 as usize] = 377 ImageFormat::R16.bytes_per_pixel() * 378 config.alpha16_texture_size * config.alpha16_texture_size; 379 380 // The primary cache for images, etc. 381 let color8_linear = AllocatorList::new( 382 config.color8_linear_texture_size, 383 ShelfAllocatorOptions { 384 num_columns: if config.color8_linear_texture_size >= 1024 { 2 } else { 1 }, 385 alignment: size2(16, 16), 386 .. ShelfAllocatorOptions::default() 387 }, 388 TextureParameters { 389 formats: color_formats.clone(), 390 filter: TextureFilter::Linear, 391 }, 392 ); 393 bytes_per_texture_of_type[BudgetType::SharedColor8Linear as usize] = 394 color_formats.internal.bytes_per_pixel() * 395 config.color8_linear_texture_size * config.color8_linear_texture_size; 396 397 // The cache for subpixel-AA and bitmap glyphs (separate to help with batching). 398 let color8_glyphs = AllocatorList::new( 399 config.color8_glyph_texture_size, 400 ShelfAllocatorOptions { 401 num_columns: if config.color8_glyph_texture_size >= 1024 { 2 } else { 1 }, 402 alignment: size2(4, 8), 403 .. ShelfAllocatorOptions::default() 404 }, 405 TextureParameters { 406 formats: color_formats.clone(), 407 filter: TextureFilter::Linear, 408 }, 409 ); 410 bytes_per_texture_of_type[BudgetType::SharedColor8Glyphs as usize] = 411 color_formats.internal.bytes_per_pixel() * 412 config.color8_glyph_texture_size * config.color8_glyph_texture_size; 413 414 // Used for image-rendering: crisp. This is mostly favicons, which 415 // are small. Some other images use it too, but those tend to be 416 // larger than 512x512 and thus don't use the shared cache anyway. 417 let color8_nearest = AllocatorList::new( 418 config.color8_nearest_texture_size, 419 ShelfAllocatorOptions::default(), 420 TextureParameters { 421 formats: color_formats.clone(), 422 filter: TextureFilter::Nearest, 423 } 424 ); 425 bytes_per_texture_of_type[BudgetType::SharedColor8Nearest as usize] = 426 color_formats.internal.bytes_per_pixel() * 427 config.color8_nearest_texture_size * config.color8_nearest_texture_size; 428 429 Self { 430 alpha8_linear, 431 alpha8_glyphs, 432 alpha16_linear, 433 color8_linear, 434 color8_glyphs, 435 color8_nearest, 436 bytes_per_texture_of_type, 437 next_compaction_idx: 0, 438 } 439 } 440 441 /// Clears each texture in the set, with the given set of pending updates. 442 fn clear(&mut self, updates: &mut TextureUpdateList) { 443 let texture_dealloc_cb = &mut |texture_id| { 444 updates.push_free(texture_id); 445 }; 446 447 self.alpha8_linear.clear(texture_dealloc_cb); 448 self.alpha8_glyphs.clear(texture_dealloc_cb); 449 self.alpha16_linear.clear(texture_dealloc_cb); 450 self.color8_linear.clear(texture_dealloc_cb); 451 self.color8_nearest.clear(texture_dealloc_cb); 452 self.color8_glyphs.clear(texture_dealloc_cb); 453 } 454 455 /// Returns a mutable borrow for the shared texture array matching the parameters. 456 fn select( 457 &mut self, external_format: ImageFormat, filter: TextureFilter, shader: TargetShader, 458 ) -> (&mut dyn AtlasAllocatorList<TextureParameters>, BudgetType) { 459 match external_format { 460 ImageFormat::R8 => { 461 assert_eq!(filter, TextureFilter::Linear); 462 match shader { 463 TargetShader::Text => { 464 (&mut self.alpha8_glyphs, BudgetType::SharedAlpha8Glyphs) 465 }, 466 _ => (&mut self.alpha8_linear, BudgetType::SharedAlpha8), 467 } 468 } 469 ImageFormat::R16 => { 470 assert_eq!(filter, TextureFilter::Linear); 471 (&mut self.alpha16_linear, BudgetType::SharedAlpha16) 472 } 473 ImageFormat::RGBA8 | 474 ImageFormat::BGRA8 => { 475 match (filter, shader) { 476 (TextureFilter::Linear, TargetShader::Text) => { 477 (&mut self.color8_glyphs, BudgetType::SharedColor8Glyphs) 478 }, 479 (TextureFilter::Linear, _) => { 480 (&mut self.color8_linear, BudgetType::SharedColor8Linear) 481 }, 482 (TextureFilter::Nearest, _) => { 483 (&mut self.color8_nearest, BudgetType::SharedColor8Nearest) 484 }, 485 _ => panic!("Unexpected filter {:?}", filter), 486 } 487 } 488 _ => panic!("Unexpected format {:?}", external_format), 489 } 490 } 491 492 /// How many bytes a single texture of the given type takes up, for the 493 /// configured texture sizes. 494 fn bytes_per_shared_texture(&self, budget_type: BudgetType) -> usize { 495 self.bytes_per_texture_of_type[budget_type as usize] as usize 496 } 497 498 fn has_multiple_textures(&self, budget_type: BudgetType) -> bool { 499 match budget_type { 500 BudgetType::SharedColor8Linear => self.color8_linear.allocated_textures() > 1, 501 BudgetType::SharedColor8Nearest => self.color8_nearest.allocated_textures() > 1, 502 BudgetType::SharedColor8Glyphs => self.color8_glyphs.allocated_textures() > 1, 503 BudgetType::SharedAlpha8 => self.alpha8_linear.allocated_textures() > 1, 504 BudgetType::SharedAlpha8Glyphs => self.alpha8_glyphs.allocated_textures() > 1, 505 BudgetType::SharedAlpha16 => self.alpha16_linear.allocated_textures() > 1, 506 BudgetType::Standalone => false, 507 } 508 } 509 } 510 511 /// Container struct for the various parameters used in cache allocation. 512 struct CacheAllocParams { 513 descriptor: ImageDescriptor, 514 filter: TextureFilter, 515 user_data: [f32; 4], 516 uv_rect_kind: UvRectKind, 517 shader: TargetShader, 518 } 519 520 /// Startup parameters for the texture cache. 521 /// 522 /// Texture sizes must be at least 512. 523 #[derive(Clone)] 524 pub struct TextureCacheConfig { 525 pub color8_linear_texture_size: i32, 526 pub color8_nearest_texture_size: i32, 527 pub color8_glyph_texture_size: i32, 528 pub alpha8_texture_size: i32, 529 pub alpha8_glyph_texture_size: i32, 530 pub alpha16_texture_size: i32, 531 } 532 533 impl TextureCacheConfig { 534 pub const DEFAULT: Self = TextureCacheConfig { 535 color8_linear_texture_size: 2048, 536 color8_nearest_texture_size: 512, 537 color8_glyph_texture_size: 2048, 538 alpha8_texture_size: 1024, 539 alpha8_glyph_texture_size: 2048, 540 alpha16_texture_size: 512, 541 }; 542 } 543 544 /// General-purpose manager for images in GPU memory. This includes images, 545 /// rasterized glyphs, rasterized blobs, cached render tasks, etc. 546 /// 547 /// The texture cache is owned and managed by the RenderBackend thread, and 548 /// produces a series of commands to manipulate the textures on the Renderer 549 /// thread. These commands are executed before any rendering is performed for 550 /// a given frame. 551 /// 552 /// Entries in the texture cache are not guaranteed to live past the end of the 553 /// frame in which they are requested, and may be evicted. The API supports 554 /// querying whether an entry is still available. 555 /// 556 /// The texture cache can be visualized, which is a good way to understand how 557 /// it works. Enabling gfx.webrender.debug.texture-cache shows a live view of 558 /// its contents in Firefox. 559 #[cfg_attr(feature = "capture", derive(Serialize))] 560 #[cfg_attr(feature = "replay", derive(Deserialize))] 561 pub struct TextureCache { 562 /// Set of texture arrays in different formats used for the shared cache. 563 shared_textures: SharedTextures, 564 565 /// Maximum texture size supported by hardware. 566 max_texture_size: i32, 567 568 /// Maximum texture size before it is considered preferable to break the 569 /// texture into tiles. 570 tiling_threshold: i32, 571 572 /// Settings on using texture unit swizzling. 573 swizzle: Option<SwizzleSettings>, 574 575 /// The current set of debug flags. 576 debug_flags: DebugFlags, 577 578 /// The next unused virtual texture ID. Monotonically increasing. 579 pub next_id: CacheTextureId, 580 581 /// A list of allocations and updates that need to be applied to the texture 582 /// cache in the rendering thread this frame. 583 #[cfg_attr(all(feature = "serde", any(feature = "capture", feature = "replay")), serde(skip))] 584 pub pending_updates: TextureUpdateList, 585 586 /// The current `FrameStamp`. Used for cache eviction policies. 587 pub now: FrameStamp, 588 589 /// Cache of texture cache handles with automatic lifetime management, evicted 590 /// in a least-recently-used order. 591 lru_cache: LRUCache<CacheEntry, AutoCacheEntryMarker>, 592 593 /// Cache of texture cache entries with manual liftime management. 594 manual_entries: FreeList<CacheEntry, ManualCacheEntryMarker>, 595 596 /// Strong handles for the manual_entries FreeList. 597 manual_handles: Vec<FreeListHandle<ManualCacheEntryMarker>>, 598 599 /// Memory usage of allocated entries in all of the shared or standalone 600 /// textures. Includes both manually and automatically evicted entries. 601 bytes_allocated: [usize ; BudgetType::COUNT], 602 } 603 604 impl TextureCache { 605 /// The maximum number of items that will be evicted per frame. This limit helps avoid jank 606 /// on frames where we want to evict a large number of items. Instead, we'd prefer to drop 607 /// the items incrementally over a number of frames, even if that means the total allocated 608 /// size of the cache is above the desired threshold for a small number of frames. 609 const MAX_EVICTIONS_PER_FRAME: usize = 32; 610 611 pub fn new( 612 max_texture_size: i32, 613 tiling_threshold: i32, 614 color_formats: TextureFormatPair<ImageFormat>, 615 swizzle: Option<SwizzleSettings>, 616 config: &TextureCacheConfig, 617 ) -> Self { 618 let pending_updates = TextureUpdateList::new(); 619 620 // Shared texture cache controls swizzling on a per-entry basis, assuming that 621 // the texture as a whole doesn't need to be swizzled (but only some entries do). 622 // It would be possible to support this, but not needed at the moment. 623 assert!(color_formats.internal != ImageFormat::BGRA8 || 624 swizzle.map_or(true, |s| s.bgra8_sampling_swizzle == Swizzle::default()) 625 ); 626 627 let next_texture_id = CacheTextureId(1); 628 629 TextureCache { 630 shared_textures: SharedTextures::new(color_formats, config), 631 max_texture_size, 632 tiling_threshold, 633 swizzle, 634 debug_flags: DebugFlags::empty(), 635 next_id: next_texture_id, 636 pending_updates, 637 now: FrameStamp::INVALID, 638 lru_cache: LRUCache::new(BudgetType::COUNT), 639 manual_entries: FreeList::new(), 640 manual_handles: Vec::new(), 641 bytes_allocated: [0 ; BudgetType::COUNT], 642 } 643 } 644 645 /// Creates a TextureCache and sets it up with a valid `FrameStamp`, which 646 /// is useful for avoiding panics when instantiating the `TextureCache` 647 /// directly from unit test code. 648 #[cfg(test)] 649 pub fn new_for_testing( 650 max_texture_size: i32, 651 image_format: ImageFormat, 652 ) -> Self { 653 let mut cache = Self::new( 654 max_texture_size, 655 max_texture_size, 656 TextureFormatPair::from(image_format), 657 None, 658 &TextureCacheConfig::DEFAULT, 659 ); 660 let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1)); 661 now.advance(); 662 cache.begin_frame(now, &mut TransactionProfile::new()); 663 cache 664 } 665 666 pub fn set_debug_flags(&mut self, flags: DebugFlags) { 667 self.debug_flags = flags; 668 } 669 670 /// Clear all entries in the texture cache. This is a fairly drastic 671 /// step that should only be called very rarely. 672 pub fn clear_all(&mut self) { 673 // Evict all manual eviction handles 674 let manual_handles = mem::replace( 675 &mut self.manual_handles, 676 Vec::new(), 677 ); 678 for handle in manual_handles { 679 let entry = self.manual_entries.free(handle); 680 self.evict_impl(entry); 681 } 682 683 // Evict all auto (LRU) cache handles 684 for budget_type in BudgetType::iter() { 685 while let Some(entry) = self.lru_cache.pop_oldest(budget_type as u8) { 686 entry.evict(); 687 self.free(&entry); 688 } 689 } 690 691 // Free the picture and shared textures 692 self.shared_textures.clear(&mut self.pending_updates); 693 self.pending_updates.note_clear(); 694 } 695 696 /// Called at the beginning of each frame. 697 pub fn begin_frame(&mut self, stamp: FrameStamp, profile: &mut TransactionProfile) { 698 debug_assert!(!self.now.is_valid()); 699 profile_scope!("begin_frame"); 700 self.now = stamp; 701 702 // Texture cache eviction is done at the start of the frame. This ensures that 703 // we won't evict items that have been requested on this frame. 704 // It also frees up space in the cache for items allocated later in the frame 705 // potentially reducing texture allocations and fragmentation. 706 self.evict_items_from_cache_if_required(profile); 707 } 708 709 pub fn end_frame(&mut self, profile: &mut TransactionProfile) { 710 debug_assert!(self.now.is_valid()); 711 712 let updates = &mut self.pending_updates; // To avoid referring to self in the closure. 713 let callback = &mut|texture_id| { updates.push_free(texture_id); }; 714 715 // Release of empty shared textures is done at the end of the frame. That way, if the 716 // eviction at the start of the frame frees up a texture, that is then subsequently 717 // used during the frame, we avoid doing a free/alloc for it. 718 self.shared_textures.alpha8_linear.release_empty_textures(callback); 719 self.shared_textures.alpha8_glyphs.release_empty_textures(callback); 720 self.shared_textures.alpha16_linear.release_empty_textures(callback); 721 self.shared_textures.color8_linear.release_empty_textures(callback); 722 self.shared_textures.color8_nearest.release_empty_textures(callback); 723 self.shared_textures.color8_glyphs.release_empty_textures(callback); 724 725 for budget in BudgetType::iter() { 726 let threshold = self.get_eviction_threshold(budget); 727 let pressure = self.bytes_allocated[budget as usize] as f32 / threshold as f32; 728 profile.set(BudgetType::PRESSURE_COUNTERS[budget as usize], pressure); 729 } 730 731 profile.set(profiler::ATLAS_A8_PIXELS, self.shared_textures.alpha8_linear.allocated_space()); 732 profile.set(profiler::ATLAS_A8_TEXTURES, self.shared_textures.alpha8_linear.allocated_textures()); 733 profile.set(profiler::ATLAS_A8_GLYPHS_PIXELS, self.shared_textures.alpha8_glyphs.allocated_space()); 734 profile.set(profiler::ATLAS_A8_GLYPHS_TEXTURES, self.shared_textures.alpha8_glyphs.allocated_textures()); 735 profile.set(profiler::ATLAS_A16_PIXELS, self.shared_textures.alpha16_linear.allocated_space()); 736 profile.set(profiler::ATLAS_A16_TEXTURES, self.shared_textures.alpha16_linear.allocated_textures()); 737 profile.set(profiler::ATLAS_RGBA8_LINEAR_PIXELS, self.shared_textures.color8_linear.allocated_space()); 738 profile.set(profiler::ATLAS_RGBA8_LINEAR_TEXTURES, self.shared_textures.color8_linear.allocated_textures()); 739 profile.set(profiler::ATLAS_RGBA8_NEAREST_PIXELS, self.shared_textures.color8_nearest.allocated_space()); 740 profile.set(profiler::ATLAS_RGBA8_NEAREST_TEXTURES, self.shared_textures.color8_nearest.allocated_textures()); 741 profile.set(profiler::ATLAS_RGBA8_GLYPHS_PIXELS, self.shared_textures.color8_glyphs.allocated_space()); 742 profile.set(profiler::ATLAS_RGBA8_GLYPHS_TEXTURES, self.shared_textures.color8_glyphs.allocated_textures()); 743 744 let shared_bytes = [ 745 BudgetType::SharedColor8Linear, 746 BudgetType::SharedColor8Nearest, 747 BudgetType::SharedColor8Glyphs, 748 BudgetType::SharedAlpha8, 749 BudgetType::SharedAlpha8Glyphs, 750 BudgetType::SharedAlpha16, 751 ].iter().map(|b| self.bytes_allocated[*b as usize]).sum(); 752 753 profile.set(profiler::ATLAS_ITEMS_MEM, profiler::bytes_to_mb(shared_bytes)); 754 755 self.now = FrameStamp::INVALID; 756 } 757 758 pub fn run_compaction(&mut self) { 759 // Use the same order as BudgetType::VALUES so that we can index self.bytes_allocated 760 // with the same index. 761 let allocator_lists = [ 762 &mut self.shared_textures.color8_linear, 763 &mut self.shared_textures.color8_nearest, 764 &mut self.shared_textures.color8_glyphs, 765 &mut self.shared_textures.alpha8_linear, 766 &mut self.shared_textures.alpha8_glyphs, 767 &mut self.shared_textures.alpha16_linear, 768 ]; 769 770 // Pick a texture type on which to try to run the compaction logic this frame. 771 let idx = self.shared_textures.next_compaction_idx; 772 773 // Number of moved pixels after which we stop attempting to move more items for this frame. 774 // The constant is up for adjustment, the main goal is to avoid causing frame spikes on 775 // low end GPUs. 776 let area_threshold = 512*512; 777 778 let mut changes = Vec::new(); 779 allocator_lists[idx].try_compaction(area_threshold, &mut changes); 780 781 if changes.is_empty() { 782 // Nothing to do, we'll try another texture type next frame. 783 self.shared_textures.next_compaction_idx = (self.shared_textures.next_compaction_idx + 1) % allocator_lists.len(); 784 } 785 786 for change in changes { 787 let bpp = allocator_lists[idx].texture_parameters().formats.internal.bytes_per_pixel(); 788 789 // While the area of the image does not change, the area it occupies in the texture 790 // atlas may (in other words the number of wasted pixels can change), so we have 791 // to keep track of that. 792 let old_bytes = (change.old_rect.area() * bpp) as usize; 793 let new_bytes = (change.new_rect.area() * bpp) as usize; 794 self.bytes_allocated[idx] -= old_bytes; 795 self.bytes_allocated[idx] += new_bytes; 796 797 let entry = match change.handle { 798 TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(&handle).unwrap(), 799 TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(&handle).unwrap(), 800 TextureCacheHandle::Empty => { panic!("invalid handle"); } 801 }; 802 entry.texture_id = change.new_tex; 803 entry.details = EntryDetails::Cache { 804 origin: change.new_rect.min, 805 alloc_id: change.new_id, 806 allocated_size_in_bytes: new_bytes, 807 }; 808 809 entry.uv_rect_handle = GpuBufferHandle::INVALID; 810 811 let src_rect = DeviceIntRect::from_origin_and_size(change.old_rect.min, entry.size); 812 let dst_rect = DeviceIntRect::from_origin_and_size(change.new_rect.min, entry.size); 813 814 self.pending_updates.push_copy(change.old_tex, &src_rect, change.new_tex, &dst_rect); 815 816 if self.debug_flags.contains( 817 DebugFlags::TEXTURE_CACHE_DBG | 818 DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) 819 { 820 self.pending_updates.push_debug_clear( 821 change.old_tex, 822 src_rect.min, 823 src_rect.width(), 824 src_rect.height(), 825 ); 826 } 827 } 828 } 829 830 // Request an item in the texture cache. All images that will 831 // be used on a frame *must* have request() called on their 832 // handle, to update the last used timestamp and ensure 833 // that resources are not flushed from the cache too early. 834 // 835 // Returns true if the image needs to be uploaded to the 836 // texture cache (either never uploaded, or has been 837 // evicted on a previous frame). 838 pub fn request(&mut self, handle: &TextureCacheHandle, gpu_buffer: &mut GpuBufferBuilderF) -> bool { 839 let now = self.now; 840 let entry = match handle { 841 TextureCacheHandle::Empty => None, 842 TextureCacheHandle::Auto(handle) => { 843 // Call touch rather than get_opt_mut so that the LRU index 844 // knows that the entry has been used. 845 self.lru_cache.touch(handle) 846 }, 847 TextureCacheHandle::Manual(handle) => { 848 self.manual_entries.get_opt_mut(handle) 849 }, 850 }; 851 entry.map_or(true, |entry| { 852 if entry.last_access != now { 853 // If an image is requested that is already in the cache, 854 // refresh the GPU buffer data associated with this item. 855 entry.last_access = now; 856 entry.write_gpu_blocks(gpu_buffer); 857 } 858 false 859 }) 860 } 861 862 fn get_entry_opt(&self, handle: &TextureCacheHandle) -> Option<&CacheEntry> { 863 match handle { 864 TextureCacheHandle::Empty => None, 865 TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt(handle), 866 TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt(handle), 867 } 868 } 869 870 fn get_entry_opt_mut(&mut self, handle: &TextureCacheHandle) -> Option<&mut CacheEntry> { 871 match handle { 872 TextureCacheHandle::Empty => None, 873 TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(handle), 874 TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(handle), 875 } 876 } 877 878 // Returns true if the image needs to be uploaded to the 879 // texture cache (either never uploaded, or has been 880 // evicted on a previous frame). 881 pub fn needs_upload(&self, handle: &TextureCacheHandle) -> bool { 882 !self.is_allocated(handle) 883 } 884 885 pub fn max_texture_size(&self) -> i32 { 886 self.max_texture_size 887 } 888 889 pub fn tiling_threshold(&self) -> i32 { 890 self.tiling_threshold 891 } 892 893 #[cfg(feature = "replay")] 894 pub fn color_formats(&self) -> TextureFormatPair<ImageFormat> { 895 self.shared_textures.color8_linear.texture_parameters().formats.clone() 896 } 897 898 #[cfg(feature = "replay")] 899 pub fn swizzle_settings(&self) -> Option<SwizzleSettings> { 900 self.swizzle 901 } 902 903 pub fn pending_updates(&mut self) -> TextureUpdateList { 904 mem::replace(&mut self.pending_updates, TextureUpdateList::new()) 905 } 906 907 // Update the data stored by a given texture cache handle. 908 pub fn update( 909 &mut self, 910 handle: &mut TextureCacheHandle, 911 descriptor: ImageDescriptor, 912 filter: TextureFilter, 913 data: Option<CachedImageData>, 914 user_data: [f32; 4], 915 mut dirty_rect: ImageDirtyRect, 916 gpu_buffer: &mut GpuBufferBuilderF, 917 eviction_notice: Option<&EvictionNotice>, 918 uv_rect_kind: UvRectKind, 919 eviction: Eviction, 920 shader: TargetShader, 921 force_standalone_texture: bool, 922 ) { 923 debug_assert!(self.now.is_valid()); 924 // Determine if we need to allocate texture cache memory 925 // for this item. We need to reallocate if any of the following 926 // is true: 927 // - Never been in the cache 928 // - Has been in the cache but was evicted. 929 // - Exists in the cache but dimensions / format have changed. 930 let realloc = match self.get_entry_opt(handle) { 931 Some(entry) => { 932 entry.size != descriptor.size || (entry.input_format != descriptor.format && 933 entry.alternative_input_format() != descriptor.format) 934 } 935 None => { 936 // Not allocated, or was previously allocated but has been evicted. 937 true 938 } 939 }; 940 941 if realloc { 942 let params = CacheAllocParams { descriptor, filter, user_data, uv_rect_kind, shader }; 943 self.allocate(¶ms, handle, eviction, force_standalone_texture); 944 945 // If we reallocated, we need to upload the whole item again. 946 dirty_rect = DirtyRect::All; 947 } 948 949 let now = self.now; 950 let entry = self.get_entry_opt_mut(handle) 951 .expect("BUG: There must be an entry at this handle now"); 952 953 // Install the new eviction notice for this update, if applicable. 954 entry.eviction_notice = eviction_notice.cloned(); 955 entry.uv_rect_kind = uv_rect_kind; 956 957 // If we just allocated the entry, its framestamp is up to date but it does 958 // not uset have up-to-date gpu blocks. 959 if entry.last_access != now || realloc { 960 entry.last_access = now; 961 // Upload the resource rect and texture array layer. 962 entry.write_gpu_blocks(gpu_buffer); 963 } 964 965 // Create an update command, which the render thread processes 966 // to upload the new image data into the correct location 967 // in GPU memory. 968 if let Some(data) = data { 969 // If the swizzling is supported, we always upload in the internal 970 // texture format (thus avoiding the conversion by the driver). 971 // Otherwise, pass the external format to the driver. 972 let origin = entry.details.describe(); 973 let texture_id = entry.texture_id; 974 let size = entry.size; 975 let use_upload_format = self.swizzle.is_none(); 976 let op = TextureCacheUpdate::new_update( 977 data, 978 &descriptor, 979 origin, 980 size, 981 use_upload_format, 982 &dirty_rect, 983 ); 984 self.pending_updates.push_update(texture_id, op); 985 } 986 } 987 988 // Check if a given texture handle has a valid allocation 989 // in the texture cache. 990 pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool { 991 self.get_entry_opt(handle).is_some() 992 } 993 994 // Return the allocated size of the texture handle's associated data, 995 // or otherwise indicate the handle is invalid. 996 pub fn get_allocated_size(&self, handle: &TextureCacheHandle) -> Option<usize> { 997 self.get_entry_opt(handle).map(|entry| { 998 (entry.input_format.bytes_per_pixel() * entry.size.area()) as usize 999 }) 1000 } 1001 1002 // Retrieve the details of an item in the cache. This is used 1003 // during batch creation to provide the resource rect address 1004 // to the shaders and texture ID to the batching logic. 1005 // This function will assert in debug modes if the caller 1006 // tries to get a handle that was not requested this frame. 1007 pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem { 1008 let (texture_id, uv_rect, swizzle, uv_rect_handle, user_data) = self.get_cache_location(handle); 1009 CacheItem { 1010 uv_rect_handle, 1011 texture_id: TextureSource::TextureCache( 1012 texture_id, 1013 swizzle, 1014 ), 1015 uv_rect, 1016 user_data, 1017 } 1018 } 1019 1020 pub fn try_get(&self, handle: &TextureCacheHandle) -> Option<CacheItem> { 1021 let (texture_id, uv_rect, swizzle, uv_rect_handle, user_data) = self.try_get_cache_location(handle)?; 1022 Some(CacheItem { 1023 uv_rect_handle, 1024 texture_id: TextureSource::TextureCache( 1025 texture_id, 1026 swizzle, 1027 ), 1028 uv_rect, 1029 user_data, 1030 }) 1031 } 1032 1033 pub fn try_get_cache_location( 1034 &self, 1035 handle: &TextureCacheHandle, 1036 ) -> Option<(CacheTextureId, DeviceIntRect, Swizzle, GpuBufferHandle, [f32; 4])> { 1037 let entry = self.get_entry_opt(handle)?; 1038 let origin = entry.details.describe(); 1039 if entry.last_access != self.now { 1040 // On rare occasions we may have an image request that does not materialize 1041 // into up to date data in the cache. For example if we failed to produce a 1042 // stacking context snapshot. 1043 return None; 1044 } 1045 Some(( 1046 entry.texture_id, 1047 DeviceIntRect::from_origin_and_size(origin, entry.size), 1048 entry.swizzle, 1049 entry.uv_rect_handle, 1050 entry.user_data, 1051 )) 1052 } 1053 1054 /// A more detailed version of get(). This allows access to the actual 1055 /// device rect of the cache allocation. 1056 /// 1057 /// Returns a tuple identifying the texture, the layer, the region, 1058 /// and its GPU handle. 1059 pub fn get_cache_location( 1060 &self, 1061 handle: &TextureCacheHandle, 1062 ) -> (CacheTextureId, DeviceIntRect, Swizzle, GpuBufferHandle, [f32; 4]) { 1063 self.try_get_cache_location(handle).expect("BUG: was dropped from cache or not updated!") 1064 } 1065 1066 /// Internal helper function to evict a strong texture cache handle 1067 fn evict_impl( 1068 &mut self, 1069 entry: CacheEntry, 1070 ) { 1071 entry.evict(); 1072 self.free(&entry); 1073 } 1074 1075 /// Evict a texture cache handle that was previously set to be in manual 1076 /// eviction mode. 1077 pub fn evict_handle(&mut self, handle: &TextureCacheHandle) { 1078 match handle { 1079 TextureCacheHandle::Manual(handle) => { 1080 // Find the strong handle that matches this weak handle. If this 1081 // ever shows up in profiles, we can make it a hash (but the number 1082 // of manual eviction handles is typically small). 1083 // Alternatively, we could make a more forgiving FreeList variant 1084 // which does not differentiate between strong and weak handles. 1085 let index = self.manual_handles.iter().position(|strong_handle| { 1086 strong_handle.matches(handle) 1087 }); 1088 if let Some(index) = index { 1089 let handle = self.manual_handles.swap_remove(index); 1090 let entry = self.manual_entries.free(handle); 1091 self.evict_impl(entry); 1092 } 1093 } 1094 TextureCacheHandle::Auto(handle) => { 1095 if let Some(entry) = self.lru_cache.remove(handle) { 1096 self.evict_impl(entry); 1097 } 1098 } 1099 _ => {} 1100 } 1101 } 1102 1103 pub fn dump_color8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { 1104 self.shared_textures.color8_linear.dump_as_svg(output) 1105 } 1106 1107 pub fn dump_color8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { 1108 self.shared_textures.color8_glyphs.dump_as_svg(output) 1109 } 1110 1111 pub fn dump_alpha8_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { 1112 self.shared_textures.alpha8_glyphs.dump_as_svg(output) 1113 } 1114 1115 pub fn dump_alpha8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> { 1116 self.shared_textures.alpha8_linear.dump_as_svg(output) 1117 } 1118 1119 /// Get the eviction threshold, in bytes, for the given budget type. 1120 fn get_eviction_threshold(&self, budget_type: BudgetType) -> usize { 1121 if budget_type == BudgetType::Standalone { 1122 // For standalone textures, the only reason to evict textures is 1123 // to save GPU memory. Batching / draw call concerns do not apply 1124 // to standalone textures, because unused textures don't cause 1125 // extra draw calls. 1126 return 8 * 1024 * 1024; 1127 } 1128 1129 // For shared textures, evicting an entry only frees up GPU memory if it 1130 // causes one of the shared textures to become empty, so we want to avoid 1131 // getting slightly above the capacity of a texture. 1132 // The other concern for shared textures is batching: The entries that 1133 // are needed in the current frame should be distributed across as few 1134 // shared textures as possible, to minimize the number of draw calls. 1135 // Ideally we only want one texture per type under simple workloads. 1136 1137 let bytes_per_texture = self.shared_textures.bytes_per_shared_texture(budget_type); 1138 1139 // Number of allocated bytes under which we don't bother with evicting anything 1140 // from the cache. Above the threshold we consider evicting the coldest items 1141 // depending on how cold they are. 1142 // 1143 // Above all else we want to make sure that even after a heavy workload, the 1144 // shared cache settles back to a single texture atlas per type over some reasonable 1145 // period of time. 1146 // This is achieved by the compaction logic which will try to consolidate items that 1147 // are spread over multiple textures into few ones, and by evicting old items 1148 // so that the compaction logic has room to do its job. 1149 // 1150 // The other goal is to leave enough empty space in the texture atlases 1151 // so that we are not too likely to have to allocate a new texture atlas on 1152 // the next frame if we switch to a new tab or load a new page. That's why 1153 // the following thresholds are rather low. Note that even when above the threshold, 1154 // we only evict cold items and ramp up the eviction pressure depending on the amount 1155 // of allocated memory (See should_continue_evicting). 1156 let ideal_utilization = match budget_type { 1157 BudgetType::SharedAlpha8Glyphs | BudgetType::SharedColor8Glyphs => { 1158 // Glyphs are usually small and tightly packed so they waste very little 1159 // space in the cache. 1160 bytes_per_texture * 2 / 3 1161 } 1162 _ => { 1163 // Other types of images come with a variety of sizes making them more 1164 // prone to wasting pixels and causing fragmentation issues so we put 1165 // more pressure on them. 1166 bytes_per_texture / 3 1167 } 1168 }; 1169 1170 ideal_utilization 1171 } 1172 1173 /// Returns whether to continue eviction and how cold an item need to be to be evicted. 1174 /// 1175 /// If the None is returned, stop evicting. 1176 /// If the Some(n) is returned, continue evicting if the coldest item hasn't been used 1177 /// for more than n frames. 1178 fn should_continue_evicting( 1179 &self, 1180 budget_type: BudgetType, 1181 eviction_count: usize, 1182 ) -> Option<u64> { 1183 1184 let threshold = self.get_eviction_threshold(budget_type); 1185 let bytes_allocated = self.bytes_allocated[budget_type as usize]; 1186 1187 let uses_multiple_atlases = self.shared_textures.has_multiple_textures(budget_type); 1188 1189 // If current memory usage is below selected threshold, we can stop evicting items 1190 // except when using shared texture atlases and more than one texture is in use. 1191 // This is not very common but can happen due to fragmentation and the only way 1192 // to get rid of that fragmentation is to continue evicting. 1193 if bytes_allocated < threshold && !uses_multiple_atlases { 1194 return None; 1195 } 1196 1197 // Number of frames since last use that is considered too recent for eviction, 1198 // depending on the cache pressure. 1199 let age_theshold = match bytes_allocated / threshold { 1200 0 => 400, 1201 1 => 200, 1202 2 => 100, 1203 3 => 50, 1204 4 => 25, 1205 5 => 10, 1206 6 => 5, 1207 _ => 1, 1208 }; 1209 1210 // If current memory usage is significantly more than the threshold, keep evicting this frame 1211 if bytes_allocated > 4 * threshold { 1212 return Some(age_theshold); 1213 } 1214 1215 // Otherwise, only allow evicting up to a certain number of items per frame. This allows evictions 1216 // to be spread over a number of frames, to avoid frame spikes. 1217 if eviction_count < Self::MAX_EVICTIONS_PER_FRAME { 1218 return Some(age_theshold) 1219 } 1220 1221 None 1222 } 1223 1224 1225 /// Evict old items from the shared and standalone caches, if we're over a 1226 /// threshold memory usage value 1227 fn evict_items_from_cache_if_required(&mut self, profile: &mut TransactionProfile) { 1228 let previous_frame_id = self.now.frame_id() - 1; 1229 let mut eviction_count = 0; 1230 let mut youngest_evicted = FrameId::first(); 1231 1232 for budget in BudgetType::iter() { 1233 while let Some(age_threshold) = self.should_continue_evicting( 1234 budget, 1235 eviction_count, 1236 ) { 1237 if let Some(entry) = self.lru_cache.peek_oldest(budget as u8) { 1238 // Only evict this item if it wasn't used in the previous frame. The reason being that if it 1239 // was used the previous frame then it will likely be used in this frame too, and we don't 1240 // want to be continually evicting and reuploading the item every frame. 1241 if entry.last_access.frame_id() + age_threshold > previous_frame_id { 1242 // Since the LRU cache is ordered by frame access, we can break out of the loop here because 1243 // we know that all remaining items were also used in the previous frame (or more recently). 1244 break; 1245 } 1246 if entry.last_access.frame_id() > youngest_evicted { 1247 youngest_evicted = entry.last_access.frame_id(); 1248 } 1249 let entry = self.lru_cache.pop_oldest(budget as u8).unwrap(); 1250 entry.evict(); 1251 self.free(&entry); 1252 eviction_count += 1; 1253 } else { 1254 // The LRU cache is empty, all remaining items use manual 1255 // eviction. In this case, there's nothing we can do until 1256 // the calling code manually evicts items to reduce the 1257 // allocated cache size. 1258 break; 1259 } 1260 } 1261 } 1262 1263 if eviction_count > 0 { 1264 profile.set(profiler::TEXTURE_CACHE_EVICTION_COUNT, eviction_count); 1265 profile.set( 1266 profiler::TEXTURE_CACHE_YOUNGEST_EVICTION, 1267 self.now.frame_id().as_u64() - youngest_evicted.as_u64() 1268 ); 1269 } 1270 } 1271 1272 // Free a cache entry from the standalone list or shared cache. 1273 fn free(&mut self, entry: &CacheEntry) { 1274 match entry.details { 1275 EntryDetails::Standalone { size_in_bytes, .. } => { 1276 self.bytes_allocated[BudgetType::Standalone as usize] -= size_in_bytes; 1277 1278 // This is a standalone texture allocation. Free it directly. 1279 self.pending_updates.push_free(entry.texture_id); 1280 } 1281 EntryDetails::Cache { origin, alloc_id, allocated_size_in_bytes } => { 1282 let (allocator_list, budget_type) = self.shared_textures.select( 1283 entry.input_format, 1284 entry.filter, 1285 entry.shader, 1286 ); 1287 1288 allocator_list.deallocate(entry.texture_id, alloc_id); 1289 1290 self.bytes_allocated[budget_type as usize] -= allocated_size_in_bytes; 1291 1292 if self.debug_flags.contains( 1293 DebugFlags::TEXTURE_CACHE_DBG | 1294 DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED) 1295 { 1296 self.pending_updates.push_debug_clear( 1297 entry.texture_id, 1298 origin, 1299 entry.size.width, 1300 entry.size.height, 1301 ); 1302 } 1303 } 1304 } 1305 } 1306 1307 /// Allocate a block from the shared cache. 1308 fn allocate_from_shared_cache( 1309 &mut self, 1310 params: &CacheAllocParams, 1311 ) -> (CacheEntry, BudgetType) { 1312 let (allocator_list, budget_type) = self.shared_textures.select( 1313 params.descriptor.format, 1314 params.filter, 1315 params.shader, 1316 ); 1317 1318 // To avoid referring to self in the closure. 1319 let next_id = &mut self.next_id; 1320 let pending_updates = &mut self.pending_updates; 1321 1322 let (texture_id, alloc_id, allocated_rect) = allocator_list.allocate( 1323 params.descriptor.size, 1324 &mut |size, parameters| { 1325 let texture_id = *next_id; 1326 next_id.0 += 1; 1327 pending_updates.push_alloc( 1328 texture_id, 1329 TextureCacheAllocInfo { 1330 target: ImageBufferKind::Texture2D, 1331 width: size.width, 1332 height: size.height, 1333 format: parameters.formats.internal, 1334 filter: parameters.filter, 1335 is_shared_cache: true, 1336 has_depth: false, 1337 category: TextureCacheCategory::Atlas, 1338 }, 1339 ); 1340 1341 texture_id 1342 }, 1343 ); 1344 1345 let formats = &allocator_list.texture_parameters().formats; 1346 1347 let swizzle = if formats.external == params.descriptor.format { 1348 Swizzle::default() 1349 } else { 1350 match self.swizzle { 1351 Some(_) => Swizzle::Bgra, 1352 None => Swizzle::default(), 1353 } 1354 }; 1355 1356 let bpp = formats.internal.bytes_per_pixel(); 1357 let allocated_size_in_bytes = (allocated_rect.area() * bpp) as usize; 1358 self.bytes_allocated[budget_type as usize] += allocated_size_in_bytes; 1359 1360 (CacheEntry { 1361 size: params.descriptor.size, 1362 user_data: params.user_data, 1363 last_access: self.now, 1364 details: EntryDetails::Cache { 1365 origin: allocated_rect.min, 1366 alloc_id, 1367 allocated_size_in_bytes, 1368 }, 1369 uv_rect_handle: GpuBufferHandle::INVALID, 1370 input_format: params.descriptor.format, 1371 filter: params.filter, 1372 swizzle, 1373 texture_id, 1374 eviction_notice: None, 1375 uv_rect_kind: params.uv_rect_kind, 1376 shader: params.shader 1377 }, budget_type) 1378 } 1379 1380 // Returns true if the given image descriptor *may* be 1381 // placed in the shared texture cache. 1382 pub fn is_allowed_in_shared_cache( 1383 &self, 1384 filter: TextureFilter, 1385 descriptor: &ImageDescriptor, 1386 ) -> bool { 1387 let mut allowed_in_shared_cache = true; 1388 1389 if matches!(descriptor.format, ImageFormat::RGBA8 | ImageFormat::BGRA8) 1390 && filter == TextureFilter::Linear 1391 { 1392 // Allow the maximum that can fit in the linear color texture's two column layout. 1393 let max = self.shared_textures.color8_linear.size() / 2; 1394 allowed_in_shared_cache = descriptor.size.width.max(descriptor.size.height) <= max; 1395 } else if descriptor.size.width > TEXTURE_REGION_DIMENSIONS { 1396 allowed_in_shared_cache = false; 1397 } 1398 1399 if descriptor.size.height > TEXTURE_REGION_DIMENSIONS { 1400 allowed_in_shared_cache = false; 1401 } 1402 1403 // TODO(gw): For now, alpha formats of the texture cache can only be linearly sampled. 1404 // Nearest sampling gets a standalone texture. 1405 // This is probably rare enough that it can be fixed up later. 1406 if filter == TextureFilter::Nearest && 1407 descriptor.format.bytes_per_pixel() <= 2 1408 { 1409 allowed_in_shared_cache = false; 1410 } 1411 1412 allowed_in_shared_cache 1413 } 1414 1415 /// Allocate a render target via the pending updates sent to the renderer 1416 pub fn alloc_render_target( 1417 &mut self, 1418 size: DeviceIntSize, 1419 format: ImageFormat, 1420 ) -> CacheTextureId { 1421 let texture_id = self.next_id; 1422 self.next_id.0 += 1; 1423 1424 // Push a command to allocate device storage of the right size / format. 1425 let info = TextureCacheAllocInfo { 1426 target: ImageBufferKind::Texture2D, 1427 width: size.width, 1428 height: size.height, 1429 format, 1430 filter: TextureFilter::Linear, 1431 is_shared_cache: false, 1432 has_depth: false, 1433 category: TextureCacheCategory::RenderTarget, 1434 }; 1435 1436 self.pending_updates.push_alloc(texture_id, info); 1437 1438 texture_id 1439 } 1440 1441 /// Free an existing render target 1442 pub fn free_render_target( 1443 &mut self, 1444 id: CacheTextureId, 1445 ) { 1446 self.pending_updates.push_free(id); 1447 } 1448 1449 /// Allocates a new standalone cache entry. 1450 fn allocate_standalone_entry( 1451 &mut self, 1452 params: &CacheAllocParams, 1453 ) -> (CacheEntry, BudgetType) { 1454 let texture_id = self.next_id; 1455 self.next_id.0 += 1; 1456 1457 // Push a command to allocate device storage of the right size / format. 1458 let info = TextureCacheAllocInfo { 1459 target: ImageBufferKind::Texture2D, 1460 width: params.descriptor.size.width, 1461 height: params.descriptor.size.height, 1462 format: params.descriptor.format, 1463 filter: params.filter, 1464 is_shared_cache: false, 1465 has_depth: false, 1466 category: TextureCacheCategory::Standalone, 1467 }; 1468 1469 let size_in_bytes = (info.width * info.height * info.format.bytes_per_pixel()) as usize; 1470 self.bytes_allocated[BudgetType::Standalone as usize] += size_in_bytes; 1471 1472 self.pending_updates.push_alloc(texture_id, info); 1473 1474 // Special handing for BGRA8 textures that may need to be swizzled. 1475 let swizzle = if params.descriptor.format == ImageFormat::BGRA8 { 1476 self.swizzle.map(|s| s.bgra8_sampling_swizzle) 1477 } else { 1478 None 1479 }; 1480 1481 (CacheEntry::new_standalone( 1482 texture_id, 1483 self.now, 1484 params, 1485 swizzle.unwrap_or_default(), 1486 size_in_bytes, 1487 ), BudgetType::Standalone) 1488 } 1489 1490 /// Allocates a cache entry for the given parameters, and updates the 1491 /// provided handle to point to the new entry. 1492 fn allocate( 1493 &mut self, 1494 params: &CacheAllocParams, 1495 handle: &mut TextureCacheHandle, 1496 eviction: Eviction, 1497 force_standalone_texture: bool, 1498 ) { 1499 debug_assert!(self.now.is_valid()); 1500 assert!(!params.descriptor.size.is_empty()); 1501 1502 // If this image doesn't qualify to go in the shared (batching) cache, 1503 // allocate a standalone entry. 1504 let use_shared_cache = !force_standalone_texture && self.is_allowed_in_shared_cache(params.filter, ¶ms.descriptor); 1505 let (new_cache_entry, budget_type) = if use_shared_cache { 1506 self.allocate_from_shared_cache(params) 1507 } else { 1508 self.allocate_standalone_entry(params) 1509 }; 1510 1511 let details = new_cache_entry.details.clone(); 1512 let texture_id = new_cache_entry.texture_id; 1513 1514 // If the handle points to a valid cache entry, we want to replace the 1515 // cache entry with our newly updated location. We also need to ensure 1516 // that the storage (region or standalone) associated with the previous 1517 // entry here gets freed. 1518 // 1519 // If the handle is invalid, we need to insert the data, and append the 1520 // result to the corresponding vector. 1521 let old_entry = match (&mut *handle, eviction) { 1522 (TextureCacheHandle::Auto(handle), Eviction::Auto) => { 1523 self.lru_cache.replace_or_insert(handle, budget_type as u8, new_cache_entry) 1524 }, 1525 (TextureCacheHandle::Manual(handle), Eviction::Manual) => { 1526 let entry = self.manual_entries.get_opt_mut(handle) 1527 .expect("Don't call this after evicting"); 1528 Some(mem::replace(entry, new_cache_entry)) 1529 }, 1530 (TextureCacheHandle::Manual(_), Eviction::Auto) | 1531 (TextureCacheHandle::Auto(_), Eviction::Manual) => { 1532 panic!("Can't change eviction policy after initial allocation"); 1533 }, 1534 (TextureCacheHandle::Empty, Eviction::Auto) => { 1535 let new_handle = self.lru_cache.push_new(budget_type as u8, new_cache_entry); 1536 *handle = TextureCacheHandle::Auto(new_handle); 1537 None 1538 }, 1539 (TextureCacheHandle::Empty, Eviction::Manual) => { 1540 let manual_handle = self.manual_entries.insert(new_cache_entry); 1541 let new_handle = manual_handle.weak(); 1542 self.manual_handles.push(manual_handle); 1543 *handle = TextureCacheHandle::Manual(new_handle); 1544 None 1545 }, 1546 }; 1547 if let Some(old_entry) = old_entry { 1548 old_entry.evict(); 1549 self.free(&old_entry); 1550 } 1551 1552 if let EntryDetails::Cache { alloc_id, .. } = details { 1553 let allocator_list = self.shared_textures.select( 1554 params.descriptor.format, 1555 params.filter, 1556 params.shader, 1557 ).0; 1558 1559 allocator_list.set_handle(texture_id, alloc_id, handle); 1560 } 1561 } 1562 1563 pub fn shared_alpha_expected_format(&self) -> ImageFormat { 1564 self.shared_textures.alpha8_linear.texture_parameters().formats.external 1565 } 1566 1567 pub fn shared_color_expected_format(&self) -> ImageFormat { 1568 self.shared_textures.color8_linear.texture_parameters().formats.external 1569 } 1570 1571 1572 #[cfg(test)] 1573 pub fn total_allocated_bytes_for_testing(&self) -> usize { 1574 BudgetType::iter().map(|b| self.bytes_allocated[b as usize]).sum() 1575 } 1576 1577 pub fn report_memory(&self, ops: &mut MallocSizeOfOps) -> usize { 1578 self.lru_cache.size_of(ops) 1579 } 1580 } 1581 1582 #[cfg_attr(feature = "capture", derive(Serialize))] 1583 #[cfg_attr(feature = "replay", derive(Deserialize))] 1584 pub struct TextureParameters { 1585 pub formats: TextureFormatPair<ImageFormat>, 1586 pub filter: TextureFilter, 1587 } 1588 1589 impl TextureCacheUpdate { 1590 // Constructs a TextureCacheUpdate operation to be passed to the 1591 // rendering thread in order to do an upload to the right 1592 // location in the texture cache. 1593 fn new_update( 1594 data: CachedImageData, 1595 descriptor: &ImageDescriptor, 1596 origin: DeviceIntPoint, 1597 size: DeviceIntSize, 1598 use_upload_format: bool, 1599 dirty_rect: &ImageDirtyRect, 1600 ) -> TextureCacheUpdate { 1601 let source = match data { 1602 CachedImageData::Snapshot => { 1603 panic!("Snapshots should not do texture uploads"); 1604 } 1605 CachedImageData::Blob => { 1606 panic!("The vector image should have been rasterized."); 1607 } 1608 CachedImageData::External(ext_image) => match ext_image.image_type { 1609 ExternalImageType::TextureHandle(_) => { 1610 panic!("External texture handle should not go through texture_cache."); 1611 } 1612 ExternalImageType::Buffer => TextureUpdateSource::External { 1613 id: ext_image.id, 1614 channel_index: ext_image.channel_index, 1615 }, 1616 }, 1617 CachedImageData::Raw(bytes) => { 1618 let finish = descriptor.offset + 1619 descriptor.size.width * descriptor.format.bytes_per_pixel() + 1620 (descriptor.size.height - 1) * descriptor.compute_stride(); 1621 assert!(bytes.len() >= finish as usize); 1622 1623 TextureUpdateSource::Bytes { data: bytes } 1624 } 1625 }; 1626 let format_override = if use_upload_format { 1627 Some(descriptor.format) 1628 } else { 1629 None 1630 }; 1631 1632 match *dirty_rect { 1633 DirtyRect::Partial(dirty) => { 1634 // the dirty rectangle doesn't have to be within the area but has to intersect it, at least 1635 let stride = descriptor.compute_stride(); 1636 let offset = descriptor.offset + dirty.min.y * stride + dirty.min.x * descriptor.format.bytes_per_pixel(); 1637 1638 TextureCacheUpdate { 1639 rect: DeviceIntRect::from_origin_and_size( 1640 DeviceIntPoint::new(origin.x + dirty.min.x, origin.y + dirty.min.y), 1641 DeviceIntSize::new( 1642 dirty.width().min(size.width - dirty.min.x), 1643 dirty.height().min(size.height - dirty.min.y), 1644 ), 1645 ), 1646 source, 1647 stride: Some(stride), 1648 offset, 1649 format_override, 1650 } 1651 } 1652 DirtyRect::All => { 1653 TextureCacheUpdate { 1654 rect: DeviceIntRect::from_origin_and_size(origin, size), 1655 source, 1656 stride: descriptor.stride, 1657 offset: descriptor.offset, 1658 format_override, 1659 } 1660 } 1661 } 1662 } 1663 } 1664 1665 #[cfg(test)] 1666 mod test_texture_cache { 1667 use crate::renderer::GpuBufferBuilderF; 1668 use crate::internal_types::FrameId; 1669 1670 #[test] 1671 fn check_allocation_size_balance() { 1672 // Allocate some glyphs, observe the total allocation size, and free 1673 // the glyphs again. Check that the total allocation size is back at the 1674 // original value. 1675 1676 use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader}; 1677 use crate::device::TextureFilter; 1678 use crate::gpu_types::UvRectKind; 1679 use crate::frame_allocator::FrameMemory; 1680 use api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, DirtyRect}; 1681 use api::units::*; 1682 use euclid::size2; 1683 let mut texture_cache = TextureCache::new_for_testing(2048, ImageFormat::BGRA8); 1684 let memory = FrameMemory::fallback(); 1685 let mut gpu_buffer = GpuBufferBuilderF::new(&memory, 0, FrameId::first()); 1686 1687 let sizes: &[DeviceIntSize] = &[ 1688 size2(23, 27), 1689 size2(15, 22), 1690 size2(11, 5), 1691 size2(20, 25), 1692 size2(38, 41), 1693 size2(11, 19), 1694 size2(13, 21), 1695 size2(37, 40), 1696 size2(13, 15), 1697 size2(14, 16), 1698 size2(10, 9), 1699 size2(25, 28), 1700 ]; 1701 1702 let bytes_at_start = texture_cache.total_allocated_bytes_for_testing(); 1703 1704 let handles: Vec<TextureCacheHandle> = sizes.iter().map(|size| { 1705 let mut texture_cache_handle = TextureCacheHandle::invalid(); 1706 texture_cache.request(&texture_cache_handle, &mut gpu_buffer); 1707 texture_cache.update( 1708 &mut texture_cache_handle, 1709 ImageDescriptor { 1710 size: *size, 1711 stride: None, 1712 format: ImageFormat::BGRA8, 1713 flags: ImageDescriptorFlags::empty(), 1714 offset: 0, 1715 }, 1716 TextureFilter::Linear, 1717 None, 1718 [0.0; 4], 1719 DirtyRect::All, 1720 &mut gpu_buffer, 1721 None, 1722 UvRectKind::Rect, 1723 Eviction::Manual, 1724 TargetShader::Text, 1725 false, 1726 ); 1727 texture_cache_handle 1728 }).collect(); 1729 1730 let bytes_after_allocating = texture_cache.total_allocated_bytes_for_testing(); 1731 assert!(bytes_after_allocating > bytes_at_start); 1732 1733 for handle in handles { 1734 texture_cache.evict_handle(&handle); 1735 } 1736 1737 let bytes_at_end = texture_cache.total_allocated_bytes_for_testing(); 1738 assert_eq!(bytes_at_end, bytes_at_start); 1739 } 1740 }