diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/wr/wrench/src/rawtest.rs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/wr/wrench/src/rawtest.rs')
-rw-r--r-- | gfx/wr/wrench/src/rawtest.rs | 1450 |
1 files changed, 1450 insertions, 0 deletions
diff --git a/gfx/wr/wrench/src/rawtest.rs b/gfx/wr/wrench/src/rawtest.rs new file mode 100644 index 0000000000..19d3b025f7 --- /dev/null +++ b/gfx/wr/wrench/src/rawtest.rs @@ -0,0 +1,1450 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use euclid::{point2, size2, rect, Box2D}; +use std::sync::Arc; +use std::sync::atomic::{AtomicIsize, Ordering}; +use std::sync::mpsc::Receiver; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use crate::{WindowWrapper, NotifierEvent}; +use crate::blob; +use crate::reftest::{ReftestImage, ReftestImageComparison}; +use crate::wrench::Wrench; + +pub struct RawtestHarness<'a> { + wrench: &'a mut Wrench, + rx: &'a Receiver<NotifierEvent>, + window: &'a mut WindowWrapper, +} + + +impl<'a> RawtestHarness<'a> { + pub fn new(wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: &'a Receiver<NotifierEvent>) -> Self { + RawtestHarness { + wrench, + rx, + window, + } + } + + pub fn run(mut self) { + self.test_hit_testing(); + self.test_resize_image(); + self.test_retained_blob_images_test(); + self.test_blob_update_test(); + self.test_blob_update_epoch_test(); + self.test_tile_decomposition(); + self.test_very_large_blob(); + self.test_blob_visible_area(); + self.test_blob_set_visible_area(); + self.test_offscreen_blob(); + self.test_save_restore(); + self.test_blur_cache(); + self.test_capture(); + self.test_zero_height_window(); + self.test_clear_cache(); + } + + fn render_and_get_pixels(&mut self, window_rect: FramebufferIntRect) -> Vec<u8> { + self.rx.recv().unwrap(); + self.wrench.render(); + self.wrench.renderer.read_pixels_rgba8(window_rect) + } + + fn compare_pixels(&self, data1: Vec<u8>, data2: Vec<u8>, size: FramebufferIntSize) { + let size = DeviceIntSize::new(size.width, size.height); + let image1 = ReftestImage { + data: data1, + size, + }; + let image2 = ReftestImage { + data: data2, + size, + }; + + match image1.compare(&image2) { + ReftestImageComparison::Equal => {} + ReftestImageComparison::NotEqual { max_difference, count_different, .. } => { + let t = "rawtest"; + println!( + "REFTEST TEST-UNEXPECTED-FAIL | {t} \ + | image comparison, max difference: {max_difference}, \ + number of differing pixels: {count_different}"); + println!("REFTEST IMAGE 1: {}", image1.create_data_uri()); + println!("REFTEST IMAGE 2: {}", image2.create_data_uri()); + println!("REFTEST TEST-END | {}", t); + panic!(); + } + } + } + + fn submit_dl( + &mut self, + epoch: &mut Epoch, + mut builder: DisplayListBuilder, + mut txn: Transaction, + ) { + txn.use_scene_builder_thread(); + + txn.set_display_list( + *epoch, + builder.end(), + ); + epoch.0 += 1; + + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn make_common_properties(&self, clip_rect: LayoutRect) -> CommonItemProperties { + let space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + CommonItemProperties { + clip_rect, + clip_chain_id: space_and_clip.clip_chain_id, + spatial_id: space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + } + } + + fn make_common_properties_with_clip_and_spatial( + &self, + clip_rect: LayoutRect, + clip_chain_id: ClipChainId, + spatial_id: SpatialId + ) -> CommonItemProperties { + CommonItemProperties { + clip_rect, + clip_chain_id, + spatial_id, + flags: PrimitiveFlags::default(), + } + } + + fn test_resize_image(&mut self) { + println!("\tresize image..."); + // This test changes the size of an image to make it go switch back and forth + // between tiled and non-tiled. + // The resource cache should be able to handle this without crashing. + + let mut txn = Transaction::new(); + let img = self.wrench.api.generate_image_key(); + + // Start with a non-tiled image. + txn.add_image( + img, + ImageDescriptor::new(64, 64, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![255; 64 * 64 * 4]), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 64.0, 64.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + let mut txn = Transaction::new(); + // Resize the image to something bigger than the max texture size (8196) to force tiling. + txn.update_image( + img, + ImageDescriptor::new(8200, 32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![255; 8200 * 32 * 4]), + &DirtyRect::All, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 1024.0, 1024.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + let mut txn = Transaction::new(); + // Resize back to something doesn't require tiling. + txn.update_image( + img, + ImageDescriptor::new(64, 64, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![64; 64 * 64 * 4]), + &DirtyRect::All, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 1024.0, 1024.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + txn = Transaction::new(); + txn.delete_image(img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_tile_decomposition(&mut self) { + println!("\ttile decomposition..."); + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(151, 56, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(DeviceIntSize::new(151, 56)), + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(448.9, 74.0, 151.000_03, 56.).to_box2d()); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + size2(151., 56.0), + size2(151.0, 56.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + self.rx.recv().unwrap(); + self.wrench.render(); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_very_large_blob(&mut self) { + println!("\tvery large blob..."); + + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(800, 800); + + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(15000, 15000, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(DeviceIntSize::new(15000, 15000)), + Some(100), + ); + + let called = Arc::new(AtomicIsize::new(0)); + let called_inner = Arc::clone(&called); + + self.wrench.callbacks.lock().unwrap().request = Box::new(move |_| { + called_inner.fetch_add(1, Ordering::SeqCst); + }); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(40., 41., 200., 201.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 800.0, 800.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + size2(15000.0, 15000.0).into(), + size2(15000.0, 15000.0), + size2(0.0, 0.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + // make sure we didn't request too many blobs + assert!(called.load(Ordering::SeqCst) < 20); + + //use crate::png; + //png::save_flipped("out.png", pixels.clone(), size2(window_rect.size.width, window_rect.size.height)); + + // make sure things are in the right spot + let w = window_rect.width() as usize; + let h = window_rect.height() as usize; + let p1 = (40 + (h - 100) * w) * 4; + assert_eq!(pixels[p1 ], 50); + assert_eq!(pixels[p1 + 1], 50); + assert_eq!(pixels[p1 + 2], 150); + assert_eq!(pixels[p1 + 3], 255); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_visible_area(&mut self) { + println!("\tblob visible area..."); + + let window_size = self.window.get_inner_size(); + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(50, 20), + max: point2(450, 420), + }, + Some(100), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let image_size = size2(400.0, 400.0); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(10.0, 10.0, 400.0, 400.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + //use super::png; + //png::save_flipped("out.png", pixels.clone(), size2(window_rect.size.width, window_rect.size.height)); + + + // make sure things are in the right spot + let w = window_rect.width() as usize; + let h = window_rect.height() as usize; + let p1 = (65 + (h - 15) * w) * 4; + assert_eq!(pixels[p1 ], 255); + assert_eq!(pixels[p1 + 1], 255); + assert_eq!(pixels[p1 + 2], 255); + assert_eq!(pixels[p1 + 3], 255); + + let p2 = (25 + (h - 15) * w) * 4; + assert_eq!(pixels[p2 ], 221); + assert_eq!(pixels[p2 + 1], 221); + assert_eq!(pixels[p2 + 2], 221); + assert_eq!(pixels[p2 + 3], 255); + + let p3 = (15 + (h - 15) * w) * 4; + assert_eq!(pixels[p3 ], 50); + assert_eq!(pixels[p3 + 1], 50); + assert_eq!(pixels[p3 + 2], 150); + assert_eq!(pixels[p3 + 3], 255); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_set_visible_area(&mut self) { + // In this test we first render a blob with a certain visible area, + // then change the visible area without updating the blob image. + + println!("\tblob visible area update..."); + + let window_size = self.window.get_inner_size(); + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(0, 0), + max: point2(500, 500), + }, + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(0.0, 0.0, 500.0, 500.0).to_box2d(), + size2(500.0, 500.0), + size2(500.0, 500.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + // Render the first display list. We don't care about the result but we + // want to make sure the next display list updates an already rendered + // state. + self.submit_dl(&mut epoch, builder, txn); + let _ = self.render_and_get_pixels(window_rect); + + // Now render a similar scene with an updated blob visible area. + // In this test we care about the fact that the visible area was updated + // without using update_blob_image. + + let mut txn = Transaction::new(); + + txn.set_blob_image_visible_area(blob_img, DeviceIntRect { + min: point2(50, 50), + max: point2(450, 450), + }); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(50.0, 50.0, 400.0, 400.0).to_box2d(), + size2(400.0, 400.0), + size2(400.0, 400.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let resized_pixels = self.render_and_get_pixels(window_rect); + + // Now render the same scene with a new blob image created with the same + // visible area as the previous scene, without going through an update. + + let mut txn = Transaction::new(); + + let blob_img2 = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(50, 50), + max: point2(450, 450), + }, + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(50.0, 50.0, 400.0, 400.0).to_box2d(), + size2(400.0, 400.0), + size2(400.0, 400.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img2.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let reference_pixels = self.render_and_get_pixels(window_rect); + + assert_eq!(resized_pixels, reference_pixels); + + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + txn.delete_blob_image(blob_img2); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_offscreen_blob(&mut self) { + println!("\toffscreen blob update..."); + + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(1510, 1510, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(1510, 1510)), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(0., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let original_pixels = self.render_and_get_pixels(window_rect); + + let mut epoch = Epoch(1); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(-10000., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, Transaction::new()); + + let _offscreen_pixels = self.render_and_get_pixels(window_rect); + + let mut txn = Transaction::new(); + + txn.update_blob_image( + blob_img, + ImageDescriptor::new(1510, 1510, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(1510, 1510)), + &Box2D { min: point2(10, 10), max: point2(110, 110) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(0., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(2); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + self.compare_pixels(original_pixels, pixels, window_rect.size()); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_retained_blob_images_test(&mut self) { + println!("\tretained blob images test..."); + let blob_img; + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + + let mut txn = Transaction::new(); + { + let api = &self.wrench.api; + + blob_img = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + } + + let called = Arc::new(AtomicIsize::new(0)); + let called_inner = Arc::clone(&called); + + self.wrench.callbacks.lock().unwrap().request = Box::new(move |_| { + assert_eq!(0, called_inner.fetch_add(1, Ordering::SeqCst)); + }); + + // draw the blob the first time + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels_first = self.render_and_get_pixels(window_rect); + + assert_eq!(1, called.load(Ordering::SeqCst)); + + // draw the blob image a second time at a different location + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(1.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut txn = Transaction::new(); + txn.resource_updates.clear(); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels_second = self.render_and_get_pixels(window_rect); + + // make sure we only requested once + assert_eq!(1, called.load(Ordering::SeqCst)); + + // use png; + // png::save_flipped("out1.png", &pixels_first, window_rect.size); + // png::save_flipped("out2.png", &pixels_second, window_rect.size); + assert!(pixels_first != pixels_second); + + // cleanup + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_update_epoch_test(&mut self) { + println!("\tblob update epoch test..."); + let (blob_img, blob_img2); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut txn = Transaction::new(); + let (blob_img, blob_img2) = { + let api = &self.wrench.api; + + blob_img = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + blob_img2 = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(80, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + (blob_img, blob_img2) + }; + + // setup some counters to count how many times each image is requested + let img1_requested = Arc::new(AtomicIsize::new(0)); + let img1_requested_inner = Arc::clone(&img1_requested); + let img2_requested = Arc::new(AtomicIsize::new(0)); + let img2_requested_inner = Arc::clone(&img2_requested); + + // track the number of times that the second image has been requested + self.wrench.callbacks.lock().unwrap().request = Box::new(move |requests| { + for item in requests { + if item.request.key == blob_img { + img1_requested_inner.fetch_add(1, Ordering::SeqCst); + } + if item.request.key == blob_img2 { + img2_requested_inner.fetch_add(1, Ordering::SeqCst); + } + } + }); + + // create two blob images and draw them + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + let info2 = self.make_common_properties(rect(200.0, 60.0, 200.0, 200.0).to_box2d()); + let push_images = |builder: &mut DisplayListBuilder| { + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + builder.push_image( + &info2, + info2.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img2.as_image(), + ColorF::WHITE, + ); + }; + + push_images(&mut builder); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + let _pixels_first = self.render_and_get_pixels(window_rect); + + // update and redraw both images + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + txn.update_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(59, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + push_images(&mut builder); + self.submit_dl(&mut epoch, builder, txn); + let _pixels_second = self.render_and_get_pixels(window_rect); + + // only update the first image + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 150, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(200, 200), max: point2(300, 300) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + push_images(&mut builder); + self.submit_dl(&mut epoch, builder, txn); + let _pixels_third = self.render_and_get_pixels(window_rect); + + // the first image should be requested 3 times + assert_eq!(img1_requested.load(Ordering::SeqCst), 3); + // the second image should've been requested twice + assert_eq!(img2_requested.load(Ordering::SeqCst), 2); + + // cleanup + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_update_test(&mut self) { + println!("\tblob update test..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = { + let img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + img + }; + + // draw the blobs the first time + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_first = self.render_and_get_pixels(window_rect); + + // draw the blob image a second time after updating it with the same color + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_second = self.render_and_get_pixels(window_rect); + + // draw the blob image a third time after updating it with a different color + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 150, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(200, 200), max: point2(300, 300) }.into(), + ); + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_third = self.render_and_get_pixels(window_rect); + + assert!(pixels_first != pixels_third); + self.compare_pixels(pixels_first, pixels_second, window_rect.size()); + } + + // Ensures that content doing a save-restore produces the same results as not + fn test_save_restore(&mut self) { + println!("\tsave/restore..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut do_test = |should_try_and_fail| { + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let spatial_id = SpatialId::root_scroll_node(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + SpatialId::root_scroll_node(self.wrench.root_pipeline_id), + rect(110., 120., 200., 200.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(100., 100., 100., 100.).to_box2d(), + clip_chain_id, + spatial_id), + rect(100., 100., 100., 100.).to_box2d(), + ColorF::new(0.0, 0.0, 1.0, 1.0), + ); + + if should_try_and_fail { + builder.save(); + let clip_id = builder.define_clip_rect( + spatial_id, + rect(80., 80., 90., 90.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + let space_and_clip = SpaceAndClipInfo { + spatial_id, + clip_chain_id, + }; + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(110., 110., 50., 50.).to_box2d(), + clip_chain_id, + spatial_id), + rect(110., 110., 50., 50.).to_box2d(), + ColorF::new(0.0, 1.0, 0.0, 1.0), + ); + builder.push_shadow( + &space_and_clip, + Shadow { + offset: LayoutVector2D::new(1.0, 1.0), + blur_radius: 1.0, + color: ColorF::new(0.0, 0.0, 0.0, 1.0), + }, + true, + ); + let info = CommonItemProperties { + clip_rect: rect(110., 110., 50., 2.).to_box2d(), + clip_chain_id, + spatial_id, + flags: PrimitiveFlags::default(), + }; + builder.push_line( + &info, + &info.clip_rect, + 0.0, LineOrientation::Horizontal, + &ColorF::new(0.0, 0.0, 0.0, 1.0), + LineStyle::Solid, + ); + builder.restore(); + } + + { + builder.save(); + let clip_id = builder.define_clip_rect( + spatial_id, + rect(80., 80., 100., 100.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(150., 150., 100., 100.).to_box2d(), + clip_chain_id, + spatial_id), + rect(150., 150., 100., 100.).to_box2d(), + ColorF::new(0.0, 0.0, 1.0, 1.0), + ); + builder.clear_save(); + } + + let txn = Transaction::new(); + + self.submit_dl(&mut Epoch(0), builder, txn); + + self.render_and_get_pixels(window_rect) + }; + + let first = do_test(false); + let second = do_test(true); + + self.compare_pixels(first, second, window_rect.size()); + } + + // regression test for #2769 + // "async scene building: cache collisions from reused picture ids" + fn test_blur_cache(&mut self) { + println!("\tblur cache..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut do_test = |shadow_is_red| { + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let shadow_color = if shadow_is_red { + ColorF::new(1.0, 0.0, 0.0, 1.0) + } else { + ColorF::new(0.0, 1.0, 0.0, 1.0) + }; + + builder.push_shadow( + &SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id), + Shadow { + offset: LayoutVector2D::new(1.0, 1.0), + blur_radius: 1.0, + color: shadow_color, + }, + true, + ); + let info = self.make_common_properties(rect(110., 110., 50., 2.).to_box2d()); + builder.push_line( + &info, + &info.clip_rect, + 0.0, LineOrientation::Horizontal, + &ColorF::new(0.0, 0.0, 0.0, 1.0), + LineStyle::Solid, + ); + builder.pop_all_shadows(); + + let txn = Transaction::new(); + self.submit_dl(&mut Epoch(0), builder, txn); + + self.render_and_get_pixels(window_rect) + }; + + let first = do_test(false); + let second = do_test(true); + + assert_ne!(first, second); + } + + fn test_capture(&mut self) { + println!("\tcapture..."); + let path = "../captures/test"; + let layout_size = LayoutSize::new(400., 400.); + let dim = self.window.get_inner_size(); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, dim.height - layout_size.height as i32), + size2(layout_size.width as i32, layout_size.height as i32), + ); + + // 1. render some scene + + let mut txn = Transaction::new(); + let image = self.wrench.api.generate_image_key(); + txn.add_image( + image, + ImageDescriptor::new(1, 1, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![0xFF, 0, 0, 0xFF]), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(300.0, 70.0, 150.0, 50.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + image, + ColorF::WHITE, + ); + + let mut txn = Transaction::new(); + + txn.set_display_list( + Epoch(0), + builder.end(), + ); + txn.generate_frame(0, RenderReasons::TESTING); + + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + let pixels0 = self.render_and_get_pixels(window_rect); + + // 2. capture it + self.wrench.api.save_capture(path.into(), CaptureBits::all()); + + // 3. set a different scene + + builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let mut txn = Transaction::new(); + txn.set_display_list( + Epoch(1), + builder.end(), + ); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + // 4. load the first one + + let mut documents = self.wrench.api.load_capture(path.into(), None); + let captured = documents.swap_remove(0); + + // 5. render the built frame and compare + let pixels1 = self.render_and_get_pixels(window_rect); + self.compare_pixels(pixels0.clone(), pixels1, window_rect.size()); + + // 6. rebuild the scene and compare again + let mut txn = Transaction::new(); + txn.set_root_pipeline(captured.root_pipeline_id.unwrap()); + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(captured.document_id, txn); + let pixels2 = self.render_and_get_pixels(window_rect); + self.compare_pixels(pixels0, pixels2, window_rect.size()); + } + + fn test_zero_height_window(&mut self) { + println!("\tzero height test..."); + + let layout_size = LayoutSize::new(120.0, 0.0); + let window_size = DeviceIntSize::new(layout_size.width as i32, layout_size.height as i32); + let doc_id = self.wrench.api.add_document(window_size); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties( + LayoutRect::from_size(LayoutSize::new(100.0, 100.0)) + ); + builder.push_rect( + &info, + info.clip_rect, + ColorF::new(0.0, 1.0, 0.0, 1.0), + ); + + let mut txn = Transaction::new(); + txn.set_root_pipeline(self.wrench.root_pipeline_id); + txn.set_display_list( + Epoch(1), + builder.end(), + ); + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(doc_id, txn); + + // Ensure we get a notification from rendering the above, even though + // there are zero visible pixels + assert!(self.rx.recv().unwrap() == NotifierEvent::WakeUp { composite_needed: true }); + } + + + fn test_hit_testing(&mut self) { + println!("\thit testing test..."); + + let layout_size = LayoutSize::new(400., 400.); + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + // Add a rectangle that covers the entire scene. + let space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + builder.push_hit_test( + LayoutRect::from_size(layout_size), + ClipChainId::INVALID, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 1), + ); + + // Add a simple 100x100 rectangle at 100,0. + builder.push_hit_test( + LayoutRect::from_origin_and_size( + LayoutPoint::new(100., 0.), + LayoutSize::new(100., 100.) + ), + ClipChainId::INVALID, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 2), + ); + + let make_rounded_complex_clip = |rect: &LayoutRect, radius: f32| -> ComplexClipRegion { + ComplexClipRegion::new( + *rect, + BorderRadius::uniform_size(LayoutSize::new(radius, radius)), + ClipMode::Clip + ) + }; + + // Add a rectangle that is clipped by a rounded rect clip item. + let rect = LayoutRect::from_origin_and_size(LayoutPoint::new(100., 100.), LayoutSize::new(100., 100.)); + let temp_clip_id = builder.define_clip_rounded_rect( + space_and_clip.spatial_id, + make_rounded_complex_clip(&rect, 20.), + ); + let clip_chain_id = builder.define_clip_chain(None, vec![temp_clip_id]); + builder.push_hit_test( + rect, + clip_chain_id, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 4), + ); + + // Add a rectangle that is clipped by a ClipChain containing a rounded rect. + let rect = LayoutRect::from_origin_and_size(LayoutPoint::new(200., 100.), LayoutSize::new(100., 100.)); + let clip_id = builder.define_clip_rounded_rect( + space_and_clip.spatial_id, + make_rounded_complex_clip(&rect, 20.), + ); + let clip_chain_id = builder.define_clip_chain(None, vec![clip_id]); + builder.push_hit_test( + rect, + clip_chain_id, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 5), + ); + + let mut epoch = Epoch(0); + let txn = Transaction::new(); + self.submit_dl(&mut epoch, builder, txn); + + // We render to ensure that the hit tester is up to date with the current scene. + self.rx.recv().unwrap(); + self.wrench.render(); + + let hit_test = |point: WorldPoint| -> HitTestResult { + self.wrench.api.hit_test( + self.wrench.document_id, + point, + ) + }; + + let assert_hit_test = |point: WorldPoint, tags: Vec<ItemTag>| { + let result = hit_test(point); + assert_eq!(result.items.len(), tags.len()); + + for (hit_test_item, item_b) in result.items.iter().zip(tags.iter()) { + assert_eq!(hit_test_item.tag, *item_b); + } + }; + + // We should not have any hits outside the boundaries of the scene. + assert_hit_test(WorldPoint::new(-10., -10.), Vec::new()); + assert_hit_test(WorldPoint::new(-10., 10.), Vec::new()); + assert_hit_test(WorldPoint::new(450., 450.), Vec::new()); + assert_hit_test(WorldPoint::new(100., 450.), Vec::new()); + + // The top left corner of the scene should only contain the background. + assert_hit_test(WorldPoint::new(50., 50.), vec![(0, 1)]); + + // The middle of the normal rectangle should be hit. + assert_hit_test(WorldPoint::new(150., 50.), vec![(0, 2), (0, 1)]); + + let test_rounded_rectangle = |point: WorldPoint, size: WorldSize, tag: ItemTag| { + // The cut out corners of the rounded rectangle should not be hit. + let top_left = point + WorldVector2D::new(5., 5.); + let bottom_right = point + size.to_vector() - WorldVector2D::new(5., 5.); + + assert_hit_test( + WorldPoint::new(point.x + (size.width / 2.), point.y + (size.height / 2.)), + vec![tag, (0, 1)] + ); + + assert_hit_test(top_left, vec![(0, 1)]); + assert_hit_test(WorldPoint::new(bottom_right.x, top_left.y), vec![(0, 1)]); + assert_hit_test(WorldPoint::new(top_left.x, bottom_right.y), vec![(0, 1)]); + assert_hit_test(bottom_right, vec![(0, 1)]); + }; + + test_rounded_rectangle(WorldPoint::new(100., 100.), WorldSize::new(100., 100.), (0, 4)); + test_rounded_rectangle(WorldPoint::new(200., 100.), WorldSize::new(100., 100.), (0, 5)); + } + + fn test_clear_cache(&mut self) { + println!("\tclear cache test..."); + + self.wrench.api.send_message(ApiMsg::DebugCommand(DebugCommand::ClearCaches(ClearCache::all()))); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let txn = Transaction::new(); + let mut epoch = Epoch(0); + self.submit_dl(&mut epoch, builder, txn); + + self.rx.recv().unwrap(); + self.wrench.render(); + } +} |