Bug 1510490 - Cache exactly the shaders that are used early in startup. r=mattwoodrow,sotaro
☠☠ backed out by b425a0d640af ☠ ☠
authorBobby Holley <bobbyholley@gmail.com>
Sat, 01 Dec 2018 03:06:10 +0000
changeset 505511 f9d8e4ebe0a2f40646dff560a0cdccd0f722f3ea
parent 505510 55fa8c9b0c7e895c476d26452debc13a8b4678c1
child 505512 6d3b77d3edc407d8d1740c56e7cb3ae139fbd4dd
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattwoodrow, sotaro
bugs1510490
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1510490 - Cache exactly the shaders that are used early in startup. r=mattwoodrow,sotaro Depends on D13443 Differential Revision: https://phabricator.services.mozilla.com/D13444
gfx/webrender_bindings/src/program_cache.rs
gfx/wr/webrender/src/device/gl.rs
--- a/gfx/webrender_bindings/src/program_cache.rs
+++ b/gfx/webrender_bindings/src/program_cache.rs
@@ -6,20 +6,18 @@ use std::path::{PathBuf};
 use std::rc::Rc;
 use std::sync::Arc;
 
 use webrender::{ProgramBinary, ProgramCache, ProgramCacheObserver};
 use bincode;
 use fxhash;
 use nsstring::nsAString;
 use rayon::ThreadPool;
-use uuid::Uuid;
 
 const MAX_LOAD_TIME_MS: u64 = 400;
-const MAX_CACHED_PROGRAM_COUNT: u32 = 15;
 
 fn deserialize_program_binary(path: &PathBuf) -> Result<Arc<ProgramBinary>, Error> {
     let mut buf = vec![];
     let mut file = File::open(path)?;
     file.read_to_end(&mut buf)?;
 
     if buf.len() <= 8 + 4 {
         return Err(Error::new(ErrorKind::InvalidData, "File size is too small"));
@@ -80,166 +78,135 @@ fn get_cache_path_from_prof_path(prof_pa
     let prof_path = OsString::from(utf8);
     let mut cache_path = PathBuf::from(&prof_path);
     cache_path.push("shader-cache");
 
     Some(cache_path)
 }
 
 struct WrProgramBinaryDiskCache {
-    cache_path: Option<PathBuf>,
-    program_count: u32,
-    is_enabled: bool,
+    cache_path: PathBuf,
     workers: Arc<ThreadPool>,
 }
 
 // Magic number + version. Increment the version when the binary format changes.
 const MAGIC: u32 = 0xB154AD30; // BI-SHADE + version.
 const VERSION: u32 = 2;
 const MAGIC_AND_VERSION: u32 = MAGIC + VERSION;
 
+/// Helper to convert a closure returning a `Result` to one that returns void.
+/// This allows the enclosed code to use the question-mark operator in a
+/// context where the calling function doesn't expect a `Result`.
+#[allow(unused_must_use)]
+fn result_to_void<F: FnOnce() -> Result<(), ()>>(f: F) { f(); }
+
 impl WrProgramBinaryDiskCache {
     #[allow(dead_code)]
-    fn new(prof_path: &nsAString, workers: &Arc<ThreadPool>) -> Self {
-        let cache_path = get_cache_path_from_prof_path(prof_path);
-        let is_enabled = cache_path.is_some();
-        let workers = Arc::clone(workers);
-
-        WrProgramBinaryDiskCache{
+    fn new(cache_path: PathBuf, workers: &Arc<ThreadPool>) -> Self {
+        WrProgramBinaryDiskCache {
             cache_path,
-            program_count: 0,
-            is_enabled,
-            workers,
+            workers: Arc::clone(workers),
         }
     }
 
-    fn notify_binary_added(&mut self, program_binary: &Arc<ProgramBinary>) {
-        if !self.is_enabled {
-            return;
+    /// Updates the on-disk cache to contain exactly the entries specified.
+    fn update(&mut self, entries: Vec<Arc<ProgramBinary>>) {
+        info!("Updating on-disk shader cache");
+
+        // Compute the digests in string form.
+        let mut entries: Vec<(String, Arc<ProgramBinary>)> =
+            entries.into_iter().map(|e| (format!("{}", e.source_digest()), e)).collect();
+
+        // For each file in the current directory, check if it corresponds to
+        // an entry we're supposed to write. If so, we don't need to write the
+        // entry. If not, we delete the file.
+        for existing in read_dir(&self.cache_path).unwrap().filter_map(|f| f.ok()) {
+            let pos = existing.file_name().to_str()
+                .and_then(|digest| entries.iter().position(|x| x.0 == digest));
+            if let Some(p) = pos {
+                info!("Found existing shader: {}", existing.file_name().to_string_lossy());
+                entries.swap_remove(p);
+            } else {
+                self.workers.spawn(move || {
+                    info!("Removing shader: {}", existing.file_name().to_string_lossy());
+                    ::std::fs::remove_file(existing.path())
+                        .unwrap_or_else(|e| error!("shader-cache: Failed to remove shader: {:?}", e));
+                });
+            }
         }
 
-        if let Some(ref cache_path) = self.cache_path {
-            if let Err(_) = create_dir_all(&cache_path) {
-                error!("failed to create dir for shader disk cache");
-                return;
-            }
+        // Write the remaining entries to disk on a worker thread.
+        for entry in entries.into_iter() {
+            let (file_name, program_binary) = entry;
+            let file_path = self.cache_path.join(&file_name);
 
-            self.program_count += 1;
-            if self.program_count > MAX_CACHED_PROGRAM_COUNT {
-                // Disable disk cache to avoid storing more shader programs to disk
-                self.is_enabled = false;
-                return;
-            }
-
-            // Use uuid for file name
-            let uuid1 = Uuid::new_v4();
-            let file_name = uuid1.hyphenated().to_string();
-            let program_binary = Arc::clone(program_binary);
-            let file_path = cache_path.join(&file_name);
-
-            let program_count = self.program_count;
-
-            // Save to disk on worker thread
-            self.workers.spawn(move || {
+            self.workers.spawn(move || result_to_void(move || {
+                info!("Writing shader: {}", file_name);
 
                 use std::time::{Instant};
                 let start = Instant::now();
 
-                let data: Vec<u8> = match bincode::serialize(&*program_binary) {
-                    Ok(data) => data,
-                    Err(err) => {
-                        error!("Failed to serialize program binary error: {}", err);
-                        return;
-                    }
-                };
+                let data: Vec<u8> = bincode::serialize(&*program_binary)
+                    .map_err(|e| error!("shader-cache: Failed to serialize: {}", e))?;
 
-                let mut file = match File::create(&file_path) {
-                    Ok(file) => file,
-                    Err(err) => {
-                        error!("Unable to create file for program binary error: {}", err);
-                        return;
-                    }
-                };
+                let mut file = File::create(&file_path)
+                    .map_err(|e| error!("shader-cache: Failed to create file: {}", e))?;
 
                 // Write magic + version.
                 let mv = MAGIC_AND_VERSION;
                 let mv = bincode::serialize(&mv).unwrap();
                 assert!(mv.len() == 4);
-                match file.write_all(&mv) {
-                    Err(err) => {
-                        error!("Failed to write magic+version to file error: {}", err);
-                    }
-                    _ => {},
-                };
+                file.write_all(&mv)
+                    .map_err(|e| error!("shader-cache: Failed to write magic + version: {}", e))?;
 
                 // Write hash
                 let hash = fxhash::hash64(&data);
                 let hash = bincode::serialize(&hash).unwrap();
                 assert!(hash.len() == 8);
-                match file.write_all(&hash) {
-                    Err(err) => {
-                        error!("Failed to write hash to file error: {}", err);
-                    }
-                    _ => {},
-                };
+                file.write_all(&hash)
+                    .map_err(|e| error!("shader-cache: Failed to write hash: {}", e))?;
 
                 // Write serialized data
-                match file.write_all(&data) {
-                    Err(err) => {
-                        error!("Failed to write program binary to file error: {}", err);
-                    }
-                    _ => {},
-                };
+                file.write_all(&data)
+                    .map_err(|e| error!("shader-cache: Failed to write program binary: {}", e))?;
 
-                let elapsed = start.elapsed();
-                info!("notify_binary_added: {} ms program_count {}",
-                    (elapsed.as_secs() * 1_000) + (elapsed.subsec_nanos() / 1_000_000) as u64, program_count);
-
-            });
+                info!("Wrote shader {} in {:?}", file_name, start.elapsed());
+                Ok(())
+            }));
         }
     }
 
     pub fn try_load_from_disk(&mut self, program_cache: &Rc<ProgramCache>) {
-        if !self.is_enabled {
-            return;
-        }
-
-        if let Some(ref cache_path) = self.cache_path {
-            use std::time::{Instant};
-            let start = Instant::now();
+        use std::time::{Instant};
+        let start = Instant::now();
 
-            // Load program binaries if exist
-            if cache_path.exists() && cache_path.is_dir() {
-                for entry in read_dir(cache_path).unwrap() {
-                    let entry = entry.unwrap();
-                    let path = entry.path();
+        // Load program binaries if exist
+        for entry in read_dir(&self.cache_path).unwrap() {
+            let entry = entry.unwrap();
+            let path = entry.path();
 
-                    info!("loading shader file");
+            info!("Loading shader: {}", entry.file_name().to_string_lossy());
 
-                    match deserialize_program_binary(&path) {
-                        Ok(program) => {
-                            program_cache.load_program_binary(program);
-                        }
-                        Err(err) => {
-                            error!("Failed to desriralize program binary error: {}", err);
-                        }
-                    };
-
-                    self.program_count += 1;
+            match deserialize_program_binary(&path) {
+                Ok(program) => {
+                    program_cache.load_program_binary(program);
+                }
+                Err(err) => {
+                    error!("shader-cache: Failed to deserialize program binary: {}", err);
+                }
+            };
 
-                    let elapsed = start.elapsed();
-                    let elapsed_ms = (elapsed.as_secs() * 1_000) + (elapsed.subsec_nanos() / 1_000_000) as u64;
-                    info!("deserialize_program_binary: {} ms program_count {}", elapsed_ms, self.program_count);
+            let elapsed = start.elapsed();
+            info!("Loaded shader in {:?}", elapsed);
+            let elapsed_ms = (elapsed.as_secs() * 1_000) +
+                (elapsed.subsec_nanos() / 1_000_000) as u64;
 
-                    if self.program_count > MAX_CACHED_PROGRAM_COUNT || elapsed_ms > MAX_LOAD_TIME_MS {
-                        // Disable disk cache to avoid storing more shader programs to disk
-                        self.is_enabled = false;
-                        break;
-                    }
-                }
+            if elapsed_ms > MAX_LOAD_TIME_MS {
+                error!("shader-cache: Timed out before finishing loads");
+                break;
             }
         }
     }
 }
 
 pub struct WrProgramCacheObserver {
     disk_cache: Rc<RefCell<WrProgramBinaryDiskCache>>,
 }
@@ -249,65 +216,69 @@ impl WrProgramCacheObserver {
     fn new(disk_cache: Rc<RefCell<WrProgramBinaryDiskCache>>) -> Self {
         WrProgramCacheObserver{
             disk_cache,
         }
     }
 }
 
 impl ProgramCacheObserver for WrProgramCacheObserver {
-    fn notify_binary_added(&self, program_binary: &Arc<ProgramBinary>) {
-        self.disk_cache.borrow_mut().notify_binary_added(program_binary);
+    fn update_disk_cache(&self, entries: Vec<Arc<ProgramBinary>>) {
+        self.disk_cache.borrow_mut().update(entries);
     }
 
     fn notify_program_binary_failed(&self, _program_binary: &Arc<ProgramBinary>) {
-        error!("Failed program_binary");
+        error!("shader-cache: Failed program_binary");
     }
 }
 
 
 pub struct WrProgramCache {
     pub program_cache: Rc<ProgramCache>,
     disk_cache: Option<Rc<RefCell<WrProgramBinaryDiskCache>>>,
 }
 
 impl WrProgramCache {
     pub fn new(prof_path: &nsAString, workers: &Arc<ThreadPool>) -> Self {
-        let disk_cache = Rc::new(RefCell::new(WrProgramBinaryDiskCache::new(prof_path, workers)));
-        let program_cache_observer = Box::new(WrProgramCacheObserver::new(Rc::clone(&disk_cache)));
-        let program_cache = ProgramCache::new(Some(program_cache_observer));
+        let cache_path = get_cache_path_from_prof_path(prof_path);
+        let use_disk_cache = cache_path.as_ref().map_or(false, |p| create_dir_all(p).is_ok());
+        let (disk_cache, program_cache_observer) = if use_disk_cache {
+            let cache = Rc::new(RefCell::new(WrProgramBinaryDiskCache::new(cache_path.unwrap(), workers)));
+            let obs = Box::new(WrProgramCacheObserver::new(Rc::clone(&cache))) as
+                Box<dyn ProgramCacheObserver>;
+            (Some(cache), Some(obs))
+        } else {
+            (None, None)
+        };
+        let program_cache = ProgramCache::new(program_cache_observer);
 
         WrProgramCache {
             program_cache,
-            disk_cache: Some(disk_cache),
+            disk_cache: disk_cache,
         }
     }
 
     pub fn rc_get(&self) -> &Rc<ProgramCache> {
         &self.program_cache
     }
 
     pub fn try_load_from_disk(&self) {
         if let Some(ref disk_cache) = self.disk_cache {
             disk_cache.borrow_mut().try_load_from_disk(&self.program_cache);
         } else {
-            error!("Shader disk cache is not supported");
+            error!("shader-cache: Shader disk cache is not supported");
         }
     }
 }
 
 pub fn remove_disk_cache(prof_path: &nsAString) -> Result<(), Error> {
     use std::fs::remove_dir_all;
     use std::time::{Instant};
 
     if let Some(cache_path) = get_cache_path_from_prof_path(prof_path) {
         if cache_path.exists() {
             let start = Instant::now();
-
             remove_dir_all(&cache_path)?;
-
-            let elapsed = start.elapsed();
-            let elapsed_ms = (elapsed.as_secs() * 1_000) + (elapsed.subsec_nanos() / 1_000_000) as u64;
-            info!("remove_disk_cache: {} ms", elapsed_ms);
+            info!("removed all disk cache shaders in {:?}", start.elapsed());
         }
     }
     Ok(())
 }
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -11,17 +11,17 @@ use api::VoidPtrToSizeFn;
 use api::ImageDescriptor;
 use euclid::Transform3D;
 use gleam::gl;
 use internal_types::{FastHashMap, LayerIndex, RenderTargetInfo};
 use log::Level;
 use sha2::{Digest, Sha256};
 use smallvec::SmallVec;
 use std::borrow::Cow;
-use std::cell::RefCell;
+use std::cell::{Cell, RefCell};
 use std::cmp;
 use std::collections::hash_map::Entry;
 use std::fs::File;
 use std::io::Read;
 use std::marker::PhantomData;
 use std::mem;
 use std::os::raw::c_void;
 use std::ops::Add;
@@ -690,16 +690,25 @@ pub struct VBOId(gl::GLuint);
 
 #[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
 struct IBOId(gl::GLuint);
 
 #[derive(PartialEq, Eq, Hash, Debug, Clone, Default)]
 #[cfg_attr(feature = "serialize_program", derive(Deserialize, Serialize))]
 pub struct ProgramSourceDigest([u8; 32]);
 
+impl ::std::fmt::Display for ProgramSourceDigest {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        for byte in self.0.iter() {
+            f.write_fmt(format_args!("{:02x}", byte))?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Clone, PartialEq, Eq, Hash, Debug)]
 pub struct ProgramSourceInfo {
     base_filename: &'static str,
     features: String,
     digest: ProgramSourceDigest,
 }
 
 impl ProgramSourceInfo {
@@ -756,68 +765,106 @@ impl ProgramSourceInfo {
             |s| src.push_str(s),
         );
         src
     }
 }
 
 #[cfg_attr(feature = "serialize_program", derive(Deserialize, Serialize))]
 pub struct ProgramBinary {
-    binary: Vec<u8>,
+    bytes: Vec<u8>,
     format: gl::GLenum,
     source_digest: ProgramSourceDigest,
 }
 
 impl ProgramBinary {
-    fn new(binary: Vec<u8>,
+    fn new(bytes: Vec<u8>,
            format: gl::GLenum,
            source_digest: ProgramSourceDigest) -> Self {
         ProgramBinary {
-            binary,
+            bytes,
             format,
             source_digest,
         }
     }
+
+    /// Returns a reference to the source digest hash.
+    pub fn source_digest(&self) -> &ProgramSourceDigest {
+        &self.source_digest
+    }
 }
 
 /// The interfaces that an application can implement to handle ProgramCache update
 pub trait ProgramCacheObserver {
-    fn notify_binary_added(&self, program_binary: &Arc<ProgramBinary>);
+    fn update_disk_cache(&self, entries: Vec<Arc<ProgramBinary>>);
     fn notify_program_binary_failed(&self, program_binary: &Arc<ProgramBinary>);
 }
 
+struct ProgramCacheEntry {
+    /// The binary.
+    binary: Arc<ProgramBinary>,
+    /// True if the binary has been linked, i.e. used for rendering.
+    linked: bool,
+}
+
 pub struct ProgramCache {
-    binaries: RefCell<FastHashMap<ProgramSourceDigest, Arc<ProgramBinary>>>,
+    entries: RefCell<FastHashMap<ProgramSourceDigest, ProgramCacheEntry>>,
+
+    /// True if we've already updated the disk cache with the shaders used during startup.
+    updated_disk_cache: Cell<bool>,
 
     /// Optional trait object that allows the client
     /// application to handle ProgramCache updating
     program_cache_handler: Option<Box<ProgramCacheObserver>>,
 }
 
 impl ProgramCache {
     pub fn new(program_cache_observer: Option<Box<ProgramCacheObserver>>) -> Rc<Self> {
         Rc::new(
             ProgramCache {
-                binaries: RefCell::new(FastHashMap::default()),
+                entries: RefCell::new(FastHashMap::default()),
+                updated_disk_cache: Cell::new(false),
                 program_cache_handler: program_cache_observer,
             }
         )
     }
+
+    /// Notify that we've rendered the first few frames, and that the shaders
+    /// we've loaded correspond to the shaders needed during startup, and thus
+    /// should be the ones cached to disk.
+    fn startup_complete(&self) {
+        if self.updated_disk_cache.get() {
+            return;
+        }
+
+        if let Some(ref handler) = self.program_cache_handler {
+            let active_shaders = self.entries.borrow().values()
+                .filter(|e| e.linked).map(|e| e.binary.clone())
+                .collect::<Vec<_>>();
+            handler.update_disk_cache(active_shaders);
+            self.updated_disk_cache.set(true);
+        }
+    }
+
     /// Load ProgramBinary to ProgramCache.
     /// The function is typically used to load ProgramBinary from disk.
     #[cfg(feature = "serialize_program")]
     pub fn load_program_binary(&self, program_binary: Arc<ProgramBinary>) {
         let digest = program_binary.source_digest.clone();
-        self.binaries.borrow_mut().insert(digest, program_binary);
+        let entry = ProgramCacheEntry {
+            binary: program_binary,
+            linked: false,
+        };
+        self.entries.borrow_mut().insert(digest, entry);
     }
 
     /// Returns the number of bytes allocated for shaders in the cache.
     pub fn report_memory(&self, op: VoidPtrToSizeFn) -> usize {
-        self.binaries.borrow().values()
-            .map(|b| unsafe { op(b.binary.as_ptr() as *const c_void ) })
+        self.entries.borrow().values()
+            .map(|e| unsafe { op(e.binary.bytes.as_ptr() as *const c_void ) })
             .sum()
     }
 }
 
 #[derive(Debug, Copy, Clone)]
 pub enum VertexUsageHint {
     Static,
     Dynamic,
@@ -1432,33 +1479,34 @@ impl Device {
         descriptor: &VertexDescriptor,
     ) -> Result<(), ShaderError> {
         assert!(!program.is_initialized());
         let mut build_program = true;
         let info = &program.source_info;
 
         // See if we hit the binary shader cache
         if let Some(ref cached_programs) = self.cached_programs {
-            if let Some(binary) = cached_programs.binaries.borrow().get(&info.digest) {
+            if let Some(entry) = cached_programs.entries.borrow_mut().get_mut(&info.digest) {
                 let mut link_status = [0];
                 unsafe {
                     self.gl.get_program_iv(program.id, gl::LINK_STATUS, &mut link_status);
                 }
                 if link_status[0] == 0 {
                     let error_log = self.gl.get_program_info_log(program.id);
                     error!(
                       "Failed to load a program object with a program binary: {} renderer {}\n{}",
                       &info.base_filename,
                       self.renderer_name,
                       error_log
                     );
                     if let Some(ref program_cache_handler) = cached_programs.program_cache_handler {
-                        program_cache_handler.notify_program_binary_failed(&binary);
+                        program_cache_handler.notify_program_binary_failed(&entry.binary);
                     }
                 } else {
+                    entry.linked = true;
                     build_program = false;
                 }
             }
         }
 
         // If not, we need to do a normal compile + link pass.
         if build_program {
             // Compile the vertex shader
@@ -1520,24 +1568,24 @@ impl Device {
                     &info.base_filename,
                     error_log
                 );
                 self.gl.delete_program(program.id);
                 return Err(ShaderError::Link(info.base_filename.to_owned(), error_log));
             }
 
             if let Some(ref cached_programs) = self.cached_programs {
-                if !cached_programs.binaries.borrow().contains_key(&info.digest) {
+                if !cached_programs.entries.borrow().contains_key(&info.digest) {
                     let (buffer, format) = self.gl.get_program_binary(program.id);
                     if buffer.len() > 0 {
-                        let program_binary = Arc::new(ProgramBinary::new(buffer, format, info.digest.clone()));
-                        if let Some(ref program_cache_handler) = cached_programs.program_cache_handler {
-                            program_cache_handler.notify_binary_added(&program_binary);
-                        }
-                        cached_programs.binaries.borrow_mut().insert(info.digest.clone(), program_binary);
+                        let entry = ProgramCacheEntry {
+                            binary: Arc::new(ProgramBinary::new(buffer, format, info.digest.clone())),
+                            linked: true,
+                        };
+                        cached_programs.entries.borrow_mut().insert(info.digest.clone(), entry);
                     }
                 }
             }
         }
 
         // If we get here, the link succeeded, so get the uniforms.
         program.is_initialized = true;
         program.u_transform = self.gl.get_uniform_location(program.id, "uTransform");
@@ -1970,18 +2018,18 @@ impl Device {
 
         let source_info = ProgramSourceInfo::new(self, base_filename, features);
 
         // Create program
         let pid = self.gl.create_program();
 
         // Attempt to load a cached binary if possible.
         if let Some(ref cached_programs) = self.cached_programs {
-            if let Some(binary) = cached_programs.binaries.borrow().get(&source_info.digest) {
-                self.gl.program_binary(pid, binary.format, &binary.binary);
+            if let Some(entry) = cached_programs.entries.borrow().get(&source_info.digest) {
+                self.gl.program_binary(pid, entry.binary.format, &entry.binary.bytes);
             }
         }
 
         // Use 0 for the uniforms as they are initialized by link_program.
         let program = Program {
             id: pid,
             u_transform: 0,
             u_mode: 0,
@@ -2514,16 +2562,25 @@ impl Device {
         for i in 0 .. self.bound_textures.len() {
             self.gl.active_texture(gl::TEXTURE0 + i as gl::GLuint);
             self.gl.bind_texture(gl::TEXTURE_2D, 0);
         }
 
         self.gl.active_texture(gl::TEXTURE0);
 
         self.frame_id.0 += 1;
+
+        // Declare startup complete after the first ten frames. This number is
+        // basically a heuristic, which dictates how early a shader needs to be
+        // used in order to be cached to disk.
+        if self.frame_id.0 == 10 {
+            if let Some(ref cache) = self.cached_programs {
+                cache.startup_complete();
+            }
+        }
     }
 
     pub fn clear_target(
         &self,
         color: Option<[f32; 4]>,
         depth: Option<f32>,
         rect: Option<DeviceIntRect>,
     ) {