From ca47cc2fa3d3d4664fd84c0b564758655beabf82 Mon Sep 17 00:00:00 2001 From: Narfinger Date: Thu, 3 Jul 2025 22:16:43 +0900 Subject: [PATCH] Add a basic caching mechanism for ImageKeys. (#37369) This creates a new method in shared/compositing/lib to generate image keys that are send over the webview. This does not immediately return the keys but goes over the constellation to receive the keys from the IOCompositor. To make this more efficient, we now cache the keys in image_cache in a simple FIFO order. The old blocking method stays intact for now but got renamed to make the blocking clear. The blocking calls that are left are in: - `components/canvas/canvas_data.rs` - `components/script/dom/htmlmediaelement.rs` Testing: WPT tests should cover this as this doesn't change any functionality. Fixes: Was mentioned in https://github.com/servo/servo/issues/37161#issuecomment-2915750051 and part of https://github.com/servo/servo/issues/37086 --------- Signed-off-by: Narfinger Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com> Co-authored-by: gterzian <2792687+gterzian@users.noreply.github.com> --- components/canvas/canvas_data.rs | 2 +- components/compositing/compositor.rs | 15 +- components/compositing/tracing.rs | 1 + components/config/prefs.rs | 3 + components/constellation/constellation.rs | 20 ++ components/constellation/tracing.rs | 1 + components/net/image_cache.rs | 255 ++++++++++++++---- components/script/dom/htmlmediaelement.rs | 4 +- components/script/dom/testworklet.rs | 4 +- components/script/dom/worklet.rs | 4 +- components/script/messaging.rs | 1 + components/script/script_thread.rs | 21 +- components/shared/compositing/lib.rs | 17 +- components/shared/constellation/lib.rs | 4 +- components/shared/net/image_cache.rs | 5 + components/shared/script/lib.rs | 2 + components/webgl/webgl_thread.rs | 2 +- components/webgpu/wgpu_thread.rs | 2 +- tests/wpt/mozilla/meta/MANIFEST.json | 51 ++++ .../mozilla/img_load_more_than_cache.html | 48 ++++ tests/wpt/mozilla/tests/mozilla/test1.jpg | Bin 0 -> 1702 bytes tests/wpt/mozilla/tests/mozilla/test10.jpg | Bin 0 -> 2763 bytes tests/wpt/mozilla/tests/mozilla/test11.jpg | Bin 0 -> 2215 bytes tests/wpt/mozilla/tests/mozilla/test2.jpg | Bin 0 -> 2061 bytes tests/wpt/mozilla/tests/mozilla/test3.jpg | Bin 0 -> 2241 bytes tests/wpt/mozilla/tests/mozilla/test4.jpg | Bin 0 -> 2205 bytes tests/wpt/mozilla/tests/mozilla/test5.jpg | Bin 0 -> 2070 bytes tests/wpt/mozilla/tests/mozilla/test6.jpg | Bin 0 -> 2293 bytes tests/wpt/mozilla/tests/mozilla/test7.jpg | Bin 0 -> 1871 bytes tests/wpt/mozilla/tests/mozilla/test8.jpg | Bin 0 -> 2451 bytes tests/wpt/mozilla/tests/mozilla/test9.jpg | Bin 0 -> 2268 bytes 31 files changed, 392 insertions(+), 70 deletions(-) create mode 100644 tests/wpt/mozilla/tests/mozilla/img_load_more_than_cache.html create mode 100644 tests/wpt/mozilla/tests/mozilla/test1.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test10.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test11.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test2.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test3.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test4.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test5.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test6.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test7.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test8.jpg create mode 100644 tests/wpt/mozilla/tests/mozilla/test9.jpg diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index 0e54cd2ce0f..978fb44c3bd 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -421,7 +421,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { ) -> CanvasData<'a, B> { let size = size.max(MIN_WR_IMAGE_SIZE); let draw_target = backend.create_drawtarget(size); - let image_key = compositor_api.generate_image_key().unwrap(); + let image_key = compositor_api.generate_image_key_blocking().unwrap(); let descriptor = ImageDescriptor { size: size.cast().cast_unit(), stride: None, diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 81a17b5dfda..7be3b333563 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -38,7 +38,7 @@ use pixels::{CorsStatus, ImageFrame, ImageMetadata, PixelFormat, RasterImage}; use profile_traits::mem::{ProcessReports, ProfilerRegistration, Report, ReportKind}; use profile_traits::time::{self as profile_time, ProfilerCategory}; use profile_traits::{path, time_profile}; -use servo_config::opts; +use servo_config::{opts, pref}; use servo_geometry::DeviceIndependentPixel; use style_traits::CSSPixel; use webrender::{CaptureBits, RenderApi, Transaction}; @@ -896,6 +896,19 @@ impl IOCompositor { let _ = sender.send(self.global.borrow().webrender_api.generate_image_key()); }, + CompositorMsg::GenerateImageKeysForPipeline(pipeline_id) => { + let image_keys = (0..pref!(image_key_batch_size)) + .map(|_| self.global.borrow().webrender_api.generate_image_key()) + .collect(); + if let Err(error) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::SendImageKeysForPipeline( + pipeline_id, + image_keys, + ), + ) { + warn!("Sending Image Keys to Constellation failed with({error:?})."); + } + }, CompositorMsg::UpdateImages(updates) => { let mut txn = Transaction::new(); for update in updates { diff --git a/components/compositing/tracing.rs b/components/compositing/tracing.rs index a130eaaa242..f51bf77013d 100644 --- a/components/compositing/tracing.rs +++ b/components/compositing/tracing.rs @@ -59,6 +59,7 @@ mod from_constellation { Self::GetAvailableScreenSize(..) => target!("GetAvailableScreenSize"), Self::CollectMemoryReport(..) => target!("CollectMemoryReport"), Self::Viewport(..) => target!("Viewport"), + Self::GenerateImageKeysForPipeline(..) => target!("GenerateImageKeysForPipeline"), } } } diff --git a/components/config/prefs.rs b/components/config/prefs.rs index a94daba8d34..486a3d40c7b 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -149,6 +149,8 @@ pub struct Preferences { /// Whether or not subpixel antialiasing is enabled for text rendering. pub gfx_subpixel_text_antialiasing_enabled: bool, pub gfx_texture_swizzling_enabled: bool, + /// The amount of image keys we request per batch for the image cache. + pub image_key_batch_size: i64, /// Whether or not the DOM inspector should show shadow roots of user-agent shadow trees pub inspector_show_servo_internal_shadow_roots: bool, pub js_asmjs_enabled: bool, @@ -326,6 +328,7 @@ impl Preferences { gfx_text_antialiasing_enabled: true, gfx_subpixel_text_antialiasing_enabled: true, gfx_texture_swizzling_enabled: true, + image_key_batch_size: 10, inspector_show_servo_internal_shadow_roots: false, js_asmjs_enabled: true, js_asyncstack: false, diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 14c52df80fa..19d273d84c8 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -1448,6 +1448,26 @@ where EmbedderToConstellationMessage::CreateMemoryReport(sender) => { self.mem_profiler_chan.send(ProfilerMsg::Report(sender)); }, + EmbedderToConstellationMessage::SendImageKeysForPipeline(pipeline_id, image_keys) => { + if let Some(pipeline) = self.pipelines.get(&pipeline_id) { + if pipeline + .event_loop + .send(ScriptThreadMessage::SendImageKeysBatch( + pipeline_id, + image_keys, + )) + .is_err() + { + warn!("Could not send image keys to pipeline {:?}", pipeline_id); + } + } else { + warn!( + "Keys were generated for a pipeline ({:?}) that was + closed before the request could be fulfilled.", + pipeline_id + ) + } + }, } } diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 473edd4cd20..d164319a733 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -76,6 +76,7 @@ mod from_compositor { Self::PaintMetric(..) => target!("PaintMetric"), Self::EvaluateJavaScript(..) => target!("EvaluateJavaScript"), Self::CreateMemoryReport(..) => target!("CreateMemoryReport"), + Self::SendImageKeysForPipeline(..) => target!("SendImageKeysForPipeline"), } } } diff --git a/components/net/image_cache.rs b/components/net/image_cache.rs index aa484fc3e45..8df2b277afc 100644 --- a/components/net/image_cache.rs +++ b/components/net/image_cache.rs @@ -2,8 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::collections::HashMap; +use std::cmp::min; use std::collections::hash_map::Entry::{Occupied, Vacant}; +use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex}; use std::{mem, thread}; @@ -11,7 +12,7 @@ use base::id::PipelineId; use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData}; use imsz::imsz_from_reader; use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; -use log::{debug, warn}; +use log::{debug, error, warn}; use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps}; use malloc_size_of_derive::MallocSizeOf; use mime::Mime; @@ -29,7 +30,9 @@ use resvg::{tiny_skia, usvg}; use servo_config::pref; use servo_url::{ImmutableOrigin, ServoUrl}; use webrender_api::units::DeviceIntSize; -use webrender_api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat}; +use webrender_api::{ + ImageDescriptor, ImageDescriptorFlags, ImageFormat, ImageKey as WebRenderImageKey, +}; use crate::resource_thread::CoreResourceThreadPool; @@ -95,6 +98,8 @@ fn decode_bytes_sync( DecoderMsg { key, image } } +/// This will block on getting an ImageKey +/// but that is ok because it is done once upon start-up of a script-thread. fn get_placeholder_image( compositor_api: &CrossProcessCompositorApi, data: &[u8], @@ -102,11 +107,18 @@ fn get_placeholder_image( let mut image = load_from_memory(data, CorsStatus::Unsafe) .or_else(|| load_from_memory(FALLBACK_RIPPY, CorsStatus::Unsafe)) .expect("load fallback image failed"); - set_webrender_image_key(compositor_api, &mut image); + let image_key = compositor_api + .generate_image_key_blocking() + .expect("Could not generate image key"); + set_webrender_image_key(compositor_api, &mut image, image_key); Arc::new(image) } -fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &mut RasterImage) { +fn set_webrender_image_key( + compositor_api: &CrossProcessCompositorApi, + image: &mut RasterImage, + image_key: WebRenderImageKey, +) { if image.id.is_some() { return; } @@ -146,11 +158,9 @@ fn set_webrender_image_key(compositor_api: &CrossProcessCompositorApi, image: &m offset: 0, flags, }; - if let Some(image_key) = compositor_api.generate_image_key() { - let data = SerializableImageData::Raw(IpcSharedMemory::from_bytes(&bytes)); - compositor_api.add_image(image_key, descriptor, data); - image.id = Some(image_key); - } + let data = SerializableImageData::Raw(IpcSharedMemory::from_bytes(&bytes)); + compositor_api.add_image(image_key, descriptor, data); + image.id = Some(image_key); } // ====================================================================== @@ -404,6 +414,51 @@ struct RasterizationTask { result: Option, } +/// Used for storing images that do not have a `WebRenderImageKey` yet. +#[derive(Debug, MallocSizeOf)] +enum PendingKey { + RasterImage((LoadKey, RasterImage)), + Svg((LoadKey, RasterImage, DeviceIntSize)), +} + +/// The state of the `WebRenderImageKey`` cache +#[derive(Debug, MallocSizeOf)] +enum KeyCacheState { + /// We already requested a batch of keys. + PendingBatch, + /// We have some keys in the cache. + Ready(Vec), +} + +impl KeyCacheState { + fn size(&self) -> usize { + match self { + KeyCacheState::PendingBatch => 0, + KeyCacheState::Ready(items) => items.len(), + } + } +} + +/// As getting new keys takes a round trip over the constellation, we keep a small cache of them. +/// Additionally, this cache will store image resources that do not have a key yet because those +/// are needed to complete the load. +#[derive(MallocSizeOf)] +struct KeyCache { + /// A cache of `WebRenderImageKey`. + cache: KeyCacheState, + /// These images are loaded but have no key assigned to yet. + images_pending_keys: VecDeque, +} + +impl KeyCache { + fn new() -> Self { + KeyCache { + cache: KeyCacheState::Ready(Vec::new()), + images_pending_keys: VecDeque::new(), + } + } +} + /// ## Image cache implementation. #[derive(MallocSizeOf)] struct ImageCacheStore { @@ -433,33 +488,126 @@ struct ImageCacheStore { /// Cross-process compositor API instance. #[ignore_malloc_size_of = "Channel from another crate"] compositor_api: CrossProcessCompositorApi, + + // The PipelineId will initially be None because the constructed cache is not associated + // with any pipeline yet. This will happen later by way of `create_new_image_cache`. + pipeline_id: Option, + + /// Main struct to handle the cache of `WebRenderImageKey` and + /// images that do not have a key yet. + key_cache: KeyCache, } impl ImageCacheStore { - // Change state of a url from pending -> loaded. - fn complete_load(&mut self, key: LoadKey, mut load_result: LoadResult) { + /// Finishes loading the image by setting the WebRenderImageKey and calling `compete_load` or `complete_load_svg`. + fn set_key_and_finish_load(&mut self, pending_image: PendingKey, image_key: WebRenderImageKey) { + match pending_image { + PendingKey::RasterImage((pending_id, mut raster_image)) => { + set_webrender_image_key(&self.compositor_api, &mut raster_image, image_key); + self.complete_load(pending_id, LoadResult::LoadedRasterImage(raster_image)); + }, + PendingKey::Svg((pending_id, mut raster_image, requested_size)) => { + set_webrender_image_key(&self.compositor_api, &mut raster_image, image_key); + self.complete_load_svg(raster_image, pending_id, requested_size); + }, + } + } + + /// If a key is available the image will be immediately loaded, otherwise it will load then the next batch of + /// keys is received. Only call this if the image does not have a `LoadKey` yet. + fn load_image_with_keycache(&mut self, pending_image: PendingKey) { + if let Some(pipeline_id) = self.pipeline_id { + match self.key_cache.cache { + KeyCacheState::PendingBatch => { + self.key_cache.images_pending_keys.push_back(pending_image); + }, + KeyCacheState::Ready(ref mut cache) => match cache.pop() { + Some(image_key) => { + self.set_key_and_finish_load(pending_image, image_key); + }, + None => { + self.key_cache.images_pending_keys.push_back(pending_image); + self.compositor_api.generate_image_key_async(pipeline_id); + self.key_cache.cache = KeyCacheState::PendingBatch + }, + }, + } + } else { + error!("No pipeline id for this image key cache."); + } + } + + /// Insert received keys into the cache and complete the loading of images. + fn insert_keys_and_load_images(&mut self, image_keys: Vec) { + if let KeyCacheState::PendingBatch = self.key_cache.cache { + self.key_cache.cache = KeyCacheState::Ready(image_keys); + let len = min( + self.key_cache.cache.size(), + self.key_cache.images_pending_keys.len(), + ); + let images = self + .key_cache + .images_pending_keys + .drain(0..len) + .collect::>(); + for key in images { + self.load_image_with_keycache(key); + } + if !self.key_cache.images_pending_keys.is_empty() { + self.compositor_api + .generate_image_key_async(self.pipeline_id.unwrap()); + self.key_cache.cache = KeyCacheState::PendingBatch + } + } else { + unreachable!("A batch was received while we didn't request one") + } + } + + /// Complete the loading the of the rasterized svg image. This needs the `RasterImage` to + /// already have a `WebRenderImageKey`. + fn complete_load_svg( + &mut self, + rasterized_image: RasterImage, + pending_image_id: PendingImageId, + requested_size: DeviceIntSize, + ) { + let listeners = { + self.rasterized_vector_images + .get_mut(&(pending_image_id, requested_size)) + .map(|task| { + task.result = Some(rasterized_image); + std::mem::take(&mut task.listeners) + }) + .unwrap_or_default() + }; + + for (pipeline_id, sender) in listeners { + let _ = sender.send(ImageCacheResponseMessage::VectorImageRasterizationComplete( + RasterizationCompleteResponse { + pipeline_id, + image_id: pending_image_id, + requested_size, + }, + )); + } + } + + /// The rest of complete load. This requires that images have a valid `WebRenderImageKey`. + fn complete_load(&mut self, key: LoadKey, load_result: LoadResult) { debug!("Completed decoding for {:?}", load_result); let pending_load = match self.pending_loads.remove(&key) { Some(load) => load, None => return, }; - match load_result { - LoadResult::LoadedRasterImage(ref mut raster_image) => { - set_webrender_image_key(&self.compositor_api, raster_image) - }, - LoadResult::LoadedVectorImage(ref vector_image) => { - self.vector_images.insert(key, vector_image.clone()); - }, - LoadResult::PlaceholderLoaded(..) | LoadResult::None => {}, - } - let url = pending_load.final_url.clone(); let image_response = match load_result { LoadResult::LoadedRasterImage(raster_image) => { + assert!(raster_image.id.is_some()); ImageResponse::Loaded(Image::Raster(Arc::new(raster_image)), url.unwrap()) }, LoadResult::LoadedVectorImage(vector_image) => { + self.vector_images.insert(key, vector_image.clone()); let natural_dimensions = vector_image.svg_tree.size().to_int_size(); let metadata = ImageMetadata { width: natural_dimensions.width(), @@ -523,7 +671,10 @@ impl ImageCacheStore { fn handle_decoder(&mut self, msg: DecoderMsg) { let image = match msg.image { None => LoadResult::None, - Some(DecodedImage::Raster(raster_image)) => LoadResult::LoadedRasterImage(raster_image), + Some(DecodedImage::Raster(raster_image)) => { + self.load_image_with_keycache(PendingKey::RasterImage((msg.key, raster_image))); + return; + }, Some(DecodedImage::Vector(vector_image_data)) => { LoadResult::LoadedVectorImage(vector_image_data) }, @@ -559,7 +710,9 @@ impl ImageCache for ImageCacheImpl { rasterized_vector_images: HashMap::new(), placeholder_image: get_placeholder_image(&compositor_api, &rippy_data), placeholder_url: ServoUrl::parse("chrome://resources/rippy.png").unwrap(), - compositor_api, + compositor_api: compositor_api.clone(), + pipeline_id: None, + key_cache: KeyCache::new(), })), thread_pool: Arc::new(CoreResourceThreadPool::new( thread_count, @@ -623,7 +776,7 @@ impl ImageCache for ImageCacheImpl { } } - let decoded = { + let (key, decoded) = { let result = store .pending_loads .get_cached(url.clone(), origin.clone(), cors_setting); @@ -631,11 +784,14 @@ impl ImageCache for ImageCacheImpl { CacheResult::Hit(key, pl) => match (&pl.result, &pl.metadata) { (&Some(Ok(_)), _) => { debug!("Sync decoding {} ({:?})", url, key); - decode_bytes_sync( + ( key, - pl.bytes.as_slice(), - pl.cors_status, - pl.content_type.clone(), + decode_bytes_sync( + key, + pl.bytes.as_slice(), + pl.cors_status, + pl.content_type.clone(), + ), ) }, (&None, Some(meta)) => { @@ -674,7 +830,8 @@ impl ImageCache for ImageCacheImpl { is_placeholder, }) }, - _ => ImageCacheResult::LoadError, + // Note: this happens if we are pending a batch of image keys. + _ => ImageCacheResult::Pending(key), } } @@ -767,7 +924,7 @@ impl ImageCache for ImageCacheImpl { height: tinyskia_requested_size.height(), }; - let mut rasterized_image = RasterImage { + let rasterized_image = RasterImage { metadata: ImageMetadata { width: tinyskia_requested_size.width(), height: tinyskia_requested_size.height(), @@ -779,28 +936,12 @@ impl ImageCache for ImageCacheImpl { cors_status: vector_image.cors_status, }; - let listeners = { - let mut store = store.lock().unwrap(); - set_webrender_image_key(&store.compositor_api, &mut rasterized_image); - store - .rasterized_vector_images - .get_mut(&(image_id, requested_size)) - .map(|task| { - task.result = Some(rasterized_image); - std::mem::take(&mut task.listeners) - }) - .unwrap_or_default() - }; - - for (pipeline_id, sender) in listeners { - let _ = sender.send(ImageCacheResponseMessage::VectorImageRasterizationComplete( - RasterizationCompleteResponse { - pipeline_id, - image_id, - requested_size, - }, - )); - } + let mut store = store.lock().unwrap(); + store.load_image_with_keycache(PendingKey::Svg(( + image_id, + rasterized_image, + requested_size, + ))); }); None @@ -905,6 +1046,7 @@ impl ImageCache for ImageCacheImpl { fn create_new_image_cache( &self, + pipeline_id: Option, compositor_api: CrossProcessCompositorApi, ) -> Arc { let store = self.store.lock().unwrap(); @@ -919,10 +1061,17 @@ impl ImageCache for ImageCacheImpl { compositor_api, vector_images: HashMap::new(), rasterized_vector_images: HashMap::new(), + key_cache: KeyCache::new(), + pipeline_id, })), thread_pool: self.thread_pool.clone(), }) } + + fn fill_key_cache_with_batch_of_keys(&self, image_keys: Vec) { + let mut store = self.store.lock().unwrap(); + store.insert_keys_and_load_images(image_keys); + } } impl Drop for ImageCacheStore { diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index cc1c998db93..32b2dd3e8a8 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -237,7 +237,7 @@ impl VideoFrameRenderer for MediaFrameRenderer { Some(current_frame) => { self.old_frame = Some(current_frame.image_key); - let Some(new_image_key) = self.compositor_api.generate_image_key() else { + let Some(new_image_key) = self.compositor_api.generate_image_key_blocking() else { return; }; @@ -270,7 +270,7 @@ impl VideoFrameRenderer for MediaFrameRenderer { updates.push(ImageUpdate::AddImage(new_image_key, descriptor, image_data)); }, None => { - let Some(image_key) = self.compositor_api.generate_image_key() else { + let Some(image_key) = self.compositor_api.generate_image_key_blocking() else { return; }; diff --git a/components/script/dom/testworklet.rs b/components/script/dom/testworklet.rs index 7c7630ac788..6729058345d 100644 --- a/components/script/dom/testworklet.rs +++ b/components/script/dom/testworklet.rs @@ -12,7 +12,7 @@ use crate::dom::bindings::codegen::Bindings::TestWorkletBinding::TestWorkletMeth use crate::dom::bindings::codegen::Bindings::WorkletBinding::Worklet_Binding::WorkletMethods; use crate::dom::bindings::codegen::Bindings::WorkletBinding::WorkletOptions; use crate::dom::bindings::error::Fallible; -use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; +use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::promise::Promise; @@ -70,7 +70,7 @@ impl TestWorkletMethods for TestWorklet { fn Lookup(&self, key: DOMString) -> Option { let id = self.worklet.worklet_id(); - let pool = ScriptThread::worklet_thread_pool(); + let pool = ScriptThread::worklet_thread_pool(self.global().image_cache()); pool.test_worklet_lookup(id, String::from(key)) .map(DOMString::from) } diff --git a/components/script/dom/worklet.rs b/components/script/dom/worklet.rs index be0a588b279..9d5e5cbafcb 100644 --- a/components/script/dom/worklet.rs +++ b/components/script/dom/worklet.rs @@ -37,7 +37,7 @@ use crate::dom::bindings::codegen::Bindings::WorkletBinding::{WorkletMethods, Wo use crate::dom::bindings::error::Error; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::TrustedPromise; -use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; +use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot, RootCollection, ThreadLocalStackRoots}; use crate::dom::bindings::str::USVString; use crate::dom::bindings::trace::{CustomTraceable, JSTraceable, RootedTraceableBox}; @@ -153,7 +153,7 @@ impl WorkletMethods for Worklet { self.droppable_field .thread_pool - .get_or_init(ScriptThread::worklet_thread_pool) + .get_or_init(|| ScriptThread::worklet_thread_pool(self.global().image_cache())) .fetch_and_invoke_a_worklet_script( self.window.pipeline_id(), self.droppable_field.worklet_id, diff --git a/components/script/messaging.rs b/components/script/messaging.rs index a3d0e0046f1..8294d977944 100644 --- a/components/script/messaging.rs +++ b/components/script/messaging.rs @@ -92,6 +92,7 @@ impl MixedMessage { ScriptThreadMessage::SetWebGPUPort(..) => None, ScriptThreadMessage::SetScrollStates(id, ..) => Some(*id), ScriptThreadMessage::EvaluateJavaScript(id, _, _) => Some(*id), + ScriptThreadMessage::SendImageKeysBatch(..) => None, }, MixedMessage::FromScript(inner_msg) => match inner_msg { MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => { diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 3ea10ca9bdc..1826e747e65 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -735,7 +735,8 @@ impl ScriptThread { }) } - pub(crate) fn worklet_thread_pool() -> Rc { + /// The worklet will use the given `ImageCache`. + pub(crate) fn worklet_thread_pool(image_cache: Arc) -> Rc { with_optional_script_thread(|script_thread| { let script_thread = script_thread.unwrap(); script_thread @@ -752,9 +753,7 @@ impl ScriptThread { .senders .pipeline_to_constellation_sender .clone(), - image_cache: script_thread - .image_cache - .create_new_image_cache(script_thread.compositor_api.clone()), + image_cache, #[cfg(feature = "webgpu")] gpu_id_hub: script_thread.gpu_id_hub.clone(), inherited_secure_context: script_thread.inherited_secure_context, @@ -1998,6 +1997,18 @@ impl ScriptThread { ScriptThreadMessage::EvaluateJavaScript(pipeline_id, evaluation_id, script) => { self.handle_evaluate_javascript(pipeline_id, evaluation_id, script, can_gc); }, + ScriptThreadMessage::SendImageKeysBatch(pipeline_id, image_keys) => { + if let Some(window) = self.documents.borrow().find_window(pipeline_id) { + window + .image_cache() + .fill_key_cache_with_batch_of_keys(image_keys); + } else { + warn!( + "Could not find window corresponding to an image cache to send image keys to pipeline {:?}", + pipeline_id + ); + } + }, } } @@ -3321,7 +3332,7 @@ impl ScriptThread { let image_cache = self .image_cache - .create_new_image_cache(self.compositor_api.clone()); + .create_new_image_cache(Some(incomplete.pipeline_id), self.compositor_api.clone()); let layout_config = LayoutConfig { id: incomplete.pipeline_id, diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 607dda6cfe9..c9d2fe861bf 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -148,6 +148,9 @@ pub enum CompositorMsg { /// Create a new image key. The result will be returned via the /// provided channel sender. GenerateImageKey(IpcSender), + /// The same as the above but it will be forwarded to the pipeline instead + /// of send via a channel. + GenerateImageKeysForPipeline(PipelineId), /// Perform a resource update operation. UpdateImages(SmallVec<[ImageUpdate; 1]>), @@ -294,12 +297,24 @@ impl CrossProcessCompositorApi { } /// Create a new image key. Blocks until the key is available. - pub fn generate_image_key(&self) -> Option { + pub fn generate_image_key_blocking(&self) -> Option { let (sender, receiver) = ipc::channel().unwrap(); self.0.send(CompositorMsg::GenerateImageKey(sender)).ok()?; receiver.recv().ok() } + /// Sends a message to the compositor for creating new image keys. + /// The compositor will then send a batch of keys over the constellation to the script_thread + /// and the appropriate pipeline. + pub fn generate_image_key_async(&self, pipeline_id: PipelineId) { + if let Err(e) = self + .0 + .send(CompositorMsg::GenerateImageKeysForPipeline(pipeline_id)) + { + warn!("Could not send image keys to Compositor {}", e); + } + } + pub fn add_image( &self, key: ImageKey, diff --git a/components/shared/constellation/lib.rs b/components/shared/constellation/lib.rs index 95e90a95cb4..9e31e99c58b 100644 --- a/components/shared/constellation/lib.rs +++ b/components/shared/constellation/lib.rs @@ -30,8 +30,8 @@ use serde::{Deserialize, Serialize}; use servo_url::{ImmutableOrigin, ServoUrl}; pub use structured_data::*; use strum_macros::IntoStaticStr; -use webrender_api::ExternalScrollId; use webrender_api::units::LayoutVector2D; +use webrender_api::{ExternalScrollId, ImageKey}; /// Messages to the Constellation from the embedding layer, whether from `ServoRenderer` or /// from `libservo` itself. @@ -94,6 +94,8 @@ pub enum EmbedderToConstellationMessage { EvaluateJavaScript(WebViewId, JavaScriptEvaluationId, String), /// Create a memory report and return it via the ipc sender CreateMemoryReport(IpcSender), + /// Sends the generated image key to the image cache associated with this pipeline. + SendImageKeysForPipeline(PipelineId, Vec), } /// A description of a paint metric that is sent from the Servo renderer to the diff --git a/components/shared/net/image_cache.rs b/components/shared/net/image_cache.rs index eab10475e9a..cd98fc5b6e1 100644 --- a/components/shared/net/image_cache.rs +++ b/components/shared/net/image_cache.rs @@ -14,6 +14,7 @@ use pixels::{CorsStatus, ImageMetadata, RasterImage}; use profile_traits::mem::Report; use serde::{Deserialize, Serialize}; use servo_url::{ImmutableOrigin, ServoUrl}; +use webrender_api::ImageKey; use webrender_api::units::DeviceIntSize; use crate::FetchResponseMsg; @@ -225,6 +226,10 @@ pub trait ImageCache: Sync + Send { /// Create new image cache based on this one, while reusing the existing thread_pool. fn create_new_image_cache( &self, + pipeline_id: Option, compositor_api: CrossProcessCompositorApi, ) -> Arc; + + /// Fills the image cache with a batch of keys. + fn fill_key_cache_with_batch_of_keys(&self, image_keys: Vec); } diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 3086d10bc88..74d264e3697 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -251,6 +251,8 @@ pub enum ScriptThreadMessage { /// Evaluate the given JavaScript and return a result via a corresponding message /// to the Constellation. EvaluateJavaScript(PipelineId, JavaScriptEvaluationId, String), + /// A new batch of keys for the image cache for the specific pipeline. + SendImageKeysBatch(PipelineId, Vec), } impl fmt::Debug for ScriptThreadMessage { diff --git a/components/webgl/webgl_thread.rs b/components/webgl/webgl_thread.rs index 77ee8f507cd..b5af3fdb68b 100644 --- a/components/webgl/webgl_thread.rs +++ b/components/webgl/webgl_thread.rs @@ -907,7 +907,7 @@ impl WebGLThread { let descriptor = Self::image_descriptor(size, alpha); let data = Self::external_image_data(context_id, image_buffer_kind); - let image_key = compositor_api.generate_image_key().unwrap(); + let image_key = compositor_api.generate_image_key_blocking().unwrap(); compositor_api.add_image(image_key, descriptor, data); image_key diff --git a/components/webgpu/wgpu_thread.rs b/components/webgpu/wgpu_thread.rs index 6efc63a6e70..573be890eb8 100644 --- a/components/webgpu/wgpu_thread.rs +++ b/components/webgpu/wgpu_thread.rs @@ -502,7 +502,7 @@ impl WGPU { .lock() .expect("Lock poisoned?") .next_id(WebrenderImageHandlerType::WebGPU); - let image_key = self.compositor_api.generate_image_key().unwrap(); + let image_key = self.compositor_api.generate_image_key_blocking().unwrap(); let context_id = WebGPUContextId(id.0); if let Err(e) = sender.send((context_id, image_key)) { warn!("Failed to send ExternalImageId to new context ({})", e); diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 72c56c27fda..0fd6d74c3b1 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -10756,6 +10756,50 @@ "9235007d960cc6c804a93c89f24881bedc3613c3", [] ], + "test1.jpg": [ + "124d70729ea649ee253254ec0f1e2ea0f926ed31", + [] + ], + "test10.jpg": [ + "1ff396cd08dbf8a6039f39e79af860c44859da48", + [] + ], + "test11.jpg": [ + "8074fffaa33a371825c7a54c9536fc22f4d8bca4", + [] + ], + "test2.jpg": [ + "e35589b82207d655379acbcac3932ccbef846dee", + [] + ], + "test3.jpg": [ + "1f135409c45ea58b4cb7b89bdf4f71b8b43d0b88", + [] + ], + "test4.jpg": [ + "793ded09d3e65db01ea2c55c6f9f2bbe10366b94", + [] + ], + "test5.jpg": [ + "3df583072e57fd6195104e8fb16be7a91b751a70", + [] + ], + "test6.jpg": [ + "81c68f412a27fcdcb1d961db568168b0e9ea2caf", + [] + ], + "test7.jpg": [ + "b6169d4a7b804b184e4677eb9eda54c67f0d2e75", + [] + ], + "test8.jpg": [ + "295e9c523527c584eac494298060d2631d69603d", + [] + ], + "test9.jpg": [ + "c82f94f0134105792bb45a3a827c2a51cc92e674", + [] + ], "text-overflow-ellipsis-stacking-context-ref.html": [ "14215e780ab4a0cf00ef23b8472636a393aeacf1", [] @@ -13525,6 +13569,13 @@ {} ] ], + "img_load_more_than_cache.html": [ + "41f1212f8df42c9082d462d861a1ae5545ea5523", + [ + null, + {} + ] + ], "img_multiple_request.html": [ "df625a2bc338c0220808cf7a153128fe9b9d48a8", [ diff --git a/tests/wpt/mozilla/tests/mozilla/img_load_more_than_cache.html b/tests/wpt/mozilla/tests/mozilla/img_load_more_than_cache.html new file mode 100644 index 00000000000..41f1212f8df --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/img_load_more_than_cache.html @@ -0,0 +1,48 @@ + + + Test Loading more images than keys obtained in a batch by the image cache + + + + + + + + + + + + + + + + + diff --git a/tests/wpt/mozilla/tests/mozilla/test1.jpg b/tests/wpt/mozilla/tests/mozilla/test1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..124d70729ea649ee253254ec0f1e2ea0f926ed31 GIT binary patch literal 1702 zcmex=``2_j6xdp@o1cgOJMMZh|#U;coyG6VInuyV4pa*FVB^NNrR z{vTox z1u=$IrPWO>#eZCF?!Mjy5I&J4 zKhU>w0t`$*z>Lts!pg*K;=pLikWc{hF9$F*7+INqq%1w6RNZ;^k#~B=E}ny1?)eLC zxTHL`NOkn35FfCrN;=`oA*q^JL zgxVcEF8B3BawNwdp4Ip1{v?rAJ5%LK7CaX%I%V_H@toGvDV3L+=Y*ct_`Fo|!s*7W zmHA7*zVp9t542Ma7%>dY9E_~6&;y1h1GAu^gJWP(2>dcQFl*HT zo4rvMLEQm@XD94ZhSuT~{;j&wQJ_o0Ap!H0^QV_|0hK-8A6qmR#urQ$j zl1V|S9O#0Br>^8=ow)t=Rf9mAOWq6rqBrGVk6P|axN&gZq<3Ba&X!Cs3ICS8WcQ-| zn`2e)3Ir^)EOHe#VKV&o=_lAS1p%OCpmc_8nWAB!V}e6L;Y36-6Zn!m@zDh7s98#$ z6FfNHITfAkQZ_xH;<8Tf?+=46#m^5|uM6xt`bzLm!%y*Nd@~J~9GfiV{5@L#6-# literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test10.jpg b/tests/wpt/mozilla/tests/mozilla/test10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1ff396cd08dbf8a6039f39e79af860c44859da48 GIT binary patch literal 2763 zcmcImc|26>8$UB=W{hpjFk@HtC3O*Fxt1|MyI)~ai7S~x`60U!GD9N2>_rKUEla5| zC}bDWVk})0A$#hl>n2NM?x9Qfzu)Knai8~m&Uw%KocDd7=lMS0=RKT3&KQ8THn%bd zU@#bP3Tgo74PXL*aQNm02?Dy1Xe1JWK=N>Lq0oFhe0;n-yuADvK`cK8hvDVL3S)6X zcmjdICm%JkwZeyz|Zf?+7Tp zaZJqWW?rcNq*z$I>vSoYL=wS@|}K&y3pxbNXW`ut?T2US)5_&urOWG2$fdvVf} z0|+!KNFHB+Hh|rB^xp@mzv+ilmO^8mL^{u|$tt>6u3Jhu|5aUQzzTe9_9D!6_5!s? zK{?&di2xjjRv@hfjxMKZO2KCNNVY~8Ku!OOlFN3b=iM}RV0@7#3#HbdKEmJtl&2^xPEbfjR$YTk3CL|`X;bOtEqH=l%DAw+=D%D| z`c*7$*$pKK3C8x-y=~HEPwfgV4j%033%zQ}dMP#$Go?f*;N=#tc04Xyph|bBAaBTw ze{r)?;j?S0j||E-vq)DA*3Rsd1+OGJ2_(fCB%y*n3ncH-*N(n*K9z|t7_@nkpqe+R zGri~J-T4|3d)6JPB8g9IqL3?-NjLgEyHsKN?vcJJU!Ib~(pS<;LQa_*)r-fri!360 z8)Yh6ZJifdneQWZW#%6VAk0_GuM)X-i>-bl*dQxr6WT+GC_M^zE+>RK?B9@dkfPk8 z>!ck$*)QX2YIKf?;reiR%E7&yYx%Y!W0rK@2y(>L0dHYNdD9_ zI*;(>J3@H9m}RZ3!bN^F##PBjggF-n8?nJsef*lABKM)QE>^#uRn@Pn{#7SU_f}o) zlHld&jsBJdoAC`v19O|-Vo@0^-Uoh5gm?NsrxmT}K$xgQn1C=O3V{N5FaZ$&5`siV zNL@qApDw$fOu@@-B0=@b-2wu>1;nVqk%q{4=McNDiX@Y8OFD}g7$c^E8|UT#$`9KL zJWiXK;m&xg_y@gr-t)?>B+R7g@Qikn=iC!h zrlL_dzn@M=a`B7};d4qxDeHlCNYCPw82lQ5@tmTd zW)8rpTq@#YK{GNRNw14KM<-l-uJq!3r5Qs7f0(c8>h)%m)XWUu#nru+@AuTTj(^*9 zV-FT$EgbPzH*kQA2AP_RRrgP!lsAtXyZX^ti=Vd{PLf$2RUh5E)*leaLqAaKPYON7 zy;16^CL>jPeDps)6U5!IvK|2=?r6-Bufw$Y&n2$+k1ZG0vN&_(Bb`!3W4Z6o^tto$` z6#Bp2n;%}KGmBH{R}#H)$@`*Cttn!UwUyw3A#vl9xXuy%>(A)*V`EfpkaE!4=Pm7G z+`_nTqDVnU`KcS8rPn)x4H>iHGkW^BxqIr8!ykNULU4fC!bxiQQtie1d~5Um^Sg(d zZcMFOEak^Lf&I58Y)LfT1%5?U*@e8YyXHCOZLN2GspN7Gmswyk$4M`@Umv}TzP@U~ zuy9f{EkB#tR2ip6=jxxMF`m1NFfA;Vf>i)%_U4hH43#H1oCp5n z#{r5Fpn-*`OU>Pk4n4)yfBgHowpo_2tF@iP%JxONyQHx-&F4vky&EPn*=$glHZnGs z-w~xF*4)$+Uk>mfGg^)%CpAqg1!n^)WK>LP!1B!=chqUIrl-Opoo7`~pUW0~ZvODV zLmzg#$}(zUNWyWPjTrkfUl%7Y(e=-xXd24A@pWYN{ub7R?nKx|D(cQed? z-;fF>iDrrziKuP_$<}85Y_C|*;k3wcMDMZ8j-UWM^%_$=I4W z#5M1{@M1Y}imaENP1MSP?kO;=FDOzJ0c#=|_7|Vf+`so$9{VWbo?gfukAAqEH^u<1 ULO)PiG|it!1JHbp(In2$A0gC3VgLXD literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test11.jpg b/tests/wpt/mozilla/tests/mozilla/test11.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8074fffaa33a371825c7a54c9536fc22f4d8bca4 GIT binary patch literal 2215 zcmcgs3s6(n7QLU`5JCtyArJ`GCK%;ckU&x_1xZ9vLaV0W2d$AjT8Sdi0P+(CP(-jQ zqCBQnDB-6@!FhaCo+2s1mIoq;AO2viIyBhUASxhc@=j_~J8#~M)6TT}oSD1l?0fcE zv-aBCFktu&!o@+sK@beXP$XE8VHgU8Py{hNTQG_6brJ2g(a1a zTT*FM3k%#9r&%*>Y-}i2OgmeK9i3srFeZUfaEE}IW0*O^(!!GQhs~gYXe6i@Dn?-% zgwSA=1{4`g~HJ9;f!d6e+ z3{|XS?yIV(waq`-u=PRw0k6#w^&QI6Br#LaeE!<)6*cdw&W_B9IaS}k$Y|l^pF>>{C%r7(}GfXi^6i>)6e{KHFCBb~_m}l?Re!yUM zn9*#%Ilo=ocj(*H(D20;Eq+Sb`4IIw(PyP8c`bL|_mpZ6_JwG4JI5-Xs|NQ>f3U4y z%G|r{8T0(=F6O03!BJhagU{(JXObeW+_GDk)jm@*{tw;q^o;^X6?H+ksz7%afOZEm zLlDf^Y`+?epa3)5Goh7$TXP&2@I;dM?7EL46ei6P1CTj&M|W_ps;OLXYFoU0XP>+( zXyRB5HS!?8nqD2W-xrz_@53G7p^U{x_eIzuyS!AcoZg~ zCa4e?G#r_Z?a3306xpr#>$V8s%r(oJ!>1)3U9OmK(yUkdvYQ<$x+eZb74cm7=8K2x zbIht!hq4|Hs7CU(-Wki*TKTLmTPpr(5$o1?$Cj$LP3g04jbB8s`D#W;crp;gt8-Iw zOIe~=r(EqzAJcS#j7V?;^w+Ex>SF<~GT(cwDKot>ZGd`A{+t0CH~D%4G-dK<4A32u z|2KR=WoTd)AuxjdCViO3;fbJf+*2q~uz}?3ei>NC`GC)jjg{~FxhXltzx+gaYj#QX z^~!&?I4)3R?swJMr$wzHJMRb#vwO?3If@?^N;tb7N>43F%p<8hi^OqUEm9NIz9c!i zA@D@y)%I9xzvIq{>LbS+_luN0osXp(MxVx%4(R6(ju!O(R66idU-yIcIm%4^!=*M$ z_e=@os}#qUjz)1;xa1VQ<>a7u0IFJK9Ckz^P`$~Fs^W?2fUYDmMK4fQVe2pGjU9_m z?>dq=UK@^d`U%BZQD^k2=H#Pae~cBT%(Lnv3ZcW)BV9P83|SvSr|GFfg1RSO@zFgj zqF~2vqO#IgFxi^Xw1ezjxpf|MIQMr41(XtQ!ItS;P$3sT{2d-eVN@jBLGy!Juz#Gn}fgYIQrc=*xwL9L>uTsr-KGCQ8lO yi|UnPsZ*sktgZ~9mlPxp= literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test2.jpg b/tests/wpt/mozilla/tests/mozilla/test2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e35589b82207d655379acbcac3932ccbef846dee GIT binary patch literal 2061 zcmcgtX;4#F6n<+GAjFrD06|m;s1;&NNN`kygh6Ey6i|yGU;^T})EXeTA(llHMO4rd zEQ=r*wJwN?j;L6y8xpa&079z)3(|@jMG$34Z&F11tL;pu=iYg9mUrJh%XiMxHR?1F z;WNj34g|w66bTljYlb`^6hZVG5C%3*!Ep@3jmcyZg=$Qtni!jyn9>-8DUD7uF(J$e zx*3zjVo^s~Sei2}8B7*a{}C7kXE59d$Bme#CZ^1PO}c9kodRV;*(gkh5IT&~Vcm7e zO7E(kpkX3m2#%70%^GMC^e;A?gkoeEq3D_*DhjLwNsBS|W!QPLWaCTjqF#g1 zEan_9p;xt|!yrM#mu|xMx6Zrz%{66)4e9>0{O$7_5HyE?5Qqe-HT*V)LokAxu^1Aj zJzpvZp;JI&U>xb^*9UdI$O+w<<{TFJv?Nz7X2)y~+}}`m@K~K9d{x!x>!S?;mbOu;fs2VQ{7X3!xB#Sx0J$*0?31Opn9EuG0QvzNFB6txg6 zbHT#sq^znsJ11eFR3?upyRS7k0ZJo=Ke8?MoVr4F8`~ji>O2=O3hw%vle6@r=T7dp z%TJh*Co^hFjg9@bxi|QaeIg$$wq5+nChz;OSeh)|W*j>zPV1LCsc1f?plk{8p)<+w z`^%_PE+^b40dS2q+|(NfX_=x+J0kvZxGrVs4AgNo0_&&?XB;Re0*T;6|Emct9S z^0_i5w<%nC>SDwlz;*FFXKCN$+;2YhV9UwsqtCr*ANq6m1=MDZ>G~j|jbcbj!jC6x zUBd0&<*Yj0`x<1y5o7^F$*|F^f{`QU8lIQF*>YlcJ;?$M?4=YjqItY!U%V5Bd%t zZV!M2h;AFeFq61rB3V`u!4XL1F+-Rji2)|G{VvORZ%;%lcstdRFQkd$gd|4m7Fr7P z_&2lPas02oo>joU8n@c9iFnf25fgI!Nc^1Gaz*b?ZO%dS(`q-J%HQ3@4bEu2td2!5 z-{QNdSu;99q9#R5e62_4c;FBQ;TY(>A%cNB0*SjUQyUOn^@fnjTRs6)Y}+q5hfTlH zsCZ=3`rYIrq;_p5*Wp1&n+iT7Ye^qpX>9L3Iy21_S+mXf?)JD$;ofE4)7Z01JP!B< zSEFi8R6!Xh;h}#hBVJQ}yXRn3&1T+`7E2-Dh&E%-6_B2x5di5sg#hyZvIa{(-!stm znb$#sBZZqIh4mwar41vc|C|Q{gbwB+0%M;i2n;IZE`uZiNkp_%US_C|zITZMwke+K z_3Pqm4}!wrvQQ^$cTd%fO`q%JdkH)htmY@Tjyce_DqL;XSO!$@w&rO!3q=kf%O z_urlIf0o`j*w>l;7+;_EVTUrO@!gs;-r=|EQW86*rHO~1dMb;9)p3x~>Cx#26Dl0< zMY2Q3^LqO>IV(MU-@=z(zU_JOs_%<+)9>V|o0dNpi&msekaC%P5~+P{*HwVDiF7^m zaTNaqG{}Io0GY(Ss6|UeCrRa*Rdt3~^bJY$2=>Q%obJkf%r06-#%P<8m6W)7|Bl@KD^`1?zH!8ubsd63$0{n)!xk7L9b`k&!*3moq7>_qdu2L)*qhJBRj@ldU?9#ODRcF&e zm!z1t?LJSV>Rrf`&Kx2c6R`zCHQYA$;6 zdB1jkon5X)E{_;J*x)~~hqk#X<~)6?iEMU=;L4Q9W`Z3Gg06&!umB0gg$yQ(XCU&x z4CR&tM=?wMz$$*;bLkCrPU|y9{rq#EG9P&smiNBk>SP ztvgCj-XPuSx!A>Rx*lqz%^$w|_^a`C)F0z_HSw%A9UHGuywPb+oUouRh`jB-vyCT^ zZNP{sCfNE9_5jYaBv|7MgJJ?@3b909*|?JC0^Yp#WoK8;Tz!h%EPuMC#`(3|-AqVZ z@AN-W@pebb*+oAwEBlQNnc6(zRPoM><+h!g82dLDZ_SgG%}*qjI#UN6*){fu6TNPJ z9N$w#%|DQJZYSqu& z^s4?oQm|^Yt*VQD!BT14j zIc%L`Dv-KKuHE6EcN;BcD0(#HZ@O;XpGbAsf4$H*>`+Fe_pbWSa~F%EZABZmI&%!K z>`WarEc(rE#JAx7=rK2&YS)b;Nm~U~qYu6&GIQyo5CtZ$Jb{4tZ}BXJq8PctqY>i= zNn}%j;^BW+HTIsX<~1I!Rr0#xM8cK=2WLBGk41c8-;PZ6DLX%MqIIfJE2kZ31&c&c zhV~0C&i-F~`BPw_)2Hunx6-J4X|bKDK9RPMj9n6rv!osS+#6nWRG(}3ac<=7RgDcz z_YQVceGs4-{?he#RoNI5WkW;(CH)bchlGG9t0cl87ca+vSSP?1xjKeW`zJo`NAp^S zHn)U+c?B-0E_KVPX`q@nhtB*|sS8k{mc>np33cRT$N>&xW0utGSkx_bX+|R)9QGIw zO72+mQM~E)Q|8}TlwQX;CuC)>F8l*F2MQCdVj3xTe|pNuo~c&P{58Vce@&Q$Zq{p`N7=9!mGz8m3{vL DG0`$8 literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test4.jpg b/tests/wpt/mozilla/tests/mozilla/test4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..793ded09d3e65db01ea2c55c6f9f2bbe10366b94 GIT binary patch literal 2205 zcmcgtYg7~07CtkR34tVZCWNPf8lLiq5W`iGR)Qc_Q8XfjYFiS(DB2YhN`Vq8PzsWY z6chqgo+eME(*xY59>%-MDM96HN^a=3 zQ9k;Y^v(6$>LU+kK0F+MM)6>cY+7%?_nq7Rb*Sa%lH@Nw{U$s>)27=EcwMpb-aqmR z<@(h>eY`>-V2=S9hF}zZc?coFCQYGh#(Z6T^)1Pb|g1TAV1nx zFWy!%kWlS0oj6?^BdYRKzKYFW=^L|X1j?vQ@d?4997UD;{;wjtSNtLxuO8bQ8fvK6 z?7p*=JJjEUj~cJfof{+EufYKzksy?Axac2(5fVUgE}6mh708%owssz#{GFoQ^X)fH zSMh>{($wdB~mbc&Zzi%67L`lE&^oq!2xv#bF4HA~~mZLu7~5K=JvE%Dbu7U5zr; z9Jw~^yH{~~+6djNS&m#Oh#fkHd8hT3hY9B@t|`u#gg6JHq_{9Q8*Zy&Q|J7)QHyGE zS5$0z)sw2-_pv)?*-EyORkhq`@&bBHeqcni#;!8!LT=kU&JwsD zbX&*obJ+SclN>p76bt9U((?o87*=Cak~3?n896sHn6%VO*5kULMHIy;7ILp?`MhmkR2zaVIn1k<_yC zkvA)By#1i|$;GELyHrW-eYXo7*EU!W7SW|0NDqA;n~!|x1D}{a(JlE(lJJ}kwJex>f*V266IA& z7?L}^a@S42g+D8M;#_R^uC~a^@tAGiOrUGjmj3xI=}`~6iXQFgi~d5IA6)ysb>7({ zO;NA^U|pD}{nk&htGn@Dlb;*MH0fGJTjB%}p#xF$5CW5l2LDqC!eDa+zLOq&k*xhO zQDiq4tKNtGyXSg1s@QHViRFA>ea*ivK-FM(TSL32R+hy6dh_$<$eil__0gNBjy6U} ziXBI@y`9dkFq9-`s@12GJIqfkOF36aZ%TQ{$zJOx^533VG6kz}ac@djnZv~4&Db!S z`IsUeV$S?bc+^u4K!~8Q1@QkF^H2Wjrz3fBHTRa!qWXeex~MLI`k$BddDW8sKhOwv z!U^6XekiaG620gXF=6e-3uP?VAWl;3z)At6( zS>IZIbtKXD1nt1qmE^GcflECy8(Rh(;qB8EnP=?qo`8}BhUVhb-R#y-=ovACBMP&cZe=?-9NiOxyK?tm zKPj+0sgKS9(?y5=C)P4uSEOPD*yS&qf&jaU^$aw3C6Cs#;~4L)JkA^|u5!6N5s{`b zc+336cv6Xru-n{BEa|bVP_xMD2%dbeR-EhbNkPqgr~5hA;_f1Gc2h|XJRT)rZ6KXM z+vbl=y_mMYAG$XK3An(kbAwh#mcG-wQOq1{{AoZ|+Pvx3){`FJ#ic2)uef^pgYnco Uag~++9OnY_8bi=P5KrH~0p4*b8UO$Q literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test5.jpg b/tests/wpt/mozilla/tests/mozilla/test5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3df583072e57fd6195104e8fb16be7a91b751a70 GIT binary patch literal 2070 zcmcgteK=HE7(e&kxdwyo9W(hFl~H<#GE>SLGKeTPQ$338(})eVF;iBpBEuqS4SCq| z70N@U&1k6Dt)x$}VrxQ3=|ho^%GZ+FJEQ2&?T_8}oafy0o^#)G-t&8Z=RNXfc_+Zu zxVX6h2!en=QGon0Z~_PnD=Hx*qN1uON+O}^WU`8?hPs9ZMV&&?)S_aVTDTU4g3ZQo zZ5o|U*O;ZFJDa9UrO|1MMIeN*A)#t0sz%eKXwtq~*Z}48Z_^#v0;2uAlzrD>~xrv!$=@ zM3%&tg-h=$?XW9{b1L6oBvw^OQtlb+@p$vG-g$2g&#~tz$Mg*?FHj$Tl%i6lx~(B= zvEfSly&3QJA7~F-kf|JW^YF@i>_2QZ;^pLRCuvrW{oBT#IJq$fU>HIWblM0eAt(gs zmMT;(hBNsB0svJ47zl+Y*h7iTv`~rciGLxtYin+o;cyf^^GMdDtVL$QaF+$>Csjvi zSe{=uXaCrB&$OocYVVK}u}ns96Yf#byhD9SN-~HVA~s!$1BgJEV8-WNm8uHBjaXD| zuAhkRz}qNHP0N=wVl={qf|E)o5D2n|CiFJnUgcz4t1Bo%QjMG15_vobputAB3Pl<3c+{4K_-3GUG z?%G#%>4@DfXinW-5mtR|ETp*zb4?Zr3zK)=4l2jqm5Z0w3pfbR{EBNZw_Vg0MJF|$ zKA`_s;ZU(JInWP0kLXlkja^r3o5=IUd;7|BWS5RV zeyDD@sCmL!-m1slB@xVxlmh|_Q-Y%qOhRWOKwtq#C{E*2jacjRG3I;+zCbvjL;#*b zAmLVW$>uNxh=Az9l~sv!o>U#m6k1c zwx!^kGv9eNv)My)GE$b6As^_8u4<~w@vX(&qZT{PactGo@E0{$ZKC-e`zg=NFaRMi zf~tKEk|Jn0pc*aXl2{IYX_&Dik1xpo6dVYs*u%rDyad`2Ta!hW5%Cwu#ldQVwo=y8 zykOhtvBrcg&NZr5w|jU}lc#l_LIdfy^T%9^c-I$lcX_fzy`H&)j-beplOBEFRF)H` zZ7uWvU=@D<#Y7zG6qjMQmw<|i5j0e!mKld|V8r4&@G!ogNFdDrOjUcMQTDT-vf4PC z<+8$pHIdyWRYyY0PdfN9pTEpHU&GzAPW!c~VS4JFfo?G?Alt;O$JaQITX~;h$CPp3 zM|L|NvK`DJwZ2(?eQZ$(&;6pzyT4BiOf`l?!~0tQ)>{^MX64F=Vf(=8t#~DW3|&DwsOVl z4Nuz&1kU-Za-h|(X7N1mKF{3+N&Ra(UthC)n8%F8S7={7dBfMD``GRkn<&WdAZ;@=2hln0qQ$Mj1;ydea| z{6wh^JR!y>GSXB+RNO>tQkUPyBKJudJKb)@25rXY>Y?r1SASk!$SGOL2;$szk7nLzd+{PxFUhs!7GOlxY# z`xTxj$qu7LF8(A_o;YAUA%Uo$Ab&tI&39~+z29vtT3gLJ$SEyGU7F&*zGG7uw9n+K zd3U{jITk0mIq`5fr}N+jH#=>WTcZu_OARc!WsG`UXkJk(ZuVg}t>qtH@{_GT68z}F zu~jXd7mk+(FzaKs`y5S4;U5n>#<2k-I%^#z5ld>LjXSs2CODP-8rj}aEsMSz7p;pt IIw)`X2fTIMOaK4? literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test6.jpg b/tests/wpt/mozilla/tests/mozilla/test6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81c68f412a27fcdcb1d961db568168b0e9ea2caf GIT binary patch literal 2293 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 bAk!*6?2DJ$H?p~PERGvvD}SpUOd{RiQ|W@A literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test8.jpg b/tests/wpt/mozilla/tests/mozilla/test8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..295e9c523527c584eac494298060d2631d69603d GIT binary patch literal 2451 zcmcgtdpuNWA3tYi&I~hVI>!u+8p&lMDfhD0U9K$>k%=`UxrJuZ79lfw+ay~qEm3)C zlaOj!tBc%vl@b|5LNOI>C-aq&KWB2!*&-3{`&vTyVe9!m$`~4nCr=$;n zo$a0M0T2X%U1$R&-GD8C#bBflYB= z5h_hVK|yZWa=IdoE>BaSNe=uo zRsbM?!Ggg57=*{-2pDvYf&K)#hW1N_C!qK-pp2vokVAK2U@Qy(;9EWa_SH-8{B?)e z>jf>pseFGFsyGoeI&{EFx|Nc^EHK4I4xQ*exp~BX*f*B3aNv)<$xmmD%l%g_zcpN+ z^`J&N3EHK3lvf#NkgBPl-ZIZ-u=&zi#s?RN36u8T(kz>^Jtexl?iA@PfF4l0U$v?# zu$*_LG-YSa!sK7*;7`qvl6RpB!_X z+vox7i2T@Y<(F4h-X7m@LftXSkyOi>8$ux%BLIj6A=G651OZ}DJpeo+pRcNK<=#TI z=J3QYO+ihlZeVD`<_c=Z7J#85T7q*acfF5#_QziFuQNULoECfE?k+-Qnps_#4WLvP4GQlg?*0^DxJB_!Fr~wVao0Ql654#4ve9s-V zOXaZ+uG-bczgU%H7ZI9qJ;JquET|7Ddsphw=`pH&#GmAQdC0R+;~;qKgIgiEqoQT7 zH}s_UHy^K8Y`-M|G-6bA6HgocdeDAq7Zw&lzKdU1i58c3$i?E+n_)O+jv}qmG-6+1NdVVSz>p!P}`IFM(d5 z#d!3DltNTof6A5`A88YlU%xYV>Z?EhxO#CgzdK>kl>l%2zxofJef9qauMW^eQ1NhRo-C=i zARek6$4Zc<#~$P63v0zowFaJ(quaL4pz zt=}IKhlI8m*qm+%CV$@MAK=n|xWdlhaZ!3SyISrQUsl~=E1q16-Sau@c=srHQfKY^ z334)mtZ~7#tq61tXcwrf+)FagR<1(1^stj?*zEfVvY?Pye{Hrk39UL)O^z~ z54f*=Eym<|dBfF>PRI8S=F~O3t3DV8XRo;>d-$wHxC+noMWSFBm$O?ld$fo^^d_sA z5c`axi)6L+bc)CC24x3}%=*@w=ZfCq#RDqbnpY}jy-(1Pr~{Z^@(=g*wwFq1t?$mW z;t106g=}t1?Na)|b95z|1}RU;zbU9xh#eE#p4e70rhPDYN_$wzR&UUC%kk&G>zGV% z*j%bOAGWto_dE5dAeS?4;|!z~`{p+L;mVzEDk)cg-#=MjBe$5)Ck zJtgg0wd~`lNHztzU}2wWaOvJ`>4NlJ! zXAG?zRnyV^XYo9PByjb-O+(ed$3Al7Ar3A~kI?;1#VXo5+VZQA{_ Iqmt+U1%+9YGXMYp literal 0 HcmV?d00001 diff --git a/tests/wpt/mozilla/tests/mozilla/test9.jpg b/tests/wpt/mozilla/tests/mozilla/test9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c82f94f0134105792bb45a3a827c2a51cc92e674 GIT binary patch literal 2268 zcmcgtdsGug7oXkDLIMFcknm7>Bv2n9N`fGWltd^6wAes-2tq@p7y&IHEiEWdUno$+ z8^qQ$1q>nxhuUf>B7!dj!GO@9G$0hEP*FrBg6SqLaK8Wgo%5Y_s-0nx%YQ} zqv%)20OsZH=?)+W0=w`I6a&B&ATX@_@J7Htl!&4P0;-{|u0|wlkja`FnwnbLx|o(W zMO#x7)5j=!R2q#&)-f>DryA-~X;kGV5Q48Epd=I}QMEL+sQ-0Sv;Ybb6oNtoq5zlz zArwf_3YIC43IPNxHB?ODF|7zY>wUDfqcl_z}!E=uUex*M&Pw+1nE7efvi@A+wBHrx0^woo3 zt~ft7s=2S4y}jW5Qw=RXCz95MChg_iZ&oR1vKvPetY&6}d)Ue`(cYndy8b?IyrwT& zrBEvWRl|c|jsb{(cXV+90S^F51!^=KHifZ)7lX%wh(`%R;W<{P#3)GX(39M~!LpL? z-y4L(2Oi(V1Q+MpWp;CW&sjugZ+@9`Tc>1@$6VGs(EbN4-mh!&_oTVyCkRPVXsbcf z%`LN9j3hv#30ssn7b)qKpkAK`b0R?IlI#ZXy=7b6A^r$RuY@Bciqv_l6 zV$yQ2c27N19SKjJ3p8sK7=_(9Rod{2w@JK(b6Dbv`V-*;XSdO1hQ8l+`1NY&*h-8q z{<$KdTh#EaSpYGI{}^i{k)|!rza(YLA3n~H2$<+$OI!YY_>ovUYx6noHB9D`4cWQF z@z;Yz{P~JLw<6Ct=$@UhKF`*Z>EYuMS6~ke?v=l3oxeI90{a+O*R9>To=jj0s~pdF z#3m$kX3M1Pi`@RPvhw9`OVbOCm;3FAvH@BO@cZGQ2ua{KwS31d&Q=aSwb!$D=~g6{ z)S6C~5?(}~=p}w(IkF>rc*UBD2LoAc4iA@u#!UQYBBxhM{eK1zO_E`2P=f{}zQ!YKkv{>ulqqsv{dHl7U-vT|w zM~iNf4T|{E)5A_|&!)&2;#$W~wp_lKX+0krwbI&HROTJphzE2PhGP#WA9MkN0K$*R zWOKt}IRatbGYm)F)uDwdaG}3uj(!(%rR;;|Dv#&Zx|`zDq?&}RDdd|riR8nuycR=q zw+zv2>Ml~q2NlTbUsoZ4tlHhxpC`WtsTCI(H}}02ZmMO8n=|VRHLqmR_MOYj$S#sx z;#s;K_Ppep())&Bv`61E+IbdtsG=YhcVPer28LAt-ykUQ!)gr;TT$A3p`PPi}M7Ic1*yFnNp=m$qd-MA$B-({{c!z_%b5* zwDH!4Q%}5ak9+6e>x^+~cN;m#n6XLA+aEi=VMey`4m$KT?-XZ>J5tzOFf!#I{^oKl z^j5LSuCMPV79gCNX;BV%^C%IsAhScNhwggcw->B~X12&4UEEjJ*jZ1|4fB=4G<$gb@_QjpxG=jgb*wkbvN>t7KyJe~jm literal 0 HcmV?d00001