From 6b29ecc8f625f5a1d4708c75743f7a9bac53c120 Mon Sep 17 00:00:00 2001 From: Nicolas Silva Date: Wed, 16 May 2018 18:45:24 +0200 Subject: [PATCH] Rasterize blob images on the scene builder thread. This commit changes the blob image entry points to allow rasterizing blob images eagerly during scene building to avoid rasterizing them lazily during frame building. This is a tradeoff that in some case will cause more rasterization than is necessary but moves slow rasterization out of the critical path for smooth scrolling. --- examples/blob.rs | 108 ++---- webrender/doc/blob.md | 17 + webrender/src/image.rs | 41 ++- webrender/src/render_backend.rs | 73 +++- webrender/src/renderer.rs | 10 +- webrender/src/resource_cache.rs | 590 +++++++++++++++++++++++++------- webrender/src/scene_builder.rs | 14 +- webrender_api/src/api.rs | 7 +- webrender_api/src/image.rs | 55 ++- webrender_api/src/units.rs | 7 + wrench/src/blob.rs | 88 +++-- wrench/src/json_frame_writer.rs | 1 + wrench/src/rawtest.rs | 120 ++++++- wrench/src/ron_frame_writer.rs | 1 + wrench/src/wrench.rs | 2 +- wrench/src/yaml_frame_writer.rs | 1 + 16 files changed, 855 insertions(+), 280 deletions(-) create mode 100644 webrender/doc/blob.md diff --git a/examples/blob.rs b/examples/blob.rs index de2ef5c09a..b996dc5864 100644 --- a/examples/blob.rs +++ b/examples/blob.rs @@ -13,13 +13,12 @@ mod boilerplate; use boilerplate::{Example, HandyDandyRectBuilder}; use rayon::{ThreadPool, ThreadPoolBuilder}; +use rayon::prelude::*; use std::collections::HashMap; -use std::collections::hash_map::Entry; use std::sync::Arc; -use std::sync::mpsc::{Receiver, Sender, channel}; use webrender::api::{self, DisplayListBuilder, DocumentId, PipelineId, RenderApi, Transaction}; -// This example shows how to implement a very basic BlobImageRenderer that can only render +// This example shows how to implement a very basic BlobImageHandler that can only render // a checkerboard pattern. // The deserialized command list internally used by this example is just a color. @@ -97,7 +96,7 @@ fn render_blob( } Ok(api::RasterizedBlobImage { - data: texels, + data: Arc::new(texels), size: descriptor.size, }) } @@ -109,35 +108,24 @@ struct CheckerboardRenderer { // want to). workers: Arc, - // the workers will use an mpsc channel to communicate the result. - tx: Sender<(api::BlobImageRequest, api::BlobImageResult)>, - rx: Receiver<(api::BlobImageRequest, api::BlobImageResult)>, - // The deserialized drawing commands. // In this example we store them in Arcs. This isn't necessary since in this simplified // case the command list is a simple 32 bits value and would be cheap to clone before sending // to the workers. But in a more realistic scenario the commands would typically be bigger // and more expensive to clone, so let's pretend it is also the case here. image_cmds: HashMap>, - - // The images rendered in the current frame (not kept here between frames). - rendered_images: HashMap>, } impl CheckerboardRenderer { fn new(workers: Arc) -> Self { - let (tx, rx) = channel(); CheckerboardRenderer { image_cmds: HashMap::new(), - rendered_images: HashMap::new(), workers, - tx, - rx, } } } -impl api::BlobImageRenderer for CheckerboardRenderer { +impl api::BlobImageHandler for CheckerboardRenderer { fn add(&mut self, key: api::ImageKey, cmds: Arc, _: Option) { self.image_cmds .insert(key, Arc::new(deserialize_blob(&cmds[..]).unwrap())); @@ -154,68 +142,40 @@ impl api::BlobImageRenderer for CheckerboardRenderer { self.image_cmds.remove(&key); } - fn request( + fn prepare_resources( &mut self, - _resources: &api::BlobImageResources, - request: api::BlobImageRequest, - descriptor: &api::BlobImageDescriptor, - _dirty_rect: Option, - ) { - // This method is where we kick off our rendering jobs. - // It should avoid doing work on the calling thread as much as possible. - // In this example we will use the thread pool to render individual tiles. - - // Gather the input data to send to a worker thread. - let cmds = Arc::clone(&self.image_cmds.get(&request.key).unwrap()); - let tx = self.tx.clone(); - let descriptor = descriptor.clone(); - - self.workers.spawn(move || { - let result = render_blob(cmds, &descriptor, request.tile); - tx.send((request, result)).unwrap(); - }); - - // Add None in the map of rendered images. This makes it possible to differentiate - // between commands that aren't finished yet (entry in the map is equal to None) and - // keys that have never been requested (entry not in the map), which would cause deadlocks - // if we were to block upon receiving their result in resolve! - self.rendered_images.insert(request, None); - } - - fn resolve(&mut self, request: api::BlobImageRequest) -> api::BlobImageResult { - // In this method we wait until the work is complete on the worker threads and - // gather the results. - - // First look at whether we have already received the rendered image - // that we are looking for. - match self.rendered_images.entry(request) { - Entry::Vacant(_) => { - return Err(api::BlobImageError::InvalidKey); - } - Entry::Occupied(entry) => { - // None means we haven't yet received the result. - if entry.get().is_some() { - let result = entry.remove(); - return result.unwrap(); - } - } - } - - // We haven't received it yet, pull from the channel until we receive it. - while let Ok((req, result)) = self.rx.recv() { - if req == request { - // There it is! - return result; - } - self.rendered_images.insert(req, Some(result)); - } + _services: &api::BlobImageResources, + _requests: &[api::BlobImageParams], + ) {} - // If we break out of the loop above it means the channel closed unexpectedly. - Err(api::BlobImageError::Other("Channel closed".into())) - } fn delete_font(&mut self, _font: api::FontKey) {} fn delete_font_instance(&mut self, _instance: api::FontInstanceKey) {} fn clear_namespace(&mut self, _namespace: api::IdNamespace) {} + fn create_blob_rasterizer(&mut self) -> Box { + Box::new(Rasterizer { + workers: Arc::clone(&self.workers), + image_cmds: self.image_cmds.clone(), + }) + } +} + +struct Rasterizer { + workers: Arc, + image_cmds: HashMap>, +} + +impl api::AsyncBlobImageRasterizer for Rasterizer { + fn rasterize(&mut self, requests: &[api::BlobImageParams]) -> Vec<(api::BlobImageRequest, api::BlobImageResult)> { + let requests: Vec<(&api::BlobImageParams, Arc)> = requests.into_iter().map(|params| { + (params, Arc::clone(&self.image_cmds[¶ms.request.key])) + }).collect(); + + self.workers.install(|| { + requests.into_par_iter().map(|(params, commands)| { + (params.request, render_blob(commands, ¶ms.descriptor, params.request.tile)) + }).collect() + }) + } } struct App {} @@ -292,7 +252,7 @@ fn main() { workers: Some(Arc::clone(&workers)), // Register our blob renderer, so that WebRender integrates it in the resource cache.. // Share the same pool of worker threads between WebRender and our blob renderer. - blob_image_renderer: Some(Box::new(CheckerboardRenderer::new(Arc::clone(&workers)))), + blob_image_handler: Some(Box::new(CheckerboardRenderer::new(Arc::clone(&workers)))), ..Default::default() }; diff --git a/webrender/doc/blob.md b/webrender/doc/blob.md new file mode 100644 index 0000000000..d7ae917a7e --- /dev/null +++ b/webrender/doc/blob.md @@ -0,0 +1,17 @@ +# Blob images + +The blob image mechanism now has two traits: +- [`BlobImageHandler`](https://github.com/servo/webrender/pull/2785/files#diff-2b72a28a40b83edf41a59adfd46b1a11R188) is roughly the equivalent of the previous `BlobImageRenderer` except that it doesn't do any rendering (it manages the state of the blob commands, and resources like fonts). +- [`AsyncBlobImageRasterizer`](https://github.com/servo/webrender/pull/2785/files#diff-2b72a28a40b83edf41a59adfd46b1a11R211) is created by the handler and sent over to the scene builder thread. the async rasterizer is meant to be a snapshot of the state of blob image commands that can execute the commands if provided some requests. + +When receiving a transaction, the render backend / resource cache look at the list of added and updated blob images in that transaction, [collect the list of blob images and tiles that need to be rendered](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R848), create a rasterizer, and ship the two to the scene builder. +After building the scene the rasterizer gets handed the list of blob requests and [does all of the rasterization](https://github.com/servo/webrender/pull/2785/files#diff-856af4d4ff2333d4204e7e5a87a93c58R153), blocking the scene builder thread until the work is done. + +When the scene building and rasterization is done, the render backend receives the rasterized blobs and [stores them](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R520) so that they are available when frame building needs them. + +Because blob images can be huge, we don't always want to rasterize them entirely during scene building. To decide what should be rasterized, we rely on gecko giving us a hint through the added `set_image_visible_area` API. When the render backend receives that message [it decides which tiles are going to be rasterized](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R469). This information is also used to [decide which tiles to evict](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R430), so that we don't keep thousands of tiles if we scroll through a massive blob image. The idea is for the visible area to correspond to the size of the display list. + +Sometimes, however, Gecko gets this visible area "wrong", or at least gives webrender a certain visible area but eventually webrender requests tiles during frame building that weren't in that area. I think that this is inevitable because the culling logic in gecko and webrender works very differently, so relying on them to match exactly is fragile at best. +So to work around this type of situation, [keep around the async blob rasterizer](https://github.com/servo/webrender/pull/2785/files#diff-3722af8f0bcba9c3ce197a9aa3052014R769) that we sent to the scene builder, and store it in the resource cache when we swap the scene. This blob rasterizer represents the state of the blob commands at the time the transaction was built (and is potentially different from the state of the blob image handler). Frame building [collects a list of blob images](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R811) (or blob tiles) that are not already rasterized, and asks the current async blob rasterizer to rasterize them synchronously on the render backend. The hope is that this would happen rarely. + +Another important detail is that for this to work, resources that are used by blob images (so currently only fonts), need to be in sync with the blobs. Fortunately, fonts are currently immutable so we mostly need to make sure they are added [before the transaction](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R440) is built and [removed after](https://github.com/servo/webrender/pull/2785/files#diff-77cbdf7ba9ebae81feb38a64c21b8454R400) the transaction is swapped. If blob images were to use images, then we'd have to either do the same for these images (and disallow updating them), or maintain the state of images before and after scene building like we effectively do for blobs. diff --git a/webrender/src/image.rs b/webrender/src/image.rs index dcf9088211..f36703ffb1 100644 --- a/webrender/src/image.rs +++ b/webrender/src/image.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 http://mozilla.org/MPL/2.0/. */ -use api::{TileOffset, LayoutRect, LayoutSize, LayoutPoint, DeviceUintSize}; -use euclid::vec2; +use api::{TileOffset, TileRange, LayoutRect, LayoutSize, LayoutPoint}; +use api::{DeviceUintSize, NormalizedRect}; +use euclid::{vec2, point2}; use prim_store::EdgeAaSegmentMask; /// If repetitions are far enough apart that only one is within @@ -225,6 +226,42 @@ pub fn for_each_tile( } } +pub fn compute_tile_range( + visible_area: &NormalizedRect, + image_size: &DeviceUintSize, + tile_size: u16, +) -> TileRange { + // Tile dimensions in normalized coordinates. + let tw = (image_size.width as f32) / (tile_size as f32); + let th = (image_size.height as f32) / (tile_size as f32); + + let t0 = point2( + f32::floor(visible_area.origin.x * tw), + f32::floor(visible_area.origin.y * th), + ).cast::(); + + let t1 = point2( + f32::ceil(visible_area.max_x() * tw), + f32::ceil(visible_area.max_y() * th), + ).cast::(); + + TileRange { + origin: t0, + size: (t1 - t0).to_size(), + } +} + +pub fn for_each_tile_in_range( + range: &TileRange, + callback: &mut FnMut(TileOffset), +) { + for y in 0..range.size.height { + for x in 0..range.size.width { + callback(range.origin + vec2(x, y)); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/webrender/src/render_backend.rs b/webrender/src/render_backend.rs index 9e6ce9027d..a410a9e1c8 100644 --- a/webrender/src/render_backend.rs +++ b/webrender/src/render_backend.rs @@ -8,7 +8,7 @@ use api::{BuiltDisplayListIter, SpecificDisplayItem}; use api::{DeviceIntPoint, DevicePixelScale, DeviceUintPoint, DeviceUintRect, DeviceUintSize}; use api::{DocumentId, DocumentLayer, ExternalScrollId, FrameMsg, HitTestFlags, HitTestResult}; use api::{IdNamespace, LayoutPoint, PipelineId, RenderNotifier, SceneMsg, ScrollClamping}; -use api::{ScrollLocation, ScrollNodeState, TransactionMsg}; +use api::{ScrollLocation, ScrollNodeState, TransactionMsg, ResourceUpdate, ImageKey}; use api::channel::{MsgReceiver, Payload}; #[cfg(feature = "capture")] use api::CaptureBits; @@ -226,10 +226,11 @@ impl Document { fn forward_transaction_to_scene_builder( &mut self, transaction_msg: TransactionMsg, + blobs_to_rasterize: &[ImageKey], document_ops: &DocumentOps, document_id: DocumentId, scene_id: u64, - resource_cache: &ResourceCache, + resource_cache: &mut ResourceCache, scene_tx: &Sender, ) { // Do as much of the error handling as possible here before dispatching to @@ -252,8 +253,14 @@ impl Document { None }; + let (blob_rasterizer, blob_requests) = resource_cache.create_blob_scene_builder_requests( + blobs_to_rasterize + ); + scene_tx.send(SceneBuilderRequest::Transaction { scene: scene_request, + blob_requests, + blob_rasterizer, resource_updates: transaction_msg.resource_updates, frame_ops: transaction_msg.frame_ops, render: transaction_msg.generate_frame, @@ -718,6 +725,8 @@ impl RenderBackend { frame_ops, render, result_tx, + rasterized_blobs, + blob_rasterizer, } => { let mut ops = DocumentOps::nop(); if let Some(doc) = self.documents.get_mut(&document_id) { @@ -755,10 +764,16 @@ impl RenderBackend { use_scene_builder_thread: false, }; + self.resource_cache.add_rasterized_blob_images(rasterized_blobs); + if let Some(rasterizer) = blob_rasterizer { + self.resource_cache.set_blob_rasterizer(rasterizer); + } + if !transaction_msg.is_empty() || ops.render { self.update_document( document_id, transaction_msg, + &[], &mut frame_counter, &mut profile_counters, ops, @@ -825,9 +840,15 @@ impl RenderBackend { ApiMsg::FlushSceneBuilder(tx) => { self.scene_tx.send(SceneBuilderRequest::Flush(tx)).unwrap(); } - ApiMsg::UpdateResources(updates) => { - self.resource_cache - .update_resources(updates, &mut profile_counters.resources); + ApiMsg::UpdateResources(mut updates) => { + self.resource_cache.pre_scene_building_update( + &mut updates, + &mut profile_counters.resources + ); + self.resource_cache.post_scene_building_update( + updates, + &mut profile_counters.resources + ); } ApiMsg::GetGlyphDimensions(instance_key, glyph_indices, tx) => { let mut glyph_dimensions = Vec::with_capacity(glyph_indices.len()); @@ -956,10 +977,18 @@ impl RenderBackend { ApiMsg::ShutDown => { return false; } - ApiMsg::UpdateDocument(document_id, doc_msgs) => { + ApiMsg::UpdateDocument(document_id, mut doc_msgs) => { + let blob_requests = get_blob_image_updates(&doc_msgs.resource_updates); + + self.resource_cache.pre_scene_building_update( + &mut doc_msgs.resource_updates, + &mut profile_counters.resources, + ); + self.update_document( document_id, doc_msgs, + &blob_requests, frame_counter, profile_counters, DocumentOps::nop(), @@ -975,6 +1004,7 @@ impl RenderBackend { &mut self, document_id: DocumentId, mut transaction_msg: TransactionMsg, + blob_requests: &[ImageKey], frame_counter: &mut u32, profile_counters: &mut BackendProfileCounters, initial_op: DocumentOps, @@ -982,6 +1012,10 @@ impl RenderBackend { ) { let mut op = initial_op; + if !blob_requests.is_empty() { + transaction_msg.use_scene_builder_thread = true; + } + for scene_msg in transaction_msg.scene_ops.drain(..) { let _timer = profile_counters.total_time.timer(); op.combine( @@ -1000,17 +1034,18 @@ impl RenderBackend { doc.forward_transaction_to_scene_builder( transaction_msg, + blob_requests, &op, document_id, scene_id, - &self.resource_cache, + &mut self.resource_cache, &self.scene_tx, ); return; } - self.resource_cache.update_resources( + self.resource_cache.post_scene_building_update( transaction_msg.resource_updates, &mut profile_counters.resources, ); @@ -1208,6 +1243,28 @@ impl RenderBackend { } } +fn get_blob_image_updates(updates: &[ResourceUpdate]) -> Vec { + let mut requests = Vec::new(); + for update in updates { + match *update { + ResourceUpdate::AddImage(ref img) => { + if img.data.is_blob() { + requests.push(img.key); + } + } + ResourceUpdate::UpdateImage(ref img) => { + if img.data.is_blob() { + requests.push(img.key); + } + } + _ => {} + } + } + + requests +} + + #[cfg(feature = "debugger")] trait ToDebugString { fn debug_string(&self) -> String; diff --git a/webrender/src/renderer.rs b/webrender/src/renderer.rs index 28e39e09f1..975fec04ae 100644 --- a/webrender/src/renderer.rs +++ b/webrender/src/renderer.rs @@ -9,7 +9,7 @@ //! //! [renderer]: struct.Renderer.html -use api::{BlobImageRenderer, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize}; +use api::{BlobImageHandler, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize}; use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentId, Epoch, ExternalImageId}; use api::{ExternalImageType, FontRenderMode, FrameMsg, ImageFormat, PipelineId}; use api::{RenderApiSender, RenderNotifier, TexelRect, TextureTarget}; @@ -1693,7 +1693,7 @@ impl Renderer { let sampler = options.sampler; let enable_render_on_scroll = options.enable_render_on_scroll; - let blob_image_renderer = options.blob_image_renderer.take(); + let blob_image_handler = options.blob_image_handler.take(); let thread_listener_for_render_backend = thread_listener.clone(); let thread_listener_for_scene_builder = thread_listener.clone(); let scene_builder_hooks = options.scene_builder_hooks; @@ -1729,7 +1729,7 @@ impl Renderer { let resource_cache = ResourceCache::new( texture_cache, glyph_rasterizer, - blob_image_renderer, + blob_image_handler, ); let mut backend = RenderBackend::new( @@ -4095,7 +4095,7 @@ pub struct RendererOptions { pub scatter_gpu_cache_updates: bool, pub upload_method: UploadMethod, pub workers: Option>, - pub blob_image_renderer: Option>, + pub blob_image_handler: Option>, pub recorder: Option>, pub thread_listener: Option>, pub enable_render_on_scroll: bool, @@ -4130,7 +4130,7 @@ impl Default for RendererOptions { // but we are unable to make this decision here, so picking the reasonable medium. upload_method: UploadMethod::PixelBuffer(VertexUsageHint::Stream), workers: None, - blob_image_renderer: None, + blob_image_handler: None, recorder: None, thread_listener: None, enable_render_on_scroll: true, diff --git a/webrender/src/resource_cache.rs b/webrender/src/resource_cache.rs index 5493cd9371..8184eaec95 100644 --- a/webrender/src/resource_cache.rs +++ b/webrender/src/resource_cache.rs @@ -2,15 +2,15 @@ * 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 api::{AddFont, BlobImageResources, ResourceUpdate}; -use api::{BlobImageDescriptor, BlobImageError, BlobImageRenderer, BlobImageRequest}; +use api::{AddFont, BlobImageResources, AsyncBlobImageRasterizer, ResourceUpdate}; +use api::{BlobImageDescriptor, BlobImageHandler, BlobImageRequest}; use api::{ClearCache, ColorF, DevicePoint, DeviceUintPoint, DeviceUintRect, DeviceUintSize}; use api::{FontInstanceKey, FontKey, FontTemplate, GlyphIndex}; -use api::{ExternalImageData, ExternalImageType}; +use api::{ExternalImageData, ExternalImageType, BlobImageResult, BlobImageParams}; use api::{FontInstanceOptions, FontInstancePlatformOptions, FontVariation}; use api::{GlyphDimensions, IdNamespace}; use api::{ImageData, ImageDescriptor, ImageKey, ImageRendering}; -use api::{TileOffset, TileSize}; +use api::{TileOffset, TileSize, TileRange, NormalizedRect, BlobImageData}; use app_units::Au; #[cfg(feature = "capture")] use capture::ExternalCaptureImage; @@ -19,13 +19,14 @@ use capture::PlainExternalImage; #[cfg(any(feature = "replay", feature = "png"))] use capture::CaptureConfig; use device::TextureFilter; -use euclid::size2; +use euclid::{point2, size2}; use glyph_cache::GlyphCache; #[cfg(not(feature = "pathfinder"))] use glyph_cache::GlyphCacheEntry; use glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer}; use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle}; use gpu_types::UvRectKind; +use image::{compute_tile_range, for_each_tile_in_range}; use internal_types::{FastHashMap, FastHashSet, SourceTexture, TextureUpdateList}; use profiler::{ResourceProfileCounters, TextureCacheProfileCounters}; use render_backend::FrameId; @@ -95,11 +96,25 @@ enum State { QueryResources, } -#[derive(Debug)] +/// Post scene building state. +struct RasterizedBlobImage { + data: FastHashMap, BlobImageResult>, +} + +/// Pre scene building state. +/// We use this to generate the async blob rendering requests. +struct BlobImageTemplate { + descriptor: ImageDescriptor, + tiling: Option, + dirty_rect: Option, + viewport_tiles: Option, +} + struct ImageResource { data: ImageData, descriptor: ImageDescriptor, tiling: Option, + viewport_tiles: Option, } #[derive(Clone, Debug)] @@ -360,14 +375,22 @@ pub struct ResourceCache { // both blobs and regular images. pending_image_requests: FastHashSet, - blob_image_renderer: Option>, + blob_image_handler: Option>, + rasterized_blob_images: FastHashMap, + blob_image_templates: FastHashMap, + + // If while building a frame we encounter blobs that we didn't already + // rasterize, add them to this list and rasterize them synchronously. + missing_blob_images: Vec, + // The rasterizer associated with the current scene. + blob_image_rasterizer: Option>, } impl ResourceCache { pub fn new( texture_cache: TextureCache, glyph_rasterizer: GlyphRasterizer, - blob_image_renderer: Option>, + blob_image_handler: Option>, ) -> Self { ResourceCache { cached_glyphs: GlyphCache::new(), @@ -380,7 +403,11 @@ impl ResourceCache { current_frame_id: FrameId(0), pending_image_requests: FastHashSet::default(), glyph_rasterizer, - blob_image_renderer, + blob_image_handler, + rasterized_blob_images: FastHashMap::default(), + blob_image_templates: FastHashMap::default(), + missing_blob_images: Vec::new(), + blob_image_rasterizer: None, } } @@ -425,7 +452,7 @@ impl ResourceCache { ).expect("Failed to request a render task from the resource cache!") } - pub fn update_resources( + pub fn post_scene_building_update( &mut self, updates: Vec, profile_counters: &mut ResourceProfileCounters, @@ -448,19 +475,78 @@ impl ResourceCache { ResourceUpdate::DeleteImage(img) => { self.delete_image_template(img); } - ResourceUpdate::AddFont(font) => match font { - AddFont::Raw(id, bytes, index) => { - profile_counters.font_templates.inc(bytes.len()); - self.add_font_template(id, FontTemplate::Raw(Arc::new(bytes), index)); - } - AddFont::Native(id, native_font_handle) => { - self.add_font_template(id, FontTemplate::Native(native_font_handle)); - } - }, ResourceUpdate::DeleteFont(font) => { self.delete_font_template(font); } - ResourceUpdate::AddFontInstance(instance) => { + ResourceUpdate::DeleteFontInstance(font) => { + self.delete_font_instance(font); + } + ResourceUpdate::SetImageVisibleArea(key, area) => { + self.discard_tiles_outside_visible_area(key, &area); + } + ResourceUpdate::AddFont(_) | + ResourceUpdate::AddFontInstance(_) => { + // Handled in update_resources_pre_scene_building + } + } + } + } + + pub fn pre_scene_building_update( + &mut self, + updates: &mut Vec, + profile_counters: &mut ResourceProfileCounters, + ) { + let mut new_updates = Vec::with_capacity(updates.len()); + for update in mem::replace(updates, Vec::new()) { + match update { + ResourceUpdate::AddImage(ref img) => { + if let ImageData::Blob(ref blob_data) = img.data { + self.add_blob_image( + img.key, + &img.descriptor, + img.tiling, + Arc::clone(blob_data), + ); + } + } + ResourceUpdate::UpdateImage(ref img) => { + if let ImageData::Blob(ref blob_data) = img.data { + self.update_blob_image( + img.key, + &img.descriptor, + &img.dirty_rect, + Arc::clone(blob_data) + ); + } + } + ResourceUpdate::SetImageVisibleArea(key, 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, + &template.descriptor.size, + tile_size, + )); + } + } + } + _ => {} + } + + match update { + ResourceUpdate::AddFont(font) => { + match font { + AddFont::Raw(id, bytes, index) => { + profile_counters.font_templates.inc(bytes.len()); + self.add_font_template(id, FontTemplate::Raw(Arc::new(bytes), index)); + } + AddFont::Native(id, native_font_handle) => { + self.add_font_template(id, FontTemplate::Native(native_font_handle)); + } + } + } + ResourceUpdate::AddFontInstance(mut instance) => { self.add_font_instance( instance.key, instance.font_key, @@ -470,11 +556,26 @@ impl ResourceCache { instance.variations, ); } - ResourceUpdate::DeleteFontInstance(instance) => { - self.delete_font_instance(instance); + other => { + new_updates.push(other); } } } + + *updates = new_updates; + } + + pub fn set_blob_rasterizer(&mut self, rasterizer: Box) { + self.blob_image_rasterizer = Some(rasterizer); + } + + pub fn add_rasterized_blob_images(&mut self, images: Vec<(BlobImageRequest, BlobImageResult)>) { + for (request, result) in images { + let image = self.rasterized_blob_images.entry(request.key).or_insert_with( + || { RasterizedBlobImage { data: FastHashMap::default() } } + ); + image.data.insert(request.tile, result); + } } pub fn add_font_template(&mut self, font_key: FontKey, template: FontTemplate) { @@ -489,7 +590,7 @@ impl ResourceCache { self.resources.font_templates.remove(&font_key); self.cached_glyphs .clear_fonts(|font| font.font_key == font_key); - if let Some(ref mut r) = self.blob_image_renderer { + if let Some(ref mut r) = self.blob_image_handler { r.delete_font(font_key); } } @@ -532,7 +633,7 @@ impl ResourceCache { .write() .unwrap() .remove(&instance_key); - if let Some(ref mut r) = self.blob_image_renderer { + if let Some(ref mut r) = self.blob_image_handler { r.delete_font_instance(instance_key); } } @@ -559,18 +660,11 @@ impl ResourceCache { tiling = Some(DEFAULT_TILE_SIZE); } - if let ImageData::Blob(ref blob) = data { - self.blob_image_renderer.as_mut().unwrap().add( - image_key, - Arc::clone(&blob), - tiling, - ); - } - let resource = ImageResource { descriptor, data, tiling, + viewport_tiles: None, }; self.resources.image_templates.insert(image_key, resource); @@ -594,13 +688,6 @@ impl ResourceCache { tiling = Some(DEFAULT_TILE_SIZE); } - if let ImageData::Blob(ref blob) = data { - self.blob_image_renderer - .as_mut() - .unwrap() - .update(image_key, Arc::clone(blob), dirty_rect); - } - // Each cache entry stores its own copy of the image's dirty rect. This allows them to be // updated independently. match self.cached_images.try_get_mut(&image_key) { @@ -619,6 +706,66 @@ impl ResourceCache { descriptor, data, tiling, + viewport_tiles: image.viewport_tiles, + }; + } + + // Happens before scene building. + pub fn add_blob_image( + &mut self, + key: ImageKey, + descriptor: &ImageDescriptor, + mut tiling: Option, + data: Arc, + ) { + let max_texture_size = self.max_texture_size(); + tiling = get_blob_tiling(tiling, descriptor, max_texture_size); + + self.blob_image_handler.as_mut().unwrap().add(key, data, tiling); + + self.blob_image_templates.insert( + key, + BlobImageTemplate { + descriptor: *descriptor, + tiling, + dirty_rect: Some( + DeviceUintRect::new( + DeviceUintPoint::zero(), + descriptor.size, + ) + ), + viewport_tiles: None, + }, + ); + } + + // Happens before scene building. + pub fn update_blob_image( + &mut self, + key: ImageKey, + descriptor: &ImageDescriptor, + dirty_rect: &Option, + data: Arc, + ) { + self.blob_image_handler.as_mut().unwrap().update(key, data, *dirty_rect); + + let max_texture_size = self.max_texture_size(); + + let image = self.blob_image_templates + .get_mut(&key) + .expect("Attempt to update non-existent blob image"); + + let tiling = get_blob_tiling(image.tiling, descriptor, max_texture_size); + + *image = BlobImageTemplate { + descriptor: *descriptor, + tiling, + dirty_rect: match (*dirty_rect, image.dirty_rect) { + (Some(rect), Some(prev_rect)) => Some(rect.union(&prev_rect)), + (Some(rect), None) => Some(rect), + (None, _) => None, + }, + viewport_tiles: image.viewport_tiles, }; } @@ -629,7 +776,9 @@ impl ResourceCache { match value { Some(image) => if image.data.is_blob() { - self.blob_image_renderer.as_mut().unwrap().delete(image_key); + self.blob_image_handler.as_mut().unwrap().delete(image_key); + self.blob_image_templates.remove(&image_key); + self.rasterized_blob_images.remove(&image_key); }, None => { warn!("Delete the non-exist key"); @@ -726,71 +875,214 @@ impl ResourceCache { ImageResult::Err(_) => panic!("Errors should already have been handled"), }; - let needs_upload = self.texture_cache - .request(&entry.texture_cache_handle, gpu_cache); + self.texture_cache.request(&entry.texture_cache_handle, gpu_cache); - let dirty_rect = if needs_upload { - // the texture cache entry has been evicted, treat it as all dirty - None - } else if entry.dirty_rect.is_none() { - return - } else { - entry.dirty_rect - }; + self.pending_image_requests.insert(request); - if !self.pending_image_requests.insert(request) { - return + if template.data.is_blob() { + let request: BlobImageRequest = request.into(); + let missing = match self.rasterized_blob_images.get(&request.key) { + Some(img) => !img.data.contains_key(&request.tile), + None => true, + }; + + // For some reason the blob image is missing. We'll fall back to + // rasterizing it on the render backend thread. + if missing { + let descriptor = match template.tiling { + Some(tile_size) => { + let tile = request.tile.unwrap(); + BlobImageDescriptor { + offset: DevicePoint::new( + tile.x as f32 * tile_size as f32, + tile.y as f32 * tile_size as f32, + ), + size: compute_tile_size( + &template.descriptor, + tile_size, + tile, + ), + format: template.descriptor.format, + } + } + None => { + BlobImageDescriptor { + offset: DevicePoint::origin(), + size: template.descriptor.size, + format: template.descriptor.format, + } + } + }; + + self.missing_blob_images.push( + BlobImageParams { + request, + descriptor, + dirty_rect: None, + } + ); + } + } + } + + pub fn create_blob_scene_builder_requests( + &mut self, + keys: &[ImageKey] + ) -> (Option>, Vec) { + if self.blob_image_handler.is_none() { + return (None, Vec::new()); } - // If we are tiling, then we need to confirm the dirty rect intersects - // the tile before leaving the request in the pending queue. - // - // We can start a worker thread rasterizing right now, if: - // - The image is a blob. - // - The blob hasn't already been requested this frame. - if template.data.is_blob() || dirty_rect.is_some() { - let (offset, size) = match request.tile { - Some(tile_offset) => { - let tile_size = template.tiling.unwrap(); - let actual_size = compute_tile_size( - &template.descriptor, + let mut blob_request_params = Vec::new(); + for key in keys { + let template = self.blob_image_templates.get_mut(key).unwrap(); + + 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( + &NormalizedRect { + origin: point2(0.0, 0.0), + size: size2(1.0, 1.0), + }, + &template.descriptor.size, + tile_size, + ) + }); + + // Don't request tiles that weren't invalidated. + if let Some(dirty_rect) = template.dirty_rect { + let f32_size = template.descriptor.size.to_f32(); + let normalized_dirty_rect = NormalizedRect { + origin: point2( + dirty_rect.origin.x as f32 / f32_size.width, + dirty_rect.origin.y as f32 / f32_size.height, + ), + size: size2( + dirty_rect.size.width as f32 / f32_size.width, + dirty_rect.size.height as f32 / f32_size.height, + ), + }; + let dirty_tiles = compute_tile_range( + &normalized_dirty_rect, + &template.descriptor.size, tile_size, - tile_offset, ); - if let Some(dirty) = dirty_rect { - if intersect_for_tile(dirty, actual_size, tile_size, tile_offset).is_none() { - // don't bother requesting unchanged tiles - entry.dirty_rect = None; - self.pending_image_requests.remove(&request); - return - } - } + tiles = tiles.intersection(&dirty_tiles).unwrap_or(TileRange::zero()); + } - let offset = DevicePoint::new( - tile_offset.x as f32 * tile_size as f32, - tile_offset.y as f32 * tile_size as f32, - ); - (offset, actual_size) + // This code tries to keep things sane if Gecko sends + // nonsensical blob image requests. + // Constant here definitely needs to be tweaked. + const MAX_TILES_PER_REQUEST: u32 = 64; + while tiles.size.width as u32 * tiles.size.height as u32 > MAX_TILES_PER_REQUEST { + // Remove tiles in the largest dimension. + if tiles.size.width > tiles.size.height { + tiles.size.width -= 2; + tiles.origin.x += 1; + } else { + tiles.size.height -= 2; + tiles.origin.y += 1; + } } - None => (DevicePoint::zero(), template.descriptor.size), - }; - if template.data.is_blob() { - if let Some(ref mut renderer) = self.blob_image_renderer { - renderer.request( - &self.resources, - request.into(), - &BlobImageDescriptor { - size, - offset, + for_each_tile_in_range(&tiles, &mut|tile| { + let descriptor = BlobImageDescriptor { + offset: DevicePoint::new( + tile.x as f32 * tile_size as f32, + tile.y as f32 * tile_size as f32, + ), + size: compute_tile_size( + &template.descriptor, + tile_size, + tile, + ), + format: template.descriptor.format, + }; + + blob_request_params.push( + BlobImageParams { + request: BlobImageRequest { + key: *key, + tile: Some(tile), + }, + descriptor, + dirty_rect: None, + } + ); + }); + } else { + // TODO: to support partial rendering of non-tiled blobs we + // need to know that the current version of the blob is uploaded + // to the texture cache and get the guarantee that it will not + // get evicted by the time the updated blob is rasterized and + // uploaded. + // Alternatively we could make it the responsibility of the blob + // renderer to always output the full image. This could be based + // a similar copy-on-write mechanism as gecko tiling. + blob_request_params.push( + BlobImageParams { + request: BlobImageRequest { + key: *key, + tile: None, + }, + descriptor: BlobImageDescriptor { + offset: DevicePoint::zero(), + size: template.descriptor.size, format: template.descriptor.format, }, - dirty_rect, - ); - } + dirty_rect: None, + } + ); } + template.dirty_rect = None; } + let handler = self.blob_image_handler.as_mut().unwrap(); + handler.prepare_resources(&self.resources, &blob_request_params); + (Some(handler.create_blob_rasterizer()), blob_request_params) + } + + fn discard_tiles_outside_visible_area( + &mut self, + key: ImageKey, + area: &NormalizedRect + ) { + let template = match self.blob_image_templates.get(&key) { + Some(template) => template, + None => { + //println!("Missing image template (key={:?})!", key); + return; + } + }; + let tile_size = match template.tiling { + Some(size) => size, + None => { return; } + }; + let image = match self.rasterized_blob_images.get_mut(&key) { + Some(image) => image, + None => { + //println!("Missing rasterized blob (key={:?})!", key); + return; + } + }; + let tile_range = compute_tile_range( + &area, + &template.descriptor.size, + tile_size, + ); + image.data.retain(|tile, _| { + match *tile { + Some(offset) => tile_range.contains(&offset), + // This would be a bug. If we get here the blob should be tiled. + None => { + error!("Blob image template and image data tiling don't match."); + false + } + } + }); } pub fn request_glyphs( @@ -1021,6 +1313,8 @@ impl ResourceCache { texture_cache_profile, ); + self.rasterize_missing_blob_images(); + // Apply any updates of new / updated images (incl. blobs) to the texture cache. self.update_texture_cache(gpu_cache); render_tasks.prepare_for_render(); @@ -1032,6 +1326,26 @@ impl ResourceCache { self.texture_cache.end_frame(texture_cache_profile); } + fn rasterize_missing_blob_images(&mut self) { + if self.missing_blob_images.is_empty() { + return; + } + + self.blob_image_handler + .as_mut() + .unwrap() + .prepare_resources(&self.resources, &self.missing_blob_images); + + let rasterized_blobs = self.blob_image_rasterizer + .as_mut() + .unwrap() + .rasterize(&self.missing_blob_images); + + self.add_rasterized_blob_images(rasterized_blobs); + + self.missing_blob_images.clear(); + } + fn update_texture_cache(&mut self, gpu_cache: &mut GpuCache) { for request in self.pending_image_requests.drain() { let image_template = self.resources.image_templates.get_mut(request.key).unwrap(); @@ -1044,28 +1358,20 @@ impl ResourceCache { image_template.data.clone() } ImageData::Blob(..) => { - // Extract the rasterized image from the blob renderer. - match self.blob_image_renderer - .as_mut() - .unwrap() - .resolve(request.into()) - { - Ok(image) => ImageData::new(image.data), - // TODO(nical): I think that we should handle these somewhat gracefully, - // at least in the out-of-memory scenario. - Err(BlobImageError::Oom) => { - // This one should be recoverable-ish. - panic!("Failed to render a vector image (OOM)"); - } - Err(BlobImageError::InvalidKey) => { - panic!("Invalid vector image key"); - } - Err(BlobImageError::InvalidData) => { - // TODO(nical): If we run into this we should kill the content process. - panic!("Invalid vector image data"); + let blob_image = self.rasterized_blob_images.get(&request.key).unwrap(); + match blob_image.data.get(&request.tile) { + Some(result) => { + let result = result + .as_ref() + .expect("Failed to render a blob image"); + + // TODO: we may want to not panic and show a placeholder instead. + + ImageData::Raw(Arc::clone(&result.data)) } - Err(BlobImageError::Other(msg)) => { - panic!("Vector image error {}", msg); + None => { + debug_assert!(false, "invalid blob image request during frame building"); + continue; } } } @@ -1197,12 +1503,27 @@ impl ResourceCache { self.cached_glyphs .clear_fonts(|font| font.font_key.0 == namespace); - if let Some(ref mut r) = self.blob_image_renderer { + if let Some(ref mut r) = self.blob_image_handler { r.clear_namespace(namespace); } } } +pub fn get_blob_tiling( + tiling: Option, + descriptor: &ImageDescriptor, + max_texture_size: u32, +) -> Option { + if tiling.is_none() && + (descriptor.size.width > max_texture_size || + descriptor.size.height > max_texture_size) { + return Some(DEFAULT_TILE_SIZE); + } + + tiling +} + + // Compute the width and height of a tile depending on its position in the image. pub fn compute_tile_size( descriptor: &ImageDescriptor, @@ -1364,25 +1685,29 @@ impl ResourceCache { } ImageData::Blob(_) => { assert_eq!(template.tiling, None); - let request = BlobImageRequest { - key, - //TODO: support tiled blob images - // https://github.com/servo/webrender/issues/2236 - tile: None, - }; - let renderer = self.blob_image_renderer.as_mut().unwrap(); - renderer.request( - &self.resources, - request, - &BlobImageDescriptor { - size: desc.size, - offset: DevicePoint::zero(), - format: desc.format, - }, - None, - ); - let result = renderer.resolve(request) - .expect("Blob resolve failed"); + let blob_request_params = &[ + BlobImageParams { + request: BlobImageRequest { + key, + //TODO: support tiled blob images + // https://github.com/servo/webrender/issues/2236 + tile: None, + }, + descriptor: BlobImageDescriptor { + size: desc.size, + offset: DevicePoint::zero(), + format: desc.format, + }, + dirty_rect: None, + } + ]; + + let blob_handler = self.blob_image_handler.as_mut().unwrap(); + blob_handler.prepare_resources(&self.resources, blob_request_params); + let mut rasterizer = blob_handler.create_blob_rasterizer(); + let (_, result) = rasterizer.rasterize(blob_request_params).pop().unwrap(); + let result = result.expect("Blob rasterization failed"); + assert_eq!(result.size, desc.size); assert_eq!(result.data.len(), desc.compute_total_size() as usize); @@ -1567,6 +1892,7 @@ impl ResourceCache { data, descriptor: template.descriptor, tiling: template.tiling, + viewport_tiles: None, }); } diff --git a/webrender/src/scene_builder.rs b/webrender/src/scene_builder.rs index a40afb440a..97c635f4cc 100644 --- a/webrender/src/scene_builder.rs +++ b/webrender/src/scene_builder.rs @@ -2,6 +2,7 @@ * 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 api::{AsyncBlobImageRasterizer, BlobImageRequest, BlobImageParams, BlobImageResult}; use api::{DocumentId, PipelineId, ApiMsg, FrameMsg, ResourceUpdate}; use api::channel::MsgSender; use display_list_flattener::build_scene; @@ -20,6 +21,8 @@ pub enum SceneBuilderRequest { Transaction { document_id: DocumentId, scene: Option, + blob_requests: Vec, + blob_rasterizer: Option>, resource_updates: Vec, frame_ops: Vec, render: bool, @@ -35,6 +38,8 @@ pub enum SceneBuilderResult { document_id: DocumentId, built_scene: Option, resource_updates: Vec, + rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>, + blob_rasterizer: Option>, frame_ops: Vec, render: bool, result_tx: Option>, @@ -134,6 +139,8 @@ impl SceneBuilder { SceneBuilderRequest::Transaction { document_id, scene, + blob_requests, + mut blob_rasterizer, resource_updates, frame_ops, render, @@ -143,7 +150,10 @@ impl SceneBuilder { build_scene(&self.config, request) }); - // TODO: pre-rasterization. + let rasterized_blobs = blob_rasterizer.as_mut().map_or( + Vec::new(), + |rasterizer| rasterizer.rasterize(&blob_requests), + ); // We only need the pipeline info and the result channel if we // have a hook callback *and* if this transaction actually built @@ -172,6 +182,8 @@ impl SceneBuilder { document_id, built_scene, resource_updates, + rasterized_blobs, + blob_rasterizer, frame_ops, render, result_tx, diff --git a/webrender_api/src/api.rs b/webrender_api/src/api.rs index aa3e042bb3..1be9edd0ea 100644 --- a/webrender_api/src/api.rs +++ b/webrender_api/src/api.rs @@ -15,7 +15,7 @@ use {BuiltDisplayList, BuiltDisplayListDescriptor, ColorF, DeviceIntPoint, Devic use {DeviceUintSize, ExternalScrollId, FontInstanceKey, FontInstanceOptions}; use {FontInstancePlatformOptions, FontKey, FontVariation, GlyphDimensions, GlyphIndex, ImageData}; use {ImageDescriptor, ImageKey, ItemTag, LayoutPoint, LayoutSize, LayoutTransform, LayoutVector2D}; -use {NativeFontHandle, WorldPoint}; +use {NativeFontHandle, WorldPoint, NormalizedRect}; pub type TileSize = u16; /// Documents are rendered in the ascending order of their associated layer values. @@ -26,6 +26,7 @@ pub enum ResourceUpdate { AddImage(AddImage), UpdateImage(UpdateImage), DeleteImage(ImageKey), + SetImageVisibleArea(ImageKey, NormalizedRect), AddFont(AddFont), DeleteFont(FontKey), AddFontInstance(AddFontInstance), @@ -294,6 +295,10 @@ impl Transaction { self.resource_updates.push(ResourceUpdate::DeleteImage(key)); } + pub fn set_image_visible_area(&mut self, key: ImageKey, area: NormalizedRect) { + self.resource_updates.push(ResourceUpdate::SetImageVisibleArea(key, area)) + } + pub fn add_raw_font(&mut self, key: FontKey, bytes: Vec, index: u32) { self.resource_updates .push(ResourceUpdate::AddFont(AddFont::Raw(key, bytes, index))); diff --git a/webrender_api/src/image.rs b/webrender_api/src/image.rs index 6130b0781d..b4ee73c90c 100644 --- a/webrender_api/src/image.rs +++ b/webrender_api/src/image.rs @@ -174,35 +174,64 @@ impl ImageData { } } +/// The resources exposed by the resource cache available for use by the blob rasterizer. pub trait BlobImageResources { fn get_font_data(&self, key: FontKey) -> &FontTemplate; fn get_image(&self, key: ImageKey) -> Option<(&ImageData, &ImageDescriptor)>; } -pub trait BlobImageRenderer: Send { +/// A handler on the render backend that can create rasterizer objects which will +/// be sent to the scene builder thread to execute the rasterization. +/// +/// The handler is responsible for collecting resources, managing/updating blob commands +/// and creating the rasterizer objects, but isn't expected to do any rasterization itself. +pub trait BlobImageHandler: Send { + /// Creates a snapshot of the current state of blob images in the handler. + fn create_blob_rasterizer(&mut self) -> Box; + + /// A hook to let the blob image handler update any state related to resources that + /// are not bundled in the blob recording itself. + fn prepare_resources( + &mut self, + services: &BlobImageResources, + requests: &[BlobImageParams], + ); + + /// Register a blob image. fn add(&mut self, key: ImageKey, data: Arc, tiling: Option); + /// Update an already registered blob image. fn update(&mut self, key: ImageKey, data: Arc, dirty_rect: Option); + /// Delete an already registered blob image. fn delete(&mut self, key: ImageKey); - fn request( - &mut self, - resources: &BlobImageResources, - key: BlobImageRequest, - descriptor: &BlobImageDescriptor, - dirty_rect: Option, - ); - - fn resolve(&mut self, key: BlobImageRequest) -> BlobImageResult; - + /// A hook to let the handler clean up any state related to a font which the resource + /// cache is about to delete. fn delete_font(&mut self, key: FontKey); + /// A hook to let the handler clean up any state related to a font instance which the + /// resource cache is about to delete. fn delete_font_instance(&mut self, key: FontInstanceKey); + /// A hook to let the handler clean up any state related a given namespace before the + /// resource cache deletes them. fn clear_namespace(&mut self, namespace: IdNamespace); } +/// A group of rasterization requests to execute synchronously on the scene builder thread. +pub trait AsyncBlobImageRasterizer : Send { + fn rasterize(&mut self, requests: &[BlobImageParams]) -> Vec<(BlobImageRequest, BlobImageResult)>; +} + + +#[derive(Copy, Clone, Debug)] +pub struct BlobImageParams { + pub request: BlobImageRequest, + pub descriptor: BlobImageDescriptor, + pub dirty_rect: Option, +} + pub type BlobImageData = Vec; pub type BlobImageResult = Result; @@ -217,7 +246,7 @@ pub struct BlobImageDescriptor { pub struct RasterizedBlobImage { pub size: DeviceUintSize, - pub data: Vec, + pub data: Arc>, } #[derive(Clone, Debug)] @@ -228,7 +257,7 @@ pub enum BlobImageError { Other(String), } -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct BlobImageRequest { pub key: ImageKey, pub tile: Option, diff --git a/webrender_api/src/units.rs b/webrender_api/src/units.rs index 7ea431f5d2..a221ab6a83 100644 --- a/webrender_api/src/units.rs +++ b/webrender_api/src/units.rs @@ -86,6 +86,7 @@ pub type WorldVector3D = TypedVector3D; #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct Tiles; pub type TileOffset = TypedPoint2D; +pub type TileRange = TypedRect; /// Scaling ratio from world pixels to device pixels. pub type DevicePixelScale = TypedScale; @@ -115,6 +116,12 @@ pub fn as_scroll_parent_vector(vector: &LayoutVector2D) -> ScrollLayerVector2D { ScrollLayerVector2D::from_untyped(&vector.to_untyped()) } +/// Coordinates in normalized space (between zero and one). +#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct NormalizedCoordinates; + +pub type NormalizedRect = TypedRect; + /// Stores two coordinates in texel space. The coordinates /// are stored in texel coordinates because the texture atlas /// may grow. Storing them as texel coords and normalizing diff --git a/wrench/src/blob.rs b/wrench/src/blob.rs index c9d940a6cf..91d1e03dfe 100644 --- a/wrench/src/blob.rs +++ b/wrench/src/blob.rs @@ -2,7 +2,7 @@ * 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/. */ -// A very basic BlobImageRenderer that can only render a checkerboard pattern. +// A very basic BlobImageRasterizer that can only render a checkerboard pattern. use std::collections::HashMap; use std::sync::Arc; @@ -65,7 +65,6 @@ fn render_blob( .expect("empty rects should be culled by webrender"); } - for y in dirty_rect.min_y() .. dirty_rect.max_y() { for x in dirty_rect.min_x() .. dirty_rect.max_x() { // Apply the tile's offset. This is important: all drawing commands should be @@ -103,28 +102,26 @@ fn render_blob( } Ok(RasterizedBlobImage { - data: texels, + data: Arc::new(texels), size: descriptor.size, }) } +/// See rawtest.rs. We use this to test that blob images are requested the right +/// amount of times. pub struct BlobCallbacks { - pub request: Box, - pub resolve: Box, + pub request: Box, } impl BlobCallbacks { pub fn new() -> Self { - BlobCallbacks { request: Box::new(|_|()), resolve: Box::new(|| (())) } + BlobCallbacks { request: Box::new(|_|()) } } } pub struct CheckerboardRenderer { image_cmds: HashMap)>, callbacks: Arc>, - - // The images rendered in the current frame (not kept here between frames). - rendered_images: HashMap, } impl CheckerboardRenderer { @@ -132,12 +129,11 @@ impl CheckerboardRenderer { CheckerboardRenderer { callbacks, image_cmds: HashMap::new(), - rendered_images: HashMap::new(), } } } -impl BlobImageRenderer for CheckerboardRenderer { +impl BlobImageHandler for CheckerboardRenderer { fn add(&mut self, key: ImageKey, cmds: Arc, tile_size: Option) { self.image_cmds .insert(key, (deserialize_blob(&cmds[..]).unwrap(), tile_size)); @@ -153,37 +149,59 @@ impl BlobImageRenderer for CheckerboardRenderer { self.image_cmds.remove(&key); } - fn request( - &mut self, - _resources: &BlobImageResources, - request: BlobImageRequest, - descriptor: &BlobImageDescriptor, - dirty_rect: Option, - ) { - (self.callbacks.lock().unwrap().request)(&request); - assert!(!self.rendered_images.contains_key(&request)); - // This method is where we kick off our rendering jobs. - // It should avoid doing work on the calling thread as much as possible. - // In this example we will use the thread pool to render individual tiles. - - // Gather the input data to send to a worker thread. - let &(color, tile_size) = self.image_cmds.get(&request.key).unwrap(); + fn delete_font(&mut self, _key: FontKey) {} - let tile = request.tile.map(|tile| (tile_size.unwrap(), tile)); + fn delete_font_instance(&mut self, _key: FontInstanceKey) {} - let result = render_blob(color, descriptor, tile, dirty_rect); + fn clear_namespace(&mut self, _namespace: IdNamespace) {} - self.rendered_images.insert(request, result); + fn prepare_resources( + &mut self, + _services: &BlobImageResources, + requests: &[BlobImageParams], + ) { + if !requests.is_empty() { + (self.callbacks.lock().unwrap().request)(&requests); + } } - fn resolve(&mut self, request: BlobImageRequest) -> BlobImageResult { - (self.callbacks.lock().unwrap().resolve)(); - self.rendered_images.remove(&request).unwrap() + fn create_blob_rasterizer(&mut self) -> Box { + Box::new(Rasterizer { image_cmds: self.image_cmds.clone() }) } +} - fn delete_font(&mut self, _key: FontKey) {} +struct Command { + request: BlobImageRequest, + color: ColorU, + descriptor: BlobImageDescriptor, + tile: Option<(TileSize, TileOffset)>, + dirty_rect: Option +} - fn delete_font_instance(&mut self, _key: FontInstanceKey) {} +struct Rasterizer { + image_cmds: HashMap)>, +} - fn clear_namespace(&mut self, _namespace: IdNamespace) {} +impl AsyncBlobImageRasterizer for Rasterizer { + fn rasterize(&mut self, requests: &[BlobImageParams]) -> Vec<(BlobImageRequest, BlobImageResult)> { + let requests: Vec = requests.into_iter().map( + |item| { + let (color, tile_size) = self.image_cmds[&item.request.key]; + + let tile = item.request.tile.map(|tile| (tile_size.unwrap(), tile)); + + Command { + request: item.request, + color, + tile, + descriptor: item.descriptor, + dirty_rect: item.dirty_rect, + } + } + ).collect(); + + requests.iter().map(|cmd| { + (cmd.request, render_blob(cmd.color, &cmd.descriptor, cmd.tile, cmd.dirty_rect)) + }).collect() + } } diff --git a/wrench/src/json_frame_writer.rs b/wrench/src/json_frame_writer.rs index cb16648ad7..f93605e128 100644 --- a/wrench/src/json_frame_writer.rs +++ b/wrench/src/json_frame_writer.rs @@ -177,6 +177,7 @@ impl JsonFrameWriter { ); } ResourceUpdate::DeleteFontInstance(_) => {} + ResourceUpdate::SetImageVisibleArea(..) => {} } } } diff --git a/wrench/src/rawtest.rs b/wrench/src/rawtest.rs index d5b70d5b4b..57d3ea95a9 100644 --- a/wrench/src/rawtest.rs +++ b/wrench/src/rawtest.rs @@ -4,7 +4,7 @@ use {WindowWrapper, NotifierEvent}; use blob; -use euclid::{TypedRect, TypedSize2D, TypedPoint2D}; +use euclid::{TypedRect, TypedSize2D, TypedPoint2D, point2, size2}; use std::sync::Arc; use std::sync::atomic::{AtomicIsize, Ordering}; use std::sync::mpsc::Receiver; @@ -47,6 +47,7 @@ impl<'a> RawtestHarness<'a> { self.test_blob_update_epoch_test(); self.test_tile_decomposition(); self.test_very_large_blob(); + self.test_insufficient_blob_visible_area(); self.test_offscreen_blob(); self.test_save_restore(); self.test_blur_cache(); @@ -180,6 +181,14 @@ impl<'a> RawtestHarness<'a> { AlphaType::PremultipliedAlpha, blob_img, ); + txn.set_image_visible_area( + blob_img, + NormalizedRect { + origin: point2(0.0, 0.03), + size: size2(1.0, 0.03), + } + ); + builder.pop_clip_id(); let mut epoch = Epoch(0); @@ -189,7 +198,7 @@ impl<'a> RawtestHarness<'a> { let pixels = self.render_and_get_pixels(window_rect); // make sure we didn't request too many blobs - assert_eq!(called.load(Ordering::SeqCst), 16); + assert!(called.load(Ordering::SeqCst) < 20); // make sure things are in the right spot assert!( @@ -230,6 +239,99 @@ impl<'a> RawtestHarness<'a> { *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); } + fn test_insufficient_blob_visible_area(&mut self) { + println!("\tinsufficient blob visible area."); + + // This test compares two almost identical display lists containing the a blob + // image. The only difference is that one of the display lists specifies a visible + // area for its blob image which is too small, causing frame building to run into + // missing tiles, and forcing it to exercise the code path where missing tiles are + // rendered synchronously on demand. + + assert_eq!(self.wrench.device_pixel_ratio, 1.); + + let window_size = self.window.get_inner_size(); + let test_size = DeviceUintSize::new(800, 800); + let window_rect = DeviceUintRect::new( + DeviceUintPoint::new(0, window_size.height - test_size.height), + test_size, + ); + let layout_size = LayoutSize::new(800.0, 800.0); + let image_size = size(800.0, 800.0); + let info = LayoutPrimitiveInfo::new(rect(0.0, 0.0, 800.0, 800.0)); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size); + let mut txn = Transaction::new(); + + let blob_img1 = self.wrench.api.generate_image_key(); + txn.add_image( + blob_img1, + ImageDescriptor::new( + image_size.width as u32, + image_size.height as u32, + ImageFormat::BGRA8, + false, + false + ), + ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))), + Some(100), + ); + + builder.push_image( + &info, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img1, + ); + + self.submit_dl(&mut Epoch(0), layout_size, builder, &txn.resource_updates); + let pixels1 = self.render_and_get_pixels(window_rect); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size); + let mut txn = Transaction::new(); + + let blob_img2 = self.wrench.api.generate_image_key(); + txn.add_image( + blob_img2, + ImageDescriptor::new( + image_size.width as u32, + image_size.height as u32, + ImageFormat::BGRA8, + false, + false + ), + ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))), + Some(100), + ); + // Set a visible rectangle that is too small. + // This will force sync rasterization of the missing tiles during frame building. + txn.set_image_visible_area(blob_img2, NormalizedRect { + origin: point2(0.25, 0.25), + size: size2(0.1, 0.1), + }); + + builder.push_image( + &info, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img2, + ); + + self.submit_dl(&mut Epoch(1), layout_size, builder, &txn.resource_updates); + let pixels2 = self.render_and_get_pixels(window_rect); + + assert!(pixels1 == pixels2); + + txn = Transaction::new(); + txn.delete_image(blob_img1); + txn.delete_image(blob_img2); + self.wrench.api.update_resources(txn.resource_updates); + } + fn test_offscreen_blob(&mut self) { println!("\toffscreen blob update."); @@ -466,12 +568,14 @@ impl<'a> RawtestHarness<'a> { 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 |&desc| { - if desc.key == blob_img { - img1_requested_inner.fetch_add(1, Ordering::SeqCst); - } - if desc.key == blob_img2 { - img2_requested_inner.fetch_add(1, Ordering::SeqCst); + 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); + } } }); diff --git a/wrench/src/ron_frame_writer.rs b/wrench/src/ron_frame_writer.rs index 82c38b3397..b02ceefbae 100644 --- a/wrench/src/ron_frame_writer.rs +++ b/wrench/src/ron_frame_writer.rs @@ -141,6 +141,7 @@ impl RonFrameWriter { ResourceUpdate::DeleteFont(_) => {} ResourceUpdate::AddFontInstance(_) => {} ResourceUpdate::DeleteFontInstance(_) => {} + ResourceUpdate::SetImageVisibleArea(..) => {} } } } diff --git a/wrench/src/wrench.rs b/wrench/src/wrench.rs index 7ecf54cc8a..e621b83562 100644 --- a/wrench/src/wrench.rs +++ b/wrench/src/wrench.rs @@ -214,7 +214,7 @@ impl Wrench { enable_clear_scissor: !no_scissor, max_recorded_profiles: 16, precache_shaders, - blob_image_renderer: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))), + blob_image_handler: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))), disable_dual_source_blending, chase_primitive, ..Default::default() diff --git a/wrench/src/yaml_frame_writer.rs b/wrench/src/yaml_frame_writer.rs index fa383b3dbf..85238a31f0 100644 --- a/wrench/src/yaml_frame_writer.rs +++ b/wrench/src/yaml_frame_writer.rs @@ -590,6 +590,7 @@ impl YamlFrameWriter { ); } ResourceUpdate::DeleteFontInstance(_) => {} + ResourceUpdate::SetImageVisibleArea(..) => {} } } }