diff --git a/gfx/webrender_bindings/Moz2DImageRenderer.cpp b/gfx/webrender_bindings/Moz2DImageRenderer.cpp index ad34fcd1744e..cea0c99a1341 100644 --- a/gfx/webrender_bindings/Moz2DImageRenderer.cpp +++ b/gfx/webrender_bindings/Moz2DImageRenderer.cpp @@ -336,21 +336,23 @@ struct Reader { }; static bool Moz2DRenderCallback(const Range aBlob, - gfx::IntSize aSize, gfx::SurfaceFormat aFormat, + gfx::SurfaceFormat aFormat, const mozilla::wr::DeviceIntRect* aVisibleRect, + const mozilla::wr::LayoutIntRect* aRenderRect, const uint16_t* aTileSize, const mozilla::wr::TileOffset* aTileOffset, const mozilla::wr::LayoutIntRect* aDirtyRect, Range aOutput) { + IntSize size(aRenderRect->size.width, aRenderRect->size.height); AUTO_PROFILER_TRACING("WebRender", "RasterizeSingleBlob", GRAPHICS); - MOZ_RELEASE_ASSERT(aSize.width > 0 && aSize.height > 0); - if (aSize.width <= 0 || aSize.height <= 0) { + MOZ_RELEASE_ASSERT(size.width > 0 && size.height > 0); + if (size.width <= 0 || size.height <= 0) { return false; } - auto stride = aSize.width * gfx::BytesPerPixel(aFormat); + auto stride = size.width * gfx::BytesPerPixel(aFormat); - if (aOutput.length() < static_cast(aSize.height * stride)) { + if (aOutput.length() < static_cast(size.height * stride)) { return false; } @@ -358,7 +360,7 @@ static bool Moz2DRenderCallback(const Range aBlob, bool uninitialized = false; RefPtr dt = gfx::Factory::CreateDrawTargetForData( - gfx::BackendType::SKIA, aOutput.begin().get(), aSize, stride, aFormat, + gfx::BackendType::SKIA, aOutput.begin().get(), size, stride, aFormat, uninitialized); if (!dt) { @@ -370,22 +372,18 @@ static bool Moz2DRenderCallback(const Range aBlob, size_t footerSize = sizeof(size_t) + sizeof(IntPoint); MOZ_RELEASE_ASSERT(aBlob.length() >= footerSize); size_t indexOffset = ConvertFromBytes(aBlob.end().get() - footerSize); - IntPoint origin = ConvertFromBytes(aBlob.end().get() - footerSize + - sizeof(size_t)); - // Apply the visibleRect's offset to make (0, 0) in the DT correspond to (0, - // 0) in the texture + + // aRenderRect is the part of the blob that we are currently rendering + // (for example a tile) in the same coordinate space as aVisibleRect. + IntPoint origin = gfx::IntPoint(aRenderRect->origin.x, aRenderRect->origin.y); MOZ_RELEASE_ASSERT(indexOffset <= aBlob.length() - footerSize); Reader reader(aBlob.begin().get() + indexOffset, aBlob.length() - footerSize - indexOffset); - if (aTileOffset) { - origin += - gfx::IntPoint(aTileOffset->x * *aTileSize, aTileOffset->y * *aTileSize); - } dt = gfx::Factory::CreateOffsetDrawTarget(dt, origin); - auto bounds = gfx::IntRect(origin, aSize); + auto bounds = gfx::IntRect(origin, size); if (aDirtyRect) { gfx::Rect dirty(aDirtyRect->origin.x, aDirtyRect->origin.y, @@ -436,7 +434,7 @@ static bool Moz2DRenderCallback(const Range aBlob, float r = float(rand()) / float(RAND_MAX); float g = float(rand()) / float(RAND_MAX); float b = float(rand()) / float(RAND_MAX); - dt->FillRect(gfx::Rect(origin.x, origin.y, aSize.width, aSize.height), + dt->FillRect(gfx::Rect(origin.x, origin.y, size.width, size.height), gfx::ColorPattern(gfx::Color(r, g, b, 0.5))); } @@ -459,17 +457,19 @@ static bool Moz2DRenderCallback(const Range aBlob, extern "C" { -bool wr_moz2d_render_cb(const mozilla::wr::ByteSlice blob, int32_t width, - int32_t height, mozilla::wr::ImageFormat aFormat, +bool wr_moz2d_render_cb(const mozilla::wr::ByteSlice blob, + mozilla::wr::ImageFormat aFormat, + const mozilla::wr::LayoutIntRect* aRenderRect, const mozilla::wr::DeviceIntRect* aVisibleRect, const uint16_t* aTileSize, const mozilla::wr::TileOffset* aTileOffset, const mozilla::wr::LayoutIntRect* aDirtyRect, mozilla::wr::MutByteSlice output) { return mozilla::wr::Moz2DRenderCallback( - mozilla::wr::ByteSliceToRange(blob), mozilla::gfx::IntSize(width, height), - mozilla::wr::ImageFormatToSurfaceFormat(aFormat), aVisibleRect, aTileSize, - aTileOffset, aDirtyRect, mozilla::wr::MutByteSliceToRange(output)); + mozilla::wr::ByteSliceToRange(blob), + mozilla::wr::ImageFormatToSurfaceFormat(aFormat), aVisibleRect, + aRenderRect, aTileSize, aTileOffset, aDirtyRect, + mozilla::wr::MutByteSliceToRange(output)); } } // extern diff --git a/gfx/webrender_bindings/src/bindings.rs b/gfx/webrender_bindings/src/bindings.rs index 689589f2c540..082e96195f18 100644 --- a/gfx/webrender_bindings/src/bindings.rs +++ b/gfx/webrender_bindings/src/bindings.rs @@ -3258,9 +3258,8 @@ pub unsafe extern "C" fn wr_dec_ref_arc(arc: *const VecU8) { extern "C" { // TODO: figure out the API for tiled blob images. pub fn wr_moz2d_render_cb(blob: ByteSlice, - width: i32, - height: i32, format: ImageFormat, + render_rect: &LayoutIntRect, visible_rect: &DeviceIntRect, tile_size: Option<&u16>, tile_offset: Option<&TileOffset>, diff --git a/gfx/webrender_bindings/src/moz2d_renderer.rs b/gfx/webrender_bindings/src/moz2d_renderer.rs index a431bfc8f3b6..ede43b976f38 100644 --- a/gfx/webrender_bindings/src/moz2d_renderer.rs +++ b/gfx/webrender_bindings/src/moz2d_renderer.rs @@ -553,9 +553,8 @@ fn rasterize_blob(job: Job) -> (BlobImageRequest, BlobImageResult) { let result = unsafe { if wr_moz2d_render_cb( ByteSlice::new(&job.commands[..]), - descriptor.rect.size.width, - descriptor.rect.size.height, descriptor.format, + &descriptor.rect, &job.visible_rect, job.tile_size.as_ref(), job.request.tile.as_ref(), diff --git a/gfx/wr/webrender/src/clip.rs b/gfx/wr/webrender/src/clip.rs index 95855c7ad48b..83f1330b7a49 100644 --- a/gfx/wr/webrender/src/clip.rs +++ b/gfx/wr/webrender/src/clip.rs @@ -380,10 +380,6 @@ impl ClipNodeInfo { rect.size, ); - // TODO: As a followup, if the image is a tiled blob, the device_image_rect below - // will be set to the blob's visible area. - let device_image_rect = DeviceIntRect::from_size(props.descriptor.size); - for Repetition { origin, .. } in repetitions { let layout_image_rect = LayoutRect { origin, @@ -392,7 +388,7 @@ impl ClipNodeInfo { let tiles = image::tiles( &layout_image_rect, &visible_rect, - &device_image_rect, + &props.visible_rect, tile_size as i32, ); for tile in tiles { diff --git a/gfx/wr/webrender/src/image.rs b/gfx/wr/webrender/src/image.rs index 7c277747a594..7c8a0794c079 100644 --- a/gfx/wr/webrender/src/image.rs +++ b/gfx/wr/webrender/src/image.rs @@ -491,6 +491,37 @@ fn last_tile_size_1d( ) } +pub fn compute_tile_rect( + image_rect: &DeviceIntRect, + regular_tile_size: TileSize, + tile: TileOffset, +) -> DeviceIntRect { + let regular_tile_size = regular_tile_size as i32; + DeviceIntRect { + origin: point2( + compute_tile_origin_1d(image_rect.x_range(), regular_tile_size, tile.x as i32), + compute_tile_origin_1d(image_rect.y_range(), regular_tile_size, tile.y as i32), + ), + size: size2( + compute_tile_size_1d(image_rect.x_range(), regular_tile_size, tile.x as i32), + compute_tile_size_1d(image_rect.y_range(), regular_tile_size, tile.y as i32), + ), + } +} + +fn compute_tile_origin_1d( + img_range: Range, + regular_tile_size: i32, + tile_offset: i32, +) -> i32 { + let tile_range = tile_range_1d(&img_range, regular_tile_size); + if tile_offset == tile_range.start { + img_range.start + } else { + tile_offset * regular_tile_size + } +} + // Compute the width and height in pixels of a tile depending on its position in the image. pub fn compute_tile_size( image_rect: &DeviceIntRect, @@ -551,6 +582,47 @@ pub fn for_each_tile_in_range( } } +pub fn compute_valid_tiles_if_bounds_change( + prev_rect: &DeviceIntRect, + new_rect: &DeviceIntRect, + tile_size: u16, +) -> Option { + let intersection = prev_rect.intersection(new_rect); + + if intersection.is_none() { + return Some(TileRange::zero()); + } + + let intersection = intersection.unwrap_or(DeviceIntRect::zero()); + + let left = prev_rect.min_x() != new_rect.min_x(); + let right = prev_rect.max_x() != new_rect.max_x(); + let top = prev_rect.min_y() != new_rect.min_y(); + let bottom = prev_rect.max_y() != new_rect.max_y(); + + if !left && !right && !top && !bottom { + // Bounds have not changed. + return None; + } + + let tw = 1.0 / (tile_size as f32); + let th = 1.0 / (tile_size as f32); + + let tiles = intersection + .cast::() + .scale(tw, th); + + let min_x = if left { f32::ceil(tiles.min_x()) } else { f32::floor(tiles.min_x()) }; + let min_y = if top { f32::ceil(tiles.min_y()) } else { f32::floor(tiles.min_y()) }; + let max_x = if right { f32::floor(tiles.max_x()) } else { f32::ceil(tiles.max_x()) }; + let max_y = if bottom { f32::floor(tiles.max_y()) } else { f32::ceil(tiles.max_y()) }; + + Some(TileRange { + origin: point2(min_x as i32, min_y as i32), + size: size2((max_x - min_x) as i32, (max_y - min_y) as i32), + }) +} + #[cfg(test)] mod tests { use super::*; @@ -714,4 +786,26 @@ mod tests { assert_eq!(result.first_tile_layout_size, 10.0); assert_eq!(result.last_tile_layout_size, 10.0); } + + #[test] + fn smaller_than_tile_size_at_origin() { + let r = compute_tile_rect( + &rect(0, 0, 80, 80), + 256, + point2(0, 0), + ); + + assert_eq!(r, rect(0, 0, 80, 80)); + } + + #[test] + fn smaller_than_tile_size_with_offset() { + let r = compute_tile_rect( + &rect(20, 20, 80, 80), + 256, + point2(0, 0), + ); + + assert_eq!(r, rect(20, 20, 80, 80)); + } } diff --git a/gfx/wr/webrender/src/prim_store/mod.rs b/gfx/wr/webrender/src/prim_store/mod.rs index db3d938ab688..49871002cfa4 100644 --- a/gfx/wr/webrender/src/prim_store/mod.rs +++ b/gfx/wr/webrender/src/prim_store/mod.rs @@ -2289,9 +2289,12 @@ impl PrimitiveStore { frame_state.gpu_cache, ); } - Some(ImageProperties { descriptor, tiling: Some(tile_size), .. }) => { + Some(ImageProperties { tiling: Some(tile_size), visible_rect, .. }) => { image_instance.visible_tiles.clear(); - let device_image_rect = DeviceIntRect::from_size(descriptor.size); + // TODO: rename the blob's visible_rect into something that doesn't conflict + // with the terminology we use during culling since it's not really the same + // thing. + let active_rect = visible_rect; // Tighten the clip rect because decomposing the repeated image can // produce primitives that are partially covering the original image @@ -2344,7 +2347,7 @@ impl PrimitiveStore { let tiles = crate::image::tiles( &layout_image_rect, &visible_rect, - &device_image_rect, + &active_rect, tile_size as i32, ); diff --git a/gfx/wr/webrender/src/resource_cache.rs b/gfx/wr/webrender/src/resource_cache.rs index 6ecbce9ef220..728a91954a02 100644 --- a/gfx/wr/webrender/src/resource_cache.rs +++ b/gfx/wr/webrender/src/resource_cache.rs @@ -24,7 +24,8 @@ use crate::glyph_cache::GlyphCacheEntry; use crate::glyph_rasterizer::{BaseFontInstance, FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer}; use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle}; use crate::gpu_types::UvRectKind; -use crate::image::{compute_tile_size, compute_tile_range, for_each_tile_in_range}; +use crate::image::{compute_tile_size, compute_tile_rect, compute_tile_range, for_each_tile_in_range}; +use crate::image::compute_valid_tiles_if_bounds_change; use crate::internal_types::{FastHashMap, FastHashSet, TextureSource, TextureUpdateList}; use crate::profiler::{ResourceProfileCounters, TextureCacheProfileCounters}; use crate::render_backend::{FrameId, FrameStamp}; @@ -143,6 +144,9 @@ pub struct ImageProperties { pub descriptor: ImageDescriptor, pub external_image: Option, pub tiling: Option, + // Potentially a subset of the image's total rectangle. This rectangle is what + // we map to the (layout space) display item bounds. + pub visible_rect: DeviceIntRect, } #[derive(Debug, Copy, Clone, PartialEq)] @@ -164,13 +168,24 @@ struct BlobImageTemplate { descriptor: ImageDescriptor, tiling: Option, dirty_rect: BlobDirtyRect, - viewport_tiles: Option, + /// See ImageResource::visible_rect. + visible_rect: DeviceIntRect, + // If the active rect of the blob changes, this represents the + // range of tiles that remain valid. This must be taken into + // account in addition to the valid rect when submitting blob + // rasterization requests. + // `None` means the bounds have not changed (tiles are still valid). + // `Some(TileRange::zero())` means all of the tiles are invalid. + valid_tiles_after_bounds_change: Option, } struct ImageResource { data: CachedImageData, descriptor: ImageDescriptor, tiling: Option, + /// This is used to express images that are virtually very large + /// but with only a visible sub-set that is valid at a given time. + visible_rect: DeviceIntRect, } #[derive(Clone, Debug)] @@ -586,7 +601,13 @@ impl ResourceCache { if let ImageData::Raw(ref bytes) = img.data { profile_counters.image_templates.inc(bytes.len()); } - self.add_image_template(img.key, img.descriptor, img.data.into(), img.tiling); + self.add_image_template( + img.key, + img.descriptor, + img.data.into(), + &img.descriptor.size.into(), + img.tiling, + ); } ResourceUpdate::UpdateImage(img) => { self.update_image_template(img.key, img.descriptor, img.data.into(), &img.dirty_rect); @@ -596,6 +617,7 @@ impl ResourceCache { img.key.as_image(), img.descriptor, CachedImageData::Blob, + &img.visible_rect, img.tiling, ); } @@ -608,7 +630,8 @@ impl ResourceCache { &img.dirty_rect ), ); - self.discard_tiles_outside_visible_area(img.key, &img.visible_rect); + self.discard_tiles_outside_visible_area(img.key, &img.visible_rect); // TODO: remove? + self.set_image_visible_rect(img.key.as_image(), &img.visible_rect); } ResourceUpdate::DeleteImage(img) => { self.delete_image_template(img); @@ -621,6 +644,7 @@ impl ResourceCache { } ResourceUpdate::SetBlobImageVisibleArea(key, area) => { self.discard_tiles_outside_visible_area(key, &area); + self.set_image_visible_rect(key.as_image(), &area); } ResourceUpdate::AddFont(_) | ResourceUpdate::AddFontInstance(_) => { @@ -657,12 +681,7 @@ impl ResourceCache { } ResourceUpdate::SetBlobImageVisibleArea(ref key, ref area) => { if let Some(template) = self.blob_image_templates.get_mut(&key) { - if let Some(tile_size) = template.tiling { - template.viewport_tiles = Some(compute_tile_range( - &area, - tile_size, - )); - } + template.visible_rect = *area; } } _ => {} @@ -773,6 +792,20 @@ impl ResourceCache { if let RasterizedBlob::Tiled(ref mut tiles) = *image { tiles.insert(tile, data); } + + match self.cached_images.try_get_mut(&request.key.as_image()) { + Some(&mut ImageResult::Multi(ref mut entries)) => { + let cached_key = CachedImageKey { + rendering: ImageRendering::Auto, // TODO(nical) + tile: Some(tile), + }; + if let Some(entry) = entries.try_get_mut(&cached_key) { + entry.dirty_rect = DirtyRect::All; + } + } + _ => {} + } + } else { if let RasterizedBlob::NonTiled(ref mut queue) = *image { // If our new rasterized rect overwrites items in the queue, discard them. @@ -862,6 +895,7 @@ impl ResourceCache { image_key: ImageKey, descriptor: ImageDescriptor, data: CachedImageData, + visible_rect: &DeviceIntRect, mut tiling: Option, ) { if tiling.is_none() && Self::should_tile(self.max_texture_size(), &descriptor, &data) { @@ -874,6 +908,7 @@ impl ResourceCache { descriptor, data, tiling, + visible_rect: *visible_rect, }; self.resources.image_templates.insert(image_key, resource); @@ -941,6 +976,7 @@ impl ResourceCache { descriptor, data, tiling, + visible_rect: descriptor.size.into(), }; } @@ -956,8 +992,6 @@ impl ResourceCache { let max_texture_size = self.max_texture_size(); tiling = get_blob_tiling(tiling, descriptor, max_texture_size); - let viewport_tiles = tiling.map(|tile_size| compute_tile_range(&visible_rect, tile_size)); - self.blob_image_handler.as_mut().unwrap().add(key, data, visible_rect, tiling); self.blob_image_templates.insert( @@ -966,7 +1000,8 @@ impl ResourceCache { descriptor: *descriptor, tiling, dirty_rect: DirtyRect::All, - viewport_tiles, + valid_tiles_after_bounds_change: None, + visible_rect: *visible_rect, }, ); } @@ -990,13 +1025,32 @@ impl ResourceCache { let tiling = get_blob_tiling(image.tiling, descriptor, max_texture_size); - let viewport_tiles = image.tiling.map(|tile_size| compute_tile_range(&visible_rect, tile_size)); + let mut valid_tiles_after_bounds_change = None; + + if let Some(tile_size) = image.tiling { + valid_tiles_after_bounds_change = compute_valid_tiles_if_bounds_change( + &image.visible_rect, + visible_rect, + tile_size, + ); + } + + match (image.valid_tiles_after_bounds_change, valid_tiles_after_bounds_change) { + (Some(old), Some(ref mut new)) => { + *new = new.intersection(&old).unwrap_or(TileRange::zero()); + } + (Some(old), None) => { + valid_tiles_after_bounds_change = Some(old); + } + _ => {} + } *image = BlobImageTemplate { descriptor: *descriptor, tiling, dirty_rect: dirty_rect.union(&image.dirty_rect), - viewport_tiles, + valid_tiles_after_bounds_change, + visible_rect: *visible_rect, }; } @@ -1166,14 +1220,11 @@ impl ResourceCache { rect: match template.tiling { Some(tile_size) => { let tile = request.tile.unwrap(); - LayoutIntRect { - origin: point2(tile.x, tile.y) * tile_size as i32, - size: blob_size(compute_tile_size( - &template.descriptor.size.into(), - tile_size, - tile, - )), - } + blob_rect(compute_tile_rect( + &template.visible_rect, + tile_size, + tile, + )) } None => blob_size(template.descriptor.size).into(), }, @@ -1213,37 +1264,33 @@ impl ResourceCache { if let Some(tile_size) = template.tiling { // If we know that only a portion of the blob image is in the viewport, // only request these visible tiles since blob images can be huge. - let mut tiles = template.viewport_tiles.unwrap_or_else(|| { - // Default to requesting the full range of tiles. - compute_tile_range( - &DeviceIntRect { - origin: point2(0, 0), - size: template.descriptor.size, - }, - tile_size, - ) - }); + let mut tiles = compute_tile_range( + &template.visible_rect, + tile_size, + ); let image_dirty_rect = to_image_dirty_rect(&template.dirty_rect); // Don't request tiles that weren't invalidated. - if let DirtyRect::Partial(dirty_rect) = image_dirty_rect { - let dirty_rect = DeviceIntRect { - origin: point2( - dirty_rect.origin.x, - dirty_rect.origin.y, - ), - size: size2( - dirty_rect.size.width, - dirty_rect.size.height, - ), - }; - let dirty_tiles = compute_tile_range( - &dirty_rect, - tile_size, - ); + let dirty_tiles = match image_dirty_rect { + DirtyRect::Partial(dirty_rect) => { + let dirty_rect = DeviceIntRect { + origin: point2( + dirty_rect.origin.x, + dirty_rect.origin.y, + ), + size: size2( + dirty_rect.size.width, + dirty_rect.size.height, + ), + }; - tiles = tiles.intersection(&dirty_tiles).unwrap_or_else(TileRange::zero); - } + compute_tile_range( + &dirty_rect, + tile_size, + ) + } + DirtyRect::All => tiles, + }; let original_tile_range = tiles; @@ -1284,16 +1331,22 @@ impl ResourceCache { blob_tiles_clear_requests.push(clear_params); } + for_each_tile_in_range(&tiles, |tile| { + let still_valid = template.valid_tiles_after_bounds_change + .map(|valid_tiles| valid_tiles.contains(tile)) + .unwrap_or(true); + + if still_valid && !dirty_tiles.contains(tile) { + return; + } + let descriptor = BlobImageDescriptor { - rect: LayoutIntRect { - origin: point2(tile.x, tile.y) * tile_size as i32, - size: blob_size(compute_tile_size( - &template.descriptor.size.into(), - tile_size, - tile, - )), - }, + rect: blob_rect(compute_tile_rect( + &template.visible_rect, + tile_size, + tile, + )), format: template.descriptor.format, }; @@ -1312,6 +1365,8 @@ impl ResourceCache { } ); }); + + template.valid_tiles_after_bounds_change = None; } else { let mut needs_upload = match self.cached_images.try_get(&key.as_image()) { Some(&ImageResult::UntiledAuto(ref entry)) => { @@ -1320,7 +1375,7 @@ impl ResourceCache { _ => true, }; - // If the queue of ratserized updates is growing it probably means that + // If the queue of rasterized updates is growing it probably means that // the texture is not getting uploaded because the display item is off-screen. // In that case we are better off // - Either not kicking rasterization for that image (avoid wasted cpu work @@ -1417,6 +1472,12 @@ impl ResourceCache { } } + fn set_image_visible_rect(&mut self, key: ImageKey, rect: &DeviceIntRect) { + if let Some(image) = self.resources.image_templates.get_mut(key) { + image.visible_rect = * rect; + } + } + pub fn request_glyphs( &mut self, mut font: FontInstance, @@ -1555,6 +1616,7 @@ impl ResourceCache { descriptor: image_template.descriptor, external_image, tiling: image_template.tiling, + visible_rect: image_template.visible_rect, } }) } @@ -1698,13 +1760,15 @@ impl ResourceCache { if let Some(tile) = request.tile { let tile_size = image_template.tiling.unwrap(); - let clipped_tile_size = compute_tile_size(&descriptor.size.into(), tile_size, tile); - + let clipped_tile_size = compute_tile_size(&image_template.visible_rect, tile_size, tile); // The tiled image could be stored on the CPU as one large image or be // already broken up into tiles. This affects the way we compute the stride // and offset. let tiled_on_cpu = image_template.data.is_blob(); if !tiled_on_cpu { + // we don't expect to have partial tiles at the top and left of non-blob + // images. + debug_assert_eq!(image_template.visible_rect.origin, point2(0, 0)); let bpp = descriptor.format.bytes_per_pixel(); let stride = descriptor.compute_stride(); descriptor.stride = Some(stride); @@ -2283,6 +2347,7 @@ impl ResourceCache { data, descriptor: template.descriptor, tiling: template.tiling, + visible_rect: template.descriptor.size.into(), }); } @@ -2298,3 +2363,11 @@ impl ResourceCache { fn blob_size(device_size: DeviceIntSize) -> LayoutIntSize { size2(device_size.width, device_size.height) } + +#[inline] +fn blob_rect(device_rect: DeviceIntRect) -> LayoutIntRect { + LayoutIntRect { + origin: point2(device_rect.origin.x, device_rect.origin.y), + size: blob_size(device_rect.size), + } +}