summaryrefslogtreecommitdiffstats
path: root/gfx/wr/webrender/src
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/webrender/src')
-rw-r--r--gfx/wr/webrender/src/api_resources.rs299
-rw-r--r--gfx/wr/webrender/src/batch.rs3862
-rw-r--r--gfx/wr/webrender/src/border.rs1492
-rw-r--r--gfx/wr/webrender/src/box_shadow.rs275
-rw-r--r--gfx/wr/webrender/src/capture.rs290
-rw-r--r--gfx/wr/webrender/src/clip.rs2075
-rw-r--r--gfx/wr/webrender/src/composite.rs1250
-rw-r--r--gfx/wr/webrender/src/debug_colors.rs159
-rw-r--r--gfx/wr/webrender/src/debug_font_data.rs1914
-rw-r--r--gfx/wr/webrender/src/debug_item.rs20
-rw-r--r--gfx/wr/webrender/src/debug_server.rs402
-rw-r--r--gfx/wr/webrender/src/device/gl.rs4589
-rw-r--r--gfx/wr/webrender/src/device/mod.rs9
-rw-r--r--gfx/wr/webrender/src/device/query_gl.rs318
-rw-r--r--gfx/wr/webrender/src/ellipse.rs187
-rw-r--r--gfx/wr/webrender/src/filterdata.rs219
-rw-r--r--gfx/wr/webrender/src/frame_builder.rs1106
-rw-r--r--gfx/wr/webrender/src/frame_graph.rs950
-rw-r--r--gfx/wr/webrender/src/freelist.rs264
-rw-r--r--gfx/wr/webrender/src/gamma_lut.rs414
-rw-r--r--gfx/wr/webrender/src/glyph_cache.rs173
-rw-r--r--gfx/wr/webrender/src/glyph_rasterizer/mod.rs1338
-rw-r--r--gfx/wr/webrender/src/gpu_cache.rs935
-rw-r--r--gfx/wr/webrender/src/gpu_types.rs825
-rw-r--r--gfx/wr/webrender/src/hit_test.rs499
-rw-r--r--gfx/wr/webrender/src/image_tiling.rs816
-rw-r--r--gfx/wr/webrender/src/intern.rs465
-rw-r--r--gfx/wr/webrender/src/internal_types.rs586
-rw-r--r--gfx/wr/webrender/src/lib.rs236
-rw-r--r--gfx/wr/webrender/src/lru_cache.rs665
-rw-r--r--gfx/wr/webrender/src/picture.rs7192
-rw-r--r--gfx/wr/webrender/src/platform/macos/font.rs1040
-rw-r--r--gfx/wr/webrender/src/platform/unix/font.rs1053
-rw-r--r--gfx/wr/webrender/src/platform/windows/font.rs603
-rw-r--r--gfx/wr/webrender/src/prepare.rs1889
-rw-r--r--gfx/wr/webrender/src/prim_store/backdrop.rs98
-rw-r--r--gfx/wr/webrender/src/prim_store/borders.rs376
-rw-r--r--gfx/wr/webrender/src/prim_store/gradient.rs1031
-rw-r--r--gfx/wr/webrender/src/prim_store/image.rs521
-rw-r--r--gfx/wr/webrender/src/prim_store/interned.rs14
-rw-r--r--gfx/wr/webrender/src/prim_store/line_dec.rs257
-rw-r--r--gfx/wr/webrender/src/prim_store/mod.rs1364
-rw-r--r--gfx/wr/webrender/src/prim_store/picture.rs322
-rw-r--r--gfx/wr/webrender/src/prim_store/storage.rs134
-rw-r--r--gfx/wr/webrender/src/prim_store/text_run.rs492
-rw-r--r--gfx/wr/webrender/src/print_tree.rs105
-rw-r--r--gfx/wr/webrender/src/profiler.rs1656
-rw-r--r--gfx/wr/webrender/src/render_api.rs1545
-rw-r--r--gfx/wr/webrender/src/render_backend.rs2008
-rw-r--r--gfx/wr/webrender/src/render_target.rs993
-rw-r--r--gfx/wr/webrender/src/render_task.rs1549
-rw-r--r--gfx/wr/webrender/src/render_task_cache.rs336
-rw-r--r--gfx/wr/webrender/src/render_task_graph.rs270
-rw-r--r--gfx/wr/webrender/src/renderer/debug.rs416
-rw-r--r--gfx/wr/webrender/src/renderer/gpu_cache.rs507
-rw-r--r--gfx/wr/webrender/src/renderer/mod.rs6165
-rw-r--r--gfx/wr/webrender/src/renderer/shade.rs1215
-rw-r--r--gfx/wr/webrender/src/renderer/vertex.rs976
-rw-r--r--gfx/wr/webrender/src/resource_cache.rs2097
-rw-r--r--gfx/wr/webrender/src/scene.rs347
-rw-r--r--gfx/wr/webrender/src/scene_builder_thread.rs840
-rw-r--r--gfx/wr/webrender/src/scene_building.rs3792
-rw-r--r--gfx/wr/webrender/src/screen_capture.rs496
-rw-r--r--gfx/wr/webrender/src/segment.rs1352
-rw-r--r--gfx/wr/webrender/src/space.rs254
-rw-r--r--gfx/wr/webrender/src/spatial_node.rs963
-rw-r--r--gfx/wr/webrender/src/spatial_tree.rs1075
-rw-r--r--gfx/wr/webrender/src/texture_cache.rs1886
-rw-r--r--gfx/wr/webrender/src/texture_pack/guillotine.rs279
-rw-r--r--gfx/wr/webrender/src/texture_pack/mod.rs329
-rw-r--r--gfx/wr/webrender/src/texture_pack/slab.rs356
-rw-r--r--gfx/wr/webrender/src/tile_cache.rs547
-rw-r--r--gfx/wr/webrender/src/util.rs1538
-rw-r--r--gfx/wr/webrender/src/visibility.rs922
74 files changed, 77832 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/api_resources.rs b/gfx/wr/webrender/src/api_resources.rs
new file mode 100644
index 0000000000..0a48858fc4
--- /dev/null
+++ b/gfx/wr/webrender/src/api_resources.rs
@@ -0,0 +1,299 @@
+/* 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 crate::api::{BlobImageKey, ImageDescriptor, DirtyRect, TileSize};
+use crate::api::{BlobImageHandler, AsyncBlobImageRasterizer, BlobImageData, BlobImageParams};
+use crate::api::{BlobImageRequest, BlobImageDescriptor, BlobImageResources};
+use crate::api::{FontKey, FontTemplate, FontInstanceData, FontInstanceKey};
+use crate::api::SharedFontInstanceMap;
+use crate::api::units::*;
+use crate::render_api::{ResourceUpdate, TransactionMsg, AddFont};
+use crate::image_tiling::*;
+use crate::profiler;
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+/// We use this to generate the async blob rendering requests.
+struct BlobImageTemplate {
+ descriptor: ImageDescriptor,
+ tile_size: TileSize,
+ dirty_rect: BlobDirtyRect,
+ /// 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<TileRange>,
+}
+
+struct FontResources {
+ templates: HashMap<FontKey, FontTemplate>,
+ instances: SharedFontInstanceMap,
+}
+
+pub struct ApiResources {
+ blob_image_templates: HashMap<BlobImageKey, BlobImageTemplate>,
+ pub blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ fonts: FontResources,
+}
+
+impl BlobImageResources for FontResources {
+ fn get_font_data(&self, key: FontKey) -> &FontTemplate {
+ self.templates.get(&key).unwrap()
+ }
+ fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> {
+ self.instances.get_font_instance_data(key)
+ }
+}
+
+impl ApiResources {
+ pub fn new(
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ instances: SharedFontInstanceMap,
+ ) -> Self {
+ ApiResources {
+ blob_image_templates: HashMap::new(),
+ blob_image_handler,
+ fonts: FontResources {
+ templates: HashMap::new(),
+ instances,
+ }
+ }
+ }
+
+ pub fn get_shared_font_instances(&self) -> SharedFontInstanceMap {
+ self.fonts.instances.clone()
+ }
+
+ pub fn update(&mut self, transaction: &mut TransactionMsg) {
+ let mut blobs_to_rasterize = Vec::new();
+ for update in &transaction.resource_updates {
+ match *update {
+ ResourceUpdate::AddBlobImage(ref img) => {
+ self.blob_image_handler
+ .as_mut()
+ .unwrap()
+ .add(img.key, Arc::clone(&img.data), &img.visible_rect, img.tile_size);
+
+ self.blob_image_templates.insert(
+ img.key,
+ BlobImageTemplate {
+ descriptor: img.descriptor,
+ tile_size: img.tile_size,
+ dirty_rect: DirtyRect::All,
+ valid_tiles_after_bounds_change: None,
+ visible_rect: img.visible_rect,
+ },
+ );
+ blobs_to_rasterize.push(img.key);
+ }
+ ResourceUpdate::UpdateBlobImage(ref img) => {
+ debug_assert_eq!(img.visible_rect.size, img.descriptor.size);
+ self.update_blob_image(
+ img.key,
+ Some(&img.descriptor),
+ Some(&img.dirty_rect),
+ Some(Arc::clone(&img.data)),
+ &img.visible_rect,
+ );
+ blobs_to_rasterize.push(img.key);
+ }
+ ResourceUpdate::DeleteBlobImage(key) => {
+ transaction.use_scene_builder_thread = true;
+ self.blob_image_templates.remove(&key);
+ if let Some(ref mut handler) = self.blob_image_handler {
+ handler.delete(key);
+ }
+ }
+ ResourceUpdate::SetBlobImageVisibleArea(ref key, ref area) => {
+ self.update_blob_image(*key, None, None, None, area);
+ blobs_to_rasterize.push(*key);
+ }
+ ResourceUpdate::AddFont(ref font) => {
+ match font {
+ AddFont::Raw(key, bytes, index) => {
+ self.fonts.templates.insert(
+ *key,
+ FontTemplate::Raw(Arc::clone(bytes), *index),
+ );
+ }
+ AddFont::Native(key, native_font_handle) => {
+ self.fonts.templates.insert(
+ *key,
+ FontTemplate::Native(native_font_handle.clone()),
+ );
+ }
+ }
+ }
+ ResourceUpdate::AddFontInstance(ref instance) => {
+ // TODO(nical): Don't clone these.
+ self.fonts.instances.add_font_instance(
+ instance.key,
+ instance.font_key,
+ instance.glyph_size,
+ instance.options.clone(),
+ instance.platform_options.clone(),
+ instance.variations.clone(),
+ );
+ }
+ ResourceUpdate::DeleteFont(key) => {
+ transaction.use_scene_builder_thread = true;
+ self.fonts.templates.remove(&key);
+ if let Some(ref mut handler) = self.blob_image_handler {
+ handler.delete_font(key);
+ }
+ }
+ ResourceUpdate::DeleteFontInstance(key) => {
+ transaction.use_scene_builder_thread = true;
+ // We will delete from the shared font instance map in the resource cache
+ // after scene swap.
+
+ if let Some(ref mut r) = self.blob_image_handler {
+ r.delete_font_instance(key);
+ }
+ }
+ ResourceUpdate::DeleteImage(..) => {
+ transaction.use_scene_builder_thread = true;
+ }
+ _ => {}
+ }
+ }
+
+ let (rasterizer, requests) = self.create_blob_scene_builder_requests(&blobs_to_rasterize);
+ transaction.profile.set(profiler::RASTERIZED_BLOBS, blobs_to_rasterize.len());
+ transaction.profile.set(profiler::RASTERIZED_BLOB_TILES, requests.len());
+ transaction.use_scene_builder_thread |= !requests.is_empty();
+ transaction.use_scene_builder_thread |= !transaction.scene_ops.is_empty();
+ transaction.blob_rasterizer = rasterizer;
+ transaction.blob_requests = requests;
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ if let Some(ref mut handler) = self.blob_image_handler {
+ handler.enable_multithreading(enable);
+ }
+ }
+
+ fn update_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: Option<&ImageDescriptor>,
+ dirty_rect: Option<&BlobDirtyRect>,
+ data: Option<Arc<BlobImageData>>,
+ visible_rect: &DeviceIntRect,
+ ) {
+ if let Some(data) = data {
+ let dirty_rect = dirty_rect.unwrap();
+ self.blob_image_handler.as_mut().unwrap().update(key, data, visible_rect, dirty_rect);
+ }
+
+ let image = self.blob_image_templates
+ .get_mut(&key)
+ .expect("Attempt to update non-existent blob image");
+
+ let mut valid_tiles_after_bounds_change = compute_valid_tiles_if_bounds_change(
+ &image.visible_rect,
+ visible_rect,
+ image.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_else(TileRange::zero);
+ }
+ (Some(old), None) => {
+ valid_tiles_after_bounds_change = Some(old);
+ }
+ _ => {}
+ }
+
+ let blob_size = visible_rect.size;
+
+ if let Some(descriptor) = descriptor {
+ image.descriptor = *descriptor;
+ } else {
+ // make sure the descriptor size matches the visible rect.
+ // This might not be necessary but let's stay on the safe side.
+ image.descriptor.size = blob_size;
+ }
+
+ if let Some(dirty_rect) = dirty_rect {
+ image.dirty_rect = image.dirty_rect.union(dirty_rect);
+ }
+
+ image.valid_tiles_after_bounds_change = valid_tiles_after_bounds_change;
+ image.visible_rect = *visible_rect;
+ }
+
+ pub fn create_blob_scene_builder_requests(
+ &mut self,
+ keys: &[BlobImageKey]
+ ) -> (Option<Box<dyn AsyncBlobImageRasterizer>>, Vec<BlobImageParams>) {
+ if self.blob_image_handler.is_none() || keys.is_empty() {
+ return (None, Vec::new());
+ }
+
+ let mut blob_request_params = Vec::new();
+ for key in keys {
+ let template = self.blob_image_templates.get_mut(key).unwrap();
+
+ // 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 tiles = compute_tile_range(
+ &template.visible_rect,
+ template.tile_size,
+ );
+
+ // Don't request tiles that weren't invalidated.
+ let dirty_tiles = match template.dirty_rect {
+ DirtyRect::Partial(dirty_rect) => {
+ compute_tile_range(
+ &dirty_rect.cast_unit(),
+ template.tile_size,
+ )
+ }
+ DirtyRect::All => tiles,
+ };
+
+ 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: compute_tile_rect(
+ &template.visible_rect,
+ template.tile_size,
+ tile,
+ ).cast_unit(),
+ format: template.descriptor.format,
+ };
+
+ assert!(descriptor.rect.size.width > 0 && descriptor.rect.size.height > 0);
+ blob_request_params.push(
+ BlobImageParams {
+ request: BlobImageRequest { key: *key, tile },
+ descriptor,
+ dirty_rect: DirtyRect::All,
+ }
+ );
+ });
+
+ template.dirty_rect = DirtyRect::empty();
+ template.valid_tiles_after_bounds_change = None;
+ }
+
+ let handler = self.blob_image_handler.as_mut().unwrap();
+ handler.prepare_resources(&self.fonts, &blob_request_params);
+ (Some(handler.create_blob_rasterizer()), blob_request_params)
+ }
+}
diff --git a/gfx/wr/webrender/src/batch.rs b/gfx/wr/webrender/src/batch.rs
new file mode 100644
index 0000000000..95824d7edf
--- /dev/null
+++ b/gfx/wr/webrender/src/batch.rs
@@ -0,0 +1,3862 @@
+/* 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 api::{AlphaType, ClipMode, ExternalImageType, ImageRendering, ImageBufferKind};
+use api::{FontInstanceFlags, YuvColorSpace, YuvFormat, ColorDepth, ColorRange, PremultipliedColorF};
+use api::units::*;
+use crate::clip::{ClipDataStore, ClipNodeFlags, ClipNodeRange, ClipItemKind, ClipStore};
+use crate::spatial_tree::{SpatialTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex, CoordinateSystemId};
+use crate::composite::{CompositeState};
+use crate::glyph_rasterizer::{GlyphFormat, SubpixelDirection};
+use crate::gpu_cache::{GpuBlockData, GpuCache, GpuCacheHandle, GpuCacheAddress};
+use crate::gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders, ZBufferId, ZBufferIdGenerator};
+use crate::gpu_types::{SplitCompositeInstance};
+use crate::gpu_types::{PrimitiveInstanceData, RasterizationSpace, GlyphInstance};
+use crate::gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
+use crate::gpu_types::{ImageBrushData, get_shader_opacity, BoxShadowData};
+use crate::gpu_types::{ClipMaskInstanceCommon, ClipMaskInstanceImage, ClipMaskInstanceRect, ClipMaskInstanceBoxShadow};
+use crate::internal_types::{FastHashMap, Swizzle, TextureSource, Filter, DeferredResolveIndex};
+use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive};
+use crate::picture::{ClusterFlags, SurfaceIndex, SurfaceRenderTasks};
+use crate::prim_store::{DeferredResolve, PrimitiveInstanceKind, ClipData};
+use crate::prim_store::{VisibleGradientTile, PrimitiveInstance, PrimitiveOpacity, SegmentInstanceIndex};
+use crate::prim_store::{BrushSegment, ClipMaskKind, ClipTaskIndex};
+use crate::prim_store::VECS_PER_SEGMENT;
+use crate::prim_store::image::ImageSource;
+use crate::render_target::RenderTargetContext;
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraph};
+use crate::render_task::RenderTaskAddress;
+use crate::renderer::{BlendMode, ShaderColorMode};
+use crate::renderer::{BLOCKS_PER_UV_RECT, MAX_VERTEX_TEXTURE_WIDTH};
+use crate::resource_cache::{CacheItem, GlyphFetchResult, ImageProperties, ImageRequest, ResourceCache};
+use crate::space::SpaceMapper;
+use crate::visibility::{PrimitiveVisibilityMask, PrimitiveVisibility, PrimitiveVisibilityFlags, VisibilityState};
+use smallvec::SmallVec;
+use std::{f32, i32, usize};
+use crate::util::{project_rect, MaxRect, TransformedRectKind};
+use crate::segment::EdgeAaSegmentMask;
+
+// Special sentinel value recognized by the shader. It is considered to be
+// a dummy task that doesn't mask out anything.
+const OPAQUE_TASK_ADDRESS: RenderTaskAddress = RenderTaskAddress(0x7fff);
+
+/// Used to signal there are no segments provided with this primitive.
+const INVALID_SEGMENT_INDEX: i32 = 0xffff;
+
+/// Size in device pixels for tiles that clip masks are drawn in.
+const CLIP_RECTANGLE_TILE_SIZE: i32 = 128;
+
+/// The minimum size of a clip mask before trying to draw in tiles.
+const CLIP_RECTANGLE_AREA_THRESHOLD: f32 = (CLIP_RECTANGLE_TILE_SIZE * CLIP_RECTANGLE_TILE_SIZE * 4) as f32;
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BrushBatchKind {
+ Solid,
+ Image(ImageBufferKind),
+ Blend,
+ MixBlend {
+ task_id: RenderTaskId,
+ source_id: RenderTaskId,
+ backdrop_id: RenderTaskId,
+ },
+ YuvImage(ImageBufferKind, YuvFormat, ColorDepth, YuvColorSpace, ColorRange),
+ ConicGradient,
+ RadialGradient,
+ LinearGradient,
+ Opacity,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BatchKind {
+ SplitComposite,
+ TextRun(GlyphFormat),
+ Brush(BrushBatchKind),
+}
+
+/// Input textures for a primitive, without consideration of clip mask
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureSet {
+ pub colors: [TextureSource; 3],
+}
+
+impl TextureSet {
+ const UNTEXTURED: TextureSet = TextureSet {
+ colors: [
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ };
+
+ /// A textured primitive
+ fn prim_textured(
+ color: TextureSource,
+ ) -> Self {
+ TextureSet {
+ colors: [
+ color,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ }
+ }
+
+ fn is_compatible_with(&self, other: &TextureSet) -> bool {
+ self.colors[0].is_compatible(&other.colors[0]) &&
+ self.colors[1].is_compatible(&other.colors[1]) &&
+ self.colors[2].is_compatible(&other.colors[2])
+ }
+}
+
+impl TextureSource {
+ fn combine(&self, other: TextureSource) -> TextureSource {
+ if other == TextureSource::Invalid {
+ *self
+ } else {
+ other
+ }
+ }
+}
+
+/// Optional textures that can be used as a source in the shaders.
+/// Textures that are not used by the batch are equal to TextureId::invalid().
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BatchTextures {
+ pub input: TextureSet,
+ pub clip_mask: TextureSource,
+}
+
+impl BatchTextures {
+ /// An empty batch textures (no binding slots set)
+ pub fn empty() -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::UNTEXTURED,
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ /// A textured primitive with optional clip mask
+ pub fn prim_textured(
+ color: TextureSource,
+ clip_mask: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::prim_textured(color),
+ clip_mask,
+ }
+ }
+
+ /// An untextured primitive with optional clip mask
+ pub fn prim_untextured(
+ clip_mask: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet::UNTEXTURED,
+ clip_mask,
+ }
+ }
+
+ /// A composite style effect with single input texture
+ pub fn composite_rgb(
+ texture: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet {
+ colors: [
+ texture,
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ },
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ /// A composite style effect with up to 3 input textures
+ pub fn composite_yuv(
+ color0: TextureSource,
+ color1: TextureSource,
+ color2: TextureSource,
+ ) -> BatchTextures {
+ BatchTextures {
+ input: TextureSet {
+ colors: [color0, color1, color2],
+ },
+ clip_mask: TextureSource::Invalid,
+ }
+ }
+
+ pub fn is_compatible_with(&self, other: &BatchTextures) -> bool {
+ if !self.clip_mask.is_compatible(&other.clip_mask) {
+ return false;
+ }
+
+ self.input.is_compatible_with(&other.input)
+ }
+
+ pub fn combine_textures(&self, other: BatchTextures) -> Option<BatchTextures> {
+ if !self.is_compatible_with(&other) {
+ return None;
+ }
+
+ let mut new_textures = BatchTextures::empty();
+
+ new_textures.clip_mask = self.clip_mask.combine(other.clip_mask);
+
+ for i in 0 .. 3 {
+ new_textures.input.colors[i] = self.input.colors[i].combine(other.input.colors[i]);
+ }
+
+ Some(new_textures)
+ }
+
+ fn merge(&mut self, other: &BatchTextures) {
+ self.clip_mask = self.clip_mask.combine(other.clip_mask);
+
+ for (s, o) in self.input.colors.iter_mut().zip(other.input.colors.iter()) {
+ *s = s.combine(*o);
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BatchKey {
+ pub kind: BatchKind,
+ pub blend_mode: BlendMode,
+ pub textures: BatchTextures,
+}
+
+impl BatchKey {
+ pub fn new(kind: BatchKind, blend_mode: BlendMode, textures: BatchTextures) -> Self {
+ BatchKey {
+ kind,
+ blend_mode,
+ textures,
+ }
+ }
+
+ pub fn is_compatible_with(&self, other: &BatchKey) -> bool {
+ self.kind == other.kind && self.blend_mode == other.blend_mode && self.textures.is_compatible_with(&other.textures)
+ }
+}
+
+pub struct BatchRects {
+ /// Union of all of the batch's item rects.
+ ///
+ /// Very often we can skip iterating over item rects by testing against
+ /// this one first.
+ batch: PictureRect,
+ /// When the batch rectangle above isn't a good enough approximation, we
+ /// store per item rects.
+ items: Option<Vec<PictureRect>>,
+}
+
+impl BatchRects {
+ fn new() -> Self {
+ BatchRects {
+ batch: PictureRect::zero(),
+ items: None,
+ }
+ }
+
+ #[inline]
+ fn add_rect(&mut self, rect: &PictureRect) {
+ let union = self.batch.union(rect);
+ // If we have already started storing per-item rects, continue doing so.
+ // Otherwise, check whether only storing the batch rect is a good enough
+ // apporximation.
+ if let Some(items) = &mut self.items {
+ items.push(*rect);
+ } else if self.batch.area() + rect.area() > union.area() {
+ let mut items = Vec::with_capacity(16);
+ items.push(self.batch);
+ items.push(*rect);
+ self.items = Some(items);
+ }
+
+ self.batch = union;
+ }
+
+ #[inline]
+ fn intersects(&mut self, rect: &PictureRect) -> bool {
+ if !self.batch.intersects(rect) {
+ return false;
+ }
+
+ if let Some(items) = &self.items {
+ items.iter().any(|item| item.intersects(rect))
+ } else {
+ // If we don't have per-item rects it means the batch rect is a good
+ // enough approximation and we didn't bother storing per-rect items.
+ true
+ }
+ }
+}
+
+
+pub struct AlphaBatchList {
+ pub batches: Vec<PrimitiveBatch>,
+ pub batch_rects: Vec<BatchRects>,
+ current_batch_index: usize,
+ current_z_id: ZBufferId,
+ break_advanced_blend_batches: bool,
+}
+
+impl AlphaBatchList {
+ fn new(break_advanced_blend_batches: bool, preallocate: usize) -> Self {
+ AlphaBatchList {
+ batches: Vec::with_capacity(preallocate),
+ batch_rects: Vec::with_capacity(preallocate),
+ current_z_id: ZBufferId::invalid(),
+ current_batch_index: usize::MAX,
+ break_advanced_blend_batches,
+ }
+ }
+
+ /// Clear all current batches in this list. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.current_batch_index = usize::MAX;
+ self.current_z_id = ZBufferId::invalid();
+ self.batches.clear();
+ self.batch_rects.clear();
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ // The bounding box of everything at this Z plane. We expect potentially
+ // multiple primitive segments coming with the same `z_id`.
+ z_bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ if z_id != self.current_z_id ||
+ self.current_batch_index == usize::MAX ||
+ !self.batches[self.current_batch_index].key.is_compatible_with(&key)
+ {
+ let mut selected_batch_index = None;
+
+ match key.blend_mode {
+ BlendMode::SubpixelWithBgColor => {
+ for (batch_index, batch) in self.batches.iter().enumerate().rev() {
+ // Some subpixel batches are drawn in two passes. Because of this, we need
+ // to check for overlaps with every batch (which is a bit different
+ // than the normal batching below).
+ if self.batch_rects[batch_index].intersects(z_bounding_rect) {
+ break;
+ }
+
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+ }
+ }
+ BlendMode::Advanced(_) if self.break_advanced_blend_batches => {
+ // don't try to find a batch
+ }
+ _ => {
+ for (batch_index, batch) in self.batches.iter().enumerate().rev() {
+ // For normal batches, we only need to check for overlaps for batches
+ // other than the first batch we consider. If the first batch
+ // is compatible, then we know there isn't any potential overlap
+ // issues to worry about.
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+
+ // check for intersections
+ if self.batch_rects[batch_index].intersects(z_bounding_rect) {
+ break;
+ }
+ }
+ }
+ }
+
+ if selected_batch_index.is_none() {
+ // Text runs tend to have a lot of instances per batch, causing a lot of reallocation
+ // churn as items are added one by one, so we give it a head start. Ideally we'd start
+ // with a larger number, closer to 1k but in some bad cases with lots of batch break
+ // we would be wasting a lot of memory.
+ // Generally it is safe to preallocate small-ish values for other batch kinds because
+ // the items are small and there are no zero-sized batches so there will always be
+ // at least one allocation.
+ let prealloc = match key.kind {
+ BatchKind::TextRun(..) => 128,
+ _ => 16,
+ };
+ let mut new_batch = PrimitiveBatch::new(key);
+ new_batch.instances.reserve(prealloc);
+ selected_batch_index = Some(self.batches.len());
+ self.batches.push(new_batch);
+ self.batch_rects.push(BatchRects::new());
+ }
+
+ self.current_batch_index = selected_batch_index.unwrap();
+ self.batch_rects[self.current_batch_index].add_rect(z_bounding_rect);
+ self.current_z_id = z_id;
+ }
+
+ let batch = &mut self.batches[self.current_batch_index];
+ batch.features |= features;
+ batch.key.textures.merge(&key.textures);
+
+ &mut batch.instances
+ }
+}
+
+pub struct OpaqueBatchList {
+ pub pixel_area_threshold_for_new_batch: f32,
+ pub batches: Vec<PrimitiveBatch>,
+ pub current_batch_index: usize,
+ lookback_count: usize,
+}
+
+impl OpaqueBatchList {
+ fn new(pixel_area_threshold_for_new_batch: f32, lookback_count: usize) -> Self {
+ OpaqueBatchList {
+ batches: Vec::new(),
+ pixel_area_threshold_for_new_batch,
+ current_batch_index: usize::MAX,
+ lookback_count,
+ }
+ }
+
+ /// Clear all current batches in this list. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.current_batch_index = usize::MAX;
+ self.batches.clear();
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ // The bounding box of everything at the current Z, whatever it is. We expect potentially
+ // multiple primitive segments produced by a primitive, which we allow to check
+ // `current_batch_index` instead of iterating the batches.
+ z_bounding_rect: &PictureRect,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ if self.current_batch_index == usize::MAX ||
+ !self.batches[self.current_batch_index].key.is_compatible_with(&key) {
+ let mut selected_batch_index = None;
+ let item_area = z_bounding_rect.size.area();
+
+ // If the area of this primitive is larger than the given threshold,
+ // then it is large enough to warrant breaking a batch for. In this
+ // case we just see if it can be added to the existing batch or
+ // create a new one.
+ if item_area > self.pixel_area_threshold_for_new_batch {
+ if let Some(batch) = self.batches.last() {
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(self.batches.len() - 1);
+ }
+ }
+ } else {
+ // Otherwise, look back through a reasonable number of batches.
+ for (batch_index, batch) in self.batches.iter().enumerate().rev().take(self.lookback_count) {
+ if batch.key.is_compatible_with(&key) {
+ selected_batch_index = Some(batch_index);
+ break;
+ }
+ }
+ }
+
+ if selected_batch_index.is_none() {
+ let new_batch = PrimitiveBatch::new(key);
+ selected_batch_index = Some(self.batches.len());
+ self.batches.push(new_batch);
+ }
+
+ self.current_batch_index = selected_batch_index.unwrap();
+ }
+
+ let batch = &mut self.batches[self.current_batch_index];
+ batch.features |= features;
+ batch.key.textures.merge(&key.textures);
+
+ &mut batch.instances
+ }
+
+ fn finalize(&mut self) {
+ // Reverse the instance arrays in the opaque batches
+ // to get maximum z-buffer efficiency by drawing
+ // front-to-back.
+ // TODO(gw): Maybe we can change the batch code to
+ // build these in reverse and avoid having
+ // to reverse the instance array here.
+ for batch in &mut self.batches {
+ batch.instances.reverse();
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveBatch {
+ pub key: BatchKey,
+ pub instances: Vec<PrimitiveInstanceData>,
+ pub features: BatchFeatures,
+}
+
+bitflags! {
+ /// Features of the batch that, if not requested, may allow a fast-path.
+ ///
+ /// Rather than breaking batches when primitives request different features,
+ /// we always request the minimum amount of features to satisfy all items in
+ /// the batch.
+ /// The goal is to let the renderer be optionally select more specialized
+ /// versions of a shader if the batch doesn't require code certain code paths.
+ /// Not all shaders necessarily implement all of these features.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ pub struct BatchFeatures: u8 {
+ const ALPHA_PASS = 1 << 0;
+ const ANTIALIASING = 1 << 1;
+ const REPETITION = 1 << 2;
+ /// Indicates a primitive in this batch may use a clip mask.
+ const CLIP_MASK = 1 << 3;
+ }
+}
+
+impl PrimitiveBatch {
+ fn new(key: BatchKey) -> PrimitiveBatch {
+ PrimitiveBatch {
+ key,
+ instances: Vec::new(),
+ features: BatchFeatures::empty(),
+ }
+ }
+
+ fn merge(&mut self, other: PrimitiveBatch) {
+ self.instances.extend(other.instances);
+ self.features |= other.features;
+ self.key.textures.merge(&other.key.textures);
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AlphaBatchContainer {
+ pub opaque_batches: Vec<PrimitiveBatch>,
+ pub alpha_batches: Vec<PrimitiveBatch>,
+ /// The overall scissor rect for this render task, if one
+ /// is required.
+ pub task_scissor_rect: Option<DeviceIntRect>,
+ /// The rectangle of the owning render target that this
+ /// set of batches affects.
+ pub task_rect: DeviceIntRect,
+}
+
+impl AlphaBatchContainer {
+ pub fn new(
+ task_scissor_rect: Option<DeviceIntRect>,
+ ) -> AlphaBatchContainer {
+ AlphaBatchContainer {
+ opaque_batches: Vec::new(),
+ alpha_batches: Vec::new(),
+ task_scissor_rect,
+ task_rect: DeviceIntRect::zero(),
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.opaque_batches.is_empty() &&
+ self.alpha_batches.is_empty()
+ }
+
+ fn merge(&mut self, builder: AlphaBatchBuilder, task_rect: &DeviceIntRect) {
+ self.task_rect = self.task_rect.union(task_rect);
+
+ for other_batch in builder.opaque_batch_list.batches {
+ let batch_index = self.opaque_batches.iter().position(|batch| {
+ batch.key.is_compatible_with(&other_batch.key)
+ });
+
+ match batch_index {
+ Some(batch_index) => {
+ self.opaque_batches[batch_index].merge(other_batch);
+ }
+ None => {
+ self.opaque_batches.push(other_batch);
+ }
+ }
+ }
+
+ let mut min_batch_index = 0;
+
+ for other_batch in builder.alpha_batch_list.batches {
+ let batch_index = self.alpha_batches.iter().skip(min_batch_index).position(|batch| {
+ batch.key.is_compatible_with(&other_batch.key)
+ });
+
+ match batch_index {
+ Some(batch_index) => {
+ let index = batch_index + min_batch_index;
+ self.alpha_batches[index].merge(other_batch);
+ min_batch_index = index;
+ }
+ None => {
+ self.alpha_batches.push(other_batch);
+ min_batch_index = self.alpha_batches.len();
+ }
+ }
+ }
+ }
+}
+
+/// Each segment can optionally specify a per-segment
+/// texture set and one user data field.
+#[derive(Debug, Copy, Clone)]
+struct SegmentInstanceData {
+ textures: TextureSet,
+ specific_resource_address: i32,
+}
+
+/// Encapsulates the logic of building batches for items that are blended.
+pub struct AlphaBatchBuilder {
+ pub alpha_batch_list: AlphaBatchList,
+ pub opaque_batch_list: OpaqueBatchList,
+ pub render_task_id: RenderTaskId,
+ render_task_address: RenderTaskAddress,
+ pub vis_mask: PrimitiveVisibilityMask,
+}
+
+impl AlphaBatchBuilder {
+ pub fn new(
+ screen_size: DeviceIntSize,
+ break_advanced_blend_batches: bool,
+ lookback_count: usize,
+ render_task_id: RenderTaskId,
+ render_task_address: RenderTaskAddress,
+ vis_mask: PrimitiveVisibilityMask,
+ preallocate: usize,
+ ) -> Self {
+ // The threshold for creating a new batch is
+ // one quarter the screen size.
+ let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
+
+ AlphaBatchBuilder {
+ alpha_batch_list: AlphaBatchList::new(break_advanced_blend_batches, preallocate),
+ opaque_batch_list: OpaqueBatchList::new(batch_area_threshold, lookback_count),
+ render_task_id,
+ render_task_address,
+ vis_mask,
+ }
+ }
+
+ /// Clear all current batches in this builder. This is typically used
+ /// when a primitive is encountered that occludes all previous
+ /// content in this batch list.
+ fn clear(&mut self) {
+ self.alpha_batch_list.clear();
+ self.opaque_batch_list.clear();
+ }
+
+ pub fn build(
+ mut self,
+ batch_containers: &mut Vec<AlphaBatchContainer>,
+ merged_batches: &mut AlphaBatchContainer,
+ task_rect: DeviceIntRect,
+ task_scissor_rect: Option<DeviceIntRect>,
+ ) {
+ self.opaque_batch_list.finalize();
+
+ if task_scissor_rect.is_none() {
+ merged_batches.merge(self, &task_rect);
+ } else {
+ batch_containers.push(AlphaBatchContainer {
+ alpha_batches: self.alpha_batch_list.batches,
+ opaque_batches: self.opaque_batch_list.batches,
+ task_scissor_rect,
+ task_rect,
+ });
+ }
+ }
+
+ pub fn push_single_instance(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ instance: PrimitiveInstanceData,
+ ) {
+ self.set_params_and_get_batch(key, features, bounding_rect, z_id)
+ .push(instance);
+ }
+
+ pub fn set_params_and_get_batch(
+ &mut self,
+ key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ ) -> &mut Vec<PrimitiveInstanceData> {
+ match key.blend_mode {
+ BlendMode::None => {
+ self.opaque_batch_list
+ .set_params_and_get_batch(key, features, bounding_rect)
+ }
+ BlendMode::Alpha |
+ BlendMode::PremultipliedAlpha |
+ BlendMode::PremultipliedDestOut |
+ BlendMode::SubpixelConstantTextColor(..) |
+ BlendMode::SubpixelWithBgColor |
+ BlendMode::SubpixelDualSource |
+ BlendMode::Advanced(_) => {
+ self.alpha_batch_list
+ .set_params_and_get_batch(key, features, bounding_rect, z_id)
+ }
+ }
+ }
+}
+
+/// Supports (recursively) adding a list of primitives and pictures to an alpha batch
+/// builder. In future, it will support multiple dirty regions / slices, allowing the
+/// contents of a picture to be spliced into multiple batch builders.
+pub struct BatchBuilder {
+ /// A temporary buffer that is used during glyph fetching, stored here
+ /// to reduce memory allocations.
+ glyph_fetch_buffer: Vec<GlyphFetchResult>,
+
+ pub batchers: Vec<AlphaBatchBuilder>,
+}
+
+impl BatchBuilder {
+ pub fn new(batchers: Vec<AlphaBatchBuilder>) -> Self {
+ BatchBuilder {
+ glyph_fetch_buffer: Vec::new(),
+ batchers,
+ }
+ }
+
+ pub fn finalize(self) -> Vec<AlphaBatchBuilder> {
+ self.batchers
+ }
+
+ fn add_brush_instance_to_batches(
+ &mut self,
+ batch_key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ segment_index: i32,
+ edge_flags: EdgeAaSegmentMask,
+ clip_task_address: RenderTaskAddress,
+ brush_flags: BrushFlags,
+ prim_header_index: PrimitiveHeaderIndex,
+ resource_address: i32,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ ) {
+ for batcher in &mut self.batchers {
+ if batcher.vis_mask.intersects(prim_vis_mask) {
+ let render_task_address = batcher.render_task_address;
+
+ let instance = BrushInstance {
+ segment_index,
+ edge_flags,
+ clip_task_address,
+ render_task_address,
+ brush_flags,
+ prim_header_index,
+ resource_address,
+ };
+
+ batcher.push_single_instance(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ PrimitiveInstanceData::from(instance),
+ );
+ }
+ }
+ }
+
+ fn add_split_composite_instance_to_batches(
+ &mut self,
+ batch_key: BatchKey,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ z_id: ZBufferId,
+ prim_header_index: PrimitiveHeaderIndex,
+ polygons_address: GpuCacheAddress,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ ) {
+ for batcher in &mut self.batchers {
+ if batcher.vis_mask.intersects(prim_vis_mask) {
+ let render_task_address = batcher.render_task_address;
+
+ batcher.push_single_instance(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ PrimitiveInstanceData::from(SplitCompositeInstance {
+ prim_header_index,
+ render_task_address,
+ polygons_address,
+ z: z_id,
+ }),
+ );
+ }
+ }
+ }
+
+ /// Clear all current batchers. This is typically used when a primitive
+ /// is encountered that occludes all previous content in this batch list.
+ fn clear_batches(&mut self) {
+ for batcher in &mut self.batchers {
+ batcher.clear();
+ }
+ }
+
+ /// Add a picture to a given batch builder.
+ pub fn add_pic_to_batch(
+ &mut self,
+ pic: &PicturePrimitive,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ root_spatial_node_index: SpatialNodeIndex,
+ surface_spatial_node_index: SpatialNodeIndex,
+ z_generator: &mut ZBufferIdGenerator,
+ composite_state: &mut CompositeState,
+ ) {
+ for cluster in &pic.prim_list.clusters {
+ if !cluster.flags.contains(ClusterFlags::IS_VISIBLE) {
+ continue;
+ }
+ for prim_instance in &pic.prim_list.prim_instances[cluster.prim_range()] {
+ // Add each run in this picture to the batch.
+ self.add_prim_to_batch(
+ prim_instance,
+ cluster.spatial_node_index,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ root_spatial_node_index,
+ surface_spatial_node_index,
+ z_generator,
+ composite_state,
+ );
+ }
+ }
+ }
+
+ // If an image is being drawn as a compositor surface, we don't want
+ // to draw the surface itself into the tile. Instead, we draw a transparent
+ // rectangle that writes to the z-buffer where this compositor surface is.
+ // That ensures we 'cut out' the part of the tile that has the compositor
+ // surface on it, allowing us to draw this tile as an overlay on top of
+ // the compositor surface.
+ // TODO(gw): There's a slight performance cost to doing this cutout rectangle
+ // if we end up not needing to use overlay mode. Consider skipping
+ // the cutout completely in this path.
+ fn emit_placeholder(
+ &mut self,
+ prim_rect: LayoutRect,
+ prim_info: &PrimitiveVisibility,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ z_id: ZBufferId,
+ transform_id: TransformPaletteId,
+ batch_features: BatchFeatures,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ prim_headers: &mut PrimitiveHeaders,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Solid,
+ TextureSet::UNTEXTURED,
+ [get_shader_opacity(0.0), 0, 0, 0],
+ 0,
+ );
+
+ let prim_cache_address = gpu_cache.get_address(
+ &ctx.globals.default_transparent_rect_handle,
+ );
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let bounding_rect = &prim_info.clip_chain.pic_clip_rect;
+ let transform_kind = transform_id.transform_kind();
+
+ self.add_segmented_prim_to_batch(
+ None,
+ PrimitiveOpacity::translucent(),
+ &batch_params,
+ BlendMode::None,
+ BlendMode::None,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+
+ // Adds a primitive to a batch.
+ // It can recursively call itself in some situations, for
+ // example if it encounters a picture where the items
+ // in that picture are being drawn into the same target.
+ fn add_prim_to_batch(
+ &mut self,
+ prim_instance: &PrimitiveInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ root_spatial_node_index: SpatialNodeIndex,
+ surface_spatial_node_index: SpatialNodeIndex,
+ z_generator: &mut ZBufferIdGenerator,
+ composite_state: &mut CompositeState,
+ ) {
+ let prim_vis_mask = match prim_instance.vis.state {
+ VisibilityState::Culled => {
+ return;
+ }
+ VisibilityState::Unset | VisibilityState::Coarse { .. } => {
+ panic!("bug: invalid visibility state");
+ }
+ VisibilityState::Detailed { visibility_mask } => {
+ visibility_mask
+ }
+ };
+
+ #[cfg(debug_assertions)] //TODO: why is this needed?
+ debug_assert_eq!(prim_instance.prepared_frame_id, render_tasks.frame_id());
+
+ let transform_id = transforms
+ .get_id(
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ ctx.spatial_tree,
+ );
+
+ // TODO(gw): Calculating this for every primitive is a bit
+ // wasteful. We should probably cache this in
+ // the scroll node...
+ let transform_kind = transform_id.transform_kind();
+ let prim_info = &prim_instance.vis;
+ let bounding_rect = &prim_info.clip_chain.pic_clip_rect;
+
+ // If this primitive is a backdrop, that means that it is known to cover
+ // the entire picture cache background. In that case, the renderer will
+ // use the backdrop color as a clear color, and so we can drop this
+ // primitive and any prior primitives from the batch lists for this
+ // picture cache slice.
+ if prim_info.flags.contains(PrimitiveVisibilityFlags::IS_BACKDROP) {
+ self.clear_batches();
+ return;
+ }
+
+ let z_id = z_generator.next();
+
+ let prim_rect = ctx.data_stores.get_local_prim_rect(
+ prim_instance,
+ ctx.prim_store,
+ );
+
+ let mut batch_features = BatchFeatures::empty();
+ if ctx.data_stores.prim_may_need_repetition(prim_instance) {
+ batch_features |= BatchFeatures::REPETITION;
+ }
+
+ if transform_kind != TransformedRectKind::AxisAligned {
+ batch_features |= BatchFeatures::ANTIALIASING;
+ }
+
+ // Check if the primitive might require a clip mask.
+ if prim_info.clip_task_index != ClipTaskIndex::INVALID {
+ batch_features |= BatchFeatures::CLIP_MASK;
+ }
+
+ if !bounding_rect.is_empty() {
+ debug_assert_eq!(prim_info.clip_chain.pic_spatial_node_index, surface_spatial_node_index,
+ "The primitive's bounding box is specified in a different coordinate system from the current batch!");
+ }
+
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Clear { data_handle } => {
+ let prim_data = &ctx.data_stores.prim[data_handle];
+ let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // TODO(gw): We can abstract some of the common code below into
+ // helper methods, as we port more primitives to make
+ // use of interning.
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ [get_shader_opacity(1.0), 0, 0, 0],
+ );
+
+ let batch_key = BatchKey {
+ blend_mode: BlendMode::PremultipliedDestOut,
+ kind: BatchKind::Brush(BrushBatchKind::Solid),
+ textures: BatchTextures::prim_untextured(clip_mask_texture_id),
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, ref cache_handles, .. } => {
+ let prim_data = &ctx.data_stores.normal_border[data_handle];
+ let common_data = &prim_data.common;
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+ let cache_handles = &ctx.scratch.border_cache_handles[*cache_handles];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+ let mut segment_data: SmallVec<[SegmentInstanceData; 8]> = SmallVec::new();
+
+ // Collect the segment instance data from each render
+ // task for each valid edge / corner of the border.
+
+ for handle in cache_handles {
+ let rt_cache_entry = ctx.resource_cache
+ .get_cached_render_task(handle);
+ let cache_item = ctx.resource_cache
+ .get_texture_cache_item(&rt_cache_entry.handle);
+ segment_data.push(
+ SegmentInstanceData {
+ textures: TextureSet::prim_textured(cache_item.texture_id),
+ specific_resource_address: cache_item.uv_rect_handle.as_int(gpu_cache),
+ }
+ );
+ }
+
+ // TODO: it would be less error-prone to get this info from the texture cache.
+ let image_buffer_kind = ImageBufferKind::Texture2D;
+
+ let non_segmented_blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let batch_params = BrushBatchParameters::instanced(
+ BrushBatchKind::Image(image_buffer_kind),
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ segment_data,
+ );
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let border_data = &prim_data.kind;
+ self.add_segmented_prim_to_batch(
+ Some(border_data.brush_segments.as_slice()),
+ common_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
+ let run = &ctx.prim_store.text_runs[run_index];
+ let subpx_dir = run.used_font.get_subpx_dir();
+
+ // The GPU cache data is stored in the template and reused across
+ // frames and display lists.
+ let prim_data = &ctx.data_stores.text_run[data_handle];
+ let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ // The local prim rect is only informative for text primitives, as
+ // thus is not directly necessary for any drawing of the text run.
+ // However the glyph offsets are relative to the prim rect origin
+ // less the unsnapped reference frame offset. We also want the
+ // the snapped reference frame offset, because cannot recalculate
+ // it as it ignores the animated components for the transform. As
+ // such, we adjust the prim rect origin here, and replace the size
+ // with the unsnapped and snapped offsets respectively. This has
+ // the added bonus of avoiding quantization effects when storing
+ // floats in the extra header integers.
+ let prim_header = PrimitiveHeader {
+ local_rect: LayoutRect::new(
+ prim_rect.origin - run.reference_frame_relative_offset,
+ run.snapped_reference_frame_relative_offset.to_size(),
+ ),
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let glyph_keys = &ctx.scratch.glyph_keys[run.glyph_keys_range];
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ [
+ (run.raster_scale * 65535.0).round() as i32,
+ 0,
+ 0,
+ 0,
+ ],
+ );
+ let base_instance = GlyphInstance::new(
+ prim_header_index,
+ );
+ let batchers = &mut self.batchers;
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // The run.used_font.clone() is here instead of instead of inline in the `fetch_glyph`
+ // function call to work around a miscompilation.
+ // https://github.com/rust-lang/rust/issues/80111
+ let font = run.used_font.clone();
+ ctx.resource_cache.fetch_glyphs(
+ font,
+ &glyph_keys,
+ &mut self.glyph_fetch_buffer,
+ gpu_cache,
+ |texture_id, mut glyph_format, glyphs| {
+ debug_assert_ne!(texture_id, TextureSource::Invalid);
+
+ // Ignore color and only sample alpha when shadowing.
+ if run.shadow {
+ glyph_format = glyph_format.ignore_color();
+ }
+
+ let subpx_dir = subpx_dir.limit_by(glyph_format);
+
+ let textures = BatchTextures::prim_textured(
+ texture_id,
+ clip_mask_texture_id,
+ );
+
+ let kind = BatchKind::TextRun(glyph_format);
+
+ let (blend_mode, color_mode) = match glyph_format {
+ GlyphFormat::Subpixel |
+ GlyphFormat::TransformedSubpixel => {
+ if run.used_font.bg_color.a != 0 {
+ (
+ BlendMode::SubpixelWithBgColor,
+ ShaderColorMode::FromRenderPassMode,
+ )
+ } else if ctx.use_dual_source_blending {
+ (
+ BlendMode::SubpixelDualSource,
+ ShaderColorMode::SubpixelDualSource,
+ )
+ } else {
+ (
+ BlendMode::SubpixelConstantTextColor(run.used_font.color.into()),
+ ShaderColorMode::SubpixelConstantTextColor,
+ )
+ }
+ }
+ GlyphFormat::Alpha |
+ GlyphFormat::TransformedAlpha => {
+ (
+ BlendMode::PremultipliedAlpha,
+ ShaderColorMode::Alpha,
+ )
+ }
+ GlyphFormat::Bitmap => {
+ (
+ BlendMode::PremultipliedAlpha,
+ ShaderColorMode::Bitmap,
+ )
+ }
+ GlyphFormat::ColorBitmap => {
+ (
+ BlendMode::PremultipliedAlpha,
+ ShaderColorMode::ColorBitmap,
+ )
+ }
+ };
+
+ // Calculate a tighter bounding rect of just the glyphs passed to this
+ // callback from request_glyphs(), rather than using the bounds of the
+ // entire text run. This improves batching when glyphs are fragmented
+ // over multiple textures in the texture cache.
+ // This code is taken from the ps_text_run shader.
+ let tight_bounding_rect = {
+ let snap_bias = match subpx_dir {
+ SubpixelDirection::None => DeviceVector2D::new(0.5, 0.5),
+ SubpixelDirection::Horizontal => DeviceVector2D::new(0.125, 0.5),
+ SubpixelDirection::Vertical => DeviceVector2D::new(0.5, 0.125),
+ SubpixelDirection::Mixed => DeviceVector2D::new(0.125, 0.125),
+ };
+ let text_offset = prim_header.local_rect.size.to_vector();
+
+ let pic_bounding_rect = if run.used_font.flags.contains(FontInstanceFlags::TRANSFORM_GLYPHS) {
+ let mut device_bounding_rect = DeviceRect::default();
+
+ let glyph_transform = ctx.spatial_tree.get_relative_transform(
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ ).into_transform()
+ .with_destination::<WorldPixel>()
+ .then(&euclid::Transform3D::from_scale(ctx.global_device_pixel_scale));
+
+ let glyph_translation = DeviceVector2D::new(glyph_transform.m41, glyph_transform.m42);
+
+ for glyph in glyphs {
+ let glyph_offset = prim_data.glyphs[glyph.index_in_text_run as usize].point + prim_header.local_rect.origin.to_vector();
+
+ let raster_glyph_offset = (glyph_transform.transform_point2d(glyph_offset).unwrap() + snap_bias).floor();
+ let raster_text_offset = (
+ glyph_transform.transform_vector2d(text_offset) +
+ glyph_translation +
+ DeviceVector2D::new(0.5, 0.5)
+ ).floor() - glyph_translation;
+
+ let device_glyph_rect = DeviceRect::new(
+ glyph.offset + raster_glyph_offset.to_vector() + raster_text_offset,
+ glyph.size.to_f32(),
+ );
+
+ device_bounding_rect = device_bounding_rect.union(&device_glyph_rect);
+ }
+
+ let map_device_to_surface: SpaceMapper<DevicePixel, PicturePixel> = SpaceMapper::new_with_target(
+ surface_spatial_node_index,
+ root_spatial_node_index,
+ *bounding_rect,
+ ctx.spatial_tree,
+ );
+
+ map_device_to_surface.map(&device_bounding_rect)
+ } else {
+ let mut local_bounding_rect = LayoutRect::default();
+
+ let glyph_raster_scale = run.raster_scale * ctx.global_device_pixel_scale.get();
+
+ for glyph in glyphs {
+ let glyph_offset = prim_data.glyphs[glyph.index_in_text_run as usize].point + prim_header.local_rect.origin.to_vector();
+ let glyph_scale = LayoutToDeviceScale::new(glyph_raster_scale / glyph.scale);
+ let raster_glyph_offset = (glyph_offset * LayoutToDeviceScale::new(glyph_raster_scale) + snap_bias).floor() / glyph.scale;
+ let local_glyph_rect = LayoutRect::new(
+ (glyph.offset + raster_glyph_offset.to_vector()) / glyph_scale + text_offset,
+ glyph.size.to_f32() / glyph_scale,
+ );
+
+ local_bounding_rect = local_bounding_rect.union(&local_glyph_rect);
+ }
+
+ let map_prim_to_surface: SpaceMapper<LayoutPixel, PicturePixel> = SpaceMapper::new_with_target(
+ surface_spatial_node_index,
+ prim_spatial_node_index,
+ *bounding_rect,
+ ctx.spatial_tree,
+ );
+ map_prim_to_surface.map(&local_bounding_rect)
+ };
+
+ let intersected = match pic_bounding_rect {
+ // The text run may have been clipped, for example if part of it is offscreen.
+ // So intersect our result with the original bounding rect.
+ Some(rect) => rect.intersection(bounding_rect).unwrap_or_else(PictureRect::zero),
+ // If space mapping went off the rails, fall back to the old behavior.
+ //TODO: consider skipping the glyph run completely in this case.
+ None => *bounding_rect,
+ };
+
+ intersected
+ };
+
+ let key = BatchKey::new(kind, blend_mode, textures);
+
+ for batcher in batchers.iter_mut() {
+ if batcher.vis_mask.intersects(prim_vis_mask) {
+ let render_task_address = batcher.render_task_address;
+ let batch = batcher.alpha_batch_list.set_params_and_get_batch(
+ key,
+ batch_features,
+ &tight_bounding_rect,
+ z_id,
+ );
+
+ batch.reserve(glyphs.len());
+ for glyph in glyphs {
+ batch.push(base_instance.build(
+ render_task_address,
+ clip_task_address,
+ subpx_dir,
+ glyph.index_in_text_run,
+ glyph.uv_rect_address,
+ color_mode,
+ ));
+ }
+ }
+ }
+ },
+ );
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, ref cache_handle, .. } => {
+ // The GPU cache data is stored in the template and reused across
+ // frames and display lists.
+ let common_data = &ctx.data_stores.line_decoration[data_handle].common;
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let (batch_kind, textures, prim_user_data, specific_resource_address) = match cache_handle {
+ Some(cache_handle) => {
+ let rt_cache_entry = ctx
+ .resource_cache
+ .get_cached_render_task(cache_handle);
+ let cache_item = ctx
+ .resource_cache
+ .get_texture_cache_item(&rt_cache_entry.handle);
+ let textures = BatchTextures::prim_textured(
+ cache_item.texture_id,
+ clip_mask_texture_id,
+ );
+ (
+ BrushBatchKind::Image(cache_item.texture_id.image_buffer_kind()),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ cache_item.uv_rect_handle.as_int(gpu_cache),
+ )
+ }
+ None => {
+ (
+ BrushBatchKind::Solid,
+ BatchTextures::prim_untextured(clip_mask_texture_id),
+ [get_shader_opacity(1.0), 0, 0, 0],
+ 0,
+ )
+ }
+ };
+
+ // TODO(gw): We can abstract some of the common code below into
+ // helper methods, as we port more primitives to make
+ // use of interning.
+ let blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ BlendMode::PremultipliedAlpha
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ prim_user_data,
+ );
+
+ let batch_key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ specific_resource_address,
+ prim_vis_mask,
+ );
+ }
+ PrimitiveInstanceKind::Picture { pic_index, segment_instance_index, .. } => {
+ let picture = &ctx.prim_store.pictures[pic_index.0];
+ let non_segmented_blend_mode = BlendMode::PremultipliedAlpha;
+ let prim_cache_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
+
+ let prim_header = PrimitiveHeader {
+ local_rect: picture.precise_local_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ match picture.context_3d {
+ // Convert all children of the 3D hierarchy root into batches.
+ Picture3DContext::In { root_data: Some(ref list), .. } => {
+ for child in list {
+ let child_prim_instance = &picture.prim_list.prim_instances[child.anchor.instance_index];
+ let child_prim_info = &child_prim_instance.vis;
+
+ let child_pic_index = match child_prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
+ _ => unreachable!(),
+ };
+ let pic = &ctx.prim_store.pictures[child_pic_index.0];
+
+ let child_visibility_mask = match child_prim_info.state {
+ VisibilityState::Detailed { visibility_mask } => visibility_mask,
+ _ => panic!("bug: culled prim should not be in child list"),
+ };
+
+ // Get clip task, if set, for the picture primitive.
+ let (child_clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ child_prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let prim_header = PrimitiveHeader {
+ local_rect: pic.precise_local_rect,
+ local_clip_rect: child_prim_info.combined_local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id: transforms
+ .get_id(
+ child.spatial_node_index,
+ root_spatial_node_index,
+ ctx.spatial_tree,
+ ),
+ };
+
+ let raster_config = pic
+ .raster_config
+ .as_ref()
+ .expect("BUG: 3d primitive was not assigned a surface");
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+
+ // Need a new z-id for each child preserve-3d context added
+ // by this inner loop.
+ let z_id = z_generator.next();
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ if raster_config.establishes_raster_root { 1 } else { 0 },
+ 0,
+ child_clip_task_address.0 as i32,
+ ]);
+
+ let key = BatchKey::new(
+ BatchKind::SplitComposite,
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ self.add_split_composite_instance_to_batches(
+ key,
+ BatchFeatures::CLIP_MASK,
+ &child_prim_info.clip_chain.pic_clip_rect,
+ z_id,
+ prim_header_index,
+ child.gpu_address,
+ child_visibility_mask,
+ );
+ }
+ }
+ // Ignore the 3D pictures that are not in the root of preserve-3D
+ // hierarchy, since we process them with the root.
+ Picture3DContext::In { root_data: None, .. } => return,
+ // Proceed for non-3D pictures.
+ Picture3DContext::Out => ()
+ }
+
+ match picture.raster_config {
+ Some(ref raster_config) => {
+ // If the child picture was rendered in local space, we can safely
+ // interpolate the UV coordinates with perspective correction.
+ let brush_flags = if raster_config.establishes_raster_root {
+ BrushFlags::PERSPECTIVE_INTERPOLATION
+ } else {
+ BrushFlags::empty()
+ };
+
+ let surface = &ctx.surfaces[raster_config.surface_index.0];
+
+ let mut is_opaque = prim_info.clip_task_index == ClipTaskIndex::INVALID
+ && surface.opaque_rect.contains_rect(&surface.rect);
+
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {
+ // TODO(gw): For now, TileCache is still a composite mode, even though
+ // it will only exist as a top level primitive and never
+ // be encountered during batching. Consider making TileCache
+ // a standalone type, not a picture.
+ }
+ PictureCompositeMode::Filter(ref filter) => {
+ assert!(filter.is_visible());
+ match filter {
+ Filter::Blur(..) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D)
+ );
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+ let key = BatchKey::new(
+ kind,
+ non_segmented_blend_mode,
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ Filter::DropShadows(shadows) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // Draw an instance per shadow first, following by the content.
+
+ // The shadows and the content get drawn as a brush image.
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ );
+
+ // Gets the saved render task ID of the content, which is
+ // deeper in the render task graph than the direct child.
+ let secondary_id = picture.secondary_render_task_id.expect("no secondary!?");
+ let content_source = {
+ let secondary_task = &render_tasks[secondary_id];
+ let texture_id = secondary_task.get_target_texture();
+ TextureSource::TextureCache(
+ texture_id,
+ Swizzle::default(),
+ )
+ };
+
+ // Retrieve the UV rect addresses for shadow/content.
+ let cache_task_id = ctx.resolve_surface(raster_config.surface_index);
+ let (shadow_uv_rect_address, shadow_textures) = render_tasks.resolve_surface(
+ cache_task_id,
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+ let content_uv_rect_address = render_tasks[secondary_id]
+ .get_texture_address(gpu_cache)
+ .as_int();
+
+ // Build BatchTextures for shadow/content
+ let content_textures = BatchTextures::prim_textured(
+ content_source,
+ clip_mask_texture_id,
+ );
+
+ // Build batch keys for shadow/content
+ let shadow_key = BatchKey::new(kind, non_segmented_blend_mode, shadow_textures);
+ let content_key = BatchKey::new(kind, non_segmented_blend_mode, content_textures);
+
+ for (shadow, shadow_gpu_data) in shadows.iter().zip(picture.extra_gpu_data_handles.iter()) {
+ // Get the GPU cache address of the extra data handle.
+ let shadow_prim_address = gpu_cache.get_address(shadow_gpu_data);
+
+ let shadow_rect = prim_header.local_rect.translate(shadow.offset);
+
+ let shadow_prim_header = PrimitiveHeader {
+ local_rect: shadow_rect,
+ specific_prim_address: shadow_prim_address,
+ ..prim_header
+ };
+
+ let shadow_prim_header_index = prim_headers.push(
+ &shadow_prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Alpha,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ shadow_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ shadow_prim_header_index,
+ shadow_uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ let z_id_content = z_generator.next();
+
+ let content_prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id_content,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ content_key,
+ batch_features,
+ bounding_rect,
+ z_id_content,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ content_prim_header_index,
+ content_uv_rect_address,
+ prim_vis_mask,
+ );
+ }
+ Filter::Opacity(_, amount) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let amount = (amount * 65536.0) as i32;
+
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Opacity),
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ amount,
+ 0,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ _ => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // Must be kept in sync with brush_blend.glsl
+ let filter_mode = filter.as_int();
+
+ let user_data = match filter {
+ Filter::Identity => 0x10000i32, // matches `Contrast(1)`
+ Filter::Contrast(amount) |
+ Filter::Grayscale(amount) |
+ Filter::Invert(amount) |
+ Filter::Saturate(amount) |
+ Filter::Sepia(amount) |
+ Filter::Brightness(amount) => {
+ (amount * 65536.0) as i32
+ }
+ Filter::SrgbToLinear | Filter::LinearToSrgb => 0,
+ Filter::HueRotate(angle) => {
+ (0.01745329251 * angle * 65536.0) as i32
+ }
+ Filter::ColorMatrix(_) => {
+ picture.extra_gpu_data_handles[0].as_int(gpu_cache)
+ }
+ Filter::Flood(_) => {
+ picture.extra_gpu_data_handles[0].as_int(gpu_cache)
+ }
+
+ // These filters are handled via different paths.
+ Filter::ComponentTransfer |
+ Filter::Blur(..) |
+ Filter::DropShadows(..) |
+ Filter::Opacity(..) => unreachable!(),
+ };
+
+ // Other filters that may introduce opacity are handled via different
+ // paths.
+ if let Filter::ColorMatrix(..) = filter {
+ is_opaque = false;
+ }
+
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+
+ let blend_mode = if is_opaque {
+ BlendMode::None
+ } else {
+ BlendMode::PremultipliedAlpha
+ };
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Blend),
+ blend_mode,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ filter_mode,
+ user_data,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ }
+ }
+ PictureCompositeMode::ComponentTransferFilter(handle) => {
+ // This is basically the same as the general filter case above
+ // except we store a little more data in the filter mode and
+ // a gpu cache handle in the user data.
+ let filter_data = &ctx.data_stores.filter_data[handle];
+ let filter_mode : i32 = Filter::ComponentTransfer.as_int() |
+ ((filter_data.data.r_func.to_int() << 28 |
+ filter_data.data.g_func.to_int() << 24 |
+ filter_data.data.b_func.to_int() << 20 |
+ filter_data.data.a_func.to_int() << 16) as i32);
+
+ let user_data = filter_data.gpu_cache_handle.as_int(gpu_cache);
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+
+ let key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Blend),
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ uv_rect_address.as_int(),
+ filter_mode,
+ user_data,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ PictureCompositeMode::MixBlend(mode) if ctx.use_advanced_blending => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+ let key = BatchKey::new(
+ BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ ),
+ BlendMode::Advanced(mode),
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ PictureCompositeMode::MixBlend(mode) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+ let cache_task_id = ctx.resolve_surface(raster_config.surface_index);
+ let backdrop_id = picture.secondary_render_task_id.expect("no backdrop!?");
+
+ // TODO(gw): For now, mix-blend is not supported as a picture
+ // caching root, so we can safely assume there is
+ // only a single batcher present.
+ assert_eq!(self.batchers.len(), 1);
+
+ let color0 = render_tasks[backdrop_id].get_target_texture();
+ let color1 = render_tasks[cache_task_id].get_target_texture();
+
+ let key = BatchKey::new(
+ BatchKind::Brush(
+ BrushBatchKind::MixBlend {
+ task_id: self.batchers[0].render_task_id,
+ source_id: cache_task_id,
+ backdrop_id,
+ },
+ ),
+ BlendMode::PremultipliedAlpha,
+ BatchTextures {
+ input: TextureSet {
+ colors: [
+ TextureSource::TextureCache(
+ color0,
+ Swizzle::default(),
+ ),
+ TextureSource::TextureCache(
+ color1,
+ Swizzle::default(),
+ ),
+ TextureSource::Invalid,
+ ],
+ },
+ clip_mask: clip_mask_texture_id,
+ },
+ );
+ let backdrop_task_address: RenderTaskAddress = backdrop_id.into();
+ let source_task_address: RenderTaskAddress = cache_task_id.into();
+ let prim_header_index = prim_headers.push(&prim_header, z_id, [
+ mode as u32 as i32,
+ backdrop_task_address.0 as i32,
+ source_task_address.0 as i32,
+ 0,
+ ]);
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ PictureCompositeMode::Blit(_) => {
+ let cache_task_id = ctx.resolve_surface(raster_config.surface_index);
+ let uv_rect_address = render_tasks[cache_task_id]
+ .get_texture_address(gpu_cache)
+ .as_int();
+ let cache_render_task = &render_tasks[cache_task_id];
+ let texture_id = cache_render_task.get_target_texture();
+ let textures = TextureSet {
+ colors: [
+ TextureSource::TextureCache(
+ texture_id,
+ Swizzle::default(),
+ ),
+ TextureSource::Invalid,
+ TextureSource::Invalid,
+ ],
+ };
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ uv_rect_address,
+ );
+
+ let is_segmented =
+ segment_instance_index != SegmentInstanceIndex::INVALID &&
+ segment_instance_index != SegmentInstanceIndex::UNUSED;
+
+ let (prim_cache_address, segments) = if is_segmented {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ } else {
+ (prim_cache_address, None)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: picture.precise_local_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let (opacity, specified_blend_mode) = if is_opaque {
+ (PrimitiveOpacity::opaque(), BlendMode::None)
+ } else {
+ (PrimitiveOpacity::translucent(), BlendMode::PremultipliedAlpha)
+ };
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ PictureCompositeMode::SvgFilter(..) => {
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let kind = BatchKind::Brush(
+ BrushBatchKind::Image(ImageBufferKind::Texture2D)
+ );
+ let (uv_rect_address, textures) = render_tasks.resolve_surface(
+ ctx.resolve_surface(raster_config.surface_index),
+ clip_mask_texture_id,
+ gpu_cache,
+ );
+ let key = BatchKey::new(
+ kind,
+ non_segmented_blend_mode,
+ textures,
+ );
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ clip_task_address,
+ brush_flags,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ }
+ }
+ None => {
+ // If this picture is being drawn into an existing target (i.e. with
+ // no composition operation), recurse and add to the current batch list.
+ self.add_pic_to_batch(
+ picture,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ root_spatial_node_index,
+ surface_spatial_node_index,
+ z_generator,
+ composite_state,
+ );
+ }
+ }
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let prim_data = &ctx.data_stores.image_border[data_handle];
+ let common_data = &prim_data.common;
+ let border_data = &prim_data.kind;
+
+ let cache_item = resolve_image(
+ border_data.request,
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ if cache_item.texture_id == TextureSource::Invalid {
+ return;
+ }
+
+ let textures = TextureSet::prim_textured(
+ cache_item.texture_id,
+ );
+ let prim_cache_address = gpu_cache.get_address(&common_data.gpu_cache_handle);
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+ let non_segmented_blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(cache_item.texture_id.image_buffer_kind()),
+ textures,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode(),
+ cache_item.uv_rect_handle.as_int(gpu_cache),
+ );
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ Some(border_data.brush_segments.as_slice()),
+ common_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, .. } => {
+ let prim_data = &ctx.data_stores.prim[data_handle];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+
+ let non_segmented_blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Solid,
+ TextureSet::UNTEXTURED,
+ [get_shader_opacity(1.0), 0, 0, 0],
+ 0,
+ );
+
+ let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&prim_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, is_compositor_surface, .. } => {
+ if is_compositor_surface {
+ self.emit_placeholder(
+ prim_rect,
+ prim_info,
+ prim_vis_mask,
+ z_id,
+ transform_id,
+ batch_features,
+ ctx,
+ gpu_cache,
+ prim_headers,
+ render_tasks,
+ );
+ return;
+ }
+
+ let yuv_image_data = &ctx.data_stores.yuv_image[data_handle].kind;
+ let mut textures = TextureSet::UNTEXTURED;
+ let mut uv_rect_addresses = [0; 3];
+
+ //yuv channel
+ let channel_count = yuv_image_data.format.get_plane_num();
+ debug_assert!(channel_count <= 3);
+ for channel in 0 .. channel_count {
+ let image_key = yuv_image_data.yuv_key[channel];
+
+ let cache_item = resolve_image(
+ ImageRequest {
+ key: image_key,
+ rendering: yuv_image_data.image_rendering,
+ tile: None,
+ },
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ if cache_item.texture_id == TextureSource::Invalid {
+ warn!("Warnings: skip a PrimitiveKind::YuvImage");
+ return;
+ }
+
+ textures.colors[channel] = cache_item.texture_id;
+ uv_rect_addresses[channel] = cache_item.uv_rect_handle.as_int(gpu_cache);
+ }
+
+ // All yuv textures should be the same type.
+ let buffer_kind = textures.colors[0].image_buffer_kind();
+ assert!(
+ textures.colors[1 .. yuv_image_data.format.get_plane_num()]
+ .iter()
+ .all(|&tid| buffer_kind == tid.image_buffer_kind())
+ );
+
+ let kind = BrushBatchKind::YuvImage(
+ buffer_kind,
+ yuv_image_data.format,
+ yuv_image_data.color_depth,
+ yuv_image_data.color_space,
+ yuv_image_data.color_range,
+ );
+
+ let batch_params = BrushBatchParameters::shared(
+ kind,
+ textures,
+ [
+ uv_rect_addresses[0],
+ uv_rect_addresses[1],
+ uv_rect_addresses[2],
+ 0,
+ ],
+ 0,
+ );
+
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+ let prim_common_data = &ctx.data_stores.as_common_data(&prim_instance);
+
+ let non_segmented_blend_mode = if !prim_common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ debug_assert_ne!(segment_instance_index, SegmentInstanceIndex::INVALID);
+ let (prim_cache_address, segments) = if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&prim_common_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_common_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, is_compositor_surface, .. } => {
+ if is_compositor_surface {
+ self.emit_placeholder(
+ prim_rect,
+ prim_info,
+ prim_vis_mask,
+ z_id,
+ transform_id,
+ batch_features,
+ ctx,
+ gpu_cache,
+ prim_headers,
+ render_tasks,
+ );
+ return;
+ }
+ let image_data = &ctx.data_stores.image[data_handle].kind;
+ let common_data = &ctx.data_stores.image[data_handle].common;
+ let image_instance = &ctx.prim_store.images[image_instance_index];
+ let specified_blend_mode = match image_data.alpha_type {
+ AlphaType::PremultipliedAlpha => BlendMode::PremultipliedAlpha,
+ AlphaType::Alpha => BlendMode::Alpha,
+ };
+ let request = ImageRequest {
+ key: image_data.key,
+ rendering: image_data.image_rendering,
+ tile: None,
+ };
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: image_data.alpha_type,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+ if image_instance.visible_tiles.is_empty() {
+ if cfg!(debug_assertions) {
+ match ctx.resource_cache.get_image_properties(request.key) {
+ Some(ImageProperties { tiling: None, .. }) | None => (),
+ other => panic!("Non-tiled image with no visible images detected! Properties {:?}", other),
+ }
+ }
+
+ let cache_item = match image_data.source {
+ ImageSource::Default => {
+ resolve_image(
+ request,
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ )
+ }
+ ImageSource::Cache { ref handle, .. } => {
+ let rt_handle = handle
+ .as_ref()
+ .expect("bug: render task handle not allocated");
+ let rt_cache_entry = ctx.resource_cache
+ .get_cached_render_task(rt_handle);
+ ctx.resource_cache.get_texture_cache_item(&rt_cache_entry.handle)
+ }
+ };
+
+ if cache_item.texture_id == TextureSource::Invalid {
+ return;
+ }
+
+ let textures = TextureSet::prim_textured(
+ cache_item.texture_id,
+ );
+
+ let non_segmented_blend_mode = if !common_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::Image(cache_item.texture_id.image_buffer_kind()),
+ textures,
+ prim_user_data,
+ cache_item.uv_rect_handle.as_int(gpu_cache),
+ );
+
+ debug_assert_ne!(image_instance.segment_instance_index, SegmentInstanceIndex::INVALID);
+ let (prim_cache_address, segments) = if image_instance.segment_instance_index == SegmentInstanceIndex::UNUSED {
+ (gpu_cache.get_address(&common_data.gpu_cache_handle), None)
+ } else {
+ let segment_instance = &ctx.scratch.segment_instances[image_instance.segment_instance_index];
+ let segments = Some(&ctx.scratch.segments[segment_instance.segments_range]);
+ (gpu_cache.get_address(&segment_instance.gpu_cache_handle), segments)
+ };
+
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: prim_cache_address,
+ transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ common_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ const VECS_PER_SPECIFIC_BRUSH: usize = 3;
+ let max_tiles_per_header = (MAX_VERTEX_TEXTURE_WIDTH - VECS_PER_SPECIFIC_BRUSH) / VECS_PER_SEGMENT;
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ // use temporary block storage since we don't know the number of visible tiles beforehand
+ let mut gpu_blocks = Vec::<GpuBlockData>::with_capacity(3 + max_tiles_per_header * 2);
+ for chunk in image_instance.visible_tiles.chunks(max_tiles_per_header) {
+ gpu_blocks.clear();
+ gpu_blocks.push(PremultipliedColorF::WHITE.into()); //color
+ gpu_blocks.push(PremultipliedColorF::WHITE.into()); //bg color
+ gpu_blocks.push([-1.0, 0.0, 0.0, 0.0].into()); //stretch size
+ // negative first value makes the shader code ignore it and use the local size instead
+ for tile in chunk {
+ let tile_rect = tile.local_rect.translate(-prim_rect.origin.to_vector());
+ gpu_blocks.push(tile_rect.into());
+ gpu_blocks.push(GpuBlockData::EMPTY);
+ }
+
+ let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
+ let prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: image_instance.tight_local_clip_rect,
+ specific_prim_address: gpu_cache.get_address(&gpu_handle),
+ transform_id,
+ };
+ let prim_header_index = prim_headers.push(&prim_header, z_id, prim_user_data);
+
+ for (i, tile) in chunk.iter().enumerate() {
+ if let Some((batch_kind, textures, uv_rect_address)) = get_image_tile_params(
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ request.with_tile(tile.tile_offset),
+ clip_mask_texture_id,
+ ) {
+ let batch_key = BatchKey {
+ blend_mode: specified_blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ i as i32,
+ tile.edge_flags,
+ clip_task_address,
+ BrushFlags::SEGMENT_RELATIVE | BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ }
+ }
+ }
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, gradient_index, .. } => {
+ let gradient = &ctx.prim_store.linear_gradients[gradient_index];
+ let prim_data = &ctx.data_stores.linear_grad[data_handle];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+
+ let mut prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id,
+ };
+
+ let non_segmented_blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ if !gradient.cache_segments.is_empty() {
+
+ for segment in &gradient.cache_segments {
+ let ref cache_handle = segment.handle;
+ let rt_cache_entry = ctx.resource_cache
+ .get_cached_render_task(cache_handle);
+ let cache_item = ctx.resource_cache
+ .get_texture_cache_item(&rt_cache_entry.handle);
+
+ if cache_item.texture_id == TextureSource::Invalid {
+ return;
+ }
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let textures = BatchTextures::prim_textured(
+ cache_item.texture_id,
+ clip_mask_texture_id,
+ );
+ let batch_kind = BrushBatchKind::Image(
+ cache_item.texture_id.image_buffer_kind()
+ );
+ let prim_user_data = ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Local,
+ opacity: 1.0,
+ }.encode();
+
+ let specific_resource_address = cache_item.uv_rect_handle.as_int(gpu_cache);
+ prim_header.specific_prim_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
+
+ let segment_local_clip_rect = match prim_header.local_clip_rect.intersection(&segment.local_rect) {
+ Some(rect) => rect,
+ None => { continue; }
+ };
+
+ let segment_prim_header = PrimitiveHeader {
+ local_rect: segment.local_rect,
+ local_clip_rect: segment_local_clip_rect,
+ specific_prim_address: prim_header.specific_prim_address,
+ transform_id: prim_header.transform_id,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &segment_prim_header,
+ z_id,
+ prim_user_data,
+ );
+
+ let batch_key = BatchKey {
+ blend_mode: non_segmented_blend_mode,
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ specific_resource_address,
+ prim_vis_mask,
+ );
+ }
+ } else if gradient.visible_tiles_range.is_empty() {
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::LinearGradient,
+ TextureSet::UNTEXTURED,
+ [
+ prim_data.stops_handle.as_int(gpu_cache),
+ 0,
+ 0,
+ 0,
+ ],
+ 0,
+ );
+
+ prim_header.specific_prim_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(prim_data.brush_segments.as_slice())
+ };
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[gradient.visible_tiles_range];
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ self.add_gradient_tiles(
+ visible_tiles,
+ &prim_data.stops_handle,
+ BrushBatchKind::LinearGradient,
+ specified_blend_mode,
+ batch_features,
+ bounding_rect,
+ clip_task_address,
+ gpu_cache,
+ &prim_header,
+ prim_headers,
+ z_id,
+ prim_vis_mask,
+ clip_mask_texture_id,
+ );
+ }
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.radial_grad[data_handle];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+
+ let mut prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id,
+ };
+
+ if visible_tiles_range.is_empty() {
+ let non_segmented_blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::RadialGradient,
+ TextureSet::UNTEXTURED,
+ [
+ prim_data.stops_handle.as_int(gpu_cache),
+ 0,
+ 0,
+ 0,
+ ],
+ 0,
+ );
+
+ prim_header.specific_prim_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(prim_data.brush_segments.as_slice())
+ };
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ self.add_gradient_tiles(
+ visible_tiles,
+ &prim_data.stops_handle,
+ BrushBatchKind::RadialGradient,
+ specified_blend_mode,
+ batch_features,
+ bounding_rect,
+ clip_task_address,
+ gpu_cache,
+ &prim_header,
+ prim_headers,
+ z_id,
+ prim_vis_mask,
+ clip_mask_texture_id,
+ );
+ }
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, ref visible_tiles_range, .. } => {
+ let prim_data = &ctx.data_stores.conic_grad[data_handle];
+ let specified_blend_mode = BlendMode::PremultipliedAlpha;
+
+ let mut prim_header = PrimitiveHeader {
+ local_rect: prim_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ specific_prim_address: GpuCacheAddress::INVALID,
+ transform_id,
+ };
+
+ if visible_tiles_range.is_empty() {
+ let non_segmented_blend_mode = if !prim_data.opacity.is_opaque ||
+ prim_info.clip_task_index != ClipTaskIndex::INVALID ||
+ transform_kind == TransformedRectKind::Complex
+ {
+ specified_blend_mode
+ } else {
+ BlendMode::None
+ };
+
+ let batch_params = BrushBatchParameters::shared(
+ BrushBatchKind::ConicGradient,
+ TextureSet::UNTEXTURED,
+ [
+ prim_data.stops_handle.as_int(gpu_cache),
+ 0,
+ 0,
+ 0,
+ ],
+ 0,
+ );
+
+ prim_header.specific_prim_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ batch_params.prim_user_data,
+ );
+
+ let segments = if prim_data.brush_segments.is_empty() {
+ None
+ } else {
+ Some(prim_data.brush_segments.as_slice())
+ };
+
+ self.add_segmented_prim_to_batch(
+ segments,
+ prim_data.opacity,
+ &batch_params,
+ specified_blend_mode,
+ non_segmented_blend_mode,
+ batch_features,
+ prim_header_index,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_info.clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ } else {
+ let visible_tiles = &ctx.scratch.gradient_tiles[*visible_tiles_range];
+
+ let (clip_task_address, clip_mask_texture_id) = ctx.get_prim_clip_task_and_texture(
+ prim_info.clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ self.add_gradient_tiles(
+ visible_tiles,
+ &prim_data.stops_handle,
+ BrushBatchKind::ConicGradient,
+ specified_blend_mode,
+ batch_features,
+ bounding_rect,
+ clip_task_address,
+ gpu_cache,
+ &prim_header,
+ prim_headers,
+ z_id,
+ prim_vis_mask,
+ clip_mask_texture_id,
+ );
+ }
+ }
+ PrimitiveInstanceKind::Backdrop { data_handle } => {
+ let prim_data = &ctx.data_stores.backdrop[data_handle];
+ let backdrop_pic_index = prim_data.kind.pic_index;
+ let backdrop_surface_index = ctx.prim_store.pictures[backdrop_pic_index.0]
+ .raster_config
+ .as_ref()
+ .expect("backdrop surface should be alloc by now")
+ .surface_index;
+ let backdrop_task_id = ctx.resolve_surface(backdrop_surface_index);
+
+ let (backdrop_uv_rect_address, textures) = render_tasks.resolve_surface(
+ backdrop_task_id,
+ TextureSource::Invalid,
+ gpu_cache,
+ );
+
+ let batch_key = BatchKey::new(
+ BatchKind::Brush(BrushBatchKind::Image(ImageBufferKind::Texture2D)),
+ BlendMode::PremultipliedAlpha,
+ textures,
+ );
+
+ let prim_cache_address = gpu_cache.get_address(&ctx.globals.default_image_handle);
+ let backdrop_picture = &ctx.prim_store.pictures[backdrop_pic_index.0];
+ let prim_header = PrimitiveHeader {
+ local_rect: backdrop_picture.precise_local_rect,
+ local_clip_rect: prim_info.combined_local_clip_rect,
+ transform_id,
+ specific_prim_address: prim_cache_address,
+ };
+
+ let prim_header_index = prim_headers.push(
+ &prim_header,
+ z_id,
+ ImageBrushData {
+ color_mode: ShaderColorMode::Image,
+ alpha_type: AlphaType::PremultipliedAlpha,
+ raster_space: RasterizationSpace::Screen,
+ opacity: 1.0,
+ }.encode(),
+ );
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ batch_features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::empty(),
+ OPAQUE_TASK_ADDRESS,
+ BrushFlags::empty(),
+ prim_header_index,
+ backdrop_uv_rect_address.as_int(),
+ prim_vis_mask,
+ );
+ }
+ }
+ }
+
+ /// Add a single segment instance to a batch.
+ fn add_segment_to_batch(
+ &mut self,
+ segment: &BrushSegment,
+ segment_data: &SegmentInstanceData,
+ segment_index: i32,
+ batch_kind: BrushBatchKind,
+ prim_header_index: PrimitiveHeaderIndex,
+ alpha_blend_mode: BlendMode,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ transform_kind: TransformedRectKind,
+ z_id: ZBufferId,
+ prim_opacity: PrimitiveOpacity,
+ clip_task_index: ClipTaskIndex,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ ctx: &RenderTargetContext,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ debug_assert!(clip_task_index != ClipTaskIndex::INVALID);
+
+ // Get GPU address of clip task for this segment, or None if
+ // the entire segment is clipped out.
+ if let Some((clip_task_address, clip_mask)) = ctx.get_clip_task_and_texture(
+ clip_task_index,
+ segment_index,
+ render_tasks,
+ ) {
+ // If a got a valid (or OPAQUE) clip task address, add the segment.
+ let is_inner = segment.edge_flags.is_empty();
+ let needs_blending = !prim_opacity.is_opaque ||
+ clip_task_address != OPAQUE_TASK_ADDRESS ||
+ (!is_inner && transform_kind == TransformedRectKind::Complex);
+
+ let textures = BatchTextures {
+ input: segment_data.textures,
+ clip_mask,
+ };
+
+ let batch_key = BatchKey {
+ blend_mode: if needs_blending { alpha_blend_mode } else { BlendMode::None },
+ kind: BatchKind::Brush(batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ segment_index,
+ segment.edge_flags,
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION | segment.brush_flags,
+ prim_header_index,
+ segment_data.specific_resource_address,
+ prim_vis_mask,
+ );
+ }
+ }
+
+ /// Add any segment(s) from a brush to batches.
+ fn add_segmented_prim_to_batch(
+ &mut self,
+ brush_segments: Option<&[BrushSegment]>,
+ prim_opacity: PrimitiveOpacity,
+ params: &BrushBatchParameters,
+ alpha_blend_mode: BlendMode,
+ non_segmented_blend_mode: BlendMode,
+ features: BatchFeatures,
+ prim_header_index: PrimitiveHeaderIndex,
+ bounding_rect: &PictureRect,
+ transform_kind: TransformedRectKind,
+ z_id: ZBufferId,
+ clip_task_index: ClipTaskIndex,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ ctx: &RenderTargetContext,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ match (brush_segments, &params.segment_data) {
+ (Some(ref brush_segments), SegmentDataKind::Instanced(ref segment_data)) => {
+ // In this case, we have both a list of segments, and a list of
+ // per-segment instance data. Zip them together to build batches.
+ debug_assert_eq!(brush_segments.len(), segment_data.len());
+ for (segment_index, (segment, segment_data)) in brush_segments
+ .iter()
+ .zip(segment_data.iter())
+ .enumerate()
+ {
+ self.add_segment_to_batch(
+ segment,
+ segment_data,
+ segment_index as i32,
+ params.batch_kind,
+ prim_header_index,
+ alpha_blend_mode,
+ features,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_opacity,
+ clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ }
+ (Some(ref brush_segments), SegmentDataKind::Shared(ref segment_data)) => {
+ // A list of segments, but the per-segment data is common
+ // between all segments.
+ for (segment_index, segment) in brush_segments
+ .iter()
+ .enumerate()
+ {
+ self.add_segment_to_batch(
+ segment,
+ segment_data,
+ segment_index as i32,
+ params.batch_kind,
+ prim_header_index,
+ alpha_blend_mode,
+ features,
+ bounding_rect,
+ transform_kind,
+ z_id,
+ prim_opacity,
+ clip_task_index,
+ prim_vis_mask,
+ ctx,
+ render_tasks,
+ );
+ }
+ }
+ (None, SegmentDataKind::Shared(ref segment_data)) => {
+ // No segments, and thus no per-segment instance data.
+ // Note: the blend mode already takes opacity into account
+
+ let (clip_task_address, clip_mask) = ctx.get_prim_clip_task_and_texture(
+ clip_task_index,
+ render_tasks,
+ ).unwrap();
+
+ let textures = BatchTextures {
+ input: segment_data.textures,
+ clip_mask,
+ };
+
+ let batch_key = BatchKey {
+ blend_mode: non_segmented_blend_mode,
+ kind: BatchKind::Brush(params.batch_kind),
+ textures,
+ };
+
+ self.add_brush_instance_to_batches(
+ batch_key,
+ features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ segment_data.specific_resource_address,
+ prim_vis_mask,
+ );
+ }
+ (None, SegmentDataKind::Instanced(..)) => {
+ // We should never hit the case where there are no segments,
+ // but a list of segment instance data.
+ unreachable!();
+ }
+ }
+ }
+
+ fn add_gradient_tiles(
+ &mut self,
+ visible_tiles: &[VisibleGradientTile],
+ stops_handle: &GpuCacheHandle,
+ kind: BrushBatchKind,
+ blend_mode: BlendMode,
+ features: BatchFeatures,
+ bounding_rect: &PictureRect,
+ clip_task_address: RenderTaskAddress,
+ gpu_cache: &GpuCache,
+ base_prim_header: &PrimitiveHeader,
+ prim_headers: &mut PrimitiveHeaders,
+ z_id: ZBufferId,
+ prim_vis_mask: PrimitiveVisibilityMask,
+ clip_mask_texture_id: TextureSource,
+ ) {
+ let key = BatchKey {
+ blend_mode,
+ kind: BatchKind::Brush(kind),
+ textures: BatchTextures::prim_untextured(clip_mask_texture_id),
+ };
+
+ let user_data = [stops_handle.as_int(gpu_cache), 0, 0, 0];
+
+ for tile in visible_tiles {
+ let prim_header = PrimitiveHeader {
+ specific_prim_address: gpu_cache.get_address(&tile.handle),
+ local_rect: tile.local_rect,
+ local_clip_rect: tile.local_clip_rect,
+ ..*base_prim_header
+ };
+ let prim_header_index = prim_headers.push(&prim_header, z_id, user_data);
+
+ self.add_brush_instance_to_batches(
+ key,
+ features,
+ bounding_rect,
+ z_id,
+ INVALID_SEGMENT_INDEX,
+ EdgeAaSegmentMask::all(),
+ clip_task_address,
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ prim_header_index,
+ 0,
+ prim_vis_mask,
+ );
+ }
+ }
+}
+
+fn get_image_tile_params(
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ request: ImageRequest,
+ clip_mask_texture_id: TextureSource,
+) -> Option<(BrushBatchKind, BatchTextures, GpuCacheAddress)> {
+
+ let cache_item = resolve_image(
+ request,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ if cache_item.texture_id == TextureSource::Invalid {
+ None
+ } else {
+ let textures = BatchTextures::prim_textured(
+ cache_item.texture_id,
+ clip_mask_texture_id,
+ );
+ Some((
+ BrushBatchKind::Image(cache_item.texture_id.image_buffer_kind()),
+ textures,
+ gpu_cache.get_address(&cache_item.uv_rect_handle),
+ ))
+ }
+}
+
+/// Either a single texture / user data for all segments,
+/// or a list of one per segment.
+enum SegmentDataKind {
+ Shared(SegmentInstanceData),
+ Instanced(SmallVec<[SegmentInstanceData; 8]>),
+}
+
+/// The parameters that are specific to a kind of brush,
+/// used by the common method to add a brush to batches.
+struct BrushBatchParameters {
+ batch_kind: BrushBatchKind,
+ prim_user_data: [i32; 4],
+ segment_data: SegmentDataKind,
+}
+
+impl BrushBatchParameters {
+ /// This brush instance has a list of per-segment
+ /// instance data.
+ fn instanced(
+ batch_kind: BrushBatchKind,
+ prim_user_data: [i32; 4],
+ segment_data: SmallVec<[SegmentInstanceData; 8]>,
+ ) -> Self {
+ BrushBatchParameters {
+ batch_kind,
+ prim_user_data,
+ segment_data: SegmentDataKind::Instanced(segment_data),
+ }
+ }
+
+ /// This brush instance shares the per-segment data
+ /// across all segments.
+ fn shared(
+ batch_kind: BrushBatchKind,
+ textures: TextureSet,
+ prim_user_data: [i32; 4],
+ specific_resource_address: i32,
+ ) -> Self {
+ BrushBatchParameters {
+ batch_kind,
+ prim_user_data,
+ segment_data: SegmentDataKind::Shared(
+ SegmentInstanceData {
+ textures,
+ specific_resource_address,
+ }
+ ),
+ }
+ }
+}
+
+impl RenderTaskGraph {
+ fn resolve_surface(
+ &self,
+ task_id: RenderTaskId,
+ clip_mask: TextureSource,
+ gpu_cache: &GpuCache,
+ ) -> (GpuCacheAddress, BatchTextures) {
+ let task = &self[task_id];
+
+ (
+ task.get_texture_address(gpu_cache),
+ BatchTextures::prim_textured(
+ TextureSource::TextureCache(
+ task.get_target_texture(),
+ Swizzle::default(),
+ ),
+ clip_mask,
+ ),
+ )
+ }
+}
+
+pub fn resolve_image(
+ request: ImageRequest,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+) -> CacheItem {
+ match resource_cache.get_image_properties(request.key) {
+ Some(image_properties) => {
+ // Check if an external image that needs to be resolved
+ // by the render thread.
+ match image_properties.external_image {
+ Some(external_image) => {
+ // This is an external texture - we will add it to
+ // the deferred resolves list to be patched by
+ // the render thread...
+ let cache_handle = gpu_cache.push_deferred_per_frame_blocks(BLOCKS_PER_UV_RECT);
+
+ let deferred_resolve_index = DeferredResolveIndex(deferred_resolves.len() as u32);
+
+ let image_buffer_kind = match external_image.image_type {
+ ExternalImageType::TextureHandle(target) => {
+ target
+ }
+ ExternalImageType::Buffer => {
+ // The ExternalImageType::Buffer should be handled by resource_cache.
+ // It should go through the non-external case.
+ panic!("Unexpected non-texture handle type");
+ }
+ };
+
+ let cache_item = CacheItem {
+ texture_id: TextureSource::External(deferred_resolve_index, image_buffer_kind),
+ uv_rect_handle: cache_handle,
+ uv_rect: DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ image_properties.descriptor.size,
+ ),
+ texture_layer: 0,
+ user_data: [0.0, 0.0, 0.0],
+ };
+
+ deferred_resolves.push(DeferredResolve {
+ image_properties,
+ address: gpu_cache.get_address(&cache_handle),
+ rendering: request.rendering,
+ });
+
+ cache_item
+ }
+ None => {
+ if let Ok(cache_item) = resource_cache.get_cached_image(request) {
+ cache_item
+ } else {
+ // There is no usable texture entry for the image key. Just return an invalid texture here.
+ CacheItem::invalid()
+ }
+ }
+ }
+ }
+ None => {
+ CacheItem::invalid()
+ }
+ }
+}
+
+/// A list of clip instances to be drawn into a target.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipBatchList {
+ /// Rectangle draws fill up the rectangles with rounded corners.
+ pub slow_rectangles: Vec<ClipMaskInstanceRect>,
+ pub fast_rectangles: Vec<ClipMaskInstanceRect>,
+ /// Image draws apply the image masking.
+ pub images: FastHashMap<TextureSource, Vec<ClipMaskInstanceImage>>,
+ pub box_shadows: FastHashMap<TextureSource, Vec<ClipMaskInstanceBoxShadow>>,
+}
+
+impl ClipBatchList {
+ fn new() -> Self {
+ ClipBatchList {
+ slow_rectangles: Vec::new(),
+ fast_rectangles: Vec::new(),
+ images: FastHashMap::default(),
+ box_shadows: FastHashMap::default(),
+ }
+ }
+}
+
+/// Batcher managing draw calls into the clip mask (in the RT cache).
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipBatcher {
+ /// The first clip in each clip task. This will overwrite all pixels
+ /// in the clip region, so we can skip doing a clear and write with
+ /// blending disabled, which is a big performance win on Intel GPUs.
+ pub primary_clips: ClipBatchList,
+ /// Any subsequent clip masks (rare) for a clip task get drawn in
+ /// a second pass with multiplicative blending enabled.
+ pub secondary_clips: ClipBatchList,
+
+ gpu_supports_fast_clears: bool,
+}
+
+impl ClipBatcher {
+ pub fn new(
+ gpu_supports_fast_clears: bool,
+ ) -> Self {
+ ClipBatcher {
+ primary_clips: ClipBatchList::new(),
+ secondary_clips: ClipBatchList::new(),
+ gpu_supports_fast_clears,
+ }
+ }
+
+ pub fn add_clip_region(
+ &mut self,
+ local_pos: LayoutPoint,
+ sub_rect: DeviceRect,
+ clip_data: ClipData,
+ task_origin: DevicePoint,
+ screen_origin: DevicePoint,
+ device_pixel_scale: f32,
+ ) {
+ let instance = ClipMaskInstanceRect {
+ common: ClipMaskInstanceCommon {
+ clip_transform_id: TransformPaletteId::IDENTITY,
+ prim_transform_id: TransformPaletteId::IDENTITY,
+ sub_rect,
+ task_origin,
+ screen_origin,
+ device_pixel_scale,
+ },
+ local_pos,
+ clip_data,
+ };
+
+ self.primary_clips.slow_rectangles.push(instance);
+ }
+
+ /// Where appropriate, draw a clip rectangle as a small series of tiles,
+ /// instead of one large rectangle.
+ fn add_tiled_clip_mask(
+ &mut self,
+ mask_screen_rect: DeviceRect,
+ local_clip_rect: LayoutRect,
+ clip_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ world_rect: &WorldRect,
+ device_pixel_scale: DevicePixelScale,
+ common: &ClipMaskInstanceCommon,
+ is_first_clip: bool,
+ ) -> bool {
+ // Only try to draw in tiles if the clip mark is big enough.
+ if mask_screen_rect.area() < CLIP_RECTANGLE_AREA_THRESHOLD {
+ return false;
+ }
+
+ let mask_screen_rect_size = mask_screen_rect.size.to_i32();
+ let clip_spatial_node = &spatial_tree
+ .spatial_nodes[clip_spatial_node_index.0 as usize];
+
+ // Only support clips that are axis-aligned to the root coordinate space,
+ // for now, to simplify the logic below. This handles the vast majority
+ // of real world cases, but could be expanded in future if needed.
+ if clip_spatial_node.coordinate_system_id != CoordinateSystemId::root() {
+ return false;
+ }
+
+ // Get the world rect of the clip rectangle. If we can't transform it due
+ // to the matrix, just fall back to drawing the entire clip mask.
+ let transform = spatial_tree.get_world_transform(
+ clip_spatial_node_index,
+ );
+ let world_clip_rect = match project_rect(
+ &transform.into_transform(),
+ &local_clip_rect,
+ world_rect,
+ ) {
+ Some(rect) => rect,
+ None => return false,
+ };
+
+ // Work out how many tiles to draw this clip mask in, stretched across the
+ // device rect of the primitive clip mask.
+ let world_device_rect = world_clip_rect * device_pixel_scale;
+ let x_tiles = (mask_screen_rect_size.width + CLIP_RECTANGLE_TILE_SIZE-1) / CLIP_RECTANGLE_TILE_SIZE;
+ let y_tiles = (mask_screen_rect_size.height + CLIP_RECTANGLE_TILE_SIZE-1) / CLIP_RECTANGLE_TILE_SIZE;
+
+ // Because we only run this code path for axis-aligned rects (the root coord system check above),
+ // and only for rectangles (not rounded etc), the world_device_rect is not conservative - we know
+ // that there is no inner_rect, and the world_device_rect should be the real, axis-aligned clip rect.
+ let mask_origin = mask_screen_rect.origin.to_vector();
+ let clip_list = self.get_batch_list(is_first_clip);
+
+ for y in 0 .. y_tiles {
+ for x in 0 .. x_tiles {
+ let p0 = DeviceIntPoint::new(
+ x * CLIP_RECTANGLE_TILE_SIZE,
+ y * CLIP_RECTANGLE_TILE_SIZE,
+ );
+ let p1 = DeviceIntPoint::new(
+ (p0.x + CLIP_RECTANGLE_TILE_SIZE).min(mask_screen_rect_size.width),
+ (p0.y + CLIP_RECTANGLE_TILE_SIZE).min(mask_screen_rect_size.height),
+ );
+ let normalized_sub_rect = DeviceIntRect::new(
+ p0,
+ DeviceIntSize::new(
+ p1.x - p0.x,
+ p1.y - p0.y,
+ ),
+ ).to_f32();
+ let world_sub_rect = normalized_sub_rect.translate(mask_origin);
+
+ // If the clip rect completely contains this tile rect, then drawing
+ // these pixels would be redundant - since this clip can't possibly
+ // affect the pixels in this tile, skip them!
+ if !world_device_rect.contains_rect(&world_sub_rect) {
+ clip_list.slow_rectangles.push(ClipMaskInstanceRect {
+ common: ClipMaskInstanceCommon {
+ sub_rect: normalized_sub_rect,
+ ..*common
+ },
+ local_pos: local_clip_rect.origin,
+ clip_data: ClipData::uniform(local_clip_rect.size, 0.0, ClipMode::Clip),
+ });
+ }
+ }
+ }
+
+ true
+ }
+
+ /// Retrieve the correct clip batch list to append to, depending
+ /// on whether this is the first clip mask for a clip task.
+ fn get_batch_list(
+ &mut self,
+ is_first_clip: bool,
+ ) -> &mut ClipBatchList {
+ if is_first_clip && !self.gpu_supports_fast_clears {
+ &mut self.primary_clips
+ } else {
+ &mut self.secondary_clips
+ }
+ }
+
+ pub fn add(
+ &mut self,
+ clip_node_range: ClipNodeRange,
+ root_spatial_node_index: SpatialNodeIndex,
+ resource_cache: &ResourceCache,
+ gpu_cache: &GpuCache,
+ clip_store: &ClipStore,
+ spatial_tree: &SpatialTree,
+ transforms: &mut TransformPalette,
+ clip_data_store: &ClipDataStore,
+ actual_rect: DeviceRect,
+ world_rect: &WorldRect,
+ device_pixel_scale: DevicePixelScale,
+ task_origin: DevicePoint,
+ screen_origin: DevicePoint,
+ ) {
+ let mut is_first_clip = true;
+
+ for i in 0 .. clip_node_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+ let clip_node = &clip_data_store[clip_instance.handle];
+
+ let clip_transform_id = transforms.get_id(
+ clip_instance.spatial_node_index,
+ ROOT_SPATIAL_NODE_INDEX,
+ spatial_tree,
+ );
+
+ let prim_transform_id = transforms.get_id(
+ root_spatial_node_index,
+ ROOT_SPATIAL_NODE_INDEX,
+ spatial_tree,
+ );
+
+ let common = ClipMaskInstanceCommon {
+ sub_rect: DeviceRect::new(
+ DevicePoint::zero(),
+ actual_rect.size,
+ ),
+ task_origin,
+ screen_origin,
+ device_pixel_scale: device_pixel_scale.0,
+ clip_transform_id,
+ prim_transform_id,
+ };
+
+ let added_clip = match clip_node.item.kind {
+ ClipItemKind::Image { image, rect, .. } => {
+ let request = ImageRequest {
+ key: image,
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+
+ let mut add_image = |request: ImageRequest, local_tile_rect: LayoutRect, sub_rect: DeviceRect| {
+ let cache_item = match resource_cache.get_cached_image(request) {
+ Ok(item) => item,
+ Err(..) => {
+ warn!("Warnings: skip a image mask");
+ debug!("request: {:?}", request);
+ return;
+ }
+ };
+
+ self.get_batch_list(is_first_clip)
+ .images
+ .entry(cache_item.texture_id)
+ .or_insert_with(Vec::new)
+ .push(ClipMaskInstanceImage {
+ common: ClipMaskInstanceCommon {
+ sub_rect,
+ ..common
+ },
+ resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+ tile_rect: local_tile_rect,
+ local_rect: rect,
+ });
+ };
+
+ match clip_instance.visible_tiles {
+ Some(ref tiles) => {
+ let clip_spatial_node = &spatial_tree.spatial_nodes[clip_instance.spatial_node_index.0 as usize];
+ let clip_is_axis_aligned = clip_spatial_node.coordinate_system_id == CoordinateSystemId::root();
+ let sub_rect_bounds = actual_rect.size.into();
+ let map_local_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ clip_instance.spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ for tile in tiles {
+ let tile_sub_rect = if clip_is_axis_aligned {
+ let tile_world_rect = map_local_to_world
+ .map(&tile.tile_rect)
+ .expect("bug: should always map as axis-aligned");
+ let tile_device_rect = tile_world_rect * device_pixel_scale;
+ tile_device_rect
+ .translate(-actual_rect.origin.to_vector())
+ .round_out()
+ .intersection(&sub_rect_bounds)
+ } else {
+ Some(common.sub_rect)
+ };
+
+ if let Some(tile_sub_rect) = tile_sub_rect {
+ assert!(sub_rect_bounds.contains_rect(&tile_sub_rect));
+ add_image(
+ request.with_tile(tile.tile_offset),
+ tile.tile_rect,
+ tile_sub_rect,
+ )
+ }
+ }
+ }
+ None => {
+ add_image(request, rect, common.sub_rect)
+ }
+ }
+
+ true
+ }
+ ClipItemKind::BoxShadow { ref source } => {
+ let rt_handle = source
+ .cache_handle
+ .as_ref()
+ .expect("bug: render task handle not allocated");
+ let rt_cache_entry = resource_cache
+ .get_cached_render_task(rt_handle);
+ let cache_item = resource_cache
+ .get_texture_cache_item(&rt_cache_entry.handle);
+ debug_assert_ne!(cache_item.texture_id, TextureSource::Invalid);
+
+ self.get_batch_list(is_first_clip)
+ .box_shadows
+ .entry(cache_item.texture_id)
+ .or_insert_with(Vec::new)
+ .push(ClipMaskInstanceBoxShadow {
+ common,
+ resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+ shadow_data: BoxShadowData {
+ src_rect_size: source.original_alloc_size,
+ clip_mode: source.clip_mode as i32,
+ stretch_mode_x: source.stretch_mode_x as i32,
+ stretch_mode_y: source.stretch_mode_y as i32,
+ dest_rect: source.prim_shadow_rect,
+ },
+ });
+
+ true
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut } => {
+ self.get_batch_list(is_first_clip)
+ .slow_rectangles
+ .push(ClipMaskInstanceRect {
+ common,
+ local_pos: rect.origin,
+ clip_data: ClipData::uniform(rect.size, 0.0, ClipMode::ClipOut),
+ });
+
+ true
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => {
+ if clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) {
+ false
+ } else {
+ if !self.add_tiled_clip_mask(
+ actual_rect,
+ rect,
+ clip_instance.spatial_node_index,
+ spatial_tree,
+ world_rect,
+ device_pixel_scale,
+ &common,
+ is_first_clip,
+ ) {
+ self.get_batch_list(is_first_clip)
+ .slow_rectangles
+ .push(ClipMaskInstanceRect {
+ common,
+ local_pos: rect.origin,
+ clip_data: ClipData::uniform(rect.size, 0.0, ClipMode::Clip),
+ });
+ }
+
+ true
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode, .. } => {
+ let batch_list = self.get_batch_list(is_first_clip);
+ let instance = ClipMaskInstanceRect {
+ common,
+ local_pos: rect.origin,
+ clip_data: ClipData::rounded_rect(rect.size, radius, mode),
+ };
+ if clip_instance.flags.contains(ClipNodeFlags::USE_FAST_PATH) {
+ batch_list.fast_rectangles.push(instance);
+ } else {
+ batch_list.slow_rectangles.push(instance);
+ }
+
+ true
+ }
+ };
+
+ is_first_clip &= !added_clip;
+ }
+ }
+}
+
+impl<'a, 'rc> RenderTargetContext<'a, 'rc> {
+ /// Retrieve the GPU task address for a given clip task instance.
+ /// Returns None if the segment was completely clipped out.
+ /// Returns Some(OPAQUE_TASK_ADDRESS) if no clip mask is needed.
+ /// Returns Some(task_address) if there was a valid clip mask.
+ fn get_clip_task_and_texture(
+ &self,
+ clip_task_index: ClipTaskIndex,
+ offset: i32,
+ render_tasks: &RenderTaskGraph,
+ ) -> Option<(RenderTaskAddress, TextureSource)> {
+ match self.scratch.clip_mask_instances[clip_task_index.0 as usize + offset as usize] {
+ ClipMaskKind::Mask(task_id) => {
+ Some((
+ task_id.into(),
+ TextureSource::TextureCache(
+ render_tasks[task_id].get_target_texture(),
+ Swizzle::default(),
+ )
+ ))
+ }
+ ClipMaskKind::None => {
+ Some((OPAQUE_TASK_ADDRESS, TextureSource::Invalid))
+ }
+ ClipMaskKind::Clipped => {
+ None
+ }
+ }
+ }
+
+ /// Helper function to get the clip task address for a
+ /// non-segmented primitive.
+ fn get_prim_clip_task_and_texture(
+ &self,
+ clip_task_index: ClipTaskIndex,
+ render_tasks: &RenderTaskGraph,
+ ) -> Option<(RenderTaskAddress, TextureSource)> {
+ self.get_clip_task_and_texture(
+ clip_task_index,
+ 0,
+ render_tasks,
+ )
+ }
+
+ /// Given a surface index, return the render task id for the output render
+ /// task of this surface.
+ fn resolve_surface(
+ &self,
+ surface_index: SurfaceIndex,
+ ) -> RenderTaskId {
+ let surface = self.surfaces[surface_index.0]
+ .render_tasks
+ .as_ref()
+ .expect("bug: no surface");
+ match surface {
+ SurfaceRenderTasks::Simple(task_id) => {
+ *task_id
+ }
+ SurfaceRenderTasks::Tiled(..) => {
+ // For now, the tiled picture cache surfaces are never used as a readback
+ // source. Soon, that will change when they are available as inputs to
+ // elements such as mix-blend and backdrop-filter.
+ unreachable!();
+ }
+ SurfaceRenderTasks::Chained { root_task_id, .. } => {
+ *root_task_id
+ }
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/border.rs b/gfx/wr/webrender/src/border.rs
new file mode 100644
index 0000000000..d37b5e6f3e
--- /dev/null
+++ b/gfx/wr/webrender/src/border.rs
@@ -0,0 +1,1492 @@
+/* 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 api::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU};
+use api::{NormalBorder as ApiNormalBorder, RepeatMode};
+use api::units::*;
+use crate::clip::ClipChainId;
+use crate::ellipse::Ellipse;
+use euclid::vec2;
+use crate::scene_building::SceneBuilder;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::gpu_types::{BorderInstance, BorderSegment, BrushFlags};
+use crate::prim_store::{BorderSegmentInfo, BrushSegment, NinePatchDescriptor};
+use crate::prim_store::borders::{NormalBorderPrim, NormalBorderData};
+use crate::util::{lerp, RectHelpers};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::segment::EdgeAaSegmentMask;
+
+// Using 2048 as the maximum radius in device space before which we
+// start stretching is up for debate.
+// the value must be chosen so that the corners will not use an
+// unreasonable amount of memory but should allow crisp corners in the
+// common cases.
+
+/// Maximum resolution in device pixels at which borders are rasterized.
+pub const MAX_BORDER_RESOLUTION: u32 = 2048;
+/// Maximum number of dots or dashes per segment to avoid freezing and filling up
+/// memory with unreasonable inputs. It would be better to address this by not building
+/// a list of per-dot information in the first place.
+pub const MAX_DASH_COUNT: u32 = 2048;
+
+// TODO(gw): Perhaps there is a better way to store
+// the border cache key than duplicating
+// all the border structs with hashable
+// variants...
+
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderRadiusAu {
+ pub top_left: LayoutSizeAu,
+ pub top_right: LayoutSizeAu,
+ pub bottom_left: LayoutSizeAu,
+ pub bottom_right: LayoutSizeAu,
+}
+
+impl From<BorderRadius> for BorderRadiusAu {
+ fn from(radius: BorderRadius) -> BorderRadiusAu {
+ BorderRadiusAu {
+ top_left: radius.top_left.to_au(),
+ top_right: radius.top_right.to_au(),
+ bottom_right: radius.bottom_right.to_au(),
+ bottom_left: radius.bottom_left.to_au(),
+ }
+ }
+}
+
+impl From<BorderRadiusAu> for BorderRadius {
+ fn from(radius: BorderRadiusAu) -> Self {
+ BorderRadius {
+ top_left: LayoutSize::from_au(radius.top_left),
+ top_right: LayoutSize::from_au(radius.top_right),
+ bottom_right: LayoutSize::from_au(radius.bottom_right),
+ bottom_left: LayoutSize::from_au(radius.bottom_left),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderSideAu {
+ pub color: ColorU,
+ pub style: BorderStyle,
+}
+
+impl From<BorderSide> for BorderSideAu {
+ fn from(side: BorderSide) -> Self {
+ BorderSideAu {
+ color: side.color.into(),
+ style: side.style,
+ }
+ }
+}
+
+impl From<BorderSideAu> for BorderSide {
+ fn from(side: BorderSideAu) -> Self {
+ BorderSide {
+ color: side.color.into(),
+ style: side.style,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Hash, Eq, MallocSizeOf, PartialEq)]
+pub struct NormalBorderAu {
+ pub left: BorderSideAu,
+ pub right: BorderSideAu,
+ pub top: BorderSideAu,
+ pub bottom: BorderSideAu,
+ pub radius: BorderRadiusAu,
+ /// Whether to apply anti-aliasing on the border corners.
+ ///
+ /// Note that for this to be `false` and work, this requires the borders to
+ /// be solid, and no border-radius.
+ pub do_aa: bool,
+}
+
+impl NormalBorderAu {
+ // Construct a border based upon self with color
+ pub fn with_color(&self, color: ColorU) -> Self {
+ let mut b = self.clone();
+ b.left.color = color;
+ b.right.color = color;
+ b.top.color = color;
+ b.bottom.color = color;
+ b
+ }
+}
+
+impl From<ApiNormalBorder> for NormalBorderAu {
+ fn from(border: ApiNormalBorder) -> Self {
+ NormalBorderAu {
+ left: border.left.into(),
+ right: border.right.into(),
+ top: border.top.into(),
+ bottom: border.bottom.into(),
+ radius: border.radius.into(),
+ do_aa: border.do_aa,
+ }
+ }
+}
+
+impl From<NormalBorderAu> for ApiNormalBorder {
+ fn from(border: NormalBorderAu) -> Self {
+ ApiNormalBorder {
+ left: border.left.into(),
+ right: border.right.into(),
+ top: border.top.into(),
+ bottom: border.bottom.into(),
+ radius: border.radius.into(),
+ do_aa: border.do_aa,
+ }
+ }
+}
+
+/// Cache key that uniquely identifies a border
+/// segment in the render task cache.
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderSegmentCacheKey {
+ pub size: LayoutSizeAu,
+ pub radius: LayoutSizeAu,
+ pub side0: BorderSideAu,
+ pub side1: BorderSideAu,
+ pub segment: BorderSegment,
+ pub do_aa: bool,
+ pub h_adjacent_corner_outer: LayoutPointAu,
+ pub h_adjacent_corner_radius: LayoutSizeAu,
+ pub v_adjacent_corner_outer: LayoutPointAu,
+ pub v_adjacent_corner_radius: LayoutSizeAu,
+}
+
+pub fn ensure_no_corner_overlap(
+ radius: &mut BorderRadius,
+ size: LayoutSize,
+) {
+ let mut ratio = 1.0;
+ let top_left_radius = &mut radius.top_left;
+ let top_right_radius = &mut radius.top_right;
+ let bottom_right_radius = &mut radius.bottom_right;
+ let bottom_left_radius = &mut radius.bottom_left;
+
+ let sum = top_left_radius.width + top_right_radius.width;
+ if size.width < sum {
+ ratio = f32::min(ratio, size.width / sum);
+ }
+
+ let sum = bottom_left_radius.width + bottom_right_radius.width;
+ if size.width < sum {
+ ratio = f32::min(ratio, size.width / sum);
+ }
+
+ let sum = top_left_radius.height + bottom_left_radius.height;
+ if size.height < sum {
+ ratio = f32::min(ratio, size.height / sum);
+ }
+
+ let sum = top_right_radius.height + bottom_right_radius.height;
+ if size.height < sum {
+ ratio = f32::min(ratio, size.height / sum);
+ }
+
+ if ratio < 1. {
+ top_left_radius.width *= ratio;
+ top_left_radius.height *= ratio;
+
+ top_right_radius.width *= ratio;
+ top_right_radius.height *= ratio;
+
+ bottom_left_radius.width *= ratio;
+ bottom_left_radius.height *= ratio;
+
+ bottom_right_radius.width *= ratio;
+ bottom_right_radius.height *= ratio;
+ }
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn add_normal_border(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ border: &ApiNormalBorder,
+ widths: LayoutSideOffsets,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ ) {
+ let mut border = *border;
+ ensure_no_corner_overlap(&mut border.radius, info.rect.size);
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ NormalBorderPrim {
+ border: border.into(),
+ widths: widths.to_au(),
+ },
+ );
+ }
+}
+
+pub trait BorderSideHelpers {
+ fn border_color(&self, is_inner_border: bool) -> ColorF;
+}
+
+impl BorderSideHelpers for BorderSide {
+ fn border_color(&self, is_inner_border: bool) -> ColorF {
+ let lighter = match self.style {
+ BorderStyle::Inset => is_inner_border,
+ BorderStyle::Outset => !is_inner_border,
+ _ => return self.color,
+ };
+
+ // The modulate colors below are not part of the specification. They are
+ // derived from the Gecko source code and experimentation, and used to
+ // modulate the colors in order to generate colors for the inset/outset
+ // and groove/ridge border styles.
+ //
+ // NOTE(emilio): Gecko at least takes the background color into
+ // account, should we do the same? Looks a bit annoying for this.
+ //
+ // NOTE(emilio): If you change this algorithm, do the same change on
+ // get_colors_for_side in cs_border_segment.glsl.
+ if self.color.r != 0.0 || self.color.g != 0.0 || self.color.b != 0.0 {
+ let scale = if lighter { 1.0 } else { 2.0 / 3.0 };
+ return self.color.scale_rgb(scale)
+ }
+
+ let black = if lighter { 0.7 } else { 0.3 };
+ ColorF::new(black, black, black, self.color.a)
+ }
+}
+
+/// The kind of border corner clip.
+#[repr(C)]
+#[derive(Copy, Debug, Clone, PartialEq)]
+pub enum BorderClipKind {
+ DashCorner = 1,
+ DashEdge = 2,
+ Dot = 3,
+}
+
+fn compute_outer_and_clip_sign(
+ corner_segment: BorderSegment,
+ radius: DeviceSize,
+) -> (DevicePoint, DeviceVector2D) {
+ let outer_scale = match corner_segment {
+ BorderSegment::TopLeft => DeviceVector2D::new(0.0, 0.0),
+ BorderSegment::TopRight => DeviceVector2D::new(1.0, 0.0),
+ BorderSegment::BottomRight => DeviceVector2D::new(1.0, 1.0),
+ BorderSegment::BottomLeft => DeviceVector2D::new(0.0, 1.0),
+ _ => panic!("bug: expected a corner segment"),
+ };
+ let outer = DevicePoint::new(
+ outer_scale.x * radius.width,
+ outer_scale.y * radius.height,
+ );
+
+ let clip_sign = DeviceVector2D::new(
+ 1.0 - 2.0 * outer_scale.x,
+ 1.0 - 2.0 * outer_scale.y,
+ );
+
+ (outer, clip_sign)
+}
+
+fn write_dashed_corner_instances(
+ corner_radius: DeviceSize,
+ widths: DeviceSize,
+ segment: BorderSegment,
+ base_instance: &BorderInstance,
+ instances: &mut Vec<BorderInstance>,
+) -> Result<(), ()> {
+ let ellipse = Ellipse::new(corner_radius);
+
+ let average_border_width = 0.5 * (widths.width + widths.height);
+
+ let (_half_dash, num_half_dashes) =
+ compute_half_dash(average_border_width, ellipse.total_arc_length);
+
+ if num_half_dashes == 0 {
+ return Err(());
+ }
+
+ let num_half_dashes = num_half_dashes.min(MAX_DASH_COUNT);
+
+ let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);
+
+ let instance_count = num_half_dashes / 4 + 1;
+ instances.reserve(instance_count as usize);
+
+ let half_dash_arc_length =
+ ellipse.total_arc_length / num_half_dashes as f32;
+ let dash_length = 2. * half_dash_arc_length;
+
+ let mut current_length = 0.;
+ for i in 0..instance_count {
+ let arc_length0 = current_length;
+ current_length += if i == 0 {
+ half_dash_arc_length
+ } else {
+ dash_length
+ };
+
+ let arc_length1 = current_length;
+ current_length += dash_length;
+
+ let alpha = ellipse.find_angle_for_arc_length(arc_length0);
+ let beta = ellipse.find_angle_for_arc_length(arc_length1);
+
+ let (point0, tangent0) = ellipse.get_point_and_tangent(alpha);
+ let (point1, tangent1) = ellipse.get_point_and_tangent(beta);
+
+ let point0 = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - point0.x),
+ outer.y + clip_sign.y * (corner_radius.height - point0.y),
+ );
+
+ let tangent0 = DeviceVector2D::new(
+ -tangent0.x * clip_sign.x,
+ -tangent0.y * clip_sign.y,
+ );
+
+ let point1 = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - point1.x),
+ outer.y + clip_sign.y * (corner_radius.height - point1.y),
+ );
+
+ let tangent1 = DeviceVector2D::new(
+ -tangent1.x * clip_sign.x,
+ -tangent1.y * clip_sign.y,
+ );
+
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::DashCorner as i32) << 24),
+ clip_params: [
+ point0.x,
+ point0.y,
+ tangent0.x,
+ tangent0.y,
+ point1.x,
+ point1.y,
+ tangent1.x,
+ tangent1.y,
+ ],
+ .. *base_instance
+ });
+ }
+
+ Ok(())
+}
+
+fn write_dotted_corner_instances(
+ corner_radius: DeviceSize,
+ widths: DeviceSize,
+ segment: BorderSegment,
+ base_instance: &BorderInstance,
+ instances: &mut Vec<BorderInstance>,
+) -> Result<(), ()> {
+ let mut corner_radius = corner_radius;
+ if corner_radius.width < (widths.width / 2.0) {
+ corner_radius.width = 0.0;
+ }
+ if corner_radius.height < (widths.height / 2.0) {
+ corner_radius.height = 0.0;
+ }
+
+ let (ellipse, max_dot_count) =
+ if corner_radius.width == 0. && corner_radius.height == 0. {
+ (Ellipse::new(corner_radius), 1)
+ } else {
+ // The centers of dots follow an ellipse along the middle of the
+ // border radius.
+ let inner_radius = (corner_radius - widths * 0.5).abs();
+ let ellipse = Ellipse::new(inner_radius);
+
+ // Allocate a "worst case" number of dot clips. This can be
+ // calculated by taking the minimum edge radius, since that
+ // will result in the maximum number of dots along the path.
+ let min_diameter = widths.width.min(widths.height);
+
+ // Get the number of circles (assuming spacing of one diameter
+ // between dots).
+ let max_dot_count = 0.5 * ellipse.total_arc_length / min_diameter;
+
+ // Add space for one extra dot since they are centered at the
+ // start of the arc.
+ (ellipse, max_dot_count.ceil() as usize)
+ };
+
+ if max_dot_count == 0 {
+ return Err(());
+ }
+
+ if max_dot_count == 1 {
+ let dot_diameter = lerp(widths.width, widths.height, 0.5);
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: [
+ widths.width / 2.0, widths.height / 2.0, 0.5 * dot_diameter, 0.,
+ 0., 0., 0., 0.,
+ ],
+ .. *base_instance
+ });
+ return Ok(());
+ }
+
+ let max_dot_count = max_dot_count.min(MAX_DASH_COUNT as usize);
+
+ // FIXME(emilio): Should probably use SmallVec.
+ let mut forward_dots = Vec::with_capacity(max_dot_count / 2 + 1);
+ let mut back_dots = Vec::with_capacity(max_dot_count / 2 + 1);
+ let mut leftover_arc_length = 0.0;
+
+ // Alternate between adding dots at the start and end of the
+ // ellipse arc. This ensures that we always end up with an exact
+ // half dot at each end of the arc, to match up with the edges.
+ forward_dots.push(DotInfo::new(widths.width, widths.width));
+ back_dots.push(DotInfo::new(
+ ellipse.total_arc_length - widths.height,
+ widths.height,
+ ));
+
+ let (outer, clip_sign) = compute_outer_and_clip_sign(segment, corner_radius);
+ for dot_index in 0 .. max_dot_count {
+ let prev_forward_pos = *forward_dots.last().unwrap();
+ let prev_back_pos = *back_dots.last().unwrap();
+
+ // Select which end of the arc to place a dot from.
+ // This just alternates between the start and end of
+ // the arc, which ensures that there is always an
+ // exact half-dot at each end of the ellipse.
+ let going_forward = dot_index & 1 == 0;
+
+ let (next_dot_pos, leftover) = if going_forward {
+ let next_dot_pos =
+ prev_forward_pos.arc_pos + 2.0 * prev_forward_pos.diameter;
+ (next_dot_pos, prev_back_pos.arc_pos - next_dot_pos)
+ } else {
+ let next_dot_pos = prev_back_pos.arc_pos - 2.0 * prev_back_pos.diameter;
+ (next_dot_pos, next_dot_pos - prev_forward_pos.arc_pos)
+ };
+
+ // Use a lerp between each edge's dot
+ // diameter, based on the linear distance
+ // along the arc to get the diameter of the
+ // dot at this arc position.
+ let t = next_dot_pos / ellipse.total_arc_length;
+ let dot_diameter = lerp(widths.width, widths.height, t);
+
+ // If we can't fit a dot, bail out.
+ if leftover < dot_diameter {
+ leftover_arc_length = leftover;
+ break;
+ }
+
+ // We can place a dot!
+ let dot = DotInfo::new(next_dot_pos, dot_diameter);
+ if going_forward {
+ forward_dots.push(dot);
+ } else {
+ back_dots.push(dot);
+ }
+ }
+
+ // Now step through the dots, and distribute any extra
+ // leftover space on the arc between them evenly. Once
+ // the final arc position is determined, generate the correct
+ // arc positions and angles that get passed to the clip shader.
+ let number_of_dots = forward_dots.len() + back_dots.len();
+ let extra_space_per_dot = leftover_arc_length / (number_of_dots - 1) as f32;
+
+ let create_dot_data = |arc_length: f32, dot_radius: f32| -> [f32; 8] {
+ // Represents the GPU data for drawing a single dot to a clip mask. The order
+ // these are specified must stay in sync with the way this data is read in the
+ // dot clip shader.
+ let theta = ellipse.find_angle_for_arc_length(arc_length);
+ let (center, _) = ellipse.get_point_and_tangent(theta);
+
+ let center = DevicePoint::new(
+ outer.x + clip_sign.x * (corner_radius.width - center.x),
+ outer.y + clip_sign.y * (corner_radius.height - center.y),
+ );
+
+ [center.x, center.y, dot_radius, 0.0, 0.0, 0.0, 0.0, 0.0]
+ };
+
+ instances.reserve(number_of_dots);
+ for (i, dot) in forward_dots.iter().enumerate() {
+ let extra_dist = i as f32 * extra_space_per_dot;
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: create_dot_data(dot.arc_pos + extra_dist, 0.5 * dot.diameter),
+ .. *base_instance
+ });
+ }
+
+ for (i, dot) in back_dots.iter().enumerate() {
+ let extra_dist = i as f32 * extra_space_per_dot;
+ instances.push(BorderInstance {
+ flags: base_instance.flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: create_dot_data(dot.arc_pos - extra_dist, 0.5 * dot.diameter),
+ .. *base_instance
+ });
+ }
+
+ Ok(())
+}
+
+#[derive(Copy, Clone, Debug)]
+struct DotInfo {
+ arc_pos: f32,
+ diameter: f32,
+}
+
+impl DotInfo {
+ fn new(arc_pos: f32, diameter: f32) -> DotInfo {
+ DotInfo { arc_pos, diameter }
+ }
+}
+
+/// Information needed to place and draw a border edge.
+#[derive(Debug)]
+struct EdgeInfo {
+ /// Offset in local space to place the edge from origin.
+ local_offset: f32,
+ /// Size of the edge in local space.
+ local_size: f32,
+ /// Local stretch size for this edge (repeat past this).
+ stretch_size: f32,
+}
+
+impl EdgeInfo {
+ fn new(
+ local_offset: f32,
+ local_size: f32,
+ stretch_size: f32,
+ ) -> Self {
+ Self {
+ local_offset,
+ local_size,
+ stretch_size,
+ }
+ }
+}
+
+// Given a side width and the available space, compute the half-dash (half of
+// the 'on' segment) and the count of them for a given segment.
+fn compute_half_dash(side_width: f32, total_size: f32) -> (f32, u32) {
+ let half_dash = side_width * 1.5;
+ let num_half_dashes = (total_size / half_dash).ceil() as u32;
+
+ if num_half_dashes == 0 {
+ return (0., 0);
+ }
+
+ // TODO(emilio): Gecko has some other heuristics here to start with a full
+ // dash when the border side is zero, for example. We might consider those
+ // in the future.
+ let num_half_dashes = if num_half_dashes % 4 != 0 {
+ num_half_dashes + 4 - num_half_dashes % 4
+ } else {
+ num_half_dashes
+ };
+
+ let half_dash = total_size / num_half_dashes as f32;
+ (half_dash, num_half_dashes)
+}
+
+
+// Get the needed size in device pixels for an edge,
+// based on the border style of that edge. This is used
+// to determine how big the render task should be.
+fn get_edge_info(
+ style: BorderStyle,
+ side_width: f32,
+ avail_size: f32,
+) -> EdgeInfo {
+ // To avoid division by zero below.
+ if side_width <= 0.0 || avail_size <= 0.0 {
+ return EdgeInfo::new(0.0, 0.0, 0.0);
+ }
+
+ match style {
+ BorderStyle::Dashed => {
+ // Basically, two times the dash size.
+ let (half_dash, _num_half_dashes) =
+ compute_half_dash(side_width, avail_size);
+ let stretch_size = 2.0 * 2.0 * half_dash;
+ EdgeInfo::new(0., avail_size, stretch_size)
+ }
+ BorderStyle::Dotted => {
+ let dot_and_space_size = 2.0 * side_width;
+ if avail_size < dot_and_space_size * 0.75 {
+ return EdgeInfo::new(0.0, 0.0, 0.0);
+ }
+ let approx_dot_count = avail_size / dot_and_space_size;
+ let dot_count = approx_dot_count.floor().max(1.0);
+ let used_size = dot_count * dot_and_space_size;
+ let extra_space = avail_size - used_size;
+ let stretch_size = dot_and_space_size;
+ let offset = (extra_space * 0.5).round();
+ EdgeInfo::new(offset, used_size, stretch_size)
+ }
+ _ => {
+ EdgeInfo::new(0.0, avail_size, 8.0)
+ }
+ }
+}
+
+/// Create the set of border segments and render task
+/// cache keys for a given CSS border.
+pub fn create_border_segments(
+ size: LayoutSize,
+ border: &ApiNormalBorder,
+ widths: &LayoutSideOffsets,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ brush_segments: &mut Vec<BrushSegment>,
+) {
+ let rect = LayoutRect::new(
+ LayoutPoint::zero(),
+ size,
+ );
+
+ let overlap = LayoutSize::new(
+ (widths.left + widths.right - size.width).max(0.0),
+ (widths.top + widths.bottom - size.height).max(0.0),
+ );
+ let non_overlapping_widths = LayoutSideOffsets::new(
+ widths.top - overlap.height / 2.0,
+ widths.right - overlap.width / 2.0,
+ widths.bottom - overlap.height / 2.0,
+ widths.left - overlap.width / 2.0,
+ );
+
+ let local_size_tl = LayoutSize::new(
+ border.radius.top_left.width.max(widths.left),
+ border.radius.top_left.height.max(widths.top),
+ );
+ let local_size_tr = LayoutSize::new(
+ border.radius.top_right.width.max(widths.right),
+ border.radius.top_right.height.max(widths.top),
+ );
+ let local_size_br = LayoutSize::new(
+ border.radius.bottom_right.width.max(widths.right),
+ border.radius.bottom_right.height.max(widths.bottom),
+ );
+ let local_size_bl = LayoutSize::new(
+ border.radius.bottom_left.width.max(widths.left),
+ border.radius.bottom_left.height.max(widths.bottom),
+ );
+
+ let top_edge_info = get_edge_info(
+ border.top.style,
+ widths.top,
+ rect.size.width - local_size_tl.width - local_size_tr.width,
+ );
+ let bottom_edge_info = get_edge_info(
+ border.bottom.style,
+ widths.bottom,
+ rect.size.width - local_size_bl.width - local_size_br.width,
+ );
+
+ let left_edge_info = get_edge_info(
+ border.left.style,
+ widths.left,
+ rect.size.height - local_size_tl.height - local_size_bl.height,
+ );
+ let right_edge_info = get_edge_info(
+ border.right.style,
+ widths.right,
+ rect.size.height - local_size_tr.height - local_size_br.height,
+ );
+
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.origin.x,
+ rect.origin.y + local_size_tl.height + left_edge_info.local_offset,
+ rect.origin.x + non_overlapping_widths.left,
+ rect.origin.y + local_size_tl.height + left_edge_info.local_offset + left_edge_info.local_size,
+ ),
+ &left_edge_info,
+ border.left,
+ non_overlapping_widths.left,
+ BorderSegment::Left,
+ EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.origin.x + local_size_tl.width + top_edge_info.local_offset,
+ rect.origin.y,
+ rect.origin.x + local_size_tl.width + top_edge_info.local_offset + top_edge_info.local_size,
+ rect.origin.y + non_overlapping_widths.top,
+ ),
+ &top_edge_info,
+ border.top,
+ non_overlapping_widths.top,
+ BorderSegment::Top,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.origin.x + rect.size.width - non_overlapping_widths.right,
+ rect.origin.y + local_size_tr.height + right_edge_info.local_offset,
+ rect.origin.x + rect.size.width,
+ rect.origin.y + local_size_tr.height + right_edge_info.local_offset + right_edge_info.local_size,
+ ),
+ &right_edge_info,
+ border.right,
+ non_overlapping_widths.right,
+ BorderSegment::Right,
+ EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_edge_segment(
+ LayoutRect::from_floats(
+ rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset,
+ rect.origin.y + rect.size.height - non_overlapping_widths.bottom,
+ rect.origin.x + local_size_bl.width + bottom_edge_info.local_offset + bottom_edge_info.local_size,
+ rect.origin.y + rect.size.height,
+ ),
+ &bottom_edge_info,
+ border.bottom,
+ non_overlapping_widths.bottom,
+ BorderSegment::Bottom,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.origin.x,
+ rect.origin.y,
+ rect.origin.x + local_size_tl.width,
+ rect.origin.y + local_size_tl.height,
+ ),
+ LayoutRect::from_floats(
+ rect.origin.x,
+ rect.origin.y,
+ rect.max_x() - non_overlapping_widths.right,
+ rect.max_y() - non_overlapping_widths.bottom
+ ),
+ border.left,
+ border.top,
+ LayoutSize::new(widths.left, widths.top),
+ border.radius.top_left,
+ BorderSegment::TopLeft,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT,
+ rect.top_right(),
+ border.radius.top_right,
+ rect.bottom_left(),
+ border.radius.bottom_left,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.origin.x + rect.size.width - local_size_tr.width,
+ rect.origin.y,
+ rect.origin.x + rect.size.width,
+ rect.origin.y + local_size_tr.height,
+ ),
+ LayoutRect::from_floats(
+ rect.origin.x + non_overlapping_widths.left,
+ rect.origin.y,
+ rect.max_x(),
+ rect.max_y() - non_overlapping_widths.bottom,
+ ),
+ border.top,
+ border.right,
+ LayoutSize::new(widths.right, widths.top),
+ border.radius.top_right,
+ BorderSegment::TopRight,
+ EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT,
+ rect.origin,
+ border.radius.top_left,
+ rect.bottom_right(),
+ border.radius.bottom_right,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.origin.x + rect.size.width - local_size_br.width,
+ rect.origin.y + rect.size.height - local_size_br.height,
+ rect.origin.x + rect.size.width,
+ rect.origin.y + rect.size.height,
+ ),
+ LayoutRect::from_floats(
+ rect.origin.x + non_overlapping_widths.left,
+ rect.origin.y + non_overlapping_widths.top,
+ rect.max_x(),
+ rect.max_y(),
+ ),
+ border.right,
+ border.bottom,
+ LayoutSize::new(widths.right, widths.bottom),
+ border.radius.bottom_right,
+ BorderSegment::BottomRight,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT,
+ rect.bottom_left(),
+ border.radius.bottom_left,
+ rect.top_right(),
+ border.radius.top_right,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+ add_corner_segment(
+ LayoutRect::from_floats(
+ rect.origin.x,
+ rect.origin.y + rect.size.height - local_size_bl.height,
+ rect.origin.x + local_size_bl.width,
+ rect.origin.y + rect.size.height,
+ ),
+ LayoutRect::from_floats(
+ rect.origin.x,
+ rect.origin.y + non_overlapping_widths.top,
+ rect.max_x() - non_overlapping_widths.right,
+ rect.max_y(),
+ ),
+ border.bottom,
+ border.left,
+ LayoutSize::new(widths.left, widths.bottom),
+ border.radius.bottom_left,
+ BorderSegment::BottomLeft,
+ EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT,
+ rect.bottom_right(),
+ border.radius.bottom_right,
+ rect.origin,
+ border.radius.top_left,
+ brush_segments,
+ border_segments,
+ border.do_aa,
+ );
+}
+
+/// Computes the maximum scale that we allow for this set of border parameters.
+/// capping the scale will result in rendering very large corners at a lower
+/// resolution and stretching them, so they will have the right shape, but
+/// blurrier.
+pub fn get_max_scale_for_border(
+ border_data: &NormalBorderData,
+) -> LayoutToDeviceScale {
+ let mut r = 1.0;
+ for segment in &border_data.border_segments {
+ let size = segment.local_task_size;
+ r = size.width.max(size.height.max(r));
+ }
+
+ LayoutToDeviceScale::new(MAX_BORDER_RESOLUTION as f32 / r)
+}
+
+fn add_segment(
+ task_rect: DeviceRect,
+ style0: BorderStyle,
+ style1: BorderStyle,
+ color0: ColorF,
+ color1: ColorF,
+ segment: BorderSegment,
+ instances: &mut Vec<BorderInstance>,
+ widths: DeviceSize,
+ radius: DeviceSize,
+ do_aa: bool,
+ h_adjacent_corner_outer: DevicePoint,
+ h_adjacent_corner_radius: DeviceSize,
+ v_adjacent_corner_outer: DevicePoint,
+ v_adjacent_corner_radius: DeviceSize,
+) {
+ let base_flags = (segment as i32) |
+ ((style0 as i32) << 8) |
+ ((style1 as i32) << 16) |
+ ((do_aa as i32) << 28);
+
+ let base_instance = BorderInstance {
+ task_origin: DevicePoint::zero(),
+ local_rect: task_rect,
+ flags: base_flags,
+ color0: color0.premultiplied(),
+ color1: color1.premultiplied(),
+ widths,
+ radius,
+ clip_params: [0.0; 8],
+ };
+
+ match segment {
+ BorderSegment::TopLeft |
+ BorderSegment::TopRight |
+ BorderSegment::BottomLeft |
+ BorderSegment::BottomRight => {
+ // TODO(gw): Similarly to the old border code, we don't correctly handle a a corner
+ // that is dashed on one edge, and dotted on another. We can handle this
+ // in the future by submitting two instances, each one with one side
+ // color set to have an alpha of 0.
+ if (style0 == BorderStyle::Dotted && style1 == BorderStyle::Dashed) ||
+ (style0 == BorderStyle::Dashed && style0 == BorderStyle::Dotted) {
+ warn!("TODO: Handle a corner with dotted / dashed transition.");
+ }
+
+ let dashed_or_dotted_corner = match style0 {
+ BorderStyle::Dashed => {
+ write_dashed_corner_instances(
+ radius,
+ widths,
+ segment,
+ &base_instance,
+ instances,
+ )
+ }
+ BorderStyle::Dotted => {
+ write_dotted_corner_instances(
+ radius,
+ widths,
+ segment,
+ &base_instance,
+ instances,
+ )
+ }
+ _ => Err(()),
+ };
+
+ if dashed_or_dotted_corner.is_err() {
+ let clip_params = [
+ h_adjacent_corner_outer.x,
+ h_adjacent_corner_outer.y,
+ h_adjacent_corner_radius.width,
+ h_adjacent_corner_radius.height,
+ v_adjacent_corner_outer.x,
+ v_adjacent_corner_outer.y,
+ v_adjacent_corner_radius.width,
+ v_adjacent_corner_radius.height,
+ ];
+
+ instances.push(BorderInstance {
+ clip_params,
+ ..base_instance
+ });
+ }
+ }
+ BorderSegment::Top |
+ BorderSegment::Bottom |
+ BorderSegment::Right |
+ BorderSegment::Left => {
+ let is_vertical = segment == BorderSegment::Left ||
+ segment == BorderSegment::Right;
+
+ match style0 {
+ BorderStyle::Dashed => {
+ let (x, y) = if is_vertical {
+ let half_dash_size = task_rect.size.height * 0.25;
+ (0., half_dash_size)
+ } else {
+ let half_dash_size = task_rect.size.width * 0.25;
+ (half_dash_size, 0.)
+ };
+
+ instances.push(BorderInstance {
+ flags: base_flags | ((BorderClipKind::DashEdge as i32) << 24),
+ clip_params: [
+ x, y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ ],
+ ..base_instance
+ });
+ }
+ BorderStyle::Dotted => {
+ let (x, y, r) = if is_vertical {
+ (widths.width * 0.5,
+ widths.width,
+ widths.width * 0.5)
+ } else {
+ (widths.height,
+ widths.height * 0.5,
+ widths.height * 0.5)
+ };
+
+ instances.push(BorderInstance {
+ flags: base_flags | ((BorderClipKind::Dot as i32) << 24),
+ clip_params: [
+ x, y, r, 0.0, 0.0, 0.0, 0.0, 0.0,
+ ],
+ ..base_instance
+ });
+ }
+ _ => {
+ instances.push(base_instance);
+ }
+ }
+ }
+ }
+}
+
+/// Add a corner segment (if valid) to the list of
+/// border segments for this primitive.
+fn add_corner_segment(
+ image_rect: LayoutRect,
+ non_overlapping_rect: LayoutRect,
+ side0: BorderSide,
+ side1: BorderSide,
+ widths: LayoutSize,
+ radius: LayoutSize,
+ segment: BorderSegment,
+ edge_flags: EdgeAaSegmentMask,
+ h_adjacent_corner_outer: LayoutPoint,
+ h_adjacent_corner_radius: LayoutSize,
+ v_adjacent_corner_outer: LayoutPoint,
+ v_adjacent_corner_radius: LayoutSize,
+ brush_segments: &mut Vec<BrushSegment>,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ do_aa: bool,
+) {
+ if side0.color.a <= 0.0 && side1.color.a <= 0.0 {
+ return;
+ }
+
+ if widths.width <= 0.0 && widths.height <= 0.0 {
+ return;
+ }
+
+ if side0.style.is_hidden() && side1.style.is_hidden() {
+ return;
+ }
+
+ let segment_rect = match image_rect.intersection(&non_overlapping_rect) {
+ Some(rect) => rect,
+ None => {
+ return;
+ }
+ };
+
+ let texture_rect = segment_rect
+ .translate(-image_rect.origin.to_vector())
+ .scale(1.0 / image_rect.size.width, 1.0 / image_rect.size.height);
+
+ brush_segments.push(
+ BrushSegment::new(
+ segment_rect,
+ /* may_need_clip_mask = */ true,
+ edge_flags,
+ [texture_rect.min_x(), texture_rect.min_y(), texture_rect.max_x(), texture_rect.max_y()],
+ BrushFlags::SEGMENT_RELATIVE | BrushFlags::SEGMENT_TEXEL_RECT,
+ )
+ );
+
+ // If the radii of the adjacent corners do not overlap with this segment,
+ // then set the outer position to this segment's corner and the radii to zero.
+ // That way the cache key is unaffected by non-overlapping corners, resulting
+ // in fewer misses.
+ let (h_corner_outer, h_corner_radius) = match segment {
+ BorderSegment::TopLeft => {
+ if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max_x() {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.max_x(), image_rect.min_y()), LayoutSize::zero())
+ }
+ }
+ BorderSegment::TopRight => {
+ if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min_x() {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min_x(), image_rect.min_y()), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomRight => {
+ if h_adjacent_corner_outer.x + h_adjacent_corner_radius.width > image_rect.min_x() {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min_x(), image_rect.max_y()), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomLeft => {
+ if h_adjacent_corner_outer.x - h_adjacent_corner_radius.width < image_rect.max_x() {
+ (h_adjacent_corner_outer, h_adjacent_corner_radius)
+ } else {
+ (image_rect.bottom_right(), LayoutSize::zero())
+ }
+ }
+ _ => unreachable!()
+ };
+
+ let (v_corner_outer, v_corner_radius) = match segment {
+ BorderSegment::TopLeft => {
+ if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max_y() {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min_x(), image_rect.max_y()), LayoutSize::zero())
+ }
+ }
+ BorderSegment::TopRight => {
+ if v_adjacent_corner_outer.y - v_adjacent_corner_radius.height < image_rect.max_y() {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (image_rect.bottom_right(), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomRight => {
+ if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min_y() {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.max_x(), image_rect.min_y()), LayoutSize::zero())
+ }
+ }
+ BorderSegment::BottomLeft => {
+ if v_adjacent_corner_outer.y + v_adjacent_corner_radius.height > image_rect.min_y() {
+ (v_adjacent_corner_outer, v_adjacent_corner_radius)
+ } else {
+ (LayoutPoint::new(image_rect.min_x(), image_rect.min_y()), LayoutSize::zero())
+ }
+ }
+ _ => unreachable!()
+ };
+
+ border_segments.push(BorderSegmentInfo {
+ local_task_size: image_rect.size,
+ cache_key: BorderSegmentCacheKey {
+ do_aa,
+ side0: side0.into(),
+ side1: side1.into(),
+ segment,
+ radius: radius.to_au(),
+ size: widths.to_au(),
+ h_adjacent_corner_outer: (h_corner_outer - image_rect.origin).to_point().to_au(),
+ h_adjacent_corner_radius: h_corner_radius.to_au(),
+ v_adjacent_corner_outer: (v_corner_outer - image_rect.origin).to_point().to_au(),
+ v_adjacent_corner_radius: v_corner_radius.to_au(),
+ },
+ });
+}
+
+/// Add an edge segment (if valid) to the list of
+/// border segments for this primitive.
+fn add_edge_segment(
+ image_rect: LayoutRect,
+ edge_info: &EdgeInfo,
+ side: BorderSide,
+ width: f32,
+ segment: BorderSegment,
+ edge_flags: EdgeAaSegmentMask,
+ brush_segments: &mut Vec<BrushSegment>,
+ border_segments: &mut Vec<BorderSegmentInfo>,
+ do_aa: bool,
+) {
+ if side.color.a <= 0.0 {
+ return;
+ }
+
+ if side.style.is_hidden() {
+ return;
+ }
+
+ let (size, brush_flags) = match segment {
+ BorderSegment::Left | BorderSegment::Right => {
+ (LayoutSize::new(width, edge_info.stretch_size), BrushFlags::SEGMENT_REPEAT_Y)
+ }
+ BorderSegment::Top | BorderSegment::Bottom => {
+ (LayoutSize::new(edge_info.stretch_size, width), BrushFlags::SEGMENT_REPEAT_X)
+ }
+ _ => {
+ unreachable!();
+ }
+ };
+
+ if image_rect.size.width <= 0. || image_rect.size.height <= 0. {
+ return;
+ }
+
+ brush_segments.push(
+ BrushSegment::new(
+ image_rect,
+ /* may_need_clip_mask = */ true,
+ edge_flags,
+ [0.0, 0.0, size.width, size.height],
+ BrushFlags::SEGMENT_RELATIVE | brush_flags,
+ )
+ );
+
+ border_segments.push(BorderSegmentInfo {
+ local_task_size: size,
+ cache_key: BorderSegmentCacheKey {
+ do_aa,
+ side0: side.into(),
+ side1: side.into(),
+ radius: LayoutSizeAu::zero(),
+ size: size.to_au(),
+ segment,
+ h_adjacent_corner_outer: LayoutPointAu::zero(),
+ h_adjacent_corner_radius: LayoutSizeAu::zero(),
+ v_adjacent_corner_outer: LayoutPointAu::zero(),
+ v_adjacent_corner_radius: LayoutSizeAu::zero(),
+ },
+ });
+}
+
+/// Build the set of border instances needed to draw a border
+/// segment into the render task cache.
+pub fn build_border_instances(
+ cache_key: &BorderSegmentCacheKey,
+ cache_size: DeviceIntSize,
+ border: &ApiNormalBorder,
+ scale: LayoutToDeviceScale,
+) -> Vec<BorderInstance> {
+ let mut instances = Vec::new();
+
+ let (side0, side1, flip0, flip1) = match cache_key.segment {
+ BorderSegment::Left => (&border.left, &border.left, false, false),
+ BorderSegment::Top => (&border.top, &border.top, false, false),
+ BorderSegment::Right => (&border.right, &border.right, true, true),
+ BorderSegment::Bottom => (&border.bottom, &border.bottom, true, true),
+ BorderSegment::TopLeft => (&border.left, &border.top, false, false),
+ BorderSegment::TopRight => (&border.top, &border.right, false, true),
+ BorderSegment::BottomRight => (&border.right, &border.bottom, true, true),
+ BorderSegment::BottomLeft => (&border.bottom, &border.left, true, false),
+ };
+
+ let style0 = if side0.style.is_hidden() {
+ side1.style
+ } else {
+ side0.style
+ };
+ let style1 = if side1.style.is_hidden() {
+ side0.style
+ } else {
+ side1.style
+ };
+
+ let color0 = side0.border_color(flip0);
+ let color1 = side1.border_color(flip1);
+
+ let widths = (LayoutSize::from_au(cache_key.size) * scale).ceil();
+ let radius = (LayoutSize::from_au(cache_key.radius) * scale).ceil();
+
+ let h_corner_outer = (LayoutPoint::from_au(cache_key.h_adjacent_corner_outer) * scale).round();
+ let h_corner_radius = (LayoutSize::from_au(cache_key.h_adjacent_corner_radius) * scale).ceil();
+ let v_corner_outer = (LayoutPoint::from_au(cache_key.v_adjacent_corner_outer) * scale).round();
+ let v_corner_radius = (LayoutSize::from_au(cache_key.v_adjacent_corner_radius) * scale).ceil();
+
+ add_segment(
+ DeviceRect::new(DevicePoint::zero(), cache_size.to_f32()),
+ style0,
+ style1,
+ color0,
+ color1,
+ cache_key.segment,
+ &mut instances,
+ widths,
+ radius,
+ border.do_aa,
+ h_corner_outer,
+ h_corner_radius,
+ v_corner_outer,
+ v_corner_radius,
+ );
+
+ instances
+}
+
+impl NinePatchDescriptor {
+ pub fn create_segments(
+ &self,
+ size: LayoutSize,
+ ) -> Vec<BrushSegment> {
+ let rect = LayoutRect::new(
+ LayoutPoint::zero(),
+ size,
+ );
+
+ // Calculate the modified rect as specific by border-image-outset
+ let origin = LayoutPoint::new(
+ rect.origin.x - self.outset.left,
+ rect.origin.y - self.outset.top,
+ );
+ let size = LayoutSize::new(
+ rect.size.width + self.outset.left + self.outset.right,
+ rect.size.height + self.outset.top + self.outset.bottom,
+ );
+ let rect = LayoutRect::new(origin, size);
+
+ // Calculate the local texel coords of the slices.
+ let px0 = 0.0;
+ let px1 = self.slice.left as f32 / self.width as f32;
+ let px2 = (self.width as f32 - self.slice.right as f32) / self.width as f32;
+ let px3 = 1.0;
+
+ let py0 = 0.0;
+ let py1 = self.slice.top as f32 / self.height as f32;
+ let py2 = (self.height as f32 - self.slice.bottom as f32) / self.height as f32;
+ let py3 = 1.0;
+
+ let tl_outer = LayoutPoint::new(rect.origin.x, rect.origin.y);
+ let tl_inner = tl_outer + vec2(self.widths.left, self.widths.top);
+
+ let tr_outer = LayoutPoint::new(rect.origin.x + rect.size.width, rect.origin.y);
+ let tr_inner = tr_outer + vec2(-self.widths.right, self.widths.top);
+
+ let bl_outer = LayoutPoint::new(rect.origin.x, rect.origin.y + rect.size.height);
+ let bl_inner = bl_outer + vec2(self.widths.left, -self.widths.bottom);
+
+ let br_outer = LayoutPoint::new(
+ rect.origin.x + rect.size.width,
+ rect.origin.y + rect.size.height,
+ );
+ let br_inner = br_outer - vec2(self.widths.right, self.widths.bottom);
+
+ fn add_segment(
+ segments: &mut Vec<BrushSegment>,
+ rect: LayoutRect,
+ uv_rect: TexelRect,
+ repeat_horizontal: RepeatMode,
+ repeat_vertical: RepeatMode,
+ extra_flags: BrushFlags,
+ ) {
+ if uv_rect.uv1.x <= uv_rect.uv0.x || uv_rect.uv1.y <= uv_rect.uv0.y {
+ return;
+ }
+
+ // Use segment relative interpolation for all
+ // instances in this primitive.
+ let mut brush_flags =
+ BrushFlags::SEGMENT_RELATIVE |
+ BrushFlags::SEGMENT_TEXEL_RECT |
+ extra_flags;
+
+ // Enable repeat modes on the segment.
+ if repeat_horizontal == RepeatMode::Repeat {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_X;
+ } else if repeat_horizontal == RepeatMode::Round {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_X | BrushFlags::SEGMENT_REPEAT_X_ROUND;
+ }
+
+ if repeat_vertical == RepeatMode::Repeat {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_Y;
+ } else if repeat_vertical == RepeatMode::Round {
+ brush_flags |= BrushFlags::SEGMENT_REPEAT_Y | BrushFlags::SEGMENT_REPEAT_Y_ROUND;
+ }
+
+ let segment = BrushSegment::new(
+ rect,
+ true,
+ EdgeAaSegmentMask::empty(),
+ [
+ uv_rect.uv0.x,
+ uv_rect.uv0.y,
+ uv_rect.uv1.x,
+ uv_rect.uv1.y,
+ ],
+ brush_flags,
+ );
+
+ segments.push(segment);
+ }
+
+ // Build the list of image segments
+ let mut segments = Vec::new();
+
+ // Top left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_outer.x, tl_outer.y, tl_inner.x, tl_inner.y),
+ TexelRect::new(px0, py0, px1, py1),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Top right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tr_inner.x, tr_outer.y, tr_outer.x, tr_inner.y),
+ TexelRect::new(px2, py0, px3, py1),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(br_inner.x, br_inner.y, br_outer.x, br_outer.y),
+ TexelRect::new(px2, py2, px3, py3),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(bl_outer.x, bl_inner.y, bl_inner.x, bl_outer.y),
+ TexelRect::new(px0, py2, px1, py3),
+ RepeatMode::Stretch,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+
+ // Center
+ if self.fill {
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_inner.x, tl_inner.y, tr_inner.x, bl_inner.y),
+ TexelRect::new(px1, py1, px2, py2),
+ self.repeat_horizontal,
+ self.repeat_vertical,
+ BrushFlags::SEGMENT_NINEPATCH_MIDDLE,
+ );
+ }
+
+ // Add edge segments.
+
+ // Top
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_inner.x, tl_outer.y, tr_inner.x, tl_inner.y),
+ TexelRect::new(px1, py0, px2, py1),
+ self.repeat_horizontal,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Bottom
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(bl_inner.x, bl_inner.y, br_inner.x, bl_outer.y),
+ TexelRect::new(px1, py2, px2, py3),
+ self.repeat_horizontal,
+ RepeatMode::Stretch,
+ BrushFlags::empty(),
+ );
+ // Left
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tl_outer.x, tl_inner.y, tl_inner.x, bl_inner.y),
+ TexelRect::new(px0, py1, px1, py2),
+ RepeatMode::Stretch,
+ self.repeat_vertical,
+ BrushFlags::empty(),
+ );
+ // Right
+ add_segment(
+ &mut segments,
+ LayoutRect::from_floats(tr_inner.x, tr_inner.y, br_outer.x, br_inner.y),
+ TexelRect::new(px2, py1, px3, py2),
+ RepeatMode::Stretch,
+ self.repeat_vertical,
+ BrushFlags::empty(),
+ );
+
+ segments
+ }
+}
diff --git a/gfx/wr/webrender/src/box_shadow.rs b/gfx/wr/webrender/src/box_shadow.rs
new file mode 100644
index 0000000000..ddc4d90854
--- /dev/null
+++ b/gfx/wr/webrender/src/box_shadow.rs
@@ -0,0 +1,275 @@
+/* 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 api::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, PrimitiveKeyKind};
+use api::PropertyBinding;
+use api::units::*;
+use crate::clip::{ClipItemKey, ClipItemKeyKind, ClipChainId};
+use crate::scene_building::SceneBuilder;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::gpu_types::BoxShadowStretchMode;
+use crate::render_task_cache::RenderTaskCacheEntryHandle;
+use crate::internal_types::LayoutPrimitiveInfo;
+
+#[derive(Debug, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BoxShadowClipSource {
+ // Parameters that define the shadow and are constant.
+ pub shadow_radius: BorderRadius,
+ pub blur_radius: f32,
+ pub clip_mode: BoxShadowClipMode,
+ pub stretch_mode_x: BoxShadowStretchMode,
+ pub stretch_mode_y: BoxShadowStretchMode,
+
+ // The current cache key (in device-pixels), and handles
+ // to the cached clip region and blurred texture.
+ pub cache_key: Option<(DeviceIntSize, BoxShadowCacheKey)>,
+ pub cache_handle: Option<RenderTaskCacheEntryHandle>,
+
+ // Local-space size of the required render task size.
+ pub shadow_rect_alloc_size: LayoutSize,
+
+ // Local-space size of the required render task size without any downscaling
+ // applied. This is needed to stretch the shadow properly.
+ pub original_alloc_size: LayoutSize,
+
+ // The minimal shadow rect for the parameters above,
+ // used when drawing the shadow rect to be blurred.
+ pub minimal_shadow_rect: LayoutRect,
+
+ // Local space rect for the shadow to be drawn or
+ // stretched in the shadow primitive.
+ pub prim_shadow_rect: LayoutRect,
+}
+
+// The blur shader samples BLUR_SAMPLE_SCALE * blur_radius surrounding texels.
+pub const BLUR_SAMPLE_SCALE: f32 = 3.0;
+
+// Maximum blur radius for box-shadows (different than blur filters).
+// Taken from nsCSSRendering.cpp in Gecko.
+pub const MAX_BLUR_RADIUS: f32 = 300.;
+
+// A cache key that uniquely identifies a minimally sized
+// and blurred box-shadow rect that can be stored in the
+// texture cache and applied to clip-masks.
+#[derive(Debug, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BoxShadowCacheKey {
+ pub blur_radius_dp: i32,
+ pub clip_mode: BoxShadowClipMode,
+ // NOTE(emilio): Only the original allocation size needs to be in the cache
+ // key, since the actual size is derived from that.
+ pub original_alloc_size: DeviceIntSize,
+ pub br_top_left: DeviceIntSize,
+ pub br_top_right: DeviceIntSize,
+ pub br_bottom_right: DeviceIntSize,
+ pub br_bottom_left: DeviceIntSize,
+ pub device_pixel_scale: Au,
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn add_box_shadow(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ prim_info: &LayoutPrimitiveInfo,
+ box_offset: &LayoutVector2D,
+ color: ColorF,
+ mut blur_radius: f32,
+ spread_radius: f32,
+ border_radius: BorderRadius,
+ clip_mode: BoxShadowClipMode,
+ ) {
+ if color.a == 0.0 {
+ return;
+ }
+
+ // Inset shadows get smaller as spread radius increases.
+ let (spread_amount, prim_clip_mode) = match clip_mode {
+ BoxShadowClipMode::Outset => (spread_radius, ClipMode::ClipOut),
+ BoxShadowClipMode::Inset => (-spread_radius, ClipMode::Clip),
+ };
+
+ // Ensure the blur radius is somewhat sensible.
+ blur_radius = f32::min(blur_radius, MAX_BLUR_RADIUS);
+
+ // Adjust the border radius of the box shadow per CSS-spec.
+ let shadow_radius = adjust_border_radius_for_box_shadow(border_radius, spread_amount);
+
+ // Apply parameters that affect where the shadow rect
+ // exists in the local space of the primitive.
+ let shadow_rect = self.snap_rect(
+ &prim_info
+ .rect
+ .translate(*box_offset)
+ .inflate(spread_amount, spread_amount),
+ spatial_node_index,
+ );
+
+ // If blur radius is zero, we can use a fast path with
+ // no blur applied.
+ if blur_radius == 0.0 {
+ // Trivial reject of box-shadows that are not visible.
+ if box_offset.x == 0.0 && box_offset.y == 0.0 && spread_amount == 0.0 {
+ return;
+ }
+
+ let mut clips = Vec::with_capacity(2);
+ let (final_prim_rect, clip_radius) = match clip_mode {
+ BoxShadowClipMode::Outset => {
+ if shadow_rect.is_empty() {
+ return;
+ }
+
+ // TODO(gw): Add a fast path for ClipOut + zero border radius!
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ prim_info.rect,
+ border_radius,
+ ClipMode::ClipOut,
+ ),
+ });
+
+ (shadow_rect, shadow_radius)
+ }
+ BoxShadowClipMode::Inset => {
+ if !shadow_rect.is_empty() {
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ shadow_rect,
+ shadow_radius,
+ ClipMode::ClipOut,
+ ),
+ });
+ }
+
+ (prim_info.rect, border_radius)
+ }
+ };
+
+ clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ final_prim_rect,
+ clip_radius,
+ ClipMode::Clip,
+ ),
+ });
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
+ clips,
+ PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(color.into()),
+ },
+ );
+ } else {
+ // Normal path for box-shadows with a valid blur radius.
+ let blur_offset = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
+ let mut extra_clips = vec![];
+
+ // Add a normal clip mask to clip out the contents
+ // of the surrounding primitive.
+ extra_clips.push(ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ prim_info.rect,
+ border_radius,
+ prim_clip_mode,
+ ),
+ });
+
+ // Get the local rect of where the shadow will be drawn,
+ // expanded to include room for the blurred region.
+ let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
+
+ // Draw the box-shadow as a solid rect, using a box-shadow
+ // clip mask item.
+ let prim = PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(color.into()),
+ };
+
+ // Create the box-shadow clip item.
+ let shadow_clip_source = ClipItemKey {
+ kind: ClipItemKeyKind::box_shadow(
+ shadow_rect,
+ shadow_radius,
+ dest_rect,
+ blur_radius,
+ clip_mode,
+ ),
+ };
+
+ let prim_info = match clip_mode {
+ BoxShadowClipMode::Outset => {
+ // Certain spread-radii make the shadow invalid.
+ if shadow_rect.is_empty() {
+ return;
+ }
+
+ // Add the box-shadow clip source.
+ extra_clips.push(shadow_clip_source);
+
+ // Outset shadows are expanded by the shadow
+ // region from the original primitive.
+ LayoutPrimitiveInfo::with_clip_rect(dest_rect, prim_info.clip_rect)
+ }
+ BoxShadowClipMode::Inset => {
+ // If the inner shadow rect contains the prim
+ // rect, no pixels will be shadowed.
+ if border_radius.is_zero() && shadow_rect
+ .inflate(-blur_radius, -blur_radius)
+ .contains_rect(&prim_info.rect)
+ {
+ return;
+ }
+
+ // Inset shadows are still visible, even if the
+ // inset shadow rect becomes invalid (they will
+ // just look like a solid rectangle).
+ if !shadow_rect.is_empty() {
+ extra_clips.push(shadow_clip_source);
+ }
+
+ // Inset shadows draw inside the original primitive.
+ prim_info.clone()
+ }
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &prim_info,
+ extra_clips,
+ prim,
+ );
+ }
+ }
+}
+
+fn adjust_border_radius_for_box_shadow(radius: BorderRadius, spread_amount: f32) -> BorderRadius {
+ BorderRadius {
+ top_left: adjust_corner_for_box_shadow(radius.top_left, spread_amount),
+ top_right: adjust_corner_for_box_shadow(radius.top_right, spread_amount),
+ bottom_right: adjust_corner_for_box_shadow(radius.bottom_right, spread_amount),
+ bottom_left: adjust_corner_for_box_shadow(radius.bottom_left, spread_amount),
+ }
+}
+
+fn adjust_corner_for_box_shadow(corner: LayoutSize, spread_amount: f32) -> LayoutSize {
+ LayoutSize::new(
+ adjust_radius_for_box_shadow(corner.width, spread_amount),
+ adjust_radius_for_box_shadow(corner.height, spread_amount),
+ )
+}
+
+fn adjust_radius_for_box_shadow(border_radius: f32, spread_amount: f32) -> f32 {
+ if border_radius > 0.0 {
+ (border_radius + spread_amount).max(0.0)
+ } else {
+ 0.0
+ }
+}
diff --git a/gfx/wr/webrender/src/capture.rs b/gfx/wr/webrender/src/capture.rs
new file mode 100644
index 0000000000..0414fcb25e
--- /dev/null
+++ b/gfx/wr/webrender/src/capture.rs
@@ -0,0 +1,290 @@
+/* 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 std::fs::File;
+use std::path::{Path, PathBuf};
+
+use api::{ExternalImageData, ImageDescriptor};
+#[cfg(feature = "png")]
+use api::ImageFormat;
+use api::units::TexelRect;
+#[cfg(feature = "png")]
+use api::units::DeviceIntSize;
+#[cfg(feature = "capture")]
+use crate::print_tree::{PrintableTree, PrintTree};
+use crate::render_api::CaptureBits;
+use ron;
+use serde;
+
+
+#[derive(Clone)]
+pub struct CaptureConfig {
+ pub root: PathBuf,
+ pub bits: CaptureBits,
+ /// Scene sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub scene_id: u32,
+ /// Frame sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub frame_id: u32,
+ /// Resource sequence ID when capturing multiple frames. Zero for a single frame capture.
+ pub resource_id: u32,
+ #[cfg(feature = "capture")]
+ pretty: ron::ser::PrettyConfig,
+}
+
+impl CaptureConfig {
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn new(root: PathBuf, bits: CaptureBits) -> Self {
+ CaptureConfig {
+ root,
+ bits,
+ scene_id: 0,
+ frame_id: 0,
+ resource_id: 0,
+ #[cfg(feature = "capture")]
+ pretty: ron::ser::PrettyConfig::new()
+ .with_enumerate_arrays(true)
+ .with_indentor(" ".to_string()),
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_scene(&mut self) {
+ use std::fs::create_dir_all;
+ self.scene_id += 1;
+ let _ = create_dir_all(&self.scene_root());
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_frame(&mut self) {
+ use std::fs::create_dir_all;
+ self.frame_id += 1;
+ let _ = create_dir_all(&self.frame_root());
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn prepare_resource(&mut self) {
+ use std::fs::create_dir_all;
+ self.resource_id += 1;
+ let _ = create_dir_all(&self.resource_root());
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn scene_root(&self) -> PathBuf {
+ if self.scene_id > 0 {
+ let path = format!("scenes/{:05}", self.scene_id);
+ self.root.join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn frame_root(&self) -> PathBuf {
+ if self.frame_id > 0 {
+ let path = format!("frames/{:05}", self.frame_id);
+ self.scene_root().join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ pub fn resource_root(&self) -> PathBuf {
+ if self.resource_id > 0 {
+ let path = format!("resources/{:05}", self.resource_id);
+ self.root.join(path)
+ } else {
+ self.root.clone()
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_scene<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.scene_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_frame<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.frame_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_for_resource<T, P>(&self, data: &T, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ self.serialize(data, self.resource_root(), name)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn file_path_for_frame<P>(&self, name: P, ext: &str) -> PathBuf
+ where P: AsRef<Path> {
+ self.frame_root().join(name).with_extension(ext)
+ }
+
+ #[cfg(feature = "capture")]
+ fn serialize<T, P>(&self, data: &T, path: PathBuf, name: P)
+ where
+ T: serde::Serialize,
+ P: AsRef<Path>,
+ {
+ use std::io::Write;
+ let ron = ron::ser::to_string_pretty(data, self.pretty.clone())
+ .unwrap();
+ let mut file = File::create(path.join(name).with_extension("ron"))
+ .unwrap();
+ write!(file, "{}\n", ron)
+ .unwrap();
+ }
+
+ #[cfg(feature = "capture")]
+ fn serialize_tree<T, P>(data: &T, root: PathBuf, name: P)
+ where
+ T: PrintableTree,
+ P: AsRef<Path>
+ {
+ let path = root
+ .join(name)
+ .with_extension("tree");
+ let file = File::create(path)
+ .unwrap();
+ let mut pt = PrintTree::new_with_sink("", file);
+ data.print_with(&mut pt);
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_tree_for_frame<T, P>(&self, data: &T, name: P)
+ where
+ T: PrintableTree,
+ P: AsRef<Path>
+ {
+ Self::serialize_tree(data, self.frame_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ fn deserialize<T, P>(root: &PathBuf, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ use std::io::Read;
+
+ let mut string = String::new();
+ let path = root
+ .join(name.as_ref())
+ .with_extension("ron");
+ File::open(path)
+ .ok()?
+ .read_to_string(&mut string)
+ .unwrap();
+ match ron::de::from_str(&string) {
+ Ok(out) => Some(out),
+ Err(e) => panic!("File {:?} deserialization failed: {:?}", name.as_ref(), e),
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_scene<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.scene_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_frame<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.frame_root(), name)
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn deserialize_for_resource<T, P>(&self, name: P) -> Option<T>
+ where
+ T: for<'a> serde::Deserialize<'a>,
+ P: AsRef<Path>,
+ {
+ Self::deserialize(&self.resource_root(), name)
+ }
+
+ #[cfg(feature = "png")]
+ pub fn save_png(
+ path: PathBuf, size: DeviceIntSize, format: ImageFormat, stride: Option<i32>, data: &[u8],
+ ) {
+ use png::{BitDepth, ColorType, Encoder};
+ use std::io::BufWriter;
+ use std::borrow::Cow;
+
+ // `png` expects
+ let data = match stride {
+ Some(stride) if stride != format.bytes_per_pixel() * size.width => {
+ let mut unstrided = Vec::new();
+ for y in 0..size.height {
+ let start = (y * stride) as usize;
+ unstrided.extend_from_slice(&data[start..start+(size.width * format.bytes_per_pixel()) as usize]);
+ }
+ Cow::from(unstrided)
+ }
+ _ => Cow::from(data),
+ };
+
+ let color_type = match format {
+ ImageFormat::RGBA8 => ColorType::RGBA,
+ ImageFormat::BGRA8 => {
+ warn!("Unable to swizzle PNG of BGRA8 type");
+ ColorType::RGBA
+ },
+ ImageFormat::R8 => ColorType::Grayscale,
+ ImageFormat::RG8 => ColorType::GrayscaleAlpha,
+ _ => {
+ error!("Unable to save PNG of {:?}", format);
+ return;
+ }
+ };
+ let w = BufWriter::new(File::create(path).unwrap());
+ let mut enc = Encoder::new(w, size.width as u32, size.height as u32);
+ enc.set_color(color_type);
+ enc.set_depth(BitDepth::Eight);
+ enc
+ .write_header()
+ .unwrap()
+ .write_image_data(&*data)
+ .unwrap();
+ }
+}
+
+/// An image that `ResourceCache` is unable to resolve during a capture.
+/// The image has to be transferred to `Renderer` and locked with the
+/// external image handler to get the actual contents and serialize them.
+#[derive(Deserialize, Serialize)]
+pub struct ExternalCaptureImage {
+ pub short_path: String,
+ pub descriptor: ImageDescriptor,
+ pub external: ExternalImageData,
+}
+
+/// A short description of an external image to be saved separately as
+/// "externals/XX.ron", redirecting into a specific texture/blob with
+/// the corresponding UV rectangle.
+#[derive(Deserialize, Serialize)]
+pub struct PlainExternalImage {
+ /// Path to the RON file describing the texel data.
+ pub data: String,
+ /// External image data source.
+ pub external: ExternalImageData,
+ /// UV sub-rectangle of the image.
+ pub uv: TexelRect,
+}
diff --git a/gfx/wr/webrender/src/clip.rs b/gfx/wr/webrender/src/clip.rs
new file mode 100644
index 0000000000..52a8b00b37
--- /dev/null
+++ b/gfx/wr/webrender/src/clip.rs
@@ -0,0 +1,2075 @@
+/* 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/. */
+
+//! Internal representation of clips in WebRender.
+//!
+//! # Data structures
+//!
+//! There are a number of data structures involved in the clip module:
+//!
+//! - ClipStore - Main interface used by other modules.
+//!
+//! - ClipItem - A single clip item (e.g. a rounded rect, or a box shadow).
+//! These are an exposed API type, stored inline in a ClipNode.
+//!
+//! - ClipNode - A ClipItem with an attached GPU handle. The GPU handle is populated
+//! when a ClipNodeInstance is built from this node (which happens while
+//! preparing primitives for render).
+//!
+//! ClipNodeInstance - A ClipNode with attached positioning information (a spatial
+//! node index). This is stored as a contiguous array of nodes
+//! within the ClipStore.
+//!
+//! ```ascii
+//! +-----------------------+-----------------------+-----------------------+
+//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance |
+//! +-----------------------+-----------------------+-----------------------+
+//! | ClipItem | ClipItem | ClipItem |
+//! | Spatial Node Index | Spatial Node Index | Spatial Node Index |
+//! | GPU cache handle | GPU cache handle | GPU cache handle |
+//! | ... | ... | ... |
+//! +-----------------------+-----------------------+-----------------------+
+//! 0 1 2
+//! +----------------+ | |
+//! | ClipNodeRange |____| |
+//! | index: 1 | |
+//! | count: 2 |___________________________________________________|
+//! +----------------+
+//! ```
+//!
+//! - ClipNodeRange - A clip item range identifies a range of clip nodes instances.
+//! It is stored as an (index, count).
+//!
+//! - ClipChainNode - A clip chain node contains a handle to an interned clip item,
+//! positioning information (from where the clip was defined), and
+//! an optional parent link to another ClipChainNode. ClipChainId
+//! is an index into an array, or ClipChainId::NONE for no parent.
+//!
+//! ```ascii
+//! +----------------+ ____+----------------+ ____+----------------+ /---> ClipChainId::NONE
+//! | ClipChainNode | | | ClipChainNode | | | ClipChainNode | |
+//! +----------------+ | +----------------+ | +----------------+ |
+//! | ClipDataHandle | | | ClipDataHandle | | | ClipDataHandle | |
+//! | Spatial index | | | Spatial index | | | Spatial index | |
+//! | Parent Id |___| | Parent Id |___| | Parent Id |___|
+//! | ... | | ... | | ... |
+//! +----------------+ +----------------+ +----------------+
+//! ```
+//!
+//! - ClipChainInstance - A ClipChain that has been built for a specific primitive + positioning node.
+//!
+//! When given a clip chain ID, and a local primitive rect and its spatial node, the clip module
+//! creates a clip chain instance. This is a struct with various pieces of useful information
+//! (such as a local clip rect). It also contains a (index, count)
+//! range specifier into an index buffer of the ClipNodeInstance structures that are actually relevant
+//! for this clip chain instance. The index buffer structure allows a single array to be used for
+//! all of the clip-chain instances built in a single frame. Each entry in the index buffer
+//! also stores some flags relevant to the clip node in this positioning context.
+//!
+//! ```ascii
+//! +----------------------+
+//! | ClipChainInstance |
+//! +----------------------+
+//! | ... |
+//! | local_clip_rect |________________________________________________________________________
+//! | clips_range |_______________ |
+//! +----------------------+ | |
+//! | |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! | flags | flags | flags | flags | flags |
+//! | ... | ... | ... | ... | ... |
+//! +------------------+------------------+------------------+------------------+------------------+
+//! ```
+//!
+//! # Rendering clipped primitives
+//!
+//! See the [`segment` module documentation][segment.rs].
+//!
+//!
+//! [segment.rs]: ../segment/index.html
+//!
+
+use api::{BorderRadius, ClipMode, ComplexClipRegion, ImageMask};
+use api::{BoxShadowClipMode, ClipId, ImageKey, ImageRendering, PipelineId};
+use api::units::*;
+use crate::image_tiling::{self, Repetition};
+use crate::border::{ensure_no_corner_overlap, BorderRadiusAu};
+use crate::box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey};
+use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialTree, SpatialNodeIndex, CoordinateSystemId};
+use crate::ellipse::Ellipse;
+use crate::gpu_cache::GpuCache;
+use crate::gpu_types::{BoxShadowStretchMode};
+use crate::intern::{self, ItemUid};
+use crate::internal_types::{FastHashMap, FastHashSet};
+use crate::prim_store::{VisibleMaskImageTile};
+use crate::prim_store::{PointKey, SizeKey, RectangleKey};
+use crate::render_task_cache::to_cache_size;
+use crate::resource_cache::{ImageRequest, ResourceCache};
+use crate::space::SpaceMapper;
+use crate::util::{clamp_to_scale_factor, MaxRect, extract_inner_rect_safe, project_rect, ScaleOffset, VecHelper};
+use euclid::approxeq::ApproxEq;
+use std::{iter, ops, u32};
+use smallvec::SmallVec;
+
+// Type definitions for interning clip nodes.
+
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum ClipIntern {}
+
+pub type ClipDataStore = intern::DataStore<ClipIntern>;
+pub type ClipDataHandle = intern::Handle<ClipIntern>;
+
+/// Defines a clip that is positioned by a specific spatial node
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Copy, Clone, PartialEq)]
+pub struct ClipInstance {
+ /// Handle to the interned clip
+ pub handle: ClipDataHandle,
+ /// Positioning node for this clip
+ pub spatial_node_index: SpatialNodeIndex,
+}
+
+impl ClipInstance {
+ /// Construct a new positioned clip
+ pub fn new(
+ handle: ClipDataHandle,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ ClipInstance {
+ handle,
+ spatial_node_index,
+ }
+ }
+}
+
+/// Defines a clip instance with some extra information that is available
+/// during scene building (since interned clips cannot retrieve the underlying
+/// data from the scene building thread).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Copy, Clone)]
+pub struct SceneClipInstance {
+ /// The interned clip + positioning information that is used during frame building.
+ pub clip: ClipInstance,
+ /// The definition of the clip, used during scene building to optimize clip-chains.
+ pub key: ClipItemKey,
+}
+
+/// A clip template defines clips in terms of the public API. Specifically,
+/// this is a parent `ClipId` and some number of clip instances. See the
+/// CLIPPING_AND_POSITIONING.md document in doc/ for more information.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipTemplate {
+ /// Parent of this clip, in terms of the public clip API
+ pub parent: ClipId,
+ /// List of instances that define this clip template
+ pub clips: SmallVec<[SceneClipInstance; 2]>,
+}
+
+/// A helper used during scene building to construct (internal) clip chains from
+/// the public API definitions (a hierarchy of ClipIds)
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipChainBuilder {
+ /// The built clip chain id for this level of the stack
+ clip_chain_id: ClipChainId,
+ /// A list of parent clips in the current clip chain, to de-duplicate clips as
+ /// we build child chains from this level.
+ parent_clips: FastHashSet<(ItemUid, SpatialNodeIndex)>,
+ /// A cache used during building child clip chains. Retained here to avoid
+ /// extra memory allocations each time we build a clip.
+ existing_clips_cache: FastHashSet<(ItemUid, SpatialNodeIndex)>,
+ /// Cache the previous ClipId we built, since it's quite common to share clip
+ /// id between primitives.
+ prev_clip_id: ClipId,
+ prev_clip_chain_id: ClipChainId,
+}
+
+impl ClipChainBuilder {
+ /// Construct a new clip chain builder with specified parent clip chain. If
+ /// the clip_id is Some(..), the clips in that template will be added to the
+ /// clip chain at this level (this functionality isn't currently used, but will
+ /// be in the follow up patches).
+ fn new(
+ parent_clip_chain_id: ClipChainId,
+ clip_id: Option<ClipId>,
+ clip_chain_nodes: &mut Vec<ClipChainNode>,
+ templates: &FastHashMap<ClipId, ClipTemplate>,
+ ) -> Self {
+ let mut parent_clips = FastHashSet::default();
+
+ // Walk the current clip chain ID, building a set of existing clips
+ let mut current_clip_chain_id = parent_clip_chain_id;
+ while current_clip_chain_id != ClipChainId::NONE {
+ let clip_chain_node = &clip_chain_nodes[current_clip_chain_id.0 as usize];
+ parent_clips.insert((clip_chain_node.handle.uid(), clip_chain_node.spatial_node_index));
+ current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+ }
+
+ // If specified, add the clips from the supplied template to this builder
+ let clip_chain_id = match clip_id {
+ Some(clip_id) => {
+ ClipChainBuilder::add_new_clips_to_chain(
+ clip_id,
+ parent_clip_chain_id,
+ &mut parent_clips,
+ clip_chain_nodes,
+ templates,
+ )
+ }
+ None => {
+ ClipChainId::NONE
+ }
+ };
+
+ ClipChainBuilder {
+ clip_chain_id,
+ existing_clips_cache: parent_clips.clone(),
+ parent_clips,
+ prev_clip_id: ClipId::root(PipelineId::dummy()),
+ prev_clip_chain_id: ClipChainId::NONE,
+ }
+ }
+
+ /// Internal helper function that appends all clip instances from a template
+ /// to a clip-chain (if they don't already exist in this chain).
+ fn add_new_clips_to_chain(
+ clip_id: ClipId,
+ parent_clip_chain_id: ClipChainId,
+ existing_clips: &mut FastHashSet<(ItemUid, SpatialNodeIndex)>,
+ clip_chain_nodes: &mut Vec<ClipChainNode>,
+ templates: &FastHashMap<ClipId, ClipTemplate>,
+ ) -> ClipChainId {
+ let template = &templates[&clip_id];
+ let mut clip_chain_id = parent_clip_chain_id;
+
+ for clip in &template.clips {
+ let key = (clip.clip.handle.uid(), clip.clip.spatial_node_index);
+
+ // If this clip chain already has this clip instance, skip it
+ if existing_clips.contains(&key) {
+ continue;
+ }
+
+ // Create a new clip-chain entry for this instance
+ let new_clip_chain_id = ClipChainId(clip_chain_nodes.len() as u32);
+ existing_clips.insert(key);
+ clip_chain_nodes.push(ClipChainNode {
+ handle: clip.clip.handle,
+ spatial_node_index: clip.clip.spatial_node_index,
+ parent_clip_chain_id: clip_chain_id,
+ });
+ clip_chain_id = new_clip_chain_id;
+ }
+
+ // The ClipId parenting is terminated when we reach the root ClipId
+ if clip_id == template.parent {
+ return clip_chain_id;
+ }
+
+ ClipChainBuilder::add_new_clips_to_chain(
+ template.parent,
+ clip_chain_id,
+ existing_clips,
+ clip_chain_nodes,
+ templates,
+ )
+ }
+
+ /// Return true if any of the clips in the hierarchy from clip_id to the
+ /// root clip are complex.
+ // TODO(gw): This method should only be required until the shared_clip
+ // optimization patches are complete, and can then be removed.
+ fn has_complex_clips(
+ &self,
+ clip_id: ClipId,
+ templates: &FastHashMap<ClipId, ClipTemplate>,
+ ) -> bool {
+ let template = &templates[&clip_id];
+
+ // Check if any of the clips in this template are complex
+ for clip in &template.clips {
+ if let ClipNodeKind::Complex = clip.key.kind.node_kind() {
+ return true;
+ }
+ }
+
+ // The ClipId parenting is terminated when we reach the root ClipId
+ if clip_id == template.parent {
+ return false;
+ }
+
+ // Recurse into parent clip template to also check those
+ self.has_complex_clips(
+ template.parent,
+ templates,
+ )
+ }
+
+ /// This is the main method used to get a clip chain for a primitive. Given a
+ /// clip id, it builds a clip-chain for that primitive, parented to the current
+ /// root clip chain hosted in this builder.
+ fn get_or_build_clip_chain_id(
+ &mut self,
+ clip_id: ClipId,
+ clip_chain_nodes: &mut Vec<ClipChainNode>,
+ templates: &FastHashMap<ClipId, ClipTemplate>,
+ ) -> ClipChainId {
+ if self.prev_clip_id == clip_id {
+ return self.prev_clip_chain_id;
+ }
+
+ // Instead of cloning here, do a clear and manual insertions, to
+ // avoid any extra heap allocations each time we build a clip-chain here.
+ // Maybe there is a better way to do this?
+ self.existing_clips_cache.clear();
+ self.existing_clips_cache.reserve(self.parent_clips.len());
+ for clip in &self.parent_clips {
+ self.existing_clips_cache.insert(*clip);
+ }
+
+ let clip_chain_id = ClipChainBuilder::add_new_clips_to_chain(
+ clip_id,
+ self.clip_chain_id,
+ &mut self.existing_clips_cache,
+ clip_chain_nodes,
+ templates,
+ );
+
+ self.prev_clip_id = clip_id;
+ self.prev_clip_chain_id = clip_chain_id;
+
+ clip_chain_id
+ }
+}
+
+/// Helper to identify simple clips (normal rects) from other kinds of clips,
+/// which can often be handled via fast code paths.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+pub enum ClipNodeKind {
+ /// A normal clip rectangle, with Clip mode.
+ Rectangle,
+ /// A rectangle with ClipOut, or any other kind of clip.
+ Complex,
+}
+
+// Result of comparing a clip node instance against a local rect.
+#[derive(Debug)]
+enum ClipResult {
+ // The clip does not affect the region at all.
+ Accept,
+ // The clip prevents the region from being drawn.
+ Reject,
+ // The clip affects part of the region. This may
+ // require a clip mask, depending on other factors.
+ Partial,
+}
+
+// A clip node is a single clip source, along with some
+// positioning information and implementation details
+// that control where the GPU data for this clip source
+// can be found.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ClipNode {
+ pub item: ClipItem,
+}
+
+// Convert from an interning key for a clip item
+// to a clip node, which is cached in the document.
+impl From<ClipItemKey> for ClipNode {
+ fn from(item: ClipItemKey) -> Self {
+ let kind = match item.kind {
+ ClipItemKeyKind::Rectangle(rect, mode) => {
+ ClipItemKind::Rectangle { rect: rect.into(), mode }
+ }
+ ClipItemKeyKind::RoundedRectangle(rect, radius, mode) => {
+ ClipItemKind::RoundedRectangle {
+ rect: rect.into(),
+ radius: radius.into(),
+ mode,
+ }
+ }
+ ClipItemKeyKind::ImageMask(rect, image, repeat) => {
+ ClipItemKind::Image {
+ image,
+ rect: rect.into(),
+ repeat,
+ }
+ }
+ ClipItemKeyKind::BoxShadow(shadow_rect_fract_offset, shadow_rect_size, shadow_radius, prim_shadow_rect, blur_radius, clip_mode) => {
+ ClipItemKind::new_box_shadow(
+ shadow_rect_fract_offset.into(),
+ shadow_rect_size.into(),
+ shadow_radius.into(),
+ prim_shadow_rect.into(),
+ blur_radius.to_f32_px(),
+ clip_mode,
+ )
+ }
+ };
+
+ ClipNode {
+ item: ClipItem {
+ kind,
+ },
+ }
+ }
+}
+
+// Flags that are attached to instances of clip nodes.
+bitflags! {
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct ClipNodeFlags: u8 {
+ const SAME_SPATIAL_NODE = 0x1;
+ const SAME_COORD_SYSTEM = 0x2;
+ const USE_FAST_PATH = 0x4;
+ }
+}
+
+// Identifier for a clip chain. Clip chains are stored
+// in a contiguous array in the clip store. They are
+// identified by a simple index into that array.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipChainId(pub u32);
+
+// The root of each clip chain is the NONE id. The
+// value is specifically set to u32::MAX so that if
+// any code accidentally tries to access the root
+// node, a bounds error will occur.
+impl ClipChainId {
+ pub const NONE: Self = ClipChainId(u32::MAX);
+ pub const INVALID: Self = ClipChainId(0xDEADBEEF);
+}
+
+// A clip chain node is an id for a range of clip sources,
+// and a link to a parent clip chain node, or ClipChainId::NONE.
+#[derive(Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipChainNode {
+ pub handle: ClipDataHandle,
+ pub spatial_node_index: SpatialNodeIndex,
+ pub parent_clip_chain_id: ClipChainId,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipSet {
+ /// Local space clip rect
+ pub local_clip_rect: LayoutRect,
+
+ /// ID of the clip chain that this set is clipped by.
+ pub clip_chain_id: ClipChainId,
+}
+
+// When a clip node is found to be valid for a
+// clip chain instance, it's stored in an index
+// buffer style structure. This struct contains
+// an index to the node data itself, as well as
+// some flags describing how this clip node instance
+// is positioned.
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipNodeInstance {
+ pub handle: ClipDataHandle,
+ pub spatial_node_index: SpatialNodeIndex,
+ pub flags: ClipNodeFlags,
+ pub visible_tiles: Option<Vec<VisibleMaskImageTile>>,
+}
+
+// A range of clip node instances that were found by
+// building a clip chain instance.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipNodeRange {
+ pub first: u32,
+ pub count: u32,
+}
+
+impl ClipNodeRange {
+ pub fn to_range(&self) -> ops::Range<usize> {
+ let start = self.first as usize;
+ let end = start + self.count as usize;
+
+ ops::Range {
+ start,
+ end,
+ }
+ }
+}
+
+/// A helper struct for converting between coordinate systems
+/// of clip sources and primitives.
+// todo(gw): optimize:
+// separate arrays for matrices
+// cache and only build as needed.
+//TODO: merge with `CoordinateSpaceMapping`?
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+enum ClipSpaceConversion {
+ Local,
+ ScaleOffset(ScaleOffset),
+ Transform(LayoutToWorldTransform),
+}
+
+impl ClipSpaceConversion {
+ /// Construct a new clip space converter between two spatial nodes.
+ fn new(
+ prim_spatial_node_index: SpatialNodeIndex,
+ clip_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> Self {
+ //Note: this code is different from `get_relative_transform` in a way that we only try
+ // getting the relative transform if it's Local or ScaleOffset,
+ // falling back to the world transform otherwise.
+ let clip_spatial_node = &spatial_tree
+ .spatial_nodes[clip_spatial_node_index.0 as usize];
+ let prim_spatial_node = &spatial_tree
+ .spatial_nodes[prim_spatial_node_index.0 as usize];
+
+ if prim_spatial_node_index == clip_spatial_node_index {
+ ClipSpaceConversion::Local
+ } else if prim_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
+ let scale_offset = prim_spatial_node.content_transform
+ .inverse()
+ .accumulate(&clip_spatial_node.content_transform);
+ ClipSpaceConversion::ScaleOffset(scale_offset)
+ } else {
+ ClipSpaceConversion::Transform(
+ spatial_tree
+ .get_world_transform(clip_spatial_node_index)
+ .into_transform()
+ )
+ }
+ }
+
+ fn to_flags(&self) -> ClipNodeFlags {
+ match *self {
+ ClipSpaceConversion::Local => {
+ ClipNodeFlags::SAME_SPATIAL_NODE | ClipNodeFlags::SAME_COORD_SYSTEM
+ }
+ ClipSpaceConversion::ScaleOffset(..) => {
+ ClipNodeFlags::SAME_COORD_SYSTEM
+ }
+ ClipSpaceConversion::Transform(..) => {
+ ClipNodeFlags::empty()
+ }
+ }
+ }
+}
+
+// Temporary information that is cached and reused
+// during building of a clip chain instance.
+#[derive(MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+struct ClipNodeInfo {
+ conversion: ClipSpaceConversion,
+ handle: ClipDataHandle,
+ spatial_node_index: SpatialNodeIndex,
+}
+
+impl ClipNodeInfo {
+ fn create_instance(
+ &self,
+ node: &ClipNode,
+ clipped_rect: &LayoutRect,
+ gpu_cache: &mut GpuCache,
+ resource_cache: &mut ResourceCache,
+ spatial_tree: &SpatialTree,
+ request_resources: bool,
+ ) -> Option<ClipNodeInstance> {
+ // Calculate some flags that are required for the segment
+ // building logic.
+ let mut flags = self.conversion.to_flags();
+
+ // Some clip shaders support a fast path mode for simple clips.
+ // TODO(gw): We could also apply fast path when segments are created, since we only write
+ // the mask for a single corner at a time then, so can always consider radii uniform.
+ let is_raster_2d =
+ flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) ||
+ spatial_tree
+ .get_world_viewport_transform(self.spatial_node_index)
+ .is_2d_axis_aligned();
+ if is_raster_2d && node.item.kind.supports_fast_path_rendering() {
+ flags |= ClipNodeFlags::USE_FAST_PATH;
+ }
+
+ let mut visible_tiles = None;
+
+ if let ClipItemKind::Image { rect, image, repeat } = node.item.kind {
+ let request = ImageRequest {
+ key: image,
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+
+ if let Some(props) = resource_cache.get_image_properties(image) {
+ if let Some(tile_size) = props.tiling {
+ let mut mask_tiles = Vec::new();
+
+ let visible_rect = if repeat {
+ *clipped_rect
+ } else {
+ // Bug 1648323 - It is unclear why on rare occasions we get
+ // a clipped_rect that does not intersect the clip's mask rect.
+ // defaulting to clipped_rect here results in zero repetitions
+ // which clips the primitive entirely.
+ clipped_rect.intersection(&rect).unwrap_or(*clipped_rect)
+ };
+
+ let repetitions = image_tiling::repetitions(
+ &rect,
+ &visible_rect,
+ rect.size,
+ );
+
+ for Repetition { origin, .. } in repetitions {
+ let layout_image_rect = LayoutRect {
+ origin,
+ size: rect.size,
+ };
+ let tiles = image_tiling::tiles(
+ &layout_image_rect,
+ &visible_rect,
+ &props.visible_rect,
+ tile_size as i32,
+ );
+ for tile in tiles {
+ if request_resources {
+ resource_cache.request_image(
+ request.with_tile(tile.offset),
+ gpu_cache,
+ );
+ }
+ mask_tiles.push(VisibleMaskImageTile {
+ tile_offset: tile.offset,
+ tile_rect: tile.rect,
+ });
+ }
+ }
+ visible_tiles = Some(mask_tiles);
+ } else if request_resources {
+ resource_cache.request_image(request, gpu_cache);
+ }
+ } else {
+ // If the supplied image key doesn't exist in the resource cache,
+ // skip the clip node since there is nothing to mask with.
+ warn!("Clip mask with missing image key {:?}", request.key);
+ return None;
+ }
+ }
+
+ Some(ClipNodeInstance {
+ handle: self.handle,
+ flags,
+ visible_tiles,
+ spatial_node_index: self.spatial_node_index,
+ })
+ }
+}
+
+impl ClipNode {
+ pub fn update(
+ &mut self,
+ device_pixel_scale: DevicePixelScale,
+ ) {
+ match self.item.kind {
+ ClipItemKind::Image { .. } |
+ ClipItemKind::Rectangle { .. } |
+ ClipItemKind::RoundedRectangle { .. } => {}
+
+ ClipItemKind::BoxShadow { ref mut source } => {
+ // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
+ // "the image that would be generated by applying to the shadow a
+ // Gaussian blur with a standard deviation equal to half the blur radius."
+ let blur_radius_dp = source.blur_radius * 0.5;
+
+ // Create scaling from requested size to cache size.
+ let mut content_scale = LayoutToWorldScale::new(1.0) * device_pixel_scale;
+ content_scale.0 = clamp_to_scale_factor(content_scale.0, false);
+
+ // Create the cache key for this box-shadow render task.
+ let cache_size = to_cache_size(source.shadow_rect_alloc_size, &mut content_scale);
+
+ let bs_cache_key = BoxShadowCacheKey {
+ blur_radius_dp: (blur_radius_dp * content_scale.0).round() as i32,
+ clip_mode: source.clip_mode,
+ original_alloc_size: (source.original_alloc_size * content_scale).round().to_i32(),
+ br_top_left: (source.shadow_radius.top_left * content_scale).round().to_i32(),
+ br_top_right: (source.shadow_radius.top_right * content_scale).round().to_i32(),
+ br_bottom_right: (source.shadow_radius.bottom_right * content_scale).round().to_i32(),
+ br_bottom_left: (source.shadow_radius.bottom_left * content_scale).round().to_i32(),
+ device_pixel_scale: Au::from_f32_px(content_scale.0),
+ };
+
+ source.cache_key = Some((cache_size, bs_cache_key));
+ }
+ }
+ }
+}
+
+/// The main clipping public interface that other modules access.
+#[derive(MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipStore {
+ pub clip_chain_nodes: Vec<ClipChainNode>,
+ pub clip_node_instances: Vec<ClipNodeInstance>,
+
+ active_clip_node_info: Vec<ClipNodeInfo>,
+ active_local_clip_rect: Option<LayoutRect>,
+ active_pic_clip_rect: PictureRect,
+
+ // No malloc sizeof since it's not implemented for ops::Range, but these
+ // allocations are tiny anyway.
+
+ /// Map of all clip templates defined by the public API to templates
+ #[ignore_malloc_size_of = "range missing"]
+ pub templates: FastHashMap<ClipId, ClipTemplate>,
+
+ /// A stack of current clip-chain builders. A new clip-chain builder is
+ /// typically created each time a clip root (such as an iframe or stacking
+ /// context) is defined.
+ #[ignore_malloc_size_of = "range missing"]
+ chain_builder_stack: Vec<ClipChainBuilder>,
+}
+
+// A clip chain instance is what gets built for a given clip
+// chain id + local primitive region + positioning node.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipChainInstance {
+ pub clips_range: ClipNodeRange,
+ // Combined clip rect for clips that are in the
+ // same coordinate system as the primitive.
+ pub local_clip_rect: LayoutRect,
+ pub has_non_local_clips: bool,
+ // If true, this clip chain requires allocation
+ // of a clip mask.
+ pub needs_mask: bool,
+ // Combined clip rect in picture space (may
+ // be more conservative that local_clip_rect).
+ pub pic_clip_rect: PictureRect,
+ // Space, in which the `pic_clip_rect` is defined.
+ pub pic_spatial_node_index: SpatialNodeIndex,
+}
+
+impl ClipChainInstance {
+ pub fn empty() -> Self {
+ ClipChainInstance {
+ clips_range: ClipNodeRange {
+ first: 0,
+ count: 0,
+ },
+ local_clip_rect: LayoutRect::zero(),
+ has_non_local_clips: false,
+ needs_mask: false,
+ pic_clip_rect: PictureRect::zero(),
+ pic_spatial_node_index: ROOT_SPATIAL_NODE_INDEX,
+ }
+ }
+}
+
+/// Maintains a (flattened) list of clips for a given level in the surface level stack.
+pub struct ClipChainLevel {
+ /// These clips will be handled when compositing this surface into the parent,
+ /// and can thus be ignored on the primitives that are drawn as part of this surface.
+ shared_clips: Vec<ClipInstance>,
+
+ /// Index of the first element in ClipChainStack::clip that belongs to this level.
+ first_clip_index: usize,
+ /// Used to sanity check push/pop balance.
+ initial_clip_counts_len: usize,
+}
+
+/// Maintains a stack of clip chain ids that are currently active,
+/// when a clip exists on a picture that has no surface, and is passed
+/// on down to the child primitive(s).
+///
+///
+/// In order to avoid many small vector allocations, all clip chain ids are
+/// stored in a single vector instead of per-level.
+/// Since we only work with the top-most level of the stack, we only need to
+/// know the first index in the clips vector that belongs to each level. The
+/// last index for the top-most level is always the end of the clips array.
+///
+/// Likewise, we push several clip chain ids to the clips array at each
+/// push_clip, and the number of clip chain ids removed during pop_clip
+/// must match. This is done by having a separate stack of clip counts
+/// in the clip-stack rather than per-level to avoid vector allocations.
+///
+/// ```ascii
+/// +----+----+---
+/// levels: | | | ...
+/// +----+----+---
+/// |first \
+/// | \
+/// | \
+/// +--+--+--+--+--+--+--
+/// clips: | | | | | | | ...
+/// +--+--+--+--+--+--+--
+/// | / /
+/// | / /
+/// | / /
+/// +--+--+--+--
+/// clip_counts: | 1| 2| 2| ...
+/// +--+--+--+--
+/// ```
+pub struct ClipChainStack {
+ /// A stack of clip chain lists. Each time a new surface is pushed,
+ /// a new level is added. Each time a new picture without surface is
+ /// pushed, it adds the picture clip chain to the clips vector in the
+ /// range belonging to the level (always the top-most level, so always
+ /// at the end of the clips array).
+ levels: Vec<ClipChainLevel>,
+ /// The actual stack of clip ids.
+ clips: Vec<ClipChainId>,
+ /// How many clip ids to pop from the vector each time we call pop_clip.
+ clip_counts: Vec<usize>,
+}
+
+impl ClipChainStack {
+ pub fn new() -> Self {
+ ClipChainStack {
+ levels: vec![
+ ClipChainLevel {
+ shared_clips: Vec::new(),
+ first_clip_index: 0,
+ initial_clip_counts_len: 0,
+ }
+ ],
+ clips: Vec::new(),
+ clip_counts: Vec::new(),
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.clips.clear();
+ self.clip_counts.clear();
+ self.levels.clear();
+ self.levels.push(ClipChainLevel {
+ shared_clips: Vec::new(),
+ first_clip_index: 0,
+ initial_clip_counts_len: 0,
+ });
+ }
+
+ pub fn take(&mut self) -> Self {
+ ClipChainStack {
+ levels: self.levels.take(),
+ clips: self.clips.take(),
+ clip_counts: self.clip_counts.take(),
+ }
+ }
+
+ /// Push a clip chain root onto the currently active list.
+ pub fn push_clip(
+ &mut self,
+ clip_chain_id: ClipChainId,
+ clip_store: &ClipStore,
+ ) {
+ let mut clip_count = 0;
+
+ let mut current_clip_chain_id = clip_chain_id;
+ while current_clip_chain_id != ClipChainId::NONE {
+ let clip_chain_node = &clip_store.clip_chain_nodes[current_clip_chain_id.0 as usize];
+ let clip_uid = clip_chain_node.handle.uid();
+
+ // The clip is required, so long as it doesn't exist in any of the shared_clips
+ // array from this or any parent surfaces.
+ // TODO(gw): We could consider making this a HashSet if it ever shows up in
+ // profiles, but the typical array length is 2-3 elements.
+ let mut valid_clip = true;
+ for level in &self.levels {
+ if level.shared_clips.iter().any(|instance| {
+ instance.handle.uid() == clip_uid &&
+ instance.spatial_node_index == clip_chain_node.spatial_node_index
+ }) {
+ valid_clip = false;
+ break;
+ }
+ }
+
+ if valid_clip {
+ self.clips.push(current_clip_chain_id);
+ clip_count += 1;
+ }
+
+ current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+ }
+
+ self.clip_counts.push(clip_count);
+ }
+
+ /// Pop a clip chain root from the currently active list.
+ pub fn pop_clip(&mut self) {
+ let count = self.clip_counts.pop().unwrap();
+ for _ in 0 .. count {
+ self.clips.pop().unwrap();
+ }
+ }
+
+ /// When a surface is created, it takes all clips and establishes a new
+ /// stack of clips to be propagated.
+ pub fn push_surface(
+ &mut self,
+ maybe_shared_clips: &[ClipInstance],
+ spatial_tree: &SpatialTree,
+ ) {
+ let mut shared_clips = Vec::with_capacity(maybe_shared_clips.len());
+
+ // If there are clips in the shared list for a picture cache, only include
+ // them if they are simple, axis-aligned clips (i.e. in the root coordinate
+ // system). This is necessary since when compositing picture cache tiles
+ // into the parent, we don't support applying a clip mask. This only ever
+ // occurs in wrench tests, not in display lists supplied by Gecko.
+ // TODO(gw): We can remove this when we update the WR API to have better
+ // knowledge of what coordinate system a clip must be in (by
+ // knowing if a reference frame exists in the chain between the
+ // clip's spatial node and the picture cache reference spatial node).
+ for clip in maybe_shared_clips {
+ let spatial_node = &spatial_tree.spatial_nodes[clip.spatial_node_index.0 as usize];
+ if spatial_node.coordinate_system_id == CoordinateSystemId::root() {
+ shared_clips.push(*clip);
+ }
+ }
+
+ let level = ClipChainLevel {
+ shared_clips,
+ first_clip_index: self.clips.len(),
+ initial_clip_counts_len: self.clip_counts.len(),
+ };
+
+ self.levels.push(level);
+ }
+
+ /// Pop a surface from the clip chain stack
+ pub fn pop_surface(&mut self) {
+ let level = self.levels.pop().unwrap();
+ assert!(self.clip_counts.len() == level.initial_clip_counts_len);
+ assert!(self.clips.len() == level.first_clip_index);
+ }
+
+ /// Get the list of currently active clip chains
+ pub fn current_clips_array(&self) -> &[ClipChainId] {
+ let first = self.levels.last().unwrap().first_clip_index;
+ &self.clips[first..]
+ }
+}
+
+impl ClipStore {
+ pub fn new() -> Self {
+ ClipStore {
+ clip_chain_nodes: Vec::new(),
+ clip_node_instances: Vec::new(),
+ active_clip_node_info: Vec::new(),
+ active_local_clip_rect: None,
+ active_pic_clip_rect: PictureRect::max_rect(),
+ templates: FastHashMap::default(),
+ chain_builder_stack: Vec::new(),
+ }
+ }
+
+ /// Register a new clip template for the clip_id defined in the display list.
+ pub fn register_clip_template(
+ &mut self,
+ clip_id: ClipId,
+ parent: ClipId,
+ clips: &[SceneClipInstance],
+ ) {
+ self.templates.insert(clip_id, ClipTemplate {
+ parent,
+ clips: clips.into(),
+ });
+ }
+
+ pub fn get_template(
+ &self,
+ clip_id: ClipId,
+ ) -> &ClipTemplate {
+ &self.templates[&clip_id]
+ }
+
+ /// The main method used to build a clip-chain for a given ClipId on a primitive
+ pub fn get_or_build_clip_chain_id(
+ &mut self,
+ clip_id: ClipId,
+ ) -> ClipChainId {
+ // TODO(gw): If many primitives reference the same ClipId, it might be worth
+ // maintaining a hash map cache of ClipId -> ClipChainId in each
+ // ClipChainBuilder
+
+ self.chain_builder_stack
+ .last_mut()
+ .unwrap()
+ .get_or_build_clip_chain_id(
+ clip_id,
+ &mut self.clip_chain_nodes,
+ &self.templates,
+ )
+ }
+
+ /// Return true if any of the clips in the hierarchy from clip_id to the
+ /// root clip are complex.
+ // TODO(gw): This method should only be required until the shared_clip
+ // optimization patches are complete, and can then be removed.
+ pub fn has_complex_clips(
+ &self,
+ clip_id: ClipId,
+ ) -> bool {
+ self.chain_builder_stack
+ .last()
+ .unwrap()
+ .has_complex_clips(
+ clip_id,
+ &self.templates,
+ )
+ }
+
+ /// Push a new clip root. This is used at boundaries of clips (such as iframes
+ /// and stacking contexts). This means that any clips on the existing clip
+ /// chain builder will not be added to clip-chains defined within this level,
+ /// since the clips will be applied by the parent.
+ pub fn push_clip_root(
+ &mut self,
+ clip_id: Option<ClipId>,
+ link_to_parent: bool,
+ ) {
+ let parent_clip_chain_id = if link_to_parent {
+ self.chain_builder_stack.last().unwrap().clip_chain_id
+ } else {
+ ClipChainId::NONE
+ };
+
+ let builder = ClipChainBuilder::new(
+ parent_clip_chain_id,
+ clip_id,
+ &mut self.clip_chain_nodes,
+ &self.templates,
+ );
+
+ self.chain_builder_stack.push(builder);
+ }
+
+ /// On completion of a stacking context or iframe, pop the current clip root.
+ pub fn pop_clip_root(
+ &mut self,
+ ) {
+ self.chain_builder_stack.pop().unwrap();
+ }
+
+ pub fn get_clip_chain(&self, clip_chain_id: ClipChainId) -> &ClipChainNode {
+ &self.clip_chain_nodes[clip_chain_id.0 as usize]
+ }
+
+ pub fn add_clip_chain_node(
+ &mut self,
+ handle: ClipDataHandle,
+ spatial_node_index: SpatialNodeIndex,
+ parent_clip_chain_id: ClipChainId,
+ ) -> ClipChainId {
+ let id = ClipChainId(self.clip_chain_nodes.len() as u32);
+ self.clip_chain_nodes.push(ClipChainNode {
+ handle,
+ spatial_node_index,
+ parent_clip_chain_id,
+ });
+ id
+ }
+
+ pub fn get_instance_from_range(
+ &self,
+ node_range: &ClipNodeRange,
+ index: u32,
+ ) -> &ClipNodeInstance {
+ &self.clip_node_instances[(node_range.first + index) as usize]
+ }
+
+ /// Setup the active clip chains for building a clip chain instance.
+ pub fn set_active_clips(
+ &mut self,
+ local_prim_clip_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_spatial_node_index: SpatialNodeIndex,
+ clip_chains: &[ClipChainId],
+ spatial_tree: &SpatialTree,
+ clip_data_store: &ClipDataStore,
+ ) {
+ self.active_clip_node_info.clear();
+ self.active_local_clip_rect = None;
+ self.active_pic_clip_rect = PictureRect::max_rect();
+
+ let mut local_clip_rect = local_prim_clip_rect;
+
+ for clip_chain_id in clip_chains {
+ let clip_chain_node = &self.clip_chain_nodes[clip_chain_id.0 as usize];
+
+ if !add_clip_node_to_current_chain(
+ clip_chain_node,
+ prim_spatial_node_index,
+ pic_spatial_node_index,
+ &mut local_clip_rect,
+ &mut self.active_clip_node_info,
+ &mut self.active_pic_clip_rect,
+ clip_data_store,
+ spatial_tree,
+ ) {
+ return;
+ }
+ }
+
+ self.active_local_clip_rect = Some(local_clip_rect);
+ }
+
+ /// Setup the active clip chains, based on an existing primitive clip chain instance.
+ pub fn set_active_clips_from_clip_chain(
+ &mut self,
+ prim_clip_chain: &ClipChainInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) {
+ // TODO(gw): Although this does less work than set_active_clips(), it does
+ // still do some unnecessary work (such as the clip space conversion).
+ // We could consider optimizing this if it ever shows up in a profile.
+
+ self.active_clip_node_info.clear();
+ self.active_local_clip_rect = Some(prim_clip_chain.local_clip_rect);
+ self.active_pic_clip_rect = prim_clip_chain.pic_clip_rect;
+
+ let clip_instances = &self
+ .clip_node_instances[prim_clip_chain.clips_range.to_range()];
+ for clip_instance in clip_instances {
+ let conversion = ClipSpaceConversion::new(
+ prim_spatial_node_index,
+ clip_instance.spatial_node_index,
+ spatial_tree,
+ );
+ self.active_clip_node_info.push(ClipNodeInfo {
+ handle: clip_instance.handle,
+ spatial_node_index: clip_instance.spatial_node_index,
+ conversion,
+ });
+ }
+ }
+
+ /// The main interface external code uses. Given a local primitive, positioning
+ /// information, and a clip chain id, build an optimized clip chain instance.
+ pub fn build_clip_chain_instance(
+ &mut self,
+ local_prim_rect: LayoutRect,
+ prim_to_pic_mapper: &SpaceMapper<LayoutPixel, PicturePixel>,
+ pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
+ spatial_tree: &SpatialTree,
+ gpu_cache: &mut GpuCache,
+ resource_cache: &mut ResourceCache,
+ device_pixel_scale: DevicePixelScale,
+ world_rect: &WorldRect,
+ clip_data_store: &mut ClipDataStore,
+ request_resources: bool,
+ is_chased: bool,
+ ) -> Option<ClipChainInstance> {
+ let local_clip_rect = match self.active_local_clip_rect {
+ Some(rect) => rect,
+ None => return None,
+ };
+ profile_scope!("build_clip_chain_instance");
+ if is_chased {
+ println!("\tbuilding clip chain instance with local rect {:?}", local_prim_rect);
+ }
+
+ let local_bounding_rect = local_prim_rect.intersection(&local_clip_rect)?;
+ let mut pic_clip_rect = prim_to_pic_mapper.map(&local_bounding_rect)?;
+ let world_clip_rect = pic_to_world_mapper.map(&pic_clip_rect)?;
+
+ // Now, we've collected all the clip nodes that *potentially* affect this
+ // primitive region, and reduced the size of the prim region as much as possible.
+
+ // Run through the clip nodes, and see which ones affect this prim region.
+
+ let first_clip_node_index = self.clip_node_instances.len() as u32;
+ let mut has_non_local_clips = false;
+ let mut needs_mask = false;
+
+ // For each potential clip node
+ for node_info in self.active_clip_node_info.drain(..) {
+ let node = &mut clip_data_store[node_info.handle];
+
+ // See how this clip affects the prim region.
+ let clip_result = match node_info.conversion {
+ ClipSpaceConversion::Local => {
+ node.item.kind.get_clip_result(&local_bounding_rect)
+ }
+ ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
+ has_non_local_clips = true;
+ node.item.kind.get_clip_result(&scale_offset.unmap_rect(&local_bounding_rect))
+ }
+ ClipSpaceConversion::Transform(ref transform) => {
+ has_non_local_clips = true;
+ node.item.kind.get_clip_result_complex(
+ transform,
+ &world_clip_rect,
+ world_rect,
+ )
+ }
+ };
+
+ if is_chased {
+ println!("\t\tclip {:?}", node.item);
+ println!("\t\tflags {:?}, resulted in {:?}", node_info.conversion.to_flags(), clip_result);
+ }
+
+ match clip_result {
+ ClipResult::Accept => {
+ // Doesn't affect the primitive at all, so skip adding to list
+ }
+ ClipResult::Reject => {
+ // Completely clips the supplied prim rect
+ return None;
+ }
+ ClipResult::Partial => {
+ // Needs a mask -> add to clip node indices
+
+ // TODO(gw): Ensure this only runs once on each node per frame?
+ node.update(device_pixel_scale);
+
+ // Create the clip node instance for this clip node
+ if let Some(instance) = node_info.create_instance(
+ node,
+ &local_bounding_rect,
+ gpu_cache,
+ resource_cache,
+ spatial_tree,
+ request_resources,
+ ) {
+ // As a special case, a partial accept of a clip rect that is
+ // in the same coordinate system as the primitive doesn't need
+ // a clip mask. Instead, it can be handled by the primitive
+ // vertex shader as part of the local clip rect. This is an
+ // important optimization for reducing the number of clip
+ // masks that are allocated on common pages.
+ needs_mask |= match node.item.kind {
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } |
+ ClipItemKind::RoundedRectangle { .. } |
+ ClipItemKind::Image { .. } |
+ ClipItemKind::BoxShadow { .. } => {
+ true
+ }
+
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ !instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM)
+ }
+ };
+
+ // Store this in the index buffer for this clip chain instance.
+ self.clip_node_instances.push(instance);
+ }
+ }
+ }
+ }
+
+ // Get the range identifying the clip nodes in the index buffer.
+ let clips_range = ClipNodeRange {
+ first: first_clip_node_index,
+ count: self.clip_node_instances.len() as u32 - first_clip_node_index,
+ };
+
+ // If this clip chain needs a mask, reduce the size of the mask allocation
+ // by any clips that were in the same space as the picture. This can result
+ // in much smaller clip mask allocations in some cases. Note that the ordering
+ // here is important - the reduction must occur *after* the clip item accept
+ // reject checks above, so that we don't eliminate masks accidentally (since
+ // we currently only support a local clip rect in the vertex shader).
+ if needs_mask {
+ pic_clip_rect = pic_clip_rect.intersection(&self.active_pic_clip_rect)?;
+ }
+
+ // Return a valid clip chain instance
+ Some(ClipChainInstance {
+ clips_range,
+ has_non_local_clips,
+ local_clip_rect,
+ pic_clip_rect,
+ pic_spatial_node_index: prim_to_pic_mapper.ref_spatial_node_index,
+ needs_mask,
+ })
+ }
+
+ pub fn clear_old_instances(&mut self) {
+ self.clip_node_instances.clear();
+ }
+}
+
+pub struct ComplexTranslateIter<I> {
+ source: I,
+ offset: LayoutVector2D,
+}
+
+impl<I: Iterator<Item = ComplexClipRegion>> Iterator for ComplexTranslateIter<I> {
+ type Item = ComplexClipRegion;
+ fn next(&mut self) -> Option<Self::Item> {
+ self.source
+ .next()
+ .map(|mut complex| {
+ complex.rect = complex.rect.translate(self.offset);
+ complex
+ })
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct ClipRegion<I> {
+ pub main: LayoutRect,
+ pub complex_clips: I,
+}
+
+impl<J> ClipRegion<ComplexTranslateIter<J>> {
+ pub fn create_for_clip_node(
+ rect: LayoutRect,
+ complex_clips: J,
+ reference_frame_relative_offset: &LayoutVector2D,
+ ) -> Self
+ where
+ J: Iterator<Item = ComplexClipRegion>
+ {
+ ClipRegion {
+ main: rect.translate(*reference_frame_relative_offset),
+ complex_clips: ComplexTranslateIter {
+ source: complex_clips,
+ offset: *reference_frame_relative_offset,
+ },
+ }
+ }
+}
+
+impl ClipRegion<Option<ComplexClipRegion>> {
+ pub fn create_for_clip_node_with_local_clip(
+ local_clip: &LayoutRect,
+ reference_frame_relative_offset: &LayoutVector2D
+ ) -> Self {
+ ClipRegion {
+ main: local_clip.translate(*reference_frame_relative_offset),
+ complex_clips: None,
+ }
+ }
+}
+
+// The ClipItemKey is a hashable representation of the contents
+// of a clip item. It is used during interning to de-duplicate
+// clip nodes between frames and display lists. This allows quick
+// comparison of clip node equality by handle, and also allows
+// the uploaded GPU cache handle to be retained between display lists.
+// TODO(gw): Maybe we should consider constructing these directly
+// in the DL builder?
+#[derive(Copy, Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ClipItemKeyKind {
+ Rectangle(RectangleKey, ClipMode),
+ RoundedRectangle(RectangleKey, BorderRadiusAu, ClipMode),
+ ImageMask(RectangleKey, ImageKey, bool),
+ BoxShadow(PointKey, SizeKey, BorderRadiusAu, RectangleKey, Au, BoxShadowClipMode),
+}
+
+impl ClipItemKeyKind {
+ pub fn rectangle(rect: LayoutRect, mode: ClipMode) -> Self {
+ ClipItemKeyKind::Rectangle(rect.into(), mode)
+ }
+
+ pub fn rounded_rect(rect: LayoutRect, mut radii: BorderRadius, mode: ClipMode) -> Self {
+ if radii.is_zero() {
+ ClipItemKeyKind::rectangle(rect, mode)
+ } else {
+ ensure_no_corner_overlap(&mut radii, rect.size);
+ ClipItemKeyKind::RoundedRectangle(
+ rect.into(),
+ radii.into(),
+ mode,
+ )
+ }
+ }
+
+ pub fn image_mask(image_mask: &ImageMask, mask_rect: LayoutRect) -> Self {
+ ClipItemKeyKind::ImageMask(
+ mask_rect.into(),
+ image_mask.image,
+ image_mask.repeat,
+ )
+ }
+
+ pub fn box_shadow(
+ shadow_rect: LayoutRect,
+ shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+ ) -> Self {
+ // Get the fractional offsets required to match the
+ // source rect with a minimal rect.
+ let fract_offset = LayoutPoint::new(
+ shadow_rect.origin.x.fract().abs(),
+ shadow_rect.origin.y.fract().abs(),
+ );
+
+ ClipItemKeyKind::BoxShadow(
+ fract_offset.into(),
+ shadow_rect.size.into(),
+ shadow_radius.into(),
+ prim_shadow_rect.into(),
+ Au::from_f32_px(blur_radius),
+ clip_mode,
+ )
+ }
+
+ pub fn node_kind(&self) -> ClipNodeKind {
+ match *self {
+ ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => ClipNodeKind::Rectangle,
+
+ ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) |
+ ClipItemKeyKind::RoundedRectangle(..) |
+ ClipItemKeyKind::ImageMask(..) |
+ ClipItemKeyKind::BoxShadow(..) => ClipNodeKind::Complex,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipItemKey {
+ pub kind: ClipItemKeyKind,
+}
+
+/// The data available about an interned clip node during scene building
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipInternData {
+ /// Whether this is a simple rectangle clip
+ pub clip_node_kind: ClipNodeKind,
+}
+
+impl intern::InternDebug for ClipItemKey {}
+
+impl intern::Internable for ClipIntern {
+ type Key = ClipItemKey;
+ type StoreData = ClipNode;
+ type InternData = ClipInternData;
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CLIPS;
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ClipItemKind {
+ Rectangle {
+ rect: LayoutRect,
+ mode: ClipMode,
+ },
+ RoundedRectangle {
+ rect: LayoutRect,
+ radius: BorderRadius,
+ mode: ClipMode,
+ },
+ Image {
+ image: ImageKey,
+ rect: LayoutRect,
+ repeat: bool,
+ },
+ BoxShadow {
+ source: BoxShadowClipSource,
+ },
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipItem {
+ pub kind: ClipItemKind,
+}
+
+fn compute_box_shadow_parameters(
+ shadow_rect_fract_offset: LayoutPoint,
+ shadow_rect_size: LayoutSize,
+ mut shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+) -> BoxShadowClipSource {
+ // Make sure corners don't overlap.
+ ensure_no_corner_overlap(&mut shadow_radius, shadow_rect_size);
+
+ let fract_size = LayoutSize::new(
+ shadow_rect_size.width.fract().abs(),
+ shadow_rect_size.height.fract().abs(),
+ );
+
+ // Create a minimal size primitive mask to blur. In this
+ // case, we ensure the size of each corner is the same,
+ // to simplify the shader logic that stretches the blurred
+ // result across the primitive.
+ let max_corner_width = shadow_radius.top_left.width
+ .max(shadow_radius.bottom_left.width)
+ .max(shadow_radius.top_right.width)
+ .max(shadow_radius.bottom_right.width);
+ let max_corner_height = shadow_radius.top_left.height
+ .max(shadow_radius.bottom_left.height)
+ .max(shadow_radius.top_right.height)
+ .max(shadow_radius.bottom_right.height);
+
+ // Get maximum distance that can be affected by given blur radius.
+ let blur_region = (BLUR_SAMPLE_SCALE * blur_radius).ceil();
+
+ // If the largest corner is smaller than the blur radius, we need to ensure
+ // that it's big enough that the corners don't affect the middle segments.
+ let used_corner_width = max_corner_width.max(blur_region);
+ let used_corner_height = max_corner_height.max(blur_region);
+
+ // Minimal nine-patch size, corner + internal + corner.
+ let min_shadow_rect_size = LayoutSize::new(
+ 2.0 * used_corner_width + blur_region,
+ 2.0 * used_corner_height + blur_region,
+ );
+
+ // The minimal rect to blur.
+ let mut minimal_shadow_rect = LayoutRect::new(
+ LayoutPoint::new(
+ blur_region + shadow_rect_fract_offset.x,
+ blur_region + shadow_rect_fract_offset.y,
+ ),
+ LayoutSize::new(
+ min_shadow_rect_size.width + fract_size.width,
+ min_shadow_rect_size.height + fract_size.height,
+ ),
+ );
+
+ // If the width or height ends up being bigger than the original
+ // primitive shadow rect, just blur the entire rect along that
+ // axis and draw that as a simple blit. This is necessary for
+ // correctness, since the blur of one corner may affect the blur
+ // in another corner.
+ let mut stretch_mode_x = BoxShadowStretchMode::Stretch;
+ if shadow_rect_size.width < minimal_shadow_rect.size.width {
+ minimal_shadow_rect.size.width = shadow_rect_size.width;
+ stretch_mode_x = BoxShadowStretchMode::Simple;
+ }
+
+ let mut stretch_mode_y = BoxShadowStretchMode::Stretch;
+ if shadow_rect_size.height < minimal_shadow_rect.size.height {
+ minimal_shadow_rect.size.height = shadow_rect_size.height;
+ stretch_mode_y = BoxShadowStretchMode::Simple;
+ }
+
+ // Expand the shadow rect by enough room for the blur to take effect.
+ let shadow_rect_alloc_size = LayoutSize::new(
+ 2.0 * blur_region + minimal_shadow_rect.size.width.ceil(),
+ 2.0 * blur_region + minimal_shadow_rect.size.height.ceil(),
+ );
+
+ BoxShadowClipSource {
+ original_alloc_size: shadow_rect_alloc_size,
+ shadow_rect_alloc_size,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius,
+ clip_mode,
+ stretch_mode_x,
+ stretch_mode_y,
+ cache_handle: None,
+ cache_key: None,
+ minimal_shadow_rect,
+ }
+}
+
+impl ClipItemKind {
+ pub fn new_box_shadow(
+ shadow_rect_fract_offset: LayoutPoint,
+ shadow_rect_size: LayoutSize,
+ mut shadow_radius: BorderRadius,
+ prim_shadow_rect: LayoutRect,
+ blur_radius: f32,
+ clip_mode: BoxShadowClipMode,
+ ) -> Self {
+ let mut source = compute_box_shadow_parameters(
+ shadow_rect_fract_offset,
+ shadow_rect_size,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius,
+ clip_mode,
+ );
+
+ fn needed_downscaling(source: &BoxShadowClipSource) -> Option<f32> {
+ // This size is fairly arbitrary, but it's the same as the size that
+ // we use to avoid caching big blurred stacking contexts.
+ //
+ // If you change it, ensure that the reftests
+ // box-shadow-large-blur-radius-* still hit the downscaling path,
+ // and that they render correctly.
+ const MAX_SIZE: f32 = 2048.;
+
+ let max_dimension =
+ source.shadow_rect_alloc_size.width.max(source.shadow_rect_alloc_size.height);
+
+ if max_dimension > MAX_SIZE {
+ Some(MAX_SIZE / max_dimension)
+ } else {
+ None
+ }
+ }
+
+ if let Some(downscale) = needed_downscaling(&source) {
+ shadow_radius.bottom_left.height *= downscale;
+ shadow_radius.bottom_left.width *= downscale;
+ shadow_radius.bottom_right.height *= downscale;
+ shadow_radius.bottom_right.width *= downscale;
+ shadow_radius.top_left.height *= downscale;
+ shadow_radius.top_left.width *= downscale;
+ shadow_radius.top_right.height *= downscale;
+ shadow_radius.top_right.width *= downscale;
+
+ let original_alloc_size = source.shadow_rect_alloc_size;
+
+ source = compute_box_shadow_parameters(
+ shadow_rect_fract_offset * downscale,
+ shadow_rect_size * downscale,
+ shadow_radius,
+ prim_shadow_rect,
+ blur_radius * downscale,
+ clip_mode,
+ );
+ source.original_alloc_size = original_alloc_size;
+ }
+ ClipItemKind::BoxShadow { source }
+ }
+
+ /// Returns true if this clip mask can run through the fast path
+ /// for the given clip item type.
+ ///
+ /// Note: this logic has to match `ClipBatcher::add` behavior.
+ fn supports_fast_path_rendering(&self) -> bool {
+ match *self {
+ ClipItemKind::Rectangle { .. } |
+ ClipItemKind::Image { .. } |
+ ClipItemKind::BoxShadow { .. } => {
+ false
+ }
+ ClipItemKind::RoundedRectangle { ref radius, .. } => {
+ // The rounded clip rect fast path shader can only work
+ // if the radii are uniform.
+ radius.is_uniform().is_some()
+ }
+ }
+ }
+
+ // Get an optional clip rect that a clip source can provide to
+ // reduce the size of a primitive region. This is typically
+ // used to eliminate redundant clips, and reduce the size of
+ // any clip mask that eventually gets drawn.
+ pub fn get_local_clip_rect(&self) -> Option<LayoutRect> {
+ match *self {
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => Some(rect),
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } => None,
+ ClipItemKind::RoundedRectangle { rect, mode: ClipMode::Clip, .. } => Some(rect),
+ ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, .. } => None,
+ ClipItemKind::Image { repeat, rect, .. } => {
+ if repeat {
+ None
+ } else {
+ Some(rect)
+ }
+ }
+ ClipItemKind::BoxShadow { .. } => None,
+ }
+ }
+
+ fn get_clip_result_complex(
+ &self,
+ transform: &LayoutToWorldTransform,
+ prim_world_rect: &WorldRect,
+ world_rect: &WorldRect,
+ ) -> ClipResult {
+ let visible_rect = match prim_world_rect.intersection(world_rect) {
+ Some(rect) => rect,
+ None => return ClipResult::Reject,
+ };
+
+ let (clip_rect, inner_rect, mode) = match *self {
+ ClipItemKind::Rectangle { rect, mode } => {
+ (rect, Some(rect), mode)
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode } => {
+ let inner_clip_rect = extract_inner_rect_safe(&rect, radius);
+ (rect, inner_clip_rect, mode)
+ }
+ ClipItemKind::Image { rect, repeat: false, .. } => {
+ (rect, None, ClipMode::Clip)
+ }
+ ClipItemKind::Image { repeat: true, .. } |
+ ClipItemKind::BoxShadow { .. } => {
+ return ClipResult::Partial;
+ }
+ };
+
+ if let Some(ref inner_clip_rect) = inner_rect {
+ if let Some(()) = projected_rect_contains(inner_clip_rect, transform, &visible_rect) {
+ return match mode {
+ ClipMode::Clip => ClipResult::Accept,
+ ClipMode::ClipOut => ClipResult::Reject,
+ };
+ }
+ }
+
+ match mode {
+ ClipMode::Clip => {
+ let outer_clip_rect = match project_rect(
+ transform,
+ &clip_rect,
+ world_rect,
+ ) {
+ Some(outer_clip_rect) => outer_clip_rect,
+ None => return ClipResult::Partial,
+ };
+
+ match outer_clip_rect.intersection(prim_world_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipMode::ClipOut => ClipResult::Partial,
+ }
+ }
+
+ // Check how a given clip source affects a local primitive region.
+ fn get_clip_result(
+ &self,
+ prim_rect: &LayoutRect,
+ ) -> ClipResult {
+ match *self {
+ ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => {
+ if rect.contains_rect(prim_rect) {
+ return ClipResult::Accept;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut } => {
+ if rect.contains_rect(prim_rect) {
+ return ClipResult::Reject;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(_) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Accept
+ }
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::Clip } => {
+ // TODO(gw): Consider caching this in the ClipNode
+ // if it ever shows in profiles.
+ if rounded_rectangle_contains_rect_quick(&rect, radius, &prim_rect) {
+ return ClipResult::Accept;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::ClipOut } => {
+ // TODO(gw): Consider caching this in the ClipNode
+ // if it ever shows in profiles.
+ if rounded_rectangle_contains_rect_quick(&rect, radius, &prim_rect) {
+ return ClipResult::Reject;
+ }
+
+ match rect.intersection(prim_rect) {
+ Some(_) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Accept
+ }
+ }
+ }
+ ClipItemKind::Image { rect, repeat, .. } => {
+ if repeat {
+ ClipResult::Partial
+ } else {
+ match rect.intersection(prim_rect) {
+ Some(..) => {
+ ClipResult::Partial
+ }
+ None => {
+ ClipResult::Reject
+ }
+ }
+ }
+ }
+ ClipItemKind::BoxShadow { .. } => {
+ ClipResult::Partial
+ }
+ }
+ }
+}
+
+/// Represents a local rect and a device space
+/// rectangles that are either outside or inside bounds.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Geometry {
+ pub local_rect: LayoutRect,
+ pub device_rect: DeviceIntRect,
+}
+
+impl From<LayoutRect> for Geometry {
+ fn from(local_rect: LayoutRect) -> Self {
+ Geometry {
+ local_rect,
+ device_rect: DeviceIntRect::zero(),
+ }
+ }
+}
+
+pub fn rounded_rectangle_contains_point(
+ point: &LayoutPoint,
+ rect: &LayoutRect,
+ radii: &BorderRadius
+) -> bool {
+ if !rect.contains(*point) {
+ return false;
+ }
+
+ let top_left_center = rect.origin + radii.top_left.to_vector();
+ if top_left_center.x > point.x && top_left_center.y > point.y &&
+ !Ellipse::new(radii.top_left).contains(*point - top_left_center.to_vector()) {
+ return false;
+ }
+
+ let bottom_right_center = rect.bottom_right() - radii.bottom_right.to_vector();
+ if bottom_right_center.x < point.x && bottom_right_center.y < point.y &&
+ !Ellipse::new(radii.bottom_right).contains(*point - bottom_right_center.to_vector()) {
+ return false;
+ }
+
+ let top_right_center = rect.top_right() +
+ LayoutVector2D::new(-radii.top_right.width, radii.top_right.height);
+ if top_right_center.x < point.x && top_right_center.y > point.y &&
+ !Ellipse::new(radii.top_right).contains(*point - top_right_center.to_vector()) {
+ return false;
+ }
+
+ let bottom_left_center = rect.bottom_left() +
+ LayoutVector2D::new(radii.bottom_left.width, -radii.bottom_left.height);
+ if bottom_left_center.x > point.x && bottom_left_center.y < point.y &&
+ !Ellipse::new(radii.bottom_left).contains(*point - bottom_left_center.to_vector()) {
+ return false;
+ }
+
+ true
+}
+
+/// Return true if the rounded rectangle described by `container` and `radii`
+/// definitely contains `containee`. May return false negatives, but never false
+/// positives.
+fn rounded_rectangle_contains_rect_quick(
+ container: &LayoutRect,
+ radii: &BorderRadius,
+ containee: &LayoutRect,
+) -> bool {
+ if !container.contains_rect(containee) {
+ return false;
+ }
+
+ /// Return true if `point` falls within `corner`. This only covers the
+ /// upper-left case; we transform the other corners into that form.
+ fn foul(point: LayoutPoint, corner: LayoutPoint) -> bool {
+ point.x < corner.x && point.y < corner.y
+ }
+
+ /// Flip `pt` about the y axis (i.e. negate `x`).
+ fn flip_x(pt: LayoutPoint) -> LayoutPoint {
+ LayoutPoint { x: -pt.x, .. pt }
+ }
+
+ /// Flip `pt` about the x axis (i.e. negate `y`).
+ fn flip_y(pt: LayoutPoint) -> LayoutPoint {
+ LayoutPoint { y: -pt.y, .. pt }
+ }
+
+ if foul(containee.top_left(), container.top_left() + radii.top_left) ||
+ foul(flip_x(containee.top_right()), flip_x(container.top_right()) + radii.top_right) ||
+ foul(flip_y(containee.bottom_left()), flip_y(container.bottom_left()) + radii.bottom_left) ||
+ foul(-containee.bottom_right(), -container.bottom_right() + radii.bottom_right)
+ {
+ return false;
+ }
+
+ true
+}
+
+pub fn projected_rect_contains(
+ source_rect: &LayoutRect,
+ transform: &LayoutToWorldTransform,
+ target_rect: &WorldRect,
+) -> Option<()> {
+ let points = [
+ transform.transform_point2d(source_rect.origin)?,
+ transform.transform_point2d(source_rect.top_right())?,
+ transform.transform_point2d(source_rect.bottom_right())?,
+ transform.transform_point2d(source_rect.bottom_left())?,
+ ];
+ let target_points = [
+ target_rect.origin,
+ target_rect.top_right(),
+ target_rect.bottom_right(),
+ target_rect.bottom_left(),
+ ];
+ // iterate the edges of the transformed polygon
+ for (a, b) in points
+ .iter()
+ .cloned()
+ .zip(points[1..].iter().cloned().chain(iter::once(points[0])))
+ {
+ // If this edge is redundant, it's a weird, case, and we shouldn't go
+ // length in trying to take the fast path (e.g. when the whole rectangle is a point).
+ // If any of edges of the target rectangle crosses the edge, it's not completely
+ // inside our transformed polygon either.
+ if a.approx_eq(&b) || target_points.iter().any(|&c| (b - a).cross(c - a) < 0.0) {
+ return None
+ }
+ }
+
+ Some(())
+}
+
+
+// Add a clip node into the list of clips to be processed
+// for the current clip chain. Returns false if the clip
+// results in the entire primitive being culled out.
+fn add_clip_node_to_current_chain(
+ node: &ClipChainNode,
+ prim_spatial_node_index: SpatialNodeIndex,
+ pic_spatial_node_index: SpatialNodeIndex,
+ local_clip_rect: &mut LayoutRect,
+ clip_node_info: &mut Vec<ClipNodeInfo>,
+ current_pic_clip_rect: &mut PictureRect,
+ clip_data_store: &ClipDataStore,
+ spatial_tree: &SpatialTree,
+) -> bool {
+ let clip_node = &clip_data_store[node.handle];
+
+ // Determine the most efficient way to convert between coordinate
+ // systems of the primitive and clip node.
+ let conversion = ClipSpaceConversion::new(
+ prim_spatial_node_index,
+ node.spatial_node_index,
+ spatial_tree,
+ );
+
+ // If we can convert spaces, try to reduce the size of the region
+ // requested, and cache the conversion information for the next step.
+ if let Some(clip_rect) = clip_node.item.kind.get_local_clip_rect() {
+ match conversion {
+ ClipSpaceConversion::Local => {
+ *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
+ Some(rect) => rect,
+ None => return false,
+ };
+ }
+ ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
+ let clip_rect = scale_offset.map_rect(&clip_rect);
+ *local_clip_rect = match local_clip_rect.intersection(&clip_rect) {
+ Some(rect) => rect,
+ None => return false,
+ };
+ }
+ ClipSpaceConversion::Transform(..) => {
+ // Map the local clip rect directly into the same space as the picture
+ // surface. This will often be the same space as the clip itself, which
+ // results in a reduction in allocated clip mask size.
+
+ // For simplicity, only apply this optimization if the clip is in the
+ // same coord system as the picture. There are some 'advanced' perspective
+ // clip tests in wrench that break without this check. Those cases are
+ // never used in Gecko, and we aim to remove support in WR for that
+ // in future to simplify the clipping pipeline.
+ let pic_coord_system = spatial_tree
+ .spatial_nodes[pic_spatial_node_index.0 as usize]
+ .coordinate_system_id;
+
+ let clip_coord_system = spatial_tree
+ .spatial_nodes[node.spatial_node_index.0 as usize]
+ .coordinate_system_id;
+
+ if pic_coord_system == clip_coord_system {
+ let mapper = SpaceMapper::new_with_target(
+ pic_spatial_node_index,
+ node.spatial_node_index,
+ PictureRect::max_rect(),
+ spatial_tree,
+ );
+
+ if let Some(pic_clip_rect) = mapper.map(&clip_rect) {
+ *current_pic_clip_rect = pic_clip_rect
+ .intersection(current_pic_clip_rect)
+ .unwrap_or(PictureRect::zero());
+ }
+ }
+ }
+ }
+ }
+
+ clip_node_info.push(ClipNodeInfo {
+ conversion,
+ spatial_node_index: node.spatial_node_index,
+ handle: node.handle,
+ });
+
+ true
+}
+
+#[cfg(test)]
+mod tests {
+ use super::projected_rect_contains;
+ use euclid::{Transform3D, rect};
+
+ #[test]
+ fn test_empty_projected_rect() {
+ assert_eq!(
+ None,
+ projected_rect_contains(
+ &rect(10.0, 10.0, 0.0, 0.0),
+ &Transform3D::identity(),
+ &rect(20.0, 20.0, 10.0, 10.0),
+ ),
+ "Empty rectangle is considered to include a non-empty!"
+ );
+ }
+}
diff --git a/gfx/wr/webrender/src/composite.rs b/gfx/wr/webrender/src/composite.rs
new file mode 100644
index 0000000000..c75b0a19b5
--- /dev/null
+++ b/gfx/wr/webrender/src/composite.rs
@@ -0,0 +1,1250 @@
+/* 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 api::{ColorF, YuvColorSpace, YuvFormat, ImageRendering, ExternalImageId, ImageBufferKind};
+use api::units::*;
+use crate::batch::{resolve_image};
+use euclid::Transform3D;
+use crate::gpu_cache::GpuCache;
+use crate::gpu_types::{ZBufferId, ZBufferIdGenerator};
+use crate::internal_types::TextureSource;
+use crate::picture::{ImageDependency, ResolvedSurfaceTexture, TileCacheInstance, TileId, TileSurface};
+use crate::prim_store::DeferredResolve;
+use crate::resource_cache::{ImageRequest, ResourceCache};
+use crate::util::Preallocator;
+use crate::tile_cache::PictureCacheDebugInfo;
+use std::{ops, u64};
+
+/*
+ Types and definitions related to compositing picture cache tiles
+ and/or OS compositor integration.
+ */
+
+/// Describes details of an operation to apply to a native surface
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum NativeSurfaceOperationDetails {
+ CreateSurface {
+ id: NativeSurfaceId,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ },
+ CreateExternalSurface {
+ id: NativeSurfaceId,
+ is_opaque: bool,
+ },
+ DestroySurface {
+ id: NativeSurfaceId,
+ },
+ CreateTile {
+ id: NativeTileId,
+ },
+ DestroyTile {
+ id: NativeTileId,
+ },
+ AttachExternalImage {
+ id: NativeSurfaceId,
+ external_image: ExternalImageId,
+ }
+}
+
+/// Describes an operation to apply to a native surface
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeSurfaceOperation {
+ pub details: NativeSurfaceOperationDetails,
+}
+
+/// Describes the source surface information for a tile to be composited. This
+/// is the analog of the TileSurface type, with target surface information
+/// resolved such that it can be used by the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum CompositeTileSurface {
+ Texture {
+ surface: ResolvedSurfaceTexture,
+ },
+ Color {
+ color: ColorF,
+ },
+ Clear,
+ ExternalSurface {
+ external_surface_index: ResolvedExternalSurfaceIndex,
+ },
+}
+
+/// The surface format for a tile being composited.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum CompositeSurfaceFormat {
+ Rgba,
+ Yuv,
+}
+
+/// Describes the geometry and surface of a tile to be composited
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CompositeTile {
+ pub surface: CompositeTileSurface,
+ pub rect: DeviceRect,
+ pub clip_rect: DeviceRect,
+ pub dirty_rect: DeviceRect,
+ pub valid_rect: DeviceRect,
+ pub transform: Option<CompositorSurfaceTransform>,
+ pub z_id: ZBufferId,
+}
+
+pub enum ExternalSurfaceDependency {
+ Yuv {
+ image_dependencies: [ImageDependency; 3],
+ color_space: YuvColorSpace,
+ format: YuvFormat,
+ rescale: f32,
+ },
+ Rgb {
+ image_dependency: ImageDependency,
+ flip_y: bool,
+ },
+}
+
+/// Describes information about drawing a primitive as a compositor surface.
+/// For now, we support only YUV images as compositor surfaces, but in future
+/// this will also support RGBA images.
+pub struct ExternalSurfaceDescriptor {
+ // Rectangle of this surface in owning picture's coordinate space
+ pub local_rect: PictureRect,
+ // Rectangle of this surface in the compositor local space
+ // TODO(gw): Switch this to CompositorSurfaceRect (CompositorSurfacePixel) in compositor trait.
+ pub surface_rect: DeviceRect,
+ // Rectangle of this surface in true device pixels
+ pub device_rect: DeviceRect,
+ pub local_clip_rect: PictureRect,
+ pub clip_rect: DeviceRect,
+ pub transform: CompositorSurfaceTransform,
+ pub image_rendering: ImageRendering,
+ pub z_id: ZBufferId,
+ pub dependency: ExternalSurfaceDependency,
+ /// If native compositing is enabled, the native compositor surface handle.
+ /// Otherwise, this will be None
+ pub native_surface_id: Option<NativeSurfaceId>,
+ /// If the native surface needs to be updated, this will contain the size
+ /// of the native surface as Some(size). If not dirty, this is None.
+ pub update_params: Option<DeviceIntSize>,
+}
+
+/// Information about a plane in a YUV or RGB surface.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct ExternalPlaneDescriptor {
+ pub texture: TextureSource,
+ pub texture_layer: i32,
+ pub uv_rect: TexelRect,
+}
+
+impl ExternalPlaneDescriptor {
+ fn invalid() -> Self {
+ ExternalPlaneDescriptor {
+ texture: TextureSource::Invalid,
+ texture_layer: 0,
+ uv_rect: TexelRect::invalid(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct ResolvedExternalSurfaceIndex(pub usize);
+
+impl ResolvedExternalSurfaceIndex {
+ pub const INVALID: ResolvedExternalSurfaceIndex = ResolvedExternalSurfaceIndex(usize::MAX);
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ResolvedExternalSurfaceColorData {
+ Yuv {
+ // YUV specific information
+ image_dependencies: [ImageDependency; 3],
+ planes: [ExternalPlaneDescriptor; 3],
+ color_space: YuvColorSpace,
+ format: YuvFormat,
+ rescale: f32,
+ },
+ Rgb {
+ image_dependency: ImageDependency,
+ plane: ExternalPlaneDescriptor,
+ flip_y: bool,
+ },
+}
+
+/// An ExternalSurfaceDescriptor that has had image keys
+/// resolved to texture handles. This contains all the
+/// information that the compositor step in renderer
+/// needs to know.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ResolvedExternalSurface {
+ pub color_data: ResolvedExternalSurfaceColorData,
+ pub image_buffer_kind: ImageBufferKind,
+ // Update information for a native surface if it's dirty
+ pub update_params: Option<(NativeSurfaceId, DeviceIntSize)>,
+}
+
+/// Public interface specified in `RendererOptions` that configures
+/// how WR compositing will operate.
+pub enum CompositorConfig {
+ /// Let WR draw tiles via normal batching. This requires no special OS support.
+ Draw {
+ /// If this is zero, a full screen present occurs at the end of the
+ /// frame. This is the simplest and default mode. If this is non-zero,
+ /// then the operating system supports a form of 'partial present' where
+ /// only dirty regions of the framebuffer need to be updated.
+ max_partial_present_rects: usize,
+ /// If this is true, WR must draw the previous frames' dirty regions when
+ /// doing a partial present. This is used for EGL which requires the front
+ /// buffer to always be fully consistent.
+ draw_previous_partial_present_regions: bool,
+ /// A client provided interface to a compositor handling partial present.
+ /// Required if webrender must query the backbuffer's age.
+ partial_present: Option<Box<dyn PartialPresentCompositor>>,
+ },
+ /// Use a native OS compositor to draw tiles. This requires clients to implement
+ /// the Compositor trait, but can be significantly more power efficient on operating
+ /// systems that support it.
+ Native {
+ /// The maximum number of dirty rects that can be provided per compositor
+ /// surface update. If this is zero, the entire compositor surface for
+ /// a given tile will be drawn if it's dirty.
+ max_update_rects: usize,
+ /// A client provided interface to a native / OS compositor.
+ compositor: Box<dyn Compositor>,
+ }
+}
+
+impl CompositorConfig {
+ pub fn compositor(&mut self) -> Option<&mut Box<dyn Compositor>> {
+ match self {
+ CompositorConfig::Native { ref mut compositor, .. } => {
+ Some(compositor)
+ }
+ CompositorConfig::Draw { .. } => {
+ None
+ }
+ }
+ }
+
+ pub fn partial_present(&mut self) -> Option<&mut Box<dyn PartialPresentCompositor>> {
+ match self {
+ CompositorConfig::Native { .. } => {
+ None
+ }
+ CompositorConfig::Draw { ref mut partial_present, .. } => {
+ partial_present.as_mut()
+ }
+ }
+ }
+
+}
+
+impl Default for CompositorConfig {
+ /// Default compositor config is full present without partial present.
+ fn default() -> Self {
+ CompositorConfig::Draw {
+ max_partial_present_rects: 0,
+ draw_previous_partial_present_regions: false,
+ partial_present: None,
+ }
+ }
+}
+
+/// This is a representation of `CompositorConfig` without the `Compositor` trait
+/// present. This allows it to be freely copied to other threads, such as the render
+/// backend where the frame builder can access it.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum CompositorKind {
+ /// WR handles compositing via drawing.
+ Draw {
+ /// Partial present support.
+ max_partial_present_rects: usize,
+ /// Draw previous regions when doing partial present.
+ draw_previous_partial_present_regions: bool,
+ },
+ /// Native OS compositor.
+ Native {
+ /// Maximum dirty rects per compositor surface.
+ max_update_rects: usize,
+ /// The virtual surface size used by underlying platform.
+ virtual_surface_size: i32,
+ },
+}
+
+impl Default for CompositorKind {
+ /// Default compositor config is full present without partial present.
+ fn default() -> Self {
+ CompositorKind::Draw {
+ max_partial_present_rects: 0,
+ draw_previous_partial_present_regions: false,
+ }
+ }
+}
+
+impl CompositorKind {
+ pub fn get_virtual_surface_size(&self) -> i32 {
+ match self {
+ CompositorKind::Draw { .. } => 0,
+ CompositorKind::Native { virtual_surface_size, .. } => *virtual_surface_size,
+ }
+ }
+
+ // We currently only support transforms for Native compositors,
+ // bug 1655639 is filed for adding support to Draw.
+ pub fn supports_transforms(&self) -> bool {
+ match self {
+ CompositorKind::Draw { .. } => false,
+ CompositorKind::Native { .. } => true,
+ }
+ }
+}
+
+/// The backing surface kind for a tile. Same as `TileSurface`, minus
+/// the texture cache handles, visibility masks etc.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub enum TileSurfaceKind {
+ Texture,
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+impl From<&TileSurface> for TileSurfaceKind {
+ fn from(surface: &TileSurface) -> Self {
+ match surface {
+ TileSurface::Texture { .. } => TileSurfaceKind::Texture,
+ TileSurface::Color { color } => TileSurfaceKind::Color { color: *color },
+ TileSurface::Clear => TileSurfaceKind::Clear,
+ }
+ }
+}
+
+/// Describes properties that identify a tile composition uniquely.
+/// The backing surface for this tile.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeTileDescriptor {
+ pub tile_id: TileId,
+ pub surface_kind: TileSurfaceKind,
+}
+
+/// Describes the properties that identify a surface composition uniquely.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeSurfaceDescriptor {
+ pub surface_id: Option<NativeSurfaceId>,
+ pub clip_rect: DeviceRect,
+ pub transform: CompositorSurfaceTransform,
+ // A list of image keys and generations that this compositor surface
+ // depends on. This avoids composites being skipped when the only
+ // thing that has changed is the generation of an compositor surface
+ // image dependency.
+ pub image_dependencies: [ImageDependency; 3],
+ pub image_rendering: ImageRendering,
+ // List of the surface information for each tile added to this virtual surface
+ pub tile_descriptors: Vec<CompositeTileDescriptor>,
+}
+
+/// Describes surface properties used to composite a frame. This
+/// is used to compare compositions between frames.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(PartialEq, Clone)]
+pub struct CompositeDescriptor {
+ pub surfaces: Vec<CompositeSurfaceDescriptor>,
+}
+
+impl CompositeDescriptor {
+ /// Construct an empty descriptor.
+ pub fn empty() -> Self {
+ CompositeDescriptor {
+ surfaces: Vec::new(),
+ }
+ }
+}
+
+pub struct CompositeStatePreallocator {
+ opaque_tiles: Preallocator,
+ alpha_tiles: Preallocator,
+ clear_tiles: Preallocator,
+ external_surfaces: Preallocator,
+ occluders: Preallocator,
+ occluders_events: Preallocator,
+ occluders_active: Preallocator,
+ descriptor_surfaces: Preallocator,
+}
+
+impl CompositeStatePreallocator {
+ pub fn record(&mut self, state: &CompositeState) {
+ self.opaque_tiles.record_vec(&state.opaque_tiles);
+ self.alpha_tiles.record_vec(&state.alpha_tiles);
+ self.clear_tiles.record_vec(&state.clear_tiles);
+ self.external_surfaces.record_vec(&state.external_surfaces);
+ self.occluders.record_vec(&state.occluders.occluders);
+ self.occluders_events.record_vec(&state.occluders.events);
+ self.occluders_active.record_vec(&state.occluders.active);
+ self.descriptor_surfaces.record_vec(&state.descriptor.surfaces);
+ }
+
+ pub fn preallocate(&self, state: &mut CompositeState) {
+ self.opaque_tiles.preallocate_vec(&mut state.opaque_tiles);
+ self.alpha_tiles.preallocate_vec(&mut state.alpha_tiles);
+ self.clear_tiles.preallocate_vec(&mut state.clear_tiles);
+ self.external_surfaces.preallocate_vec(&mut state.external_surfaces);
+ self.occluders.preallocate_vec(&mut state.occluders.occluders);
+ self.occluders_events.preallocate_vec(&mut state.occluders.events);
+ self.occluders_active.preallocate_vec(&mut state.occluders.active);
+ self.descriptor_surfaces.preallocate_vec(&mut state.descriptor.surfaces);
+ }
+}
+
+impl Default for CompositeStatePreallocator {
+ fn default() -> Self {
+ CompositeStatePreallocator {
+ opaque_tiles: Preallocator::new(40),
+ alpha_tiles: Preallocator::new(16),
+ clear_tiles: Preallocator::new(0),
+ external_surfaces: Preallocator::new(0),
+ occluders: Preallocator::new(16),
+ occluders_events: Preallocator::new(32),
+ occluders_active: Preallocator::new(16),
+ descriptor_surfaces: Preallocator::new(8),
+ }
+ }
+}
+
+/// The list of tiles to be drawn this frame
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CompositeState {
+ // TODO(gw): Consider splitting up CompositeState into separate struct types depending
+ // on the selected compositing mode. Many of the fields in this state struct
+ // are only applicable to either Native or Draw compositing mode.
+ /// List of opaque tiles to be drawn by the Draw compositor.
+ pub opaque_tiles: Vec<CompositeTile>,
+ /// List of alpha tiles to be drawn by the Draw compositor.
+ pub alpha_tiles: Vec<CompositeTile>,
+ /// List of clear tiles to be drawn by the Draw compositor.
+ pub clear_tiles: Vec<CompositeTile>,
+ /// List of primitives that were promoted to be compositor surfaces.
+ pub external_surfaces: Vec<ResolvedExternalSurface>,
+ /// Used to generate z-id values for tiles in the Draw compositor mode.
+ pub z_generator: ZBufferIdGenerator,
+ // If false, we can't rely on the dirty rects in the CompositeTile
+ // instances. This currently occurs during a scroll event, as a
+ // signal to refresh the whole screen. This is only a temporary
+ // measure until we integrate with OS compositors. In the meantime
+ // it gives us the ability to partial present for any non-scroll
+ // case as a simple win (e.g. video, animation etc).
+ pub dirty_rects_are_valid: bool,
+ /// The kind of compositor for picture cache tiles (e.g. drawn by WR, or OS compositor)
+ pub compositor_kind: CompositorKind,
+ /// The overall device pixel scale, used for tile occlusion conversions.
+ global_device_pixel_scale: DevicePixelScale,
+ /// List of registered occluders
+ pub occluders: Occluders,
+ /// Description of the surfaces and properties that are being composited.
+ pub descriptor: CompositeDescriptor,
+ /// Debugging information about the state of the pictures cached for regression testing.
+ pub picture_cache_debug: PictureCacheDebugInfo,
+}
+
+impl CompositeState {
+ /// Construct a new state for compositing picture tiles. This is created
+ /// during each frame construction and passed to the renderer.
+ pub fn new(
+ compositor_kind: CompositorKind,
+ global_device_pixel_scale: DevicePixelScale,
+ max_depth_ids: i32,
+ dirty_rects_are_valid: bool,
+ ) -> Self {
+ CompositeState {
+ opaque_tiles: Vec::new(),
+ alpha_tiles: Vec::new(),
+ clear_tiles: Vec::new(),
+ z_generator: ZBufferIdGenerator::new(max_depth_ids),
+ dirty_rects_are_valid,
+ compositor_kind,
+ global_device_pixel_scale,
+ occluders: Occluders::new(),
+ descriptor: CompositeDescriptor::empty(),
+ external_surfaces: Vec::new(),
+ picture_cache_debug: PictureCacheDebugInfo::new(),
+ }
+ }
+
+ /// Register an occluder during picture cache updates that can be
+ /// used during frame building to occlude tiles.
+ pub fn register_occluder(
+ &mut self,
+ z_id: ZBufferId,
+ rect: WorldRect,
+ ) {
+ let device_rect = (rect * self.global_device_pixel_scale).round().to_i32();
+
+ self.occluders.push(device_rect, z_id);
+ }
+
+ /// Add a picture cache to be composited
+ pub fn push_surface(
+ &mut self,
+ tile_cache: &TileCacheInstance,
+ device_clip_rect: DeviceRect,
+ global_device_pixel_scale: DevicePixelScale,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) {
+ let mut visible_opaque_tile_count = 0;
+ let mut visible_alpha_tile_count = 0;
+ let mut opaque_tile_descriptors = Vec::new();
+ let mut alpha_tile_descriptors = Vec::new();
+ let mut surface_device_rect = DeviceRect::zero();
+
+ for tile in tile_cache.tiles.values() {
+ if !tile.is_visible {
+ // This can occur when a tile is found to be occluded during frame building.
+ continue;
+ }
+
+ let device_rect = (tile.world_tile_rect * global_device_pixel_scale).round();
+ let surface = tile.surface.as_ref().expect("no tile surface set!");
+
+ // Accumulate this tile into the overall surface bounds. This is used below
+ // to clamp the size of the supplied clip rect to a reasonable value.
+ // NOTE: This clip rect must include the device_valid_rect rather than
+ // the tile device rect. This ensures that in the case of a picture
+ // cache slice that is smaller than a single tile, the clip rect in
+ // the composite descriptor will change if the position of that slice
+ // is changed. Otherwise, WR may conclude that no composite is needed
+ // if the tile itself was not invalidated due to changing content.
+ // See bug #1675414 for more detail.
+ surface_device_rect = surface_device_rect.union(&tile.device_valid_rect);
+
+ let descriptor = CompositeTileDescriptor {
+ surface_kind: surface.into(),
+ tile_id: tile.id,
+ };
+
+ let (surface, is_opaque) = match surface {
+ TileSurface::Color { color } => {
+ (CompositeTileSurface::Color { color: *color }, true)
+ }
+ TileSurface::Clear => {
+ (CompositeTileSurface::Clear, false)
+ }
+ TileSurface::Texture { descriptor, .. } => {
+ let surface = descriptor.resolve(resource_cache, tile_cache.current_tile_size);
+ (
+ CompositeTileSurface::Texture { surface },
+ // If a tile has compositor surface intersecting with it, we need to
+ // respect the tile.is_opaque property even if the overall tile cache
+ // is opaque. In this case, the tile.is_opaque property is required
+ // in order to ensure correct draw order with compositor surfaces.
+ tile.is_opaque || (!tile.has_compositor_surface && tile_cache.is_opaque()),
+ )
+ }
+ };
+
+ if is_opaque {
+ opaque_tile_descriptors.push(descriptor);
+ visible_opaque_tile_count += 1;
+ } else {
+ alpha_tile_descriptors.push(descriptor);
+ visible_alpha_tile_count += 1;
+ }
+
+ let tile = CompositeTile {
+ surface,
+ rect: device_rect,
+ valid_rect: tile.device_valid_rect.translate(-device_rect.origin.to_vector()),
+ dirty_rect: tile.device_dirty_rect.translate(-device_rect.origin.to_vector()),
+ clip_rect: device_clip_rect,
+ transform: None,
+ z_id: tile.z_id,
+ };
+
+ self.push_tile(tile, is_opaque);
+ }
+
+ // Sort the tile descriptor lists, since iterating values in the tile_cache.tiles
+ // hashmap doesn't provide any ordering guarantees, but we want to detect the
+ // composite descriptor as equal if the tiles list is the same, regardless of
+ // ordering.
+ opaque_tile_descriptors.sort_by_key(|desc| desc.tile_id);
+ alpha_tile_descriptors.sort_by_key(|desc| desc.tile_id);
+
+ // If the clip rect is too large, it can cause accuracy and correctness problems
+ // for some native compositors (specifically, CoreAnimation in this case). To
+ // work around that, intersect the supplied clip rect with the current bounds
+ // of the native surface, which ensures it is a reasonable size.
+ let surface_clip_rect = device_clip_rect
+ .intersection(&surface_device_rect)
+ .unwrap_or(DeviceRect::zero());
+
+ // Add opaque surface before any compositor surfaces
+ if visible_opaque_tile_count > 0 {
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: tile_cache.native_surface.as_ref().map(|s| s.opaque),
+ clip_rect: surface_clip_rect,
+ transform: CompositorSurfaceTransform::translation(
+ tile_cache.device_position.x,
+ tile_cache.device_position.y,
+ 0.0,
+ ),
+ image_dependencies: [ImageDependency::INVALID; 3],
+ image_rendering: ImageRendering::CrispEdges,
+ tile_descriptors: opaque_tile_descriptors,
+ }
+ );
+ }
+
+ // For each compositor surface that was promoted, build the
+ // information required for the compositor to draw it
+ for external_surface in &tile_cache.external_surfaces {
+ let clip_rect = external_surface
+ .clip_rect
+ .intersection(&device_clip_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ let required_plane_count =
+ match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv { format, .. } => {
+ format.get_plane_num()
+ },
+ ExternalSurfaceDependency::Rgb { .. } => {
+ 1
+ }
+ };
+
+ let mut image_dependencies = [ImageDependency::INVALID; 3];
+
+ for i in 0 .. required_plane_count {
+ let dependency = match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv { image_dependencies, .. } => {
+ image_dependencies[i]
+ },
+ ExternalSurfaceDependency::Rgb { image_dependency, .. } => {
+ image_dependency
+ }
+ };
+ image_dependencies[i] = dependency;
+ }
+
+ // Get a new z_id for each compositor surface, to ensure correct ordering
+ // when drawing with the simple (Draw) compositor, and to schedule compositing
+ // of any required updates into the surfaces.
+ let needs_external_surface_update = match self.compositor_kind {
+ CompositorKind::Draw { .. } => true,
+ _ => external_surface.update_params.is_some(),
+ };
+ let external_surface_index = if needs_external_surface_update {
+ let external_surface_index = self.compute_external_surface_dependencies(
+ &external_surface,
+ &image_dependencies,
+ required_plane_count,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ if external_surface_index == ResolvedExternalSurfaceIndex::INVALID {
+ continue;
+ }
+ external_surface_index
+ } else {
+ ResolvedExternalSurfaceIndex::INVALID
+ };
+
+ let tile = CompositeTile {
+ surface: CompositeTileSurface::ExternalSurface { external_surface_index },
+ rect: external_surface.surface_rect,
+ valid_rect: external_surface.surface_rect.translate(-external_surface.surface_rect.origin.to_vector()),
+ dirty_rect: external_surface.surface_rect.translate(-external_surface.surface_rect.origin.to_vector()),
+ clip_rect,
+ transform: Some(external_surface.transform),
+ z_id: external_surface.z_id,
+ };
+
+ // Add a surface descriptor for each compositor surface. For the Draw
+ // compositor, this is used to avoid composites being skipped by adding
+ // a dependency on the compositor surface external image keys / generations.
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: external_surface.native_surface_id,
+ clip_rect,
+ transform: external_surface.transform,
+ image_dependencies: image_dependencies,
+ image_rendering: external_surface.image_rendering,
+ tile_descriptors: Vec::new(),
+ }
+ );
+
+ self.push_tile(tile, true);
+ }
+
+ // Add alpha / overlay tiles after compositor surfaces
+ if visible_alpha_tile_count > 0 {
+ self.descriptor.surfaces.push(
+ CompositeSurfaceDescriptor {
+ surface_id: tile_cache.native_surface.as_ref().map(|s| s.alpha),
+ clip_rect: surface_clip_rect,
+ transform: CompositorSurfaceTransform::translation(
+ tile_cache.device_position.x,
+ tile_cache.device_position.y,
+ 0.0,
+ ),
+ image_dependencies: [ImageDependency::INVALID; 3],
+ image_rendering: ImageRendering::CrispEdges,
+ tile_descriptors: alpha_tile_descriptors,
+ }
+ );
+ }
+ }
+
+ fn compute_external_surface_dependencies(
+ &mut self,
+ external_surface: &ExternalSurfaceDescriptor,
+ image_dependencies: &[ImageDependency; 3],
+ required_plane_count: usize,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) -> ResolvedExternalSurfaceIndex {
+ let mut planes = [
+ ExternalPlaneDescriptor::invalid(),
+ ExternalPlaneDescriptor::invalid(),
+ ExternalPlaneDescriptor::invalid(),
+ ];
+
+ let mut valid_plane_count = 0;
+ for i in 0 .. required_plane_count {
+ let request = ImageRequest {
+ key: image_dependencies[i].key,
+ rendering: external_surface.image_rendering,
+ tile: None,
+ };
+
+ let cache_item = resolve_image(
+ request,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ if cache_item.texture_id != TextureSource::Invalid {
+ valid_plane_count += 1;
+ let plane = &mut planes[i];
+ *plane = ExternalPlaneDescriptor {
+ texture: cache_item.texture_id,
+ texture_layer: cache_item.texture_layer,
+ uv_rect: cache_item.uv_rect.into(),
+ };
+ }
+ }
+
+ // Check if there are valid images added for each YUV plane
+ if valid_plane_count < required_plane_count {
+ warn!("Warnings: skip a YUV/RGB compositor surface, found {}/{} valid images",
+ valid_plane_count,
+ required_plane_count,
+ );
+ return ResolvedExternalSurfaceIndex::INVALID;
+ }
+
+ let external_surface_index = ResolvedExternalSurfaceIndex(self.external_surfaces.len());
+
+ // If the external surface descriptor reports that the native surface
+ // needs to be updated, create an update params tuple for the renderer
+ // to use.
+ let update_params = external_surface.update_params.map(|surface_size| {
+ (
+ external_surface.native_surface_id.expect("bug: no native surface!"),
+ surface_size
+ )
+ });
+
+ match external_surface.dependency {
+ ExternalSurfaceDependency::Yuv{ color_space, format, rescale, .. } => {
+
+ let image_buffer_kind = planes[0].texture.image_buffer_kind();
+
+ self.external_surfaces.push(ResolvedExternalSurface {
+ color_data: ResolvedExternalSurfaceColorData::Yuv {
+ image_dependencies: *image_dependencies,
+ planes,
+ color_space,
+ format,
+ rescale,
+ },
+ image_buffer_kind,
+ update_params,
+ });
+ },
+ ExternalSurfaceDependency::Rgb{ flip_y, .. } => {
+
+ let image_buffer_kind = planes[0].texture.image_buffer_kind();
+
+ // Only propagate flip_y if the compositor doesn't support transforms,
+ // since otherwise it'll be handled as part of the transform.
+ self.external_surfaces.push(ResolvedExternalSurface {
+ color_data: ResolvedExternalSurfaceColorData::Rgb {
+ image_dependency: image_dependencies[0],
+ plane: planes[0],
+ flip_y: flip_y && !self.compositor_kind.supports_transforms(),
+ },
+ image_buffer_kind,
+ update_params,
+ });
+ },
+ }
+ external_surface_index
+ }
+
+ /// Add a tile to the appropriate array, depending on tile properties and compositor mode.
+ fn push_tile(
+ &mut self,
+ tile: CompositeTile,
+ is_opaque: bool,
+ ) {
+ match tile.surface {
+ CompositeTileSurface::Color { .. } => {
+ // Color tiles are, by definition, opaque. We might support non-opaque color
+ // tiles if we ever find pages that have a lot of these.
+ self.opaque_tiles.push(tile);
+ }
+ CompositeTileSurface::Clear => {
+ // Clear tiles have a special bucket
+ self.clear_tiles.push(tile);
+ }
+ CompositeTileSurface::Texture { .. } => {
+ // Texture surfaces get bucketed by opaque/alpha, for z-rejection
+ // on the Draw compositor mode.
+ if is_opaque {
+ self.opaque_tiles.push(tile);
+ } else {
+ self.alpha_tiles.push(tile);
+ }
+ }
+ CompositeTileSurface::ExternalSurface { .. } => {
+ self.opaque_tiles.push(tile);
+ }
+ }
+ }
+}
+
+/// An arbitrary identifier for a native (OS compositor) surface
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeSurfaceId(pub u64);
+
+impl NativeSurfaceId {
+ /// A special id for the native surface that is used for debug / profiler overlays.
+ pub const DEBUG_OVERLAY: NativeSurfaceId = NativeSurfaceId(u64::MAX);
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NativeTileId {
+ pub surface_id: NativeSurfaceId,
+ pub x: i32,
+ pub y: i32,
+}
+
+impl NativeTileId {
+ /// A special id for the native surface that is used for debug / profiler overlays.
+ pub const DEBUG_OVERLAY: NativeTileId = NativeTileId {
+ surface_id: NativeSurfaceId::DEBUG_OVERLAY,
+ x: 0,
+ y: 0,
+ };
+}
+
+/// Information about a bound surface that the native compositor
+/// returns to WR.
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct NativeSurfaceInfo {
+ /// An offset into the surface that WR should draw. Some compositing
+ /// implementations (notably, DirectComposition) use texture atlases
+ /// when the surface sizes are small. In this case, an offset can
+ /// be returned into the larger texture where WR should draw. This
+ /// can be (0, 0) if texture atlases are not used.
+ pub origin: DeviceIntPoint,
+ /// The ID of the FBO that WR should bind to, in order to draw to
+ /// the bound surface. On Windows (ANGLE) this will always be 0,
+ /// since creating a p-buffer sets the default framebuffer to
+ /// be the DirectComposition surface. On Mac, this will be non-zero,
+ /// since it identifies the IOSurface that has been bound to draw to.
+ // TODO(gw): This may need to be a larger / different type for WR
+ // backends that are not GL.
+ pub fbo_id: u32,
+}
+
+#[repr(C)]
+pub struct CompositorCapabilities {
+ pub virtual_surface_size: i32,
+}
+
+/// The transform type to apply to Compositor surfaces.
+// TODO: Should transform from CompositorSurfacePixel instead, but this requires a cleanup of the
+// Compositor API to use CompositorSurface-space geometry instead of Device-space where necessary
+// to avoid a bunch of noisy cast_unit calls and make it actually type-safe. May be difficult due
+// to pervasive use of Device-space nomenclature inside WR.
+// pub struct CompositorSurfacePixel;
+// pub type CompositorSurfaceTransform = Transform3D<f32, CompositorSurfacePixel, DevicePixel>;
+pub type CompositorSurfaceTransform = Transform3D<f32, DevicePixel, DevicePixel>;
+
+/// Defines an interface to a native (OS level) compositor. If supplied
+/// by the client application, then picture cache slices will be
+/// composited by the OS compositor, rather than drawn via WR batches.
+pub trait Compositor {
+ /// Create a new OS compositor surface with the given properties.
+ fn create_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ );
+
+ /// Create a new OS compositor surface that can be used with an
+ /// existing ExternalImageId, instead of being drawn to by WebRender.
+ /// Surfaces created by this can only be used with attach_external_image,
+ /// and not create_tile/destroy_tile/bind/unbind.
+ fn create_external_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ is_opaque: bool,
+ );
+
+ /// Destroy the surface with the specified id. WR may call this
+ /// at any time the surface is no longer required (including during
+ /// renderer deinit). It's the responsibility of the embedder
+ /// to ensure that the surface is only freed once the GPU is
+ /// no longer using the surface (if this isn't already handled
+ /// by the operating system).
+ fn destroy_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ );
+
+ /// Create a new OS compositor tile with the given properties.
+ fn create_tile(
+ &mut self,
+ id: NativeTileId,
+ );
+
+ /// Destroy an existing compositor tile.
+ fn destroy_tile(
+ &mut self,
+ id: NativeTileId,
+ );
+
+ /// Attaches an ExternalImageId to an OS compositor surface created
+ /// by create_external_surface, and uses that as the contents of
+ /// the surface. It is expected that a single surface will have
+ /// many different images attached (like one for each video frame).
+ fn attach_external_image(
+ &mut self,
+ id: NativeSurfaceId,
+ external_image: ExternalImageId
+ );
+
+ /// Mark a tile as invalid before any surfaces are queued for
+ /// composition and before it is updated with bind. This is useful
+ /// for early composition, allowing for dependency tracking of which
+ /// surfaces can be composited early while others are still updating.
+ fn invalidate_tile(
+ &mut self,
+ _id: NativeTileId,
+ _valid_rect: DeviceIntRect
+ ) {}
+
+ /// Bind this surface such that WR can issue OpenGL commands
+ /// that will target the surface. Returns an (x, y) offset
+ /// where WR should draw into the surface. This can be set
+ /// to (0, 0) if the OS doesn't use texture atlases. The dirty
+ /// rect is a local surface rect that specifies which part
+ /// of the surface needs to be updated. If max_update_rects
+ /// in CompositeConfig is 0, this will always be the size
+ /// of the entire surface. The returned offset is only
+ /// relevant to compositors that store surfaces in a texture
+ /// atlas (that is, WR expects that the dirty rect doesn't
+ /// affect the coordinates of the returned origin).
+ fn bind(
+ &mut self,
+ id: NativeTileId,
+ dirty_rect: DeviceIntRect,
+ valid_rect: DeviceIntRect,
+ ) -> NativeSurfaceInfo;
+
+ /// Unbind the surface. This is called by WR when it has
+ /// finished issuing OpenGL commands on the current surface.
+ fn unbind(
+ &mut self,
+ );
+
+ /// Begin the frame
+ fn begin_frame(&mut self);
+
+ /// Add a surface to the visual tree to be composited. Visuals must
+ /// be added every frame, between the begin/end transaction call. The
+ /// z-order of the surfaces is determined by the order they are added
+ /// to the visual tree.
+ // TODO(gw): Adding visuals every frame makes the interface simple,
+ // but may have performance implications on some compositors?
+ // We might need to change the interface to maintain a visual
+ // tree that can be mutated?
+ // TODO(gw): We might need to add a concept of a hierachy in future.
+ fn add_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ transform: CompositorSurfaceTransform,
+ clip_rect: DeviceIntRect,
+ image_rendering: ImageRendering,
+ );
+
+ /// Notify the compositor that all tiles have been invalidated and all
+ /// native surfaces have been added, thus it is safe to start compositing
+ /// valid surfaces. The dirty rects array allows native compositors that
+ /// support partial present to skip copying unchanged areas.
+ /// Optionally provides a set of rectangles for the areas known to be
+ /// opaque, this is currently only computed if the caller is SwCompositor.
+ fn start_compositing(
+ &mut self,
+ _dirty_rects: &[DeviceIntRect],
+ _opaque_rects: &[DeviceIntRect],
+ ) {}
+
+ /// Commit any changes in the compositor tree for this frame. WR calls
+ /// this once when all surface and visual updates are complete, to signal
+ /// that the OS composite transaction should be applied.
+ fn end_frame(&mut self);
+
+ /// Enable/disable native compositor usage
+ fn enable_native_compositor(&mut self, enable: bool);
+
+ /// Safely deinitialize any remaining resources owned by the compositor.
+ fn deinit(&mut self);
+
+ /// Get the capabilities struct for this compositor. This is used to
+ /// specify what features a compositor supports, depending on the
+ /// underlying platform
+ fn get_capabilities(&self) -> CompositorCapabilities;
+}
+
+/// Defines an interface to a non-native (application-level) Compositor which handles
+/// partial present. This is required if webrender must query the backbuffer's age.
+/// TODO: Use the Compositor trait for native and non-native compositors, and integrate
+/// this functionality there.
+pub trait PartialPresentCompositor {
+ /// Allows webrender to specify the total region that will be rendered to this frame,
+ /// ie the frame's dirty region and some previous frames' dirty regions, if applicable
+ /// (calculated using the buffer age). Must be called before anything has been rendered
+ /// to the main framebuffer.
+ fn set_buffer_damage_region(&mut self, rects: &[DeviceIntRect]);
+}
+
+/// Information about an opaque surface used to occlude tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Occluder {
+ z_id: ZBufferId,
+ device_rect: DeviceIntRect,
+}
+
+// Whether this event is the start or end of a rectangle
+#[derive(Debug)]
+enum OcclusionEventKind {
+ Begin,
+ End,
+}
+
+// A list of events on the y-axis, with the rectangle range that it affects on the x-axis
+#[derive(Debug)]
+struct OcclusionEvent {
+ y: i32,
+ x_range: ops::Range<i32>,
+ kind: OcclusionEventKind,
+}
+
+impl OcclusionEvent {
+ fn new(y: i32, kind: OcclusionEventKind, x0: i32, x1: i32) -> Self {
+ OcclusionEvent {
+ y,
+ x_range: ops::Range {
+ start: x0,
+ end: x1,
+ },
+ kind,
+ }
+ }
+}
+
+/// List of registered occluders.
+///
+/// Also store a couple of vectors for reuse.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Occluders {
+ occluders: Vec<Occluder>,
+
+ // The two vectors below are kept to avoid unnecessary reallocations in area().
+
+ #[cfg_attr(feature = "serde", serde(skip))]
+ events: Vec<OcclusionEvent>,
+
+ #[cfg_attr(feature = "serde", serde(skip))]
+ active: Vec<ops::Range<i32>>,
+}
+
+impl Occluders {
+ fn new() -> Self {
+ Occluders {
+ occluders: Vec::new(),
+ events: Vec::new(),
+ active: Vec::new(),
+ }
+ }
+
+ fn push(&mut self, device_rect: DeviceIntRect, z_id: ZBufferId) {
+ self.occluders.push(Occluder { device_rect, z_id });
+ }
+
+ /// Returns true if a tile with the specified rectangle and z_id
+ /// is occluded by an opaque surface in front of it.
+ pub fn is_tile_occluded(
+ &mut self,
+ z_id: ZBufferId,
+ device_rect: DeviceRect,
+ ) -> bool {
+ // It's often the case that a tile is only occluded by considering multiple
+ // picture caches in front of it (for example, the background tiles are
+ // often occluded by a combination of the content slice + the scrollbar slices).
+
+ // The basic algorithm is:
+ // For every occluder:
+ // If this occluder is in front of the tile we are querying:
+ // Clip the occluder rectangle to the query rectangle.
+ // Calculate the total non-overlapping area of those clipped occluders.
+ // If the cumulative area of those occluders is the same as the area of the query tile,
+ // Then the entire tile must be occluded and can be skipped during rasterization and compositing.
+
+ // Get the reference area we will compare against.
+ let device_rect = device_rect.round().to_i32();
+ let ref_area = device_rect.size.width * device_rect.size.height;
+
+ // Calculate the non-overlapping area of the valid occluders.
+ let cover_area = self.area(z_id, &device_rect);
+ debug_assert!(cover_area <= ref_area);
+
+ // Check if the tile area is completely covered
+ ref_area == cover_area
+ }
+
+ /// Return the total area covered by a set of occluders, accounting for
+ /// overlapping areas between those rectangles.
+ fn area(
+ &mut self,
+ z_id: ZBufferId,
+ clip_rect: &DeviceIntRect,
+ ) -> i32 {
+ // This implementation is based on the article https://leetcode.com/articles/rectangle-area-ii/.
+ // This is not a particularly efficient implementation (it skips building segment trees), however
+ // we typically use this where the length of the rectangles array is < 10, so simplicity is more important.
+
+ self.events.clear();
+ self.active.clear();
+
+ let mut area = 0;
+
+ // Step through each rectangle and build the y-axis event list
+ for occluder in &self.occluders {
+ // Only consider occluders in front of this rect
+ if occluder.z_id.0 > z_id.0 {
+ // Clip the source rect to the rectangle we care about, since we only
+ // want to record area for the tile we are comparing to.
+ if let Some(rect) = occluder.device_rect.intersection(clip_rect) {
+ let x0 = rect.origin.x;
+ let x1 = x0 + rect.size.width;
+ self.events.push(OcclusionEvent::new(rect.origin.y, OcclusionEventKind::Begin, x0, x1));
+ self.events.push(OcclusionEvent::new(rect.origin.y + rect.size.height, OcclusionEventKind::End, x0, x1));
+ }
+ }
+ }
+
+ // If we didn't end up with any valid events, the area must be 0
+ if self.events.is_empty() {
+ return 0;
+ }
+
+ // Sort the events by y-value
+ self.events.sort_by_key(|e| e.y);
+ let mut cur_y = self.events[0].y;
+
+ // Step through each y interval
+ for event in &self.events {
+ // This is the dimension of the y-axis we are accumulating areas for
+ let dy = event.y - cur_y;
+
+ // If we have active events covering x-ranges in this y-interval, process them
+ if dy != 0 && !self.active.is_empty() {
+ assert!(dy > 0);
+
+ // Step through the x-ranges, ordered by x0 of each event
+ self.active.sort_by_key(|i| i.start);
+ let mut query = 0;
+ let mut cur = self.active[0].start;
+
+ // Accumulate the non-overlapping x-interval that contributes to area for this y-interval.
+ for interval in &self.active {
+ cur = interval.start.max(cur);
+ query += (interval.end - cur).max(0);
+ cur = cur.max(interval.end);
+ }
+
+ // Accumulate total area for this y-interval
+ area += query * dy;
+ }
+
+ // Update the active events list
+ match event.kind {
+ OcclusionEventKind::Begin => {
+ self.active.push(event.x_range.clone());
+ }
+ OcclusionEventKind::End => {
+ let index = self.active.iter().position(|i| *i == event.x_range).unwrap();
+ self.active.remove(index);
+ }
+ }
+
+ cur_y = event.y;
+ }
+
+ area
+ }
+}
diff --git a/gfx/wr/webrender/src/debug_colors.rs b/gfx/wr/webrender/src/debug_colors.rs
new file mode 100644
index 0000000000..4ce8887126
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_colors.rs
@@ -0,0 +1,159 @@
+/* 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/. */
+
+#![allow(dead_code)]
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::excessive_precision))]
+
+use api::ColorF;
+
+// A subset of the standard CSS colors, useful for defining GPU tag colors etc.
+
+pub const INDIGO: ColorF = ColorF { r: 0.294117647059, g: 0.0, b: 0.509803921569, a: 1.0 };
+pub const GOLD: ColorF = ColorF { r: 1.0, g: 0.843137254902, b: 0.0, a: 1.0 };
+pub const FIREBRICK: ColorF = ColorF { r: 0.698039215686, g: 0.133333333333, b: 0.133333333333, a: 1.0 };
+pub const INDIANRED: ColorF = ColorF { r: 0.803921568627, g: 0.360784313725, b: 0.360784313725, a: 1.0 };
+pub const YELLOW: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
+pub const DARKOLIVEGREEN: ColorF = ColorF { r: 0.333333333333, g: 0.419607843137, b: 0.18431372549, a: 1.0 };
+pub const DARKSEAGREEN: ColorF = ColorF { r: 0.560784313725, g: 0.737254901961, b: 0.560784313725, a: 1.0 };
+pub const SLATEGREY: ColorF = ColorF { r: 0.439215686275, g: 0.501960784314, b: 0.564705882353, a: 1.0 };
+pub const DARKSLATEGREY: ColorF = ColorF { r: 0.18431372549, g: 0.309803921569, b: 0.309803921569, a: 1.0 };
+pub const MEDIUMVIOLETRED: ColorF = ColorF { r: 0.780392156863, g: 0.0823529411765, b: 0.521568627451, a: 1.0 };
+pub const MEDIUMORCHID: ColorF = ColorF { r: 0.729411764706, g: 0.333333333333, b: 0.827450980392, a: 1.0 };
+pub const CHARTREUSE: ColorF = ColorF { r: 0.498039215686, g: 1.0, b: 0.0, a: 1.0 };
+pub const MEDIUMSLATEBLUE: ColorF = ColorF { r: 0.482352941176, g: 0.407843137255, b: 0.933333333333, a: 1.0 };
+pub const BLACK: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
+pub const SPRINGGREEN: ColorF = ColorF { r: 0.0, g: 1.0, b: 0.498039215686, a: 1.0 };
+pub const CRIMSON: ColorF = ColorF { r: 0.862745098039, g: 0.078431372549, b: 0.235294117647, a: 1.0 };
+pub const LIGHTSALMON: ColorF = ColorF { r: 1.0, g: 0.627450980392, b: 0.478431372549, a: 1.0 };
+pub const BROWN: ColorF = ColorF { r: 0.647058823529, g: 0.164705882353, b: 0.164705882353, a: 1.0 };
+pub const TURQUOISE: ColorF = ColorF { r: 0.250980392157, g: 0.878431372549, b: 0.81568627451, a: 1.0 };
+pub const OLIVEDRAB: ColorF = ColorF { r: 0.419607843137, g: 0.556862745098, b: 0.137254901961, a: 1.0 };
+pub const CYAN: ColorF = ColorF { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const SILVER: ColorF = ColorF { r: 0.752941176471, g: 0.752941176471, b: 0.752941176471, a: 1.0 };
+pub const SKYBLUE: ColorF = ColorF { r: 0.529411764706, g: 0.807843137255, b: 0.921568627451, a: 1.0 };
+pub const GRAY: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKTURQUOISE: ColorF = ColorF { r: 0.0, g: 0.807843137255, b: 0.819607843137, a: 1.0 };
+pub const GOLDENROD: ColorF = ColorF { r: 0.854901960784, g: 0.647058823529, b: 0.125490196078, a: 1.0 };
+pub const DARKGREEN: ColorF = ColorF { r: 0.0, g: 0.392156862745, b: 0.0, a: 1.0 };
+pub const DARKVIOLET: ColorF = ColorF { r: 0.580392156863, g: 0.0, b: 0.827450980392, a: 1.0 };
+pub const DARKGRAY: ColorF = ColorF { r: 0.662745098039, g: 0.662745098039, b: 0.662745098039, a: 1.0 };
+pub const LIGHTPINK: ColorF = ColorF { r: 1.0, g: 0.713725490196, b: 0.756862745098, a: 1.0 };
+pub const TEAL: ColorF = ColorF { r: 0.0, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKMAGENTA: ColorF = ColorF { r: 0.545098039216, g: 0.0, b: 0.545098039216, a: 1.0 };
+pub const LIGHTGOLDENRODYELLOW: ColorF = ColorF { r: 0.980392156863, g: 0.980392156863, b: 0.823529411765, a: 1.0 };
+pub const LAVENDER: ColorF = ColorF { r: 0.901960784314, g: 0.901960784314, b: 0.980392156863, a: 1.0 };
+pub const YELLOWGREEN: ColorF = ColorF { r: 0.603921568627, g: 0.803921568627, b: 0.196078431373, a: 1.0 };
+pub const THISTLE: ColorF = ColorF { r: 0.847058823529, g: 0.749019607843, b: 0.847058823529, a: 1.0 };
+pub const VIOLET: ColorF = ColorF { r: 0.933333333333, g: 0.509803921569, b: 0.933333333333, a: 1.0 };
+pub const NAVY: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.501960784314, a: 1.0 };
+pub const DIMGREY: ColorF = ColorF { r: 0.411764705882, g: 0.411764705882, b: 0.411764705882, a: 1.0 };
+pub const ORCHID: ColorF = ColorF { r: 0.854901960784, g: 0.439215686275, b: 0.839215686275, a: 1.0 };
+pub const BLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const GHOSTWHITE: ColorF = ColorF { r: 0.972549019608, g: 0.972549019608, b: 1.0, a: 1.0 };
+pub const HONEYDEW: ColorF = ColorF { r: 0.941176470588, g: 1.0, b: 0.941176470588, a: 1.0 };
+pub const CORNFLOWERBLUE: ColorF = ColorF { r: 0.392156862745, g: 0.58431372549, b: 0.929411764706, a: 1.0 };
+pub const DARKBLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.545098039216, a: 1.0 };
+pub const DARKKHAKI: ColorF = ColorF { r: 0.741176470588, g: 0.717647058824, b: 0.419607843137, a: 1.0 };
+pub const MEDIUMPURPLE: ColorF = ColorF { r: 0.576470588235, g: 0.439215686275, b: 0.858823529412, a: 1.0 };
+pub const CORNSILK: ColorF = ColorF { r: 1.0, g: 0.972549019608, b: 0.862745098039, a: 1.0 };
+pub const RED: ColorF = ColorF { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
+pub const BISQUE: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.76862745098, a: 1.0 };
+pub const SLATEGRAY: ColorF = ColorF { r: 0.439215686275, g: 0.501960784314, b: 0.564705882353, a: 1.0 };
+pub const DARKCYAN: ColorF = ColorF { r: 0.0, g: 0.545098039216, b: 0.545098039216, a: 1.0 };
+pub const KHAKI: ColorF = ColorF { r: 0.941176470588, g: 0.901960784314, b: 0.549019607843, a: 1.0 };
+pub const WHEAT: ColorF = ColorF { r: 0.960784313725, g: 0.870588235294, b: 0.701960784314, a: 1.0 };
+pub const DEEPSKYBLUE: ColorF = ColorF { r: 0.0, g: 0.749019607843, b: 1.0, a: 1.0 };
+pub const REBECCAPURPLE: ColorF = ColorF { r: 0.4, g: 0.2, b: 0.6, a: 1.0 };
+pub const DARKRED: ColorF = ColorF { r: 0.545098039216, g: 0.0, b: 0.0, a: 1.0 };
+pub const STEELBLUE: ColorF = ColorF { r: 0.274509803922, g: 0.509803921569, b: 0.705882352941, a: 1.0 };
+pub const ALICEBLUE: ColorF = ColorF { r: 0.941176470588, g: 0.972549019608, b: 1.0, a: 1.0 };
+pub const LIGHTSLATEGREY: ColorF = ColorF { r: 0.466666666667, g: 0.533333333333, b: 0.6, a: 1.0 };
+pub const GAINSBORO: ColorF = ColorF { r: 0.862745098039, g: 0.862745098039, b: 0.862745098039, a: 1.0 };
+pub const MEDIUMTURQUOISE: ColorF = ColorF { r: 0.282352941176, g: 0.819607843137, b: 0.8, a: 1.0 };
+pub const FLORALWHITE: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.941176470588, a: 1.0 };
+pub const CORAL: ColorF = ColorF { r: 1.0, g: 0.498039215686, b: 0.313725490196, a: 1.0 };
+pub const PURPLE: ColorF = ColorF { r: 0.501960784314, g: 0.0, b: 0.501960784314, a: 1.0 };
+pub const LIGHTGREY: ColorF = ColorF { r: 0.827450980392, g: 0.827450980392, b: 0.827450980392, a: 1.0 };
+pub const LIGHTCYAN: ColorF = ColorF { r: 0.878431372549, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKSALMON: ColorF = ColorF { r: 0.913725490196, g: 0.588235294118, b: 0.478431372549, a: 1.0 };
+pub const BEIGE: ColorF = ColorF { r: 0.960784313725, g: 0.960784313725, b: 0.862745098039, a: 1.0 };
+pub const AZURE: ColorF = ColorF { r: 0.941176470588, g: 1.0, b: 1.0, a: 1.0 };
+pub const LIGHTSTEELBLUE: ColorF = ColorF { r: 0.690196078431, g: 0.76862745098, b: 0.870588235294, a: 1.0 };
+pub const OLDLACE: ColorF = ColorF { r: 0.992156862745, g: 0.960784313725, b: 0.901960784314, a: 1.0 };
+pub const GREENYELLOW: ColorF = ColorF { r: 0.678431372549, g: 1.0, b: 0.18431372549, a: 1.0 };
+pub const ROYALBLUE: ColorF = ColorF { r: 0.254901960784, g: 0.411764705882, b: 0.882352941176, a: 1.0 };
+pub const LIGHTSEAGREEN: ColorF = ColorF { r: 0.125490196078, g: 0.698039215686, b: 0.666666666667, a: 1.0 };
+pub const MISTYROSE: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.882352941176, a: 1.0 };
+pub const SIENNA: ColorF = ColorF { r: 0.627450980392, g: 0.321568627451, b: 0.176470588235, a: 1.0 };
+pub const LIGHTCORAL: ColorF = ColorF { r: 0.941176470588, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const ORANGERED: ColorF = ColorF { r: 1.0, g: 0.270588235294, b: 0.0, a: 1.0 };
+pub const NAVAJOWHITE: ColorF = ColorF { r: 1.0, g: 0.870588235294, b: 0.678431372549, a: 1.0 };
+pub const LIME: ColorF = ColorF { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
+pub const PALEGREEN: ColorF = ColorF { r: 0.596078431373, g: 0.98431372549, b: 0.596078431373, a: 1.0 };
+pub const BURLYWOOD: ColorF = ColorF { r: 0.870588235294, g: 0.721568627451, b: 0.529411764706, a: 1.0 };
+pub const SEASHELL: ColorF = ColorF { r: 1.0, g: 0.960784313725, b: 0.933333333333, a: 1.0 };
+pub const MEDIUMSPRINGGREEN: ColorF = ColorF { r: 0.0, g: 0.980392156863, b: 0.603921568627, a: 1.0 };
+pub const FUCHSIA: ColorF = ColorF { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const PAPAYAWHIP: ColorF = ColorF { r: 1.0, g: 0.937254901961, b: 0.835294117647, a: 1.0 };
+pub const BLANCHEDALMOND: ColorF = ColorF { r: 1.0, g: 0.921568627451, b: 0.803921568627, a: 1.0 };
+pub const PERU: ColorF = ColorF { r: 0.803921568627, g: 0.521568627451, b: 0.247058823529, a: 1.0 };
+pub const AQUAMARINE: ColorF = ColorF { r: 0.498039215686, g: 1.0, b: 0.83137254902, a: 1.0 };
+pub const WHITE: ColorF = ColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKSLATEGRAY: ColorF = ColorF { r: 0.18431372549, g: 0.309803921569, b: 0.309803921569, a: 1.0 };
+pub const TOMATO: ColorF = ColorF { r: 1.0, g: 0.388235294118, b: 0.278431372549, a: 1.0 };
+pub const IVORY: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.941176470588, a: 1.0 };
+pub const DODGERBLUE: ColorF = ColorF { r: 0.117647058824, g: 0.564705882353, b: 1.0, a: 1.0 };
+pub const LEMONCHIFFON: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.803921568627, a: 1.0 };
+pub const CHOCOLATE: ColorF = ColorF { r: 0.823529411765, g: 0.411764705882, b: 0.117647058824, a: 1.0 };
+pub const ORANGE: ColorF = ColorF { r: 1.0, g: 0.647058823529, b: 0.0, a: 1.0 };
+pub const FORESTGREEN: ColorF = ColorF { r: 0.133333333333, g: 0.545098039216, b: 0.133333333333, a: 1.0 };
+pub const DARKGREY: ColorF = ColorF { r: 0.662745098039, g: 0.662745098039, b: 0.662745098039, a: 1.0 };
+pub const OLIVE: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.0, a: 1.0 };
+pub const MINTCREAM: ColorF = ColorF { r: 0.960784313725, g: 1.0, b: 0.980392156863, a: 1.0 };
+pub const ANTIQUEWHITE: ColorF = ColorF { r: 0.980392156863, g: 0.921568627451, b: 0.843137254902, a: 1.0 };
+pub const DARKORANGE: ColorF = ColorF { r: 1.0, g: 0.549019607843, b: 0.0, a: 1.0 };
+pub const CADETBLUE: ColorF = ColorF { r: 0.372549019608, g: 0.619607843137, b: 0.627450980392, a: 1.0 };
+pub const MOCCASIN: ColorF = ColorF { r: 1.0, g: 0.894117647059, b: 0.709803921569, a: 1.0 };
+pub const LIMEGREEN: ColorF = ColorF { r: 0.196078431373, g: 0.803921568627, b: 0.196078431373, a: 1.0 };
+pub const SADDLEBROWN: ColorF = ColorF { r: 0.545098039216, g: 0.270588235294, b: 0.0745098039216, a: 1.0 };
+pub const GREY: ColorF = ColorF { r: 0.501960784314, g: 0.501960784314, b: 0.501960784314, a: 1.0 };
+pub const DARKSLATEBLUE: ColorF = ColorF { r: 0.282352941176, g: 0.239215686275, b: 0.545098039216, a: 1.0 };
+pub const LIGHTSKYBLUE: ColorF = ColorF { r: 0.529411764706, g: 0.807843137255, b: 0.980392156863, a: 1.0 };
+pub const DEEPPINK: ColorF = ColorF { r: 1.0, g: 0.078431372549, b: 0.576470588235, a: 1.0 };
+pub const PLUM: ColorF = ColorF { r: 0.866666666667, g: 0.627450980392, b: 0.866666666667, a: 1.0 };
+pub const AQUA: ColorF = ColorF { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
+pub const DARKGOLDENROD: ColorF = ColorF { r: 0.721568627451, g: 0.525490196078, b: 0.043137254902, a: 1.0 };
+pub const MAROON: ColorF = ColorF { r: 0.501960784314, g: 0.0, b: 0.0, a: 1.0 };
+pub const SANDYBROWN: ColorF = ColorF { r: 0.956862745098, g: 0.643137254902, b: 0.376470588235, a: 1.0 };
+pub const MAGENTA: ColorF = ColorF { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
+pub const TAN: ColorF = ColorF { r: 0.823529411765, g: 0.705882352941, b: 0.549019607843, a: 1.0 };
+pub const ROSYBROWN: ColorF = ColorF { r: 0.737254901961, g: 0.560784313725, b: 0.560784313725, a: 1.0 };
+pub const PINK: ColorF = ColorF { r: 1.0, g: 0.752941176471, b: 0.796078431373, a: 1.0 };
+pub const LIGHTBLUE: ColorF = ColorF { r: 0.678431372549, g: 0.847058823529, b: 0.901960784314, a: 1.0 };
+pub const PALEVIOLETRED: ColorF = ColorF { r: 0.858823529412, g: 0.439215686275, b: 0.576470588235, a: 1.0 };
+pub const MEDIUMSEAGREEN: ColorF = ColorF { r: 0.235294117647, g: 0.701960784314, b: 0.443137254902, a: 1.0 };
+pub const SLATEBLUE: ColorF = ColorF { r: 0.41568627451, g: 0.352941176471, b: 0.803921568627, a: 1.0 };
+pub const DIMGRAY: ColorF = ColorF { r: 0.411764705882, g: 0.411764705882, b: 0.411764705882, a: 1.0 };
+pub const POWDERBLUE: ColorF = ColorF { r: 0.690196078431, g: 0.878431372549, b: 0.901960784314, a: 1.0 };
+pub const SEAGREEN: ColorF = ColorF { r: 0.180392156863, g: 0.545098039216, b: 0.341176470588, a: 1.0 };
+pub const SNOW: ColorF = ColorF { r: 1.0, g: 0.980392156863, b: 0.980392156863, a: 1.0 };
+pub const MEDIUMBLUE: ColorF = ColorF { r: 0.0, g: 0.0, b: 0.803921568627, a: 1.0 };
+pub const MIDNIGHTBLUE: ColorF = ColorF { r: 0.0980392156863, g: 0.0980392156863, b: 0.439215686275, a: 1.0 };
+pub const PALETURQUOISE: ColorF = ColorF { r: 0.686274509804, g: 0.933333333333, b: 0.933333333333, a: 1.0 };
+pub const PALEGOLDENROD: ColorF = ColorF { r: 0.933333333333, g: 0.909803921569, b: 0.666666666667, a: 1.0 };
+pub const WHITESMOKE: ColorF = ColorF { r: 0.960784313725, g: 0.960784313725, b: 0.960784313725, a: 1.0 };
+pub const DARKORCHID: ColorF = ColorF { r: 0.6, g: 0.196078431373, b: 0.8, a: 1.0 };
+pub const SALMON: ColorF = ColorF { r: 0.980392156863, g: 0.501960784314, b: 0.447058823529, a: 1.0 };
+pub const LIGHTSLATEGRAY: ColorF = ColorF { r: 0.466666666667, g: 0.533333333333, b: 0.6, a: 1.0 };
+pub const LAWNGREEN: ColorF = ColorF { r: 0.486274509804, g: 0.988235294118, b: 0.0, a: 1.0 };
+pub const LIGHTGREEN: ColorF = ColorF { r: 0.564705882353, g: 0.933333333333, b: 0.564705882353, a: 1.0 };
+pub const LIGHTGRAY: ColorF = ColorF { r: 0.827450980392, g: 0.827450980392, b: 0.827450980392, a: 1.0 };
+pub const HOTPINK: ColorF = ColorF { r: 1.0, g: 0.411764705882, b: 0.705882352941, a: 1.0 };
+pub const LIGHTYELLOW: ColorF = ColorF { r: 1.0, g: 1.0, b: 0.878431372549, a: 1.0 };
+pub const LAVENDERBLUSH: ColorF = ColorF { r: 1.0, g: 0.941176470588, b: 0.960784313725, a: 1.0 };
+pub const LINEN: ColorF = ColorF { r: 0.980392156863, g: 0.941176470588, b: 0.901960784314, a: 1.0 };
+pub const MEDIUMAQUAMARINE: ColorF = ColorF { r: 0.4, g: 0.803921568627, b: 0.666666666667, a: 1.0 };
+pub const GREEN: ColorF = ColorF { r: 0.0, g: 0.501960784314, b: 0.0, a: 1.0 };
+pub const BLUEVIOLET: ColorF = ColorF { r: 0.541176470588, g: 0.16862745098, b: 0.886274509804, a: 1.0 };
+pub const PEACHPUFF: ColorF = ColorF { r: 1.0, g: 0.854901960784, b: 0.725490196078, a: 1.0 };
diff --git a/gfx/wr/webrender/src/debug_font_data.rs b/gfx/wr/webrender/src/debug_font_data.rs
new file mode 100644
index 0000000000..a891bf0d38
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_font_data.rs
@@ -0,0 +1,1914 @@
+/* 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/. */
+
+#[derive(Debug)]
+pub struct BakedGlyph {
+ pub x0: u32,
+ pub y0: u32,
+ pub x1: u32,
+ pub y1: u32,
+ pub xo: f32,
+ pub yo: f32,
+ pub xa: f32,
+}
+
+pub const FIRST_GLYPH_INDEX: i32 = 32;
+pub const BMP_WIDTH: i32 = 128;
+pub const BMP_HEIGHT: i32 = 128;
+pub const FONT_SIZE: i32 = 19;
+
+pub const GLYPHS: [BakedGlyph; 96] = [
+ BakedGlyph {
+ x0: 1,
+ y0: 1,
+ x1: 1,
+ y1: 1,
+ xo: 0.000000,
+ yo: 0.000000,
+ xa: 3.864407,
+ },
+ BakedGlyph {
+ x0: 2,
+ y0: 1,
+ x1: 5,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 4.644068,
+ },
+ BakedGlyph {
+ x0: 6,
+ y0: 1,
+ x1: 11,
+ y1: 6,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 6.644068,
+ },
+ BakedGlyph {
+ x0: 12,
+ y0: 1,
+ x1: 23,
+ y1: 13,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 11.067797,
+ },
+ BakedGlyph {
+ x0: 24,
+ y0: 1,
+ x1: 32,
+ y1: 17,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 33,
+ y0: 1,
+ x1: 46,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 14.084745,
+ },
+ BakedGlyph {
+ x0: 47,
+ y0: 1,
+ x1: 58,
+ y1: 14,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.983051,
+ },
+ BakedGlyph {
+ x0: 59,
+ y0: 1,
+ x1: 61,
+ y1: 6,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 4.067797,
+ },
+ BakedGlyph {
+ x0: 62,
+ y0: 1,
+ x1: 67,
+ y1: 19,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 68,
+ y0: 1,
+ x1: 72,
+ y1: 19,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 73,
+ y0: 1,
+ x1: 81,
+ y1: 8,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.000000,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 1,
+ x1: 91,
+ y1: 11,
+ xo: 0.000000,
+ yo: -10.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 1,
+ x1: 95,
+ y1: 6,
+ xo: 0.000000,
+ yo: -2.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 96,
+ y0: 1,
+ x1: 101,
+ y1: 3,
+ xo: 0.000000,
+ yo: -6.000000,
+ xa: 4.779661,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 1,
+ x1: 105,
+ y1: 4,
+ xo: 1.000000,
+ yo: -2.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 106,
+ y0: 1,
+ x1: 114,
+ y1: 19,
+ xo: -1.000000,
+ yo: -14.000000,
+ xa: 6.084746,
+ },
+ BakedGlyph {
+ x0: 115,
+ y0: 1,
+ x1: 123,
+ y1: 14,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 20,
+ x1: 6,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 7,
+ y0: 20,
+ x1: 15,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 20,
+ x1: 24,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 25,
+ y0: 20,
+ x1: 34,
+ y1: 32,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 35,
+ y0: 20,
+ x1: 43,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 44,
+ y0: 20,
+ x1: 52,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 53,
+ y0: 20,
+ x1: 61,
+ y1: 32,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 62,
+ y0: 20,
+ x1: 70,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 71,
+ y0: 20,
+ x1: 79,
+ y1: 33,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 80,
+ y0: 20,
+ x1: 83,
+ y1: 30,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 84,
+ y0: 20,
+ x1: 88,
+ y1: 32,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 4.169492,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 20,
+ x1: 98,
+ y1: 28,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 99,
+ y0: 20,
+ x1: 108,
+ y1: 26,
+ xo: 0.000000,
+ yo: -8.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 109,
+ y0: 20,
+ x1: 118,
+ y1: 28,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 119,
+ y0: 20,
+ x1: 125,
+ y1: 33,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 6.440678,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 34,
+ x1: 15,
+ y1: 49,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 15.932203,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 34,
+ x1: 27,
+ y1: 46,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.864407,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 34,
+ x1: 37,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.677966,
+ },
+ BakedGlyph {
+ x0: 38,
+ y0: 34,
+ x1: 47,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.322034,
+ },
+ BakedGlyph {
+ x0: 48,
+ y0: 34,
+ x1: 58,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.898305,
+ },
+ BakedGlyph {
+ x0: 59,
+ y0: 34,
+ x1: 67,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.406779,
+ },
+ BakedGlyph {
+ x0: 68,
+ y0: 34,
+ x1: 76,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 8.813560,
+ },
+ BakedGlyph {
+ x0: 77,
+ y0: 34,
+ x1: 86,
+ y1: 47,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.152542,
+ },
+ BakedGlyph {
+ x0: 87,
+ y0: 34,
+ x1: 97,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.728813,
+ },
+ BakedGlyph {
+ x0: 98,
+ y0: 34,
+ x1: 100,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 4.203390,
+ },
+ BakedGlyph {
+ x0: 101,
+ y0: 34,
+ x1: 108,
+ y1: 47,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.254237,
+ },
+ BakedGlyph {
+ x0: 109,
+ y0: 34,
+ x1: 118,
+ y1: 46,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.152542,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 50,
+ x1: 9,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 8.508474,
+ },
+ BakedGlyph {
+ x0: 10,
+ y0: 50,
+ x1: 23,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 14.661017,
+ },
+ BakedGlyph {
+ x0: 24,
+ y0: 50,
+ x1: 34,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 12.016949,
+ },
+ BakedGlyph {
+ x0: 35,
+ y0: 50,
+ x1: 47,
+ y1: 63,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 13.118644,
+ },
+ BakedGlyph {
+ x0: 48,
+ y0: 50,
+ x1: 57,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.033898,
+ },
+ BakedGlyph {
+ x0: 58,
+ y0: 50,
+ x1: 70,
+ y1: 66,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 13.118644,
+ },
+ BakedGlyph {
+ x0: 71,
+ y0: 50,
+ x1: 81,
+ y1: 62,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 10.474576,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 50,
+ x1: 91,
+ y1: 63,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 8.762712,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 50,
+ x1: 101,
+ y1: 62,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.288136,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 50,
+ x1: 112,
+ y1: 63,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 11.525424,
+ },
+ BakedGlyph {
+ x0: 113,
+ y0: 50,
+ x1: 124,
+ y1: 62,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.576271,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 67,
+ x1: 16,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 15.610169,
+ },
+ BakedGlyph {
+ x0: 17,
+ y0: 67,
+ x1: 27,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 10.305085,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 67,
+ x1: 38,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 39,
+ y0: 67,
+ x1: 48,
+ y1: 79,
+ xo: 0.000000,
+ yo: -12.000000,
+ xa: 9.491526,
+ },
+ BakedGlyph {
+ x0: 49,
+ y0: 67,
+ x1: 54,
+ y1: 85,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 55,
+ y0: 67,
+ x1: 63,
+ y1: 85,
+ xo: -1.000000,
+ yo: -14.000000,
+ xa: 6.084746,
+ },
+ BakedGlyph {
+ x0: 64,
+ y0: 67,
+ x1: 68,
+ y1: 85,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.254237,
+ },
+ BakedGlyph {
+ x0: 69,
+ y0: 67,
+ x1: 77,
+ y1: 74,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 78,
+ y0: 67,
+ x1: 88,
+ y1: 69,
+ xo: -1.000000,
+ yo: 2.000000,
+ xa: 8.305085,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 67,
+ x1: 93,
+ y1: 72,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 6.372881,
+ },
+ BakedGlyph {
+ x0: 94,
+ y0: 67,
+ x1: 102,
+ y1: 77,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.627119,
+ },
+ BakedGlyph {
+ x0: 103,
+ y0: 67,
+ x1: 111,
+ y1: 82,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 112,
+ y0: 67,
+ x1: 120,
+ y1: 77,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.796610,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 86,
+ x1: 10,
+ y1: 101,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 11,
+ y0: 86,
+ x1: 20,
+ y1: 96,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.288136,
+ },
+ BakedGlyph {
+ x0: 21,
+ y0: 86,
+ x1: 27,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 6.372881,
+ },
+ BakedGlyph {
+ x0: 28,
+ y0: 86,
+ x1: 37,
+ y1: 99,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.711864,
+ },
+ BakedGlyph {
+ x0: 38,
+ y0: 86,
+ x1: 46,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 47,
+ y0: 86,
+ x1: 49,
+ y1: 99,
+ xo: 1.000000,
+ yo: -13.000000,
+ xa: 4.016949,
+ },
+ BakedGlyph {
+ x0: 50,
+ y0: 86,
+ x1: 55,
+ y1: 103,
+ xo: -2.000000,
+ yo: -13.000000,
+ xa: 4.016949,
+ },
+ BakedGlyph {
+ x0: 56,
+ y0: 86,
+ x1: 64,
+ y1: 100,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 8.389831,
+ },
+ BakedGlyph {
+ x0: 65,
+ y0: 86,
+ x1: 68,
+ y1: 101,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 4.322034,
+ },
+ BakedGlyph {
+ x0: 69,
+ y0: 86,
+ x1: 82,
+ y1: 95,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 14.627119,
+ },
+ BakedGlyph {
+ x0: 83,
+ y0: 86,
+ x1: 91,
+ y1: 95,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 86,
+ x1: 101,
+ y1: 96,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.864407,
+ },
+ BakedGlyph {
+ x0: 102,
+ y0: 86,
+ x1: 110,
+ y1: 99,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 111,
+ y0: 86,
+ x1: 120,
+ y1: 99,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 9.881356,
+ },
+ BakedGlyph {
+ x0: 1,
+ y0: 104,
+ x1: 7,
+ y1: 113,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 6.338983,
+ },
+ BakedGlyph {
+ x0: 8,
+ y0: 104,
+ x1: 15,
+ y1: 114,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.254237,
+ },
+ BakedGlyph {
+ x0: 16,
+ y0: 104,
+ x1: 22,
+ y1: 117,
+ xo: 1.000000,
+ yo: -12.000000,
+ xa: 6.559322,
+ },
+ BakedGlyph {
+ x0: 23,
+ y0: 104,
+ x1: 31,
+ y1: 114,
+ xo: 1.000000,
+ yo: -9.000000,
+ xa: 9.644068,
+ },
+ BakedGlyph {
+ x0: 32,
+ y0: 104,
+ x1: 40,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.135593,
+ },
+ BakedGlyph {
+ x0: 41,
+ y0: 104,
+ x1: 54,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 13.135593,
+ },
+ BakedGlyph {
+ x0: 55,
+ y0: 104,
+ x1: 63,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.457627,
+ },
+ BakedGlyph {
+ x0: 64,
+ y0: 104,
+ x1: 72,
+ y1: 117,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 8.033898,
+ },
+ BakedGlyph {
+ x0: 73,
+ y0: 104,
+ x1: 81,
+ y1: 113,
+ xo: 0.000000,
+ yo: -9.000000,
+ xa: 7.711864,
+ },
+ BakedGlyph {
+ x0: 82,
+ y0: 104,
+ x1: 88,
+ y1: 122,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.406780,
+ },
+ BakedGlyph {
+ x0: 89,
+ y0: 104,
+ x1: 91,
+ y1: 122,
+ xo: 1.000000,
+ yo: -14.000000,
+ xa: 4.440678,
+ },
+ BakedGlyph {
+ x0: 92,
+ y0: 104,
+ x1: 97,
+ y1: 122,
+ xo: 0.000000,
+ yo: -14.000000,
+ xa: 5.406780,
+ },
+ BakedGlyph {
+ x0: 98,
+ y0: 104,
+ x1: 107,
+ y1: 108,
+ xo: 0.000000,
+ yo: -7.000000,
+ xa: 9.559322,
+ },
+ BakedGlyph {
+ x0: 108,
+ y0: 104,
+ x1: 116,
+ y1: 117,
+ xo: 0.000000,
+ yo: -13.000000,
+ xa: 8.474576,
+ },
+];
+
+pub const FONT_BITMAP: [u8; 16384] = [
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x34, 0xae, 0x00, 0x00, 0x81, 0x90, 0x00, 0xe0, 0x31, 0x00, 0x00, 0x00, 0x00, 0x06,
+ 0xba, 0x05, 0x00, 0x00, 0xa2, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x16, 0xb2, 0xec, 0xbc, 0x1f, 0x00, 0x00, 0x00, 0x49, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0x9d, 0xeb, 0xe5, 0x89, 0x03, 0x00, 0x00, 0x00, 0x00, 0x81, 0x90, 0x00, 0x00, 0x00,
+ 0x00, 0x13, 0x00, 0x00, 0x11, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x6b, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x76, 0x00,
+ 0x3b, 0x70, 0x70, 0x70, 0x22, 0x00, 0x56, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x2d, 0x1e, 0x00, 0x00, 0x18, 0xac, 0xea, 0xd2, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x45, 0xe9, 0x00, 0x00, 0x8a, 0x99, 0x00, 0xf6, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x2f,
+ 0xd8, 0x00, 0x00, 0x08, 0xf7, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x52, 0x00, 0x00, 0x00,
+ 0x00, 0xa5, 0x8f, 0x0c, 0x7d, 0xba, 0x00, 0x00, 0x06, 0xda, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x92, 0xb9, 0x20, 0x28, 0xd7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x99, 0x00, 0x00, 0x00,
+ 0x28, 0xdd, 0x03, 0x00, 0xa5, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x7f, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00,
+ 0x54, 0xa0, 0xa0, 0xa0, 0x31, 0x00, 0xbb, 0xe4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0xe0, 0x34, 0x00, 0x0b, 0xd7, 0x95, 0x1d, 0x40, 0xe1, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x45, 0xe9, 0x00, 0x00, 0x7a, 0x88, 0x00, 0xe6, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x60,
+ 0xa8, 0x00, 0x00, 0x34, 0xd3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x6b, 0x0b, 0x00, 0x00,
+ 0x00, 0xea, 0x22, 0x00, 0x10, 0xf7, 0x05, 0x00, 0x73, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xd6, 0x50, 0x00, 0x00, 0x81, 0x98, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x88, 0x00, 0x00, 0x00,
+ 0xb4, 0x6a, 0x00, 0x00, 0x2e, 0xe4, 0x0d, 0x00, 0x00, 0x24, 0xcd, 0x61, 0x65, 0x62, 0x61, 0xcd,
+ 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x83, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c,
+ 0xda, 0x00, 0x00, 0x5e, 0xd0, 0x03, 0x00, 0x00, 0x49, 0xe6, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x44, 0xe9, 0x00, 0x00, 0x5a, 0x67, 0x00, 0xbf, 0x02, 0x00, 0x1b, 0x86, 0x86, 0xc4,
+ 0xc5, 0x86, 0x86, 0xb0, 0xd9, 0x86, 0x24, 0x00, 0x00, 0x57, 0xc9, 0xea, 0xd3, 0xef, 0x85, 0x00,
+ 0x00, 0xed, 0x1e, 0x00, 0x0d, 0xf7, 0x07, 0x15, 0xe8, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xbf, 0x65, 0x00, 0x00, 0xc6, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x5a, 0x67, 0x00, 0x00, 0x47,
+ 0xd7, 0x04, 0x00, 0x00, 0x00, 0x9d, 0x85, 0x00, 0x00, 0x0d, 0x55, 0x8d, 0xcc, 0xcc, 0x8c, 0x54,
+ 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x2e, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96,
+ 0x81, 0x00, 0x00, 0xb7, 0x80, 0x00, 0x00, 0x00, 0x04, 0xf1, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x3d, 0xe3, 0x00, 0x00, 0x0f, 0x12, 0x00, 0x21, 0x00, 0x00, 0x13, 0x5f, 0x5f, 0xe0,
+ 0x86, 0x5f, 0x5f, 0xc2, 0x9f, 0x5f, 0x1a, 0x00, 0x16, 0xf9, 0x3d, 0x01, 0x00, 0x03, 0x13, 0x00,
+ 0x00, 0xae, 0x80, 0x04, 0x6c, 0xc4, 0x00, 0x96, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x56, 0xe2, 0x1f, 0x8e, 0xc2, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x12, 0x00, 0x00, 0xc7,
+ 0x4f, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xee, 0x0d, 0x00, 0x00, 0x00, 0x3b, 0xb4, 0xb5, 0x37, 0x00,
+ 0x00, 0x00, 0x05, 0x5f, 0x5f, 0x5f, 0xd1, 0x8a, 0x5f, 0x5f, 0x3a, 0x00, 0x0b, 0x76, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xea,
+ 0x27, 0x00, 0x00, 0xd8, 0x4f, 0x00, 0x00, 0x00, 0x00, 0xc2, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x35, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xf2,
+ 0x16, 0x00, 0x00, 0xc6, 0x3f, 0x00, 0x00, 0x00, 0x65, 0xc3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x1f, 0xc7, 0xef, 0xd1, 0x29, 0x2c, 0xe4, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0xd1, 0xfa, 0x8f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xf6,
+ 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x3b, 0x00, 0x00, 0x23, 0xec, 0x3c, 0x3d, 0xea, 0x21,
+ 0x00, 0x00, 0x07, 0x93, 0x93, 0x93, 0xe0, 0xb0, 0x93, 0x93, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0xcd,
+ 0x00, 0x00, 0x00, 0xf0, 0x3e, 0x00, 0x00, 0x00, 0x00, 0xb0, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x25, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0xe4,
+ 0x00, 0x00, 0x03, 0xf4, 0x0f, 0x00, 0x00, 0x00, 0x4d, 0xec, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0xba, 0x63, 0x07, 0x82, 0xc2, 0x8d, 0x0c, 0x00, 0x00, 0x00,
+ 0x24, 0xda, 0x7b, 0x97, 0xc2, 0x0b, 0x00, 0x01, 0xa2, 0x03, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xe1,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x69, 0x00, 0x00, 0x0a, 0x5b, 0x00, 0x00, 0x5d, 0x0a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x74,
+ 0x00, 0x00, 0x00, 0xf6, 0x3a, 0x00, 0x00, 0x00, 0x00, 0xab, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x09, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x1e, 0x67, 0xbe,
+ 0x1e, 0x1e, 0x40, 0xe4, 0x1e, 0x1e, 0x08, 0x00, 0x01, 0x96, 0xf2, 0x91, 0x2b, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xce, 0x02, 0x8c, 0xb1, 0x37, 0xa3, 0xa2, 0x00, 0x00, 0x00,
+ 0xc6, 0x75, 0x00, 0x01, 0x9c, 0xbe, 0x0a, 0x28, 0xd1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0xbc,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xf1, 0x1c,
+ 0x00, 0x00, 0x00, 0xde, 0x49, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xcb, 0xea, 0xe2,
+ 0xcb, 0xcb, 0xe1, 0xeb, 0xcb, 0xcb, 0x37, 0x00, 0x00, 0x00, 0x2d, 0x93, 0xee, 0xb4, 0x1f, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x05, 0xd8, 0x40, 0x00, 0xe2, 0x2e, 0x00, 0x19, 0xf5, 0x02, 0x00, 0x13,
+ 0xff, 0x1d, 0x00, 0x00, 0x02, 0xa1, 0xbb, 0x94, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x96,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xc0, 0x00,
+ 0x00, 0x00, 0x00, 0xc4, 0x6e, 0x00, 0x00, 0x00, 0x00, 0xe2, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x52,
+ 0x00, 0x00, 0x8b, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xa7, 0xd7, 0x04,
+ 0x00, 0x00, 0x00, 0x00, 0x70, 0xad, 0x00, 0x00, 0xf2, 0x15, 0x00, 0x06, 0xf7, 0x0a, 0x00, 0x10,
+ 0xff, 0x23, 0x00, 0x00, 0x00, 0x03, 0xb2, 0xfb, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x9b,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb3, 0x67, 0x00,
+ 0x00, 0x00, 0x00, 0x77, 0xb5, 0x00, 0x00, 0x00, 0x2b, 0xf5, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x33, 0x8c, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x20,
+ 0x00, 0x00, 0xbd, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xfc, 0x2c,
+ 0x00, 0x00, 0x00, 0x13, 0xe7, 0x23, 0x00, 0x00, 0xc1, 0x5c, 0x00, 0x47, 0xd7, 0x00, 0x00, 0x00,
+ 0xb5, 0xaf, 0x0d, 0x00, 0x0a, 0x60, 0xe1, 0xbd, 0xa5, 0x01, 0x00, 0x00, 0x00, 0x00, 0x51, 0xc1,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xf4, 0x13, 0x00,
+ 0x00, 0x00, 0x00, 0x19, 0xf3, 0x57, 0x00, 0x0b, 0xc0, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7f, 0xfe, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xec, 0x00,
+ 0x00, 0x01, 0xee, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xef, 0x0c,
+ 0x00, 0x00, 0x00, 0x94, 0x8a, 0x00, 0x00, 0x00, 0x35, 0xe8, 0xc4, 0xed, 0x44, 0x00, 0x00, 0x00,
+ 0x15, 0xa9, 0xf1, 0xdc, 0xea, 0xaf, 0x1e, 0x07, 0xc7, 0x73, 0x00, 0x00, 0x00, 0x00, 0x24, 0xe6,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0xb3, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x3c, 0xea, 0xdb, 0xf2, 0xae, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x02, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x9b, 0x61, 0x52, 0x6c, 0xd1, 0x89, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x34, 0x10, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x1b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xf4,
+ 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x5a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x2c, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x69, 0x8f, 0xe2, 0xa5, 0x28, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xba,
+ 0x5e, 0x00, 0x00, 0x00, 0x00, 0x26, 0xe9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xf2, 0x0b, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x52, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35,
+ 0xe1, 0x0a, 0x00, 0x00, 0x00, 0xaf, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xa6, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93, 0x46, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa1, 0x7d, 0x00, 0x00, 0x3e, 0xd9, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x4d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x1a, 0xd2, 0x03, 0x00, 0xa0, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, 0xed, 0x06, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x05, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x1a, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x28, 0xb6, 0x00, 0x14, 0x93, 0xe1, 0xe8, 0x99, 0x42, 0x00, 0x00, 0x00,
+ 0x2b, 0xaa, 0xea, 0xf3, 0xca, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0xbd,
+ 0x30, 0x00, 0x00, 0x00, 0x63, 0xbe, 0xbe, 0xbe, 0xbe, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+ 0x5a, 0x8c, 0x92, 0x00, 0x00, 0x9e, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x3a, 0x00, 0x00, 0x39,
+ 0xbe, 0xed, 0xde, 0x94, 0x0f, 0x00, 0x00, 0x00, 0x47, 0xc5, 0xf0, 0xce, 0x62, 0x00, 0x00, 0x00,
+ 0x6a, 0x88, 0x00, 0x00, 0x00, 0x6a, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x2a, 0x29, 0x00, 0x02, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x18, 0x00, 0x00, 0x4a, 0x0a,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xd0, 0xf3, 0xe5, 0xa3, 0x17, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x36, 0xe8, 0xfb, 0x00, 0x98, 0x8a, 0x21, 0x16, 0x68, 0xfb, 0x1d, 0x00, 0x00,
+ 0x3a, 0x6a, 0x22, 0x11, 0x62, 0xf8, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xd7, 0xed,
+ 0x41, 0x00, 0x00, 0x00, 0x8e, 0x8e, 0x38, 0x38, 0x38, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x42, 0xdd,
+ 0xa9, 0x5c, 0x2b, 0x00, 0x00, 0x39, 0x45, 0x45, 0x45, 0x45, 0x5a, 0xf1, 0x24, 0x00, 0x25, 0xef,
+ 0x54, 0x05, 0x17, 0xad, 0xb3, 0x00, 0x00, 0x3e, 0xe9, 0x46, 0x08, 0x39, 0xe1, 0x65, 0x00, 0x00,
+ 0xaf, 0xd8, 0x00, 0x00, 0x00, 0xaf, 0xd8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x50, 0xbc,
+ 0xe2, 0x4f, 0x00, 0x0a, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0xcb, 0x7c, 0x00, 0x02, 0xaa, 0xe9,
+ 0x8b, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x49, 0x0e, 0x16, 0xae, 0xb1, 0x00, 0x00, 0x00,
+ 0x00, 0x0d, 0xad, 0xdf, 0x61, 0xfb, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0xba, 0x7a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xc6, 0x50, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x9b, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xf1, 0x36,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xae, 0x71, 0x00, 0x00, 0x7d, 0xa3,
+ 0x00, 0x00, 0x00, 0x18, 0xff, 0x13, 0x00, 0xb6, 0x78, 0x00, 0x00, 0x00, 0x4b, 0xe0, 0x04, 0x00,
+ 0x02, 0x05, 0x00, 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x13, 0x76, 0xdf, 0xbe, 0x53,
+ 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d,
+ 0x84, 0xe6, 0xb1, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0xeb, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x58, 0x08, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0xa8, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x8a, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xad, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xd0, 0x54, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd4, 0x02, 0x00, 0x00, 0x8b, 0x93,
+ 0x00, 0x00, 0x00, 0x1e, 0xff, 0x18, 0x00, 0xe2, 0x4c, 0x00, 0x00, 0x00, 0x06, 0xfb, 0x24, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9d, 0xe9, 0x94, 0x2a, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x06, 0x5b, 0xc5, 0xd6, 0x4e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xbd, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x69, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x3e, 0xe4, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x39, 0xd0, 0x07, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xc5, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0xeb, 0x04, 0x01,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xcc, 0x58, 0x00, 0x00, 0x00, 0x45, 0xe5,
+ 0x1a, 0x00, 0x00, 0x74, 0xc5, 0x00, 0x00, 0xd2, 0x80, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x41, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xd8, 0xcb, 0x58, 0x06, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x08, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x97, 0x5c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x21, 0x89, 0xef, 0x73, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xe6, 0x32, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xd1, 0x06, 0x00, 0x00,
+ 0x00, 0x00, 0x82, 0xd2, 0xed, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcd, 0x3d, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0xc5, 0xf6, 0xe8, 0xb5, 0x3e, 0x00, 0x00, 0x00, 0x71, 0xdc, 0xc6, 0xef,
+ 0xf3, 0xc1, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0xd8, 0x02, 0x00, 0x00, 0x00, 0x00, 0x5c,
+ 0xe9, 0x8e, 0x7d, 0xd8, 0x20, 0x00, 0x00, 0x82, 0xd4, 0x1c, 0x00, 0x00, 0x16, 0xed, 0x39, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x46, 0xb3, 0xe4, 0x81, 0x1b,
+ 0x00, 0x00, 0x00, 0x05, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x37, 0x00, 0x00, 0x00, 0x01,
+ 0x47, 0xb2, 0xe5, 0x81, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xdf, 0x49, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x67, 0xe2, 0x1f, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x25, 0x4b, 0x95, 0xea, 0x3f, 0x00, 0x00, 0x00, 0x63, 0xac, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x6c, 0xee, 0x5c, 0x00, 0x00, 0x94, 0xb6, 0x29, 0x02,
+ 0x1d, 0x8a, 0xef, 0x11, 0x00, 0x00, 0x00, 0x00, 0xaf, 0x78, 0x00, 0x00, 0x00, 0x00, 0x03, 0x9e,
+ 0xc5, 0x6f, 0xc8, 0xc2, 0x19, 0x00, 0x00, 0x0c, 0xab, 0xfd, 0xc6, 0xcd, 0xdb, 0xff, 0x17, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x8d, 0xea,
+ 0xab, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x71, 0xda,
+ 0xc7, 0x5b, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x00, 0x6f, 0xe1, 0x24, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x6a, 0xe0, 0x01, 0x00, 0x02, 0xd9, 0x45, 0x16, 0x16, 0x16, 0xd7,
+ 0x51, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xed, 0x01, 0x00, 0xa1, 0x81, 0x00, 0x00,
+ 0x00, 0x06, 0xf1, 0x65, 0x00, 0x00, 0x00, 0x0d, 0xf6, 0x20, 0x00, 0x00, 0x00, 0x00, 0x76, 0xc8,
+ 0x07, 0x00, 0x00, 0x63, 0xe6, 0x0a, 0x00, 0x00, 0x00, 0x16, 0x3b, 0x26, 0x3e, 0xf2, 0x00, 0x00,
+ 0x56, 0x70, 0x00, 0x00, 0x00, 0x4a, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b,
+ 0x66, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x35,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x00, 0x59, 0xe6, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0x1c, 0x00, 0x1f, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xf7,
+ 0xdc, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xff, 0x20, 0x00, 0x64, 0x92, 0x00, 0x00,
+ 0x00, 0x00, 0xb4, 0x81, 0x00, 0x00, 0x00, 0x5a, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x72,
+ 0x00, 0x00, 0x00, 0x00, 0xdc, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x9b, 0x00, 0x00,
+ 0xbb, 0xe4, 0x00, 0x00, 0x00, 0x80, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x13, 0xec, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xfc, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xfe, 0x0d, 0x00, 0x21, 0xcc, 0x00, 0x00,
+ 0x00, 0x00, 0xca, 0x63, 0x00, 0x00, 0x00, 0x9b, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0xca, 0x5e,
+ 0x00, 0x00, 0x00, 0x00, 0xce, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xed, 0x18, 0x00, 0x00,
+ 0x09, 0x11, 0x00, 0x00, 0x00, 0x9b, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x6d, 0xb6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x25, 0x04, 0x00, 0x01, 0x32, 0xc1, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x22, 0x06, 0x00, 0x00, 0x10, 0xb0, 0xac, 0x00, 0x00, 0x00, 0xdd, 0x5b, 0x00,
+ 0x00, 0x4b, 0xec, 0x10, 0x00, 0x00, 0x00, 0xc6, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xcb,
+ 0x11, 0x00, 0x00, 0x52, 0xeb, 0x14, 0x00, 0x00, 0x03, 0x22, 0x88, 0xda, 0x4b, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x8e, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xfb, 0x00, 0x9b, 0xfb, 0xf6, 0xf6, 0xf6, 0xf6, 0xf6, 0x21, 0x00,
+ 0xa4, 0xf0, 0xcc, 0xe7, 0xfd, 0x9d, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4,
+ 0x41, 0x00, 0x00, 0x8f, 0xf2, 0xce, 0xd2, 0xf4, 0xa2, 0x13, 0x00, 0x00, 0x00, 0x33, 0xe0, 0xd8,
+ 0xcd, 0xe0, 0x3a, 0x00, 0x00, 0x00, 0x01, 0xf1, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x91,
+ 0xea, 0xbe, 0xcf, 0xdc, 0x3f, 0x00, 0x00, 0x52, 0xe1, 0xbf, 0x89, 0x10, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x0b, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xfe, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x17, 0x39, 0x2e, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x12, 0x37, 0x30, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x24,
+ 0x28, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0e, 0x34, 0x24, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x0e, 0x7d, 0xb2, 0xdc, 0xe5, 0xb8, 0x84, 0x14, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x30, 0xbd, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0xb2, 0xce, 0xd4,
+ 0xc2, 0x9a, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x90, 0xce, 0xf7, 0xea, 0xb6, 0x4c, 0x00,
+ 0x45, 0xb1, 0xcb, 0xd0, 0xb0, 0x8b, 0x34, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xbe, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0x3a, 0x00, 0x5a, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x2a, 0x00, 0x00, 0x00, 0x11,
+ 0x8e, 0xcb, 0xf4, 0xdf, 0xbb, 0x5f, 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4,
+ 0x27, 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x94, 0x00, 0x5a, 0x81, 0x00,
+ 0x00, 0x00, 0x00, 0x21, 0xb7, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x54, 0xe4, 0x9c, 0x4a, 0x1d, 0x13, 0x45, 0x8c, 0xe7, 0x5c, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa6, 0xec, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xc0, 0x2c, 0x27,
+ 0x39, 0x74, 0xeb, 0x79, 0x00, 0x00, 0x00, 0x37, 0xe5, 0x9f, 0x43, 0x16, 0x29, 0x63, 0x62, 0x00,
+ 0x79, 0xc1, 0x31, 0x2d, 0x4f, 0x7b, 0xe7, 0xa6, 0x0f, 0x00, 0x00, 0x79, 0xc2, 0x41, 0x41, 0x41,
+ 0x41, 0x41, 0x14, 0x00, 0x79, 0xc2, 0x41, 0x41, 0x41, 0x41, 0x41, 0x0e, 0x00, 0x00, 0x37, 0xe4,
+ 0x9e, 0x3b, 0x13, 0x28, 0x5b, 0x83, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x1b, 0xda, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x39, 0xeb, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xe5, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1f, 0xed, 0x45, 0xea, 0x09, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x42, 0xf4, 0x06, 0x00, 0x08, 0xd9, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x10, 0xb6, 0xa5, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xda, 0x73,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x1b, 0xd9, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x07, 0xd6, 0x67, 0x00, 0x00, 0x19, 0x63, 0x86, 0x70, 0x36, 0x00, 0x53, 0xe2, 0x0e, 0x00,
+ 0x00, 0x00, 0x00, 0x89, 0x93, 0x00, 0xcb, 0x61, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x1b, 0xfd, 0x13, 0x00, 0x72, 0xd4, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0xf0, 0x4d, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0xd1, 0x03,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x00,
+ 0x1d, 0xd9, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4f, 0xd4, 0x02, 0x00, 0x3f, 0xe7, 0x94, 0x64, 0x8d, 0xe1, 0x00, 0x00, 0xc8, 0x50, 0x00,
+ 0x00, 0x00, 0x08, 0xea, 0x29, 0x00, 0x60, 0xc9, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x80, 0xc2, 0x00, 0x00, 0xac, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x8d, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x81, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x22,
+ 0xda, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x7b, 0x99, 0x00, 0x05, 0xdc, 0x5a, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x91, 0x82, 0x00,
+ 0x00, 0x00, 0x5d, 0xc5, 0x00, 0x00, 0x0b, 0xf1, 0x32, 0x00, 0x00, 0x00, 0x79, 0xdf, 0x9c, 0x9c,
+ 0xa6, 0xd5, 0xcc, 0x12, 0x00, 0x00, 0xd7, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xbb, 0x00, 0x79, 0xe3, 0xa9, 0xa9, 0xa9,
+ 0xa9, 0x75, 0x00, 0x00, 0x79, 0xdf, 0x9c, 0x9c, 0x9c, 0x9c, 0x5c, 0x00, 0x00, 0xd7, 0x5c, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79, 0xe2, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xa4, 0xfa,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xcf, 0xe6,
+ 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xa7, 0x70, 0x00, 0x2b, 0xf3, 0x01, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x6c, 0xab, 0x00,
+ 0x00, 0x00, 0xc1, 0x64, 0x00, 0x00, 0x00, 0x99, 0x97, 0x00, 0x00, 0x00, 0x79, 0xcc, 0x5f, 0x5f,
+ 0x66, 0x80, 0xd4, 0xc1, 0x0f, 0x00, 0xe2, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0xc6, 0x00, 0x79, 0xc7, 0x52, 0x52, 0x52,
+ 0x52, 0x39, 0x00, 0x00, 0x79, 0xcd, 0x63, 0x63, 0x63, 0x63, 0x3b, 0x00, 0x00, 0xe2, 0x52, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x16, 0x8f, 0x00, 0x79, 0xca, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0x5b, 0xf7,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xc5, 0xba,
+ 0xd2, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xb9, 0x60, 0x00, 0x3d, 0xe4, 0x00, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0x79, 0x98, 0x00,
+ 0x00, 0x24, 0xfb, 0x41, 0x34, 0x34, 0x34, 0x68, 0xf0, 0x0a, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0xce, 0x80, 0x00, 0xbb, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x98, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x72, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0xc7, 0x00, 0x79, 0xad, 0x01,
+ 0x74, 0xea, 0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x87, 0x00, 0x16, 0xfd, 0x15, 0x00, 0x00, 0x34, 0xe1, 0x00, 0x00, 0xb4, 0x6d, 0x00,
+ 0x00, 0x85, 0xe3, 0xc7, 0xc7, 0xc7, 0xc7, 0xc7, 0xee, 0x5e, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x89, 0xb2, 0x00, 0x90, 0xae, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xdb, 0x66, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xaf, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xc0, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x50, 0xec, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x64, 0xb2, 0x00, 0x00, 0xa8, 0xae, 0x0e, 0x00, 0x3d, 0xfb, 0x16, 0x4b, 0xec, 0x0f, 0x00,
+ 0x02, 0xe2, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xbf, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb4, 0x90, 0x00, 0x21, 0xf5, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86, 0xce, 0x06, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xf6, 0x32,
+ 0x00, 0x00, 0x00, 0x00, 0x27, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0xa5, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x52, 0xee, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x2a, 0xf3, 0x24, 0x00, 0x0f, 0x96, 0xe4, 0xe3, 0xc4, 0xa4, 0xf0, 0xcd, 0x33, 0x00, 0x00,
+ 0x45, 0xe5, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xfd, 0x22, 0x00, 0x79, 0xad, 0x00, 0x00,
+ 0x00, 0x21, 0x8d, 0xed, 0x2a, 0x00, 0x00, 0x74, 0xec, 0x50, 0x04, 0x00, 0x00, 0x11, 0x33, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x0d, 0x37, 0xb5, 0xdf, 0x27, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xec,
+ 0x53, 0x05, 0x00, 0x00, 0x28, 0xff, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x4f, 0x28, 0x00, 0x04, 0x48, 0xf5, 0x51, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x00, 0x6e, 0xe1, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x8a, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x8d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc2, 0x81, 0x00, 0x65, 0xf3, 0xeb, 0xe4,
+ 0xef, 0xe5, 0x97, 0x29, 0x00, 0x00, 0x00, 0x00, 0x4c, 0xdd, 0xf5, 0xd1, 0xe2, 0xf7, 0x95, 0x00,
+ 0x65, 0xf3, 0xef, 0xeb, 0xf0, 0xcc, 0x74, 0x07, 0x00, 0x00, 0x00, 0x79, 0xff, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xbe, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b,
+ 0xd9, 0xf7, 0xd1, 0xd4, 0xf2, 0xd1, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2,
+ 0x34, 0x00, 0x79, 0xad, 0x00, 0x75, 0xf0, 0xd8, 0xe9, 0xfd, 0x86, 0x02, 0x00, 0x79, 0xad, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xad, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x08, 0xcf, 0xbd, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x11, 0x15,
+ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x3a, 0x30, 0x0a, 0x00, 0x00,
+ 0x00, 0x02, 0x10, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x10, 0x34, 0x24, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x30, 0x33, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x07, 0x78, 0xeb, 0xba, 0x91, 0x72, 0x7c, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x41, 0x68, 0x84, 0x76, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x5a, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb7, 0x37, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x6e, 0x77, 0x00, 0x00, 0x5a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x7a, 0x5d, 0x00, 0x00, 0x00, 0x13, 0x93, 0xcd, 0xf3, 0xd3, 0x9e, 0x1e, 0x00, 0x00, 0x00, 0x00,
+ 0x44, 0xaf, 0xca, 0xd3, 0xbc, 0x9b, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x93, 0xcd, 0xf3,
+ 0xd3, 0x9e, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x44, 0xaf, 0xca, 0xd3, 0xbc, 0x99, 0x36, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x09, 0x63, 0xab, 0xee, 0xed, 0xb8, 0x4b, 0x00, 0x00, 0x84, 0xbe, 0xbe, 0xbe,
+ 0xbe, 0xbe, 0xbe, 0xbe, 0xbb, 0x00, 0x64, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xbe, 0x0a,
+ 0x00, 0x85, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xb1, 0x34, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0xff, 0xc4, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1a, 0xf4, 0xb2, 0x00, 0x00, 0x79, 0xff, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x00, 0x3c, 0xe8, 0x9c, 0x3f, 0x16, 0x39, 0x8c, 0xee, 0x4f, 0x00, 0x00, 0x00,
+ 0x79, 0xc2, 0x34, 0x2b, 0x3d, 0x7d, 0xed, 0x83, 0x00, 0x00, 0x00, 0x3c, 0xe8, 0x9c, 0x3f, 0x16,
+ 0x39, 0x8c, 0xee, 0x4f, 0x00, 0x00, 0x00, 0x79, 0xc2, 0x35, 0x2b, 0x3c, 0x7b, 0xed, 0x78, 0x00,
+ 0x00, 0x00, 0x00, 0x6a, 0xc2, 0x30, 0x08, 0x1e, 0x5e, 0x58, 0x00, 0x00, 0x2d, 0x41, 0x41, 0x41,
+ 0xf2, 0x6b, 0x41, 0x41, 0x40, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x63, 0xe1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0xed, 0x07, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xef, 0xdd, 0x4e, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x98, 0xcd, 0xc8, 0x00, 0x00, 0x79, 0xc4, 0xe6, 0x4b, 0x00, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x09, 0xdd, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0xed, 0x16, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x32, 0xfe, 0x27, 0x00, 0x09, 0xdd, 0x71, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x53, 0xed, 0x16, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x3b, 0xfd, 0x1a,
+ 0x00, 0x00, 0x02, 0xd8, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x0f, 0xf7, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xd8, 0x61, 0xcb, 0x01, 0x00,
+ 0x00, 0x00, 0x1f, 0xea, 0x4a, 0xdd, 0x00, 0x00, 0x79, 0xa9, 0x3d, 0xe9, 0x1f, 0x00, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0x76, 0xd4, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb9, 0x94, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xdc, 0x5b, 0x00, 0x76, 0xd4, 0x03, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb9, 0x94, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xeb, 0x49,
+ 0x00, 0x00, 0x09, 0xfc, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0xac, 0x93, 0x00, 0x00, 0x00, 0x00, 0x04, 0xe9, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4e, 0xc4, 0x05, 0xdf, 0x4a, 0x00,
+ 0x00, 0x00, 0x97, 0x84, 0x27, 0xf3, 0x00, 0x00, 0x79, 0xa9, 0x00, 0x79, 0xc8, 0x06, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0xad, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0xcb, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x01, 0xed, 0x47, 0x00, 0xad, 0x81, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x62, 0xcb, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x09, 0xf9, 0x1f,
+ 0x00, 0x00, 0x00, 0xaf, 0xc5, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x4f, 0xea, 0x05, 0x00, 0x00, 0x00, 0x49, 0xdc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0xb1, 0x00, 0x6a, 0xc2, 0x00,
+ 0x00, 0x18, 0xed, 0x15, 0x14, 0xff, 0x09, 0x00, 0x79, 0xa9, 0x00, 0x02, 0xbc, 0x88, 0x00, 0x00,
+ 0xa4, 0x7d, 0x00, 0xd8, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xf4, 0x02, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x75, 0xeb, 0x10, 0x00, 0xd8, 0x5c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x3e, 0xf4, 0x02, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x08, 0x95, 0xc4, 0x00,
+ 0x00, 0x00, 0x00, 0x0e, 0x9f, 0xf5, 0x9e, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x05, 0xe8, 0x4a, 0x00, 0x00, 0x00, 0xa4, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x9d, 0x00, 0x09, 0xe8, 0x3a,
+ 0x00, 0x88, 0x92, 0x00, 0x03, 0xfd, 0x1b, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x1b, 0xea, 0x3c, 0x00,
+ 0xa4, 0x7d, 0x00, 0xe1, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xfa, 0x05, 0x00,
+ 0x79, 0xd9, 0x8a, 0x90, 0xa1, 0xdb, 0xda, 0x38, 0x00, 0x00, 0xe2, 0x54, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x35, 0xfc, 0x06, 0x00, 0x79, 0xe9, 0xba, 0xbd, 0xc9, 0xf2, 0x90, 0x0f, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0x92, 0xf2, 0xa9, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x86, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x8c, 0xa9, 0x00, 0x00, 0x0f, 0xf3, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0x90, 0x00, 0x00, 0x79, 0xb0,
+ 0x0d, 0xea, 0x1f, 0x00, 0x00, 0xf2, 0x29, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x57, 0xdc, 0x0b,
+ 0xa4, 0x7d, 0x00, 0xb8, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xd6, 0x00, 0x00,
+ 0x79, 0xd2, 0x75, 0x6f, 0x5a, 0x37, 0x01, 0x00, 0x00, 0x00, 0xb9, 0x77, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x59, 0xdc, 0x00, 0x00, 0x79, 0xc2, 0x41, 0x40, 0x4a, 0xf0, 0x40, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0xc5, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x84, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x0b,
+ 0x00, 0x00, 0x00, 0x29, 0xf7, 0x12, 0x00, 0x66, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91, 0x85, 0x00, 0x00, 0x10, 0xf0,
+ 0x96, 0xa0, 0x00, 0x00, 0x00, 0xe7, 0x38, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0xab, 0x8a,
+ 0xa4, 0x7d, 0x00, 0x8b, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0xa9, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0xb8, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x9a, 0xab, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x62, 0xe3, 0x12, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0xfd, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xc2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xef, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xbe, 0x6e, 0x00, 0xca, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x7a, 0x00, 0x00, 0x00, 0x8e,
+ 0xff, 0x2d, 0x00, 0x00, 0x00, 0xdb, 0x47, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x16, 0xe8,
+ 0xcf, 0x7d, 0x00, 0x1d, 0xf3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xf7, 0x31, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0xf4, 0x43, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x29, 0xf9, 0x32, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0xb1, 0xa4, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0xe9, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xf3, 0x16, 0x00, 0x00, 0x00, 0x00, 0x86, 0xc0, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x55, 0xd7, 0x35, 0xe4, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6f, 0x00, 0x00, 0x00, 0x15,
+ 0x46, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x56, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61,
+ 0xff, 0x7d, 0x00, 0x00, 0x6b, 0xf1, 0x5c, 0x05, 0x00, 0x03, 0x4c, 0xe9, 0x84, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0xf1, 0x5c, 0x05, 0x00,
+ 0x03, 0x4c, 0xe9, 0x84, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xf0, 0x44,
+ 0x00, 0x00, 0x15, 0x47, 0x02, 0x00, 0x00, 0x15, 0xb7, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0xb7, 0x14, 0x00, 0x00, 0x4f, 0xf5, 0x4a, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x04, 0xe1, 0xd3, 0x7b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x79, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0xbb, 0x64, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0x65, 0x00, 0x79, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0xc4, 0x7d, 0x00, 0x00, 0x00, 0x41, 0xd4, 0xf7, 0xd4, 0xf4, 0xde, 0x52, 0x00, 0x00, 0x00, 0x00,
+ 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0xd4, 0xf7, 0xd4,
+ 0xf4, 0xd5, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x79, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xcc,
+ 0x01, 0x00, 0x33, 0xda, 0xee, 0xc9, 0xd9, 0xdc, 0x99, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xee, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0xaf, 0xf6, 0xd5, 0xe7, 0xeb, 0x58, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0xf7, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x35, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x94,
+ 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1b, 0x38, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x32, 0x1f, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
+ 0xd6, 0xa9, 0x37, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x0a, 0x75, 0xce, 0xf0, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x3e, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0xb1,
+ 0x00, 0x18, 0xba, 0x29, 0x00, 0x00, 0x00, 0x00, 0x07, 0xac, 0x40, 0x00, 0x77, 0x85, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x07, 0xb2, 0x32, 0x00, 0x06, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0xbe, 0x7a,
+ 0x00, 0x15, 0x45, 0x45, 0x45, 0x08, 0x00, 0x18, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x3c, 0x45, 0x45, 0x27, 0x00, 0x00, 0x00, 0x07, 0xb1, 0x65, 0x00, 0x00, 0x00, 0x00, 0x12, 0xcf,
+ 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0x51, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c,
+ 0xc9, 0xea, 0xe5, 0xad, 0x20, 0x00, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x1e, 0xa5, 0xde, 0xea, 0xc5, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x2c, 0xfc, 0x09, 0x00, 0x00, 0x00, 0x00, 0x39, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x62, 0xc5,
+ 0x00, 0x00, 0x81, 0xc9, 0x04, 0x00, 0x00, 0x00, 0x87, 0xbb, 0x01, 0x00, 0x28, 0xf8, 0x29, 0x00,
+ 0x00, 0x00, 0x00, 0x6f, 0xc4, 0x01, 0x00, 0x02, 0x41, 0x41, 0x41, 0x41, 0x41, 0x49, 0xe8, 0x72,
+ 0x00, 0x4e, 0xe4, 0x93, 0x93, 0x11, 0x00, 0x20, 0xee, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x7f, 0x93, 0xc8, 0x8f, 0x00, 0x00, 0x00, 0x75, 0xae, 0xe3, 0x14, 0x00, 0x00, 0x00, 0x03, 0x23,
+ 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x23, 0x0d, 0x00, 0xae, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x09,
+ 0x35, 0x12, 0x1b, 0x8f, 0xd4, 0x03, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x1f, 0xeb, 0xb4, 0x47, 0x13, 0x3e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x05, 0xf9, 0x33, 0x00, 0x00, 0x00, 0x05, 0xec, 0x96, 0x00, 0x00, 0x00, 0x00, 0x94, 0x96,
+ 0x00, 0x00, 0x05, 0xcd, 0x78, 0x00, 0x00, 0x37, 0xec, 0x1d, 0x00, 0x00, 0x00, 0x95, 0xa9, 0x00,
+ 0x00, 0x00, 0x0c, 0xe7, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0xaa, 0x01,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc4, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x14, 0xe3, 0x19, 0x7d, 0x93, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0xe0, 0x76, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x05, 0xef, 0x2b, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xa3, 0xb3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xd2, 0x62, 0x00, 0x00, 0x00, 0x47, 0xee, 0xe7, 0x02, 0x00, 0x00, 0x00, 0xc7, 0x66,
+ 0x00, 0x00, 0x00, 0x2c, 0xf1, 0x2b, 0x08, 0xd6, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x15, 0xee, 0x35,
+ 0x00, 0x00, 0x81, 0xa2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0xd9, 0x0d, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x92, 0x7e, 0x00, 0x0a, 0xdf, 0x27, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xc2, 0x3d, 0x00, 0x00, 0x00,
+ 0x03, 0x27, 0x3b, 0x24, 0xd7, 0x44, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0xe6, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x9f, 0x96, 0x00, 0x00, 0x00, 0x9c, 0x7a, 0xe1, 0x3e, 0x00, 0x00, 0x04, 0xf5, 0x31,
+ 0x00, 0x00, 0x00, 0x00, 0x75, 0xcb, 0x8e, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0xcd,
+ 0x03, 0x23, 0xe7, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xee, 0x2f, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xf2, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x26, 0xe0, 0x0b, 0x00, 0x00, 0x61, 0xb0, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x45,
+ 0xde, 0xd0, 0xb7, 0xce, 0xfc, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0xfe, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x6a, 0xca, 0x00, 0x00, 0x05, 0xec, 0x22, 0x8d, 0x92, 0x00, 0x00, 0x2d, 0xf5, 0x05,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0xf2, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xc7,
+ 0x6e, 0xb5, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xd0, 0x6d, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb7, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0xaf, 0x62, 0x00, 0x00, 0x00, 0x02, 0xd0, 0x3f, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xf0,
+ 0x54, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0xb6, 0xa9, 0xdd, 0xd7, 0x9c, 0x15, 0x00, 0x00,
+ 0x00, 0xf0, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x34, 0xf9, 0x09, 0x00, 0x48, 0xc6, 0x00, 0x33, 0xe4, 0x03, 0x00, 0x66, 0xc3, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1e, 0xeb, 0xe9, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e,
+ 0xf4, 0xc9, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0xb9, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5e, 0xba, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x31, 0x03, 0x00, 0x00, 0x00, 0x00, 0x26, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0xfc,
+ 0x04, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0xdb, 0x56, 0x19, 0x2e, 0x95, 0xe3, 0x11, 0x00,
+ 0x00, 0xbf, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x04, 0xf0, 0x41, 0x00, 0xa7, 0x6a, 0x00, 0x00, 0xd6, 0x4a, 0x00, 0xa5, 0x84, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0xbd, 0x79, 0x49, 0xe9, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xc0, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0xec, 0x1c, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xf3, 0x18, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xf1,
+ 0x58, 0x00, 0x00, 0x00, 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x83, 0x00,
+ 0x00, 0x42, 0xef, 0x4c, 0x01, 0x00, 0x0b, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0xb2, 0x7f, 0x12, 0xef, 0x12, 0x00, 0x00, 0x76, 0xaf, 0x00, 0xe4, 0x41, 0x00,
+ 0x00, 0x00, 0x00, 0x6f, 0xc7, 0x03, 0x00, 0x9b, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xcf, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x6e, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4b,
+ 0xe4, 0xdf, 0xce, 0xdb, 0xf0, 0x39, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xc7, 0x00,
+ 0x00, 0x00, 0x50, 0xe1, 0xf1, 0xda, 0xee, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x6d, 0xc7, 0x6c, 0xa2, 0x00, 0x00, 0x00, 0x17, 0xf6, 0x3e, 0xf4, 0x08, 0x00,
+ 0x00, 0x00, 0x1c, 0xed, 0x29, 0x00, 0x00, 0x0f, 0xe4, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xc3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xc8, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x03, 0x1f, 0x2c, 0x1a, 0x01, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x44, 0xe8, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x15, 0x23, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1c, 0xfb, 0xd9, 0x3a, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xd7, 0xad, 0x00, 0x00,
+ 0x00, 0x00, 0xa8, 0x82, 0x00, 0x00, 0x00, 0x00, 0x53, 0xdf, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0xec, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xef, 0x23, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x80, 0xcd, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xc6, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xff, 0x5c, 0x00, 0x00,
+ 0x00, 0x40, 0xe4, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbd, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xbe, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9e, 0x7d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x96, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0xd6, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x26, 0x8e, 0xf0, 0x19, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe7, 0x31, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6e, 0xf4, 0xdc, 0xd5, 0xfd, 0xc8, 0x2d, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91, 0x8b, 0x00, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x1d, 0x2d, 0x0f, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xe3, 0x02, 0x00,
+ 0x00, 0x00, 0x7d, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x4e, 0xec, 0xb6, 0xb6, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xdc, 0x40, 0x00,
+ 0x9d, 0xb6, 0xda, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0b, 0x23, 0x23, 0x23, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x10, 0x00,
+ 0x1e, 0x23, 0x23, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x24, 0xae, 0xe4,
+ 0xa9, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x11, 0x00, 0x00, 0x00, 0x00, 0x17, 0x98,
+ 0xd1, 0xe4, 0xce, 0xa5, 0x20, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14,
+ 0x14, 0x00, 0x00, 0x00, 0x00, 0x14, 0x14, 0x00, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x02, 0x0e, 0x00, 0x00, 0x48, 0xb3, 0xd3, 0xe5, 0xa4, 0x3e, 0x2f, 0xa4, 0xdd, 0xdb, 0xa5,
+ 0x17, 0x00, 0x00, 0x48, 0xb1, 0xd2, 0xe6, 0xcf, 0x8e, 0x09, 0x00, 0x00, 0x00, 0x00, 0x1b, 0xa3,
+ 0xdf, 0xd8, 0x94, 0x0e, 0x00, 0x00, 0x49, 0xb7, 0xd7, 0xe7, 0xc5, 0x80, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x11, 0x8f, 0xcb, 0xe7, 0xd4, 0xb1, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xab, 0x6c, 0x00, 0x00, 0x1f, 0xed, 0x72, 0x18,
+ 0x55, 0xd2, 0x8a, 0x00, 0x00, 0x00, 0x56, 0xbc, 0xe7, 0xec, 0x4e, 0x00, 0x00, 0x1d, 0xe9, 0x93,
+ 0x30, 0x18, 0x2f, 0xe3, 0x41, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6,
+ 0xb6, 0x00, 0x00, 0x00, 0x00, 0xb6, 0xb6, 0x00, 0x89, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8d, 0x8a, 0x00, 0x00, 0x8a, 0xab, 0x24, 0x18, 0x43, 0xe0, 0xe3, 0x5f, 0x20, 0x29, 0xa4,
+ 0xc8, 0x00, 0x00, 0x8a, 0xac, 0x25, 0x17, 0x35, 0xbd, 0xab, 0x00, 0x00, 0x00, 0x1b, 0xea, 0xb6,
+ 0x3e, 0x4d, 0xc7, 0xd6, 0x09, 0x00, 0x8a, 0xac, 0x25, 0x1c, 0x6c, 0xd3, 0xcf, 0x07, 0x00, 0x00,
+ 0x15, 0xe3, 0xc8, 0x60, 0x19, 0x29, 0xc3, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0xa5, 0x8c, 0x00, 0x00,
+ 0x00, 0x46, 0xf7, 0x0d, 0x00, 0x0a, 0xf1, 0x45, 0x03, 0x01, 0x04, 0x00, 0x00, 0xa4, 0xa1, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33,
+ 0x33, 0x00, 0x00, 0x00, 0x00, 0x33, 0x33, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x68, 0xbf, 0x00, 0x00, 0x00, 0x11,
+ 0xfb, 0x1f, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x1d, 0xfa, 0x18, 0x00, 0x00, 0x9f, 0xb8, 0x00,
+ 0x00, 0x00, 0x00, 0xdb, 0x78, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x78, 0x00, 0x00,
+ 0x9b, 0xcd, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0xe3, 0x4b, 0x16, 0x16,
+ 0x16, 0x1c, 0xf5, 0x2f, 0x00, 0x4d, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x3e, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x3d, 0xe0, 0x00, 0x00, 0x00, 0x00,
+ 0xe2, 0x3a, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xe4, 0x3a, 0x00, 0x00, 0xe4, 0x6e, 0x00,
+ 0x00, 0x00, 0x00, 0x90, 0xbf, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x98, 0xc1, 0x00, 0x00,
+ 0xe2, 0x79, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x08, 0xfe, 0xdd, 0xd8, 0xd8,
+ 0xd8, 0xd8, 0xd8, 0x3a, 0x00, 0x87, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x24, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69,
+ 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x6d, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x31, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd5, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x49, 0x00, 0x08, 0xfe, 0x2e, 0x00,
+ 0x00, 0x00, 0x00, 0x50, 0xe3, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x4b, 0xe7, 0x00, 0x08,
+ 0xfe, 0x2d, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x22, 0xa8, 0xdd, 0xd8, 0x9d, 0xc7, 0x6c, 0x00, 0x00, 0xf3, 0x4f, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0xe4, 0xc2, 0xc2, 0xc2, 0x10, 0x00, 0x00, 0xe3, 0x45, 0x00,
+ 0x00, 0x00, 0x00, 0xd8, 0x41, 0x00, 0x8a, 0xcf, 0xc2, 0xe4, 0xce, 0x8e, 0x0a, 0x00, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x0d, 0xad, 0x43, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0xee, 0x5b, 0x00,
+ 0x00, 0x00, 0x00, 0x7e, 0xca, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd2, 0x00, 0x00,
+ 0xf2, 0x32, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x24, 0xed, 0x7f, 0x28, 0x1d, 0x62, 0xe8, 0x6c, 0x00, 0x00, 0xc3, 0xa2, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0xa2, 0x2b, 0x2b, 0x2b, 0x04, 0x00, 0x00, 0xa3, 0xaf, 0x00,
+ 0x00, 0x00, 0x00, 0xd9, 0x41, 0x00, 0x8a, 0xba, 0x37, 0x18, 0x35, 0xbd, 0xae, 0x00, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x09, 0xbd, 0x7e, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0xba, 0xa5, 0x00,
+ 0x00, 0x00, 0x00, 0xc8, 0x94, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x93, 0xa0, 0x00, 0x00,
+ 0xc2, 0x74, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xa5, 0x95, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x46, 0xf1, 0x75, 0x1b,
+ 0x00, 0x03, 0x18, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xe1, 0xb6,
+ 0x62, 0x6e, 0xb6, 0xff, 0x41, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x1d, 0xfb, 0x18, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x08, 0xb8, 0x85, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0x36, 0xf2, 0x77,
+ 0x0f, 0x18, 0x87, 0xf1, 0x1a, 0x00, 0x8a, 0xbf, 0x18, 0x00, 0x00, 0x4f, 0xf2, 0x28, 0x00, 0x00,
+ 0x44, 0xeb, 0x39, 0x00, 0x00, 0x22, 0xd4, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xe8, 0x3d, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x4d, 0xdd, 0xfc,
+ 0xdb, 0xf2, 0x7a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x6f,
+ 0x9e, 0x9c, 0x55, 0xde, 0x3d, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xe4, 0x3a, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x97, 0xb6, 0x88, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x30, 0xe9, 0x00, 0x00, 0x00, 0x00,
+ 0xd4, 0x45, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00, 0x00, 0x48, 0xe3,
+ 0xf0, 0xf7, 0xd4, 0x32, 0x00, 0x00, 0x8a, 0xd3, 0xe7, 0xd5, 0xea, 0xdd, 0x40, 0x00, 0x00, 0x00,
+ 0x00, 0x56, 0xe8, 0xe4, 0xd9, 0xdf, 0xdc, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x08, 0xff, 0x25, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
+ 0x25, 0x09, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x27, 0xff, 0x1c, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0xdf, 0xf4, 0x38, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x1e, 0x17, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x02, 0x20, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1f, 0x1b, 0x01, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xed, 0x62, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x09, 0x00,
+ 0x00, 0x1f, 0x9e, 0xcc, 0x01, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x4f, 0xed, 0x48, 0x00, 0x00, 0x00,
+ 0x00, 0x8f, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0xb8, 0xb5, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf2, 0xd8,
+ 0xdc, 0xd3, 0x8e, 0x22, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x8a, 0x8f, 0x00, 0x3e, 0xed, 0x3b, 0x00, 0x00,
+ 0x00, 0x8e, 0x8b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x31, 0xf6, 0x7f, 0x1c, 0x00, 0x00, 0xae, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x22,
+ 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x8a,
+ 0x8f, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8e, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x52, 0xe7, 0x1d, 0x00,
+ 0x00, 0x7b, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x3f, 0xd8, 0xfc, 0xd2, 0xe0, 0xf1, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x49, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x69, 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x92, 0xbc, 0x01,
+ 0x00, 0x1f, 0xdf, 0xa9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x2d, 0x1a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x1c, 0xe0, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0xd1, 0xe1, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x1e, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x3d, 0xac, 0xd3, 0xe8, 0xd1, 0x0b, 0x00, 0x00, 0x2a, 0x91, 0xdc, 0xe5, 0xbd, 0x0a, 0x00,
+ 0x3e, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x59, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x24, 0x00,
+ 0x6b, 0x76, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x86, 0x00, 0x62, 0x7e, 0x00, 0x00, 0x00, 0x01, 0xb6,
+ 0x24, 0x00, 0x00, 0x00, 0x54, 0x7c, 0x00, 0x17, 0xbc, 0x26, 0x00, 0x00, 0x00, 0x92, 0x57, 0x00,
+ 0x6b, 0x77, 0x00, 0x00, 0x00, 0x00, 0x6a, 0x6a, 0x00, 0x14, 0xc2, 0xc2, 0xc2, 0xc2, 0xc2, 0xaf,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x00, 0x15, 0x34, 0x00, 0x0d, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x15, 0x1a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x1c, 0xb6, 0xb6, 0xb6,
+ 0xb6, 0xb6, 0xb6, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0xab, 0x1f, 0x13, 0x2a, 0x00, 0x00, 0x00, 0xc2, 0x66, 0x0a, 0x16, 0x44, 0x00, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x4d, 0xd4, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x6f, 0x00, 0x41, 0xda, 0x00, 0x00, 0x00, 0x27, 0xff,
+ 0x5f, 0x00, 0x00, 0x00, 0xa5, 0x63, 0x00, 0x00, 0x7b, 0xc6, 0x03, 0x00, 0x5c, 0xd0, 0x06, 0x00,
+ 0x4d, 0xd8, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x51, 0x00, 0x04, 0x2b, 0x2b, 0x2b, 0x2d, 0xcd, 0x96,
+ 0x00, 0x00, 0x00, 0x00, 0x11, 0xba, 0xd8, 0x35, 0x00, 0x4e, 0xbe, 0x00, 0xb0, 0xdc, 0x48, 0x00,
+ 0x00, 0x00, 0x00, 0x71, 0xdc, 0xde, 0xb6, 0x21, 0x00, 0x60, 0x8a, 0x00, 0x27, 0xbc, 0x23, 0x23,
+ 0x23, 0x23, 0x53, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xfa, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x0b, 0xf6, 0x1c, 0x00, 0x00, 0x02, 0xe8, 0x25, 0x00, 0x06, 0xf2, 0x1d, 0x00, 0x00, 0x6a, 0xe9,
+ 0x9d, 0x00, 0x00, 0x01, 0xe5, 0x1b, 0x00, 0x00, 0x03, 0xc6, 0x76, 0x1c, 0xe7, 0x2d, 0x00, 0x00,
+ 0x0b, 0xf5, 0x26, 0x00, 0x00, 0x07, 0xf4, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xc1, 0x05,
+ 0x00, 0x00, 0x00, 0x00, 0x78, 0xaa, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x46, 0xdb, 0x00,
+ 0x00, 0x00, 0x0a, 0xdb, 0x0c, 0x02, 0x69, 0xe5, 0xc0, 0xdb, 0x21, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd4, 0xa4, 0x1b, 0x00, 0x00, 0x00, 0x00,
+ 0x9c, 0xe0, 0xc2, 0xc2, 0xbf, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0xad, 0x6b, 0x00, 0x00, 0x3c, 0xce, 0x00, 0x00, 0x00, 0xb1, 0x67, 0x00, 0x00, 0xb1, 0x67,
+ 0xe4, 0x01, 0x00, 0x31, 0xd1, 0x00, 0x00, 0x00, 0x00, 0x25, 0xec, 0xc9, 0x75, 0x00, 0x00, 0x00,
+ 0x00, 0xa8, 0x7b, 0x00, 0x00, 0x3e, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xe4, 0x19, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x9e, 0x70, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x08, 0xfe, 0x06,
+ 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x2e, 0x0a, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x99, 0xf2, 0xa7, 0x26, 0x00, 0x00,
+ 0x9c, 0x93, 0x2b, 0x2b, 0x2a, 0x00, 0x00, 0xa4, 0x75, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x56, 0xc1, 0x00, 0x00, 0x91, 0x75, 0x00, 0x00, 0x00, 0x6a, 0xb5, 0x00, 0x0a, 0xe7, 0x08,
+ 0xd7, 0x32, 0x00, 0x7d, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb2, 0xfc, 0x24, 0x00, 0x00, 0x00,
+ 0x00, 0x4d, 0xd9, 0x01, 0x00, 0x81, 0x8a, 0x00, 0x00, 0x00, 0x00, 0x13, 0xe4, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x69, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x01, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x84, 0xf1, 0x2b, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x84, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x08, 0xec, 0x22, 0x05, 0xe4, 0x19, 0x00, 0x00, 0x00, 0x17, 0xf4, 0x11, 0x51, 0xa7, 0x00,
+ 0x84, 0x84, 0x00, 0xd1, 0x31, 0x00, 0x00, 0x00, 0x00, 0x50, 0xd4, 0x80, 0xc8, 0x04, 0x00, 0x00,
+ 0x00, 0x04, 0xe1, 0x40, 0x00, 0xc9, 0x3c, 0x00, 0x00, 0x00, 0x00, 0xab, 0x8e, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x7f, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xab, 0x00, 0x00, 0x00, 0x00, 0xe9, 0x30, 0x00,
+ 0x00, 0x00, 0x8f, 0x86, 0x52, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x66, 0xa8, 0x50, 0x00,
+ 0x2d, 0xdc, 0x2c, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x16, 0xe4, 0x2f, 0x02, 0xc3, 0x7d, 0x00, 0x00,
+ 0x00, 0x00, 0x77, 0xad, 0x1a, 0xdd, 0x01, 0x00, 0x00, 0x00, 0x57, 0xda, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x64, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xfc, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x14, 0x00, 0x00, 0x10, 0xe4, 0x39, 0x00,
+ 0x9c, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xf7, 0x3f, 0x00, 0x00, 0x02, 0xeb, 0x30, 0x00,
+ 0x00, 0x00, 0x25, 0xea, 0xc8, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0xd3, 0xe2, 0x06, 0x00,
+ 0x00, 0xce, 0xc6, 0x6a, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x81, 0x00, 0x00, 0x24, 0xef, 0x27, 0x00,
+ 0x00, 0x00, 0x10, 0xed, 0x92, 0x86, 0x00, 0x00, 0x00, 0x0c, 0xe3, 0x45, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xc0, 0x49, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xe3, 0x26,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x8a, 0x8f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0xf2, 0xd7, 0xd5, 0xdf, 0x8c, 0x00, 0x00,
+ 0x9b, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0xeb, 0xe6, 0xdc, 0xf1, 0xda, 0x22, 0x00,
+ 0x00, 0x00, 0x00, 0xae, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe1, 0x8d, 0x00, 0x00,
+ 0x00, 0x68, 0xf6, 0x10, 0x00, 0x00, 0x00, 0x3b, 0xdc, 0x08, 0x00, 0x00, 0x00, 0x7e, 0xb3, 0x00,
+ 0x00, 0x00, 0x00, 0x7f, 0xff, 0x2c, 0x00, 0x00, 0x00, 0x58, 0xfe, 0xee, 0xee, 0xee, 0xee, 0xee,
+ 0x0c, 0x00, 0x17, 0x8c, 0xd4, 0x09, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x76, 0xc9,
+ 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x20, 0x21, 0x00, 0x00, 0x00, 0x00,
+ 0x8a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x1d, 0x08, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x53, 0xc7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x26, 0xc3, 0xaf, 0x02, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0x4e, 0xe4,
+ 0x67, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x5c, 0xd5, 0x0b, 0x00, 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x17, 0xd9, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0xd3, 0x3d, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xd7, 0x3b,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0xb5, 0xef, 0xd9, 0xec, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xc2, 0xc7, 0xe5, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa5, 0x60, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xf9, 0x0e,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xb1, 0x00, 0x00,
+ 0x00, 0x00, 0x38, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x14, 0x24, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x14, 0x25, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, 0xf3, 0xd8, 0xd8,
+ 0xd8, 0xd8, 0xe1, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa4, 0x68, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x00, 0xff, 0x0d,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xa1, 0x6d, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x05, 0xff, 0x09,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x87, 0x8f, 0x00, 0x00, 0x00, 0x4e, 0xbe, 0x00, 0x00, 0x2a, 0xeb, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x24, 0xe1, 0xa3, 0x26, 0x00, 0x4e, 0xbe, 0x00, 0x80, 0xd9, 0x71, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x3c, 0x16, 0x00, 0x0b, 0x1a, 0x00, 0x3e, 0x20, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+];
+
diff --git a/gfx/wr/webrender/src/debug_item.rs b/gfx/wr/webrender/src/debug_item.rs
new file mode 100644
index 0000000000..04ba632f46
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_item.rs
@@ -0,0 +1,20 @@
+/* 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 api::{units::*, ColorF};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum DebugItem {
+ Text {
+ msg: String,
+ color: ColorF,
+ position: DevicePoint,
+ },
+ Rect {
+ outer_color: ColorF,
+ inner_color: ColorF,
+ rect: DeviceRect,
+ },
+}
diff --git a/gfx/wr/webrender/src/debug_server.rs b/gfx/wr/webrender/src/debug_server.rs
new file mode 100644
index 0000000000..36ed9cd2eb
--- /dev/null
+++ b/gfx/wr/webrender/src/debug_server.rs
@@ -0,0 +1,402 @@
+/* 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 api::units::DeviceIntSize;
+use api::channel::{fast_channel, Sender, Receiver};
+use api::DebugFlags;
+use crate::render_api::{ApiMsg, DebugCommand};
+use crate::print_tree::PrintTreePrinter;
+use crate::renderer;
+use std::thread;
+use ws;
+use base64::encode;
+use image_loader;
+
+// Messages that are sent from the render backend to the renderer
+// debug command queue. These are sent in a separate queue so
+// that none of these types are exposed to the RenderApi interfaces.
+// We can't use select!() as it's not stable...
+enum DebugMsg {
+ AddSender(ws::Sender),
+ RemoveSender(ws::util::Token),
+}
+
+// Represents a connection to a client.
+struct Server {
+ ws: ws::Sender,
+ debug_tx: Sender<DebugMsg>,
+ api_tx: Sender<ApiMsg>,
+ debug_flags: DebugFlags,
+}
+
+impl ws::Handler for Server {
+ fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> {
+ self.debug_tx
+ .send(DebugMsg::AddSender(self.ws.clone()))
+ .ok();
+
+ Ok(())
+ }
+
+ fn on_close(&mut self, _: ws::CloseCode, _: &str) {
+ self.debug_tx
+ .send(DebugMsg::RemoveSender(self.ws.token()))
+ .ok();
+ }
+
+ fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> {
+ match msg {
+ ws::Message::Text(string) => {
+ // First, check for flag change commands.
+ let mut set_flags = true;
+ match string.as_str() {
+ "enable_profiler" => self.debug_flags.insert(DebugFlags::PROFILER_DBG),
+ "disable_profiler" => self.debug_flags.remove(DebugFlags::PROFILER_DBG),
+ "enable_texture_cache_debug" => self.debug_flags.insert(DebugFlags::TEXTURE_CACHE_DBG),
+ "disable_texture_cache_debug" => self.debug_flags.remove(DebugFlags::TEXTURE_CACHE_DBG),
+ "enable_render_target_debug" => self.debug_flags.insert(DebugFlags::RENDER_TARGET_DBG),
+ "disable_render_target_debug" => self.debug_flags.remove(DebugFlags::RENDER_TARGET_DBG),
+ "enable_gpu_time_queries" => self.debug_flags.insert(DebugFlags::GPU_TIME_QUERIES),
+ "disable_gpu_time_queries" => self.debug_flags.remove(DebugFlags::GPU_TIME_QUERIES),
+ "enable_gpu_sample_queries" => self.debug_flags.insert(DebugFlags::GPU_SAMPLE_QUERIES),
+ "disable_gpu_sample_queries" => self.debug_flags.remove(DebugFlags::GPU_SAMPLE_QUERIES),
+ "disable_opaque_pass" => self.debug_flags.insert(DebugFlags::DISABLE_OPAQUE_PASS),
+ "enable_opaque_pass" => self.debug_flags.remove(DebugFlags::DISABLE_OPAQUE_PASS),
+ "disable_alpha_pass" => self.debug_flags.insert(DebugFlags::DISABLE_ALPHA_PASS),
+ "enable_alpha_pass" => self.debug_flags.remove(DebugFlags::DISABLE_ALPHA_PASS),
+ "disable_clip_masks" => self.debug_flags.insert(DebugFlags::DISABLE_CLIP_MASKS),
+ "enable_clip_masks" => self.debug_flags.remove(DebugFlags::DISABLE_CLIP_MASKS),
+ "disable_text_prims" => self.debug_flags.insert(DebugFlags::DISABLE_TEXT_PRIMS),
+ "enable_text_prims" => self.debug_flags.remove(DebugFlags::DISABLE_TEXT_PRIMS),
+ "disable_gradient_prims" => self.debug_flags.insert(DebugFlags::DISABLE_GRADIENT_PRIMS),
+ "enable_gradient_prims" => self.debug_flags.remove(DebugFlags::DISABLE_GRADIENT_PRIMS),
+ _ => set_flags = false,
+ };
+
+ let cmd = if set_flags {
+ DebugCommand::SetFlags(self.debug_flags)
+ } else {
+ match string.as_str() {
+ "fetch_passes" => DebugCommand::FetchPasses,
+ "fetch_screenshot" => DebugCommand::FetchScreenshot,
+ "fetch_documents" => DebugCommand::FetchDocuments,
+ "fetch_spatial_tree" => DebugCommand::FetchClipScrollTree,
+ "fetch_render_tasks" => DebugCommand::FetchRenderTasks,
+ msg => {
+ error!("unknown msg {}", msg);
+ return Ok(());
+ }
+ }
+ };
+
+ let msg = ApiMsg::DebugCommand(cmd);
+ self.api_tx.send(msg).unwrap();
+ }
+ ws::Message::Binary(..) => {}
+ }
+
+ Ok(())
+ }
+}
+
+// Spawn a thread for a given renderer, and wait for
+// client connections.
+pub struct DebugServerImpl {
+ join_handle: Option<thread::JoinHandle<()>>,
+ broadcaster: ws::Sender,
+ debug_rx: Receiver<DebugMsg>,
+ senders: Vec<ws::Sender>,
+}
+
+impl DebugServerImpl {
+ pub fn new(api_tx: Sender<ApiMsg>) -> DebugServerImpl {
+ let (debug_tx, debug_rx) = fast_channel(64);
+
+ let socket = ws::Builder::new()
+ .build(move |out| {
+ Server {
+ ws: out,
+ debug_tx: debug_tx.clone(),
+ api_tx: api_tx.clone(),
+ debug_flags: DebugFlags::empty(),
+ }
+ })
+ .unwrap();
+
+ let broadcaster = socket.broadcaster();
+
+ let join_handle = Some(thread::spawn(move || {
+ let address = "127.0.0.1:3583";
+ debug!("WebRender debug server started: {}", address);
+ if let Err(..) = socket.listen(address) {
+ error!("ERROR: Unable to bind debugger websocket (port may be in use).");
+ }
+ }));
+
+ DebugServerImpl {
+ join_handle,
+ broadcaster,
+ debug_rx,
+ senders: Vec::new(),
+ }
+ }
+}
+
+impl renderer::DebugServer for DebugServerImpl {
+ fn send(&mut self, message: String) {
+ // Add any new connections that have been queued.
+ while let Ok(msg) = self.debug_rx.try_recv() {
+ match msg {
+ DebugMsg::AddSender(sender) => {
+ self.senders.push(sender);
+ }
+ DebugMsg::RemoveSender(token) => {
+ self.senders.retain(|sender| sender.token() != token);
+ }
+ }
+ }
+
+ // Broadcast the message to all senders. Keep
+ // track of the ones that failed, so they can
+ // be removed from the active sender list.
+ let mut disconnected_senders = Vec::new();
+
+ for (i, sender) in self.senders.iter().enumerate() {
+ if let Err(..) = sender.send(message.clone()) {
+ disconnected_senders.push(i);
+ }
+ }
+
+ // Remove the broken senders from the list
+ // for next broadcast. Remove in reverse
+ // order so the indices are valid for the
+ // entire loop.
+ for i in disconnected_senders.iter().rev() {
+ self.senders.remove(*i);
+ }
+ }
+}
+
+impl Drop for DebugServerImpl {
+ fn drop(&mut self) {
+ self.broadcaster.shutdown().ok();
+ self.join_handle.take().unwrap().join().ok();
+ }
+}
+
+// A serializable list of debug information about passes
+// that can be sent to the client.
+
+#[derive(Serialize)]
+pub enum BatchKind {
+ Clip,
+ Cache,
+ Opaque,
+ Alpha,
+}
+
+#[derive(Serialize)]
+pub struct PassList {
+ kind: &'static str,
+ passes: Vec<Pass>,
+}
+
+impl PassList {
+ pub fn new() -> PassList {
+ PassList {
+ kind: "passes",
+ passes: Vec::new(),
+ }
+ }
+
+ pub fn add(&mut self, pass: Pass) {
+ self.passes.push(pass);
+ }
+}
+
+#[derive(Serialize)]
+pub struct Pass {
+ pub targets: Vec<Target>,
+}
+
+#[derive(Serialize)]
+pub struct Target {
+ kind: &'static str,
+ batches: Vec<Batch>,
+}
+
+impl Target {
+ pub fn new(kind: &'static str) -> Target {
+ Target {
+ kind,
+ batches: Vec::new(),
+ }
+ }
+
+ pub fn add(&mut self, kind: BatchKind, description: &str, count: usize) {
+ if count > 0 {
+ self.batches.push(Batch {
+ kind,
+ description: description.to_owned(),
+ count,
+ });
+ }
+ }
+}
+
+#[derive(Serialize)]
+struct Batch {
+ kind: BatchKind,
+ description: String,
+ count: usize,
+}
+
+#[derive(Serialize)]
+pub struct TreeNode {
+ description: String,
+ children: Vec<TreeNode>,
+}
+
+impl TreeNode {
+ pub fn new(description: &str) -> TreeNode {
+ TreeNode {
+ description: description.to_owned(),
+ children: Vec::new(),
+ }
+ }
+
+ pub fn add_child(&mut self, child: TreeNode) {
+ self.children.push(child);
+ }
+
+ pub fn add_item(&mut self, description: &str) {
+ self.children.push(TreeNode::new(description));
+ }
+}
+
+#[derive(Serialize)]
+pub struct DocumentList {
+ kind: &'static str,
+ root: TreeNode,
+}
+
+impl DocumentList {
+ pub fn new() -> Self {
+ DocumentList {
+ kind: "documents",
+ root: TreeNode::new("root"),
+ }
+ }
+
+ pub fn add(&mut self, item: TreeNode) {
+ self.root.add_child(item);
+ }
+}
+
+#[derive(Serialize)]
+pub struct Screenshot {
+ kind: &'static str,
+ data: String
+}
+
+impl Screenshot {
+ pub fn new(size: DeviceIntSize, data: Vec<u8>) -> Self {
+ let mut output = Vec::with_capacity((size.width * size.height) as usize);
+ {
+ let encoder = image_loader::png::PNGEncoder::new(&mut output);
+ encoder.encode(
+ &data,
+ size.width as u32,
+ size.height as u32,
+ image_loader::ColorType::Rgba8,
+ ).unwrap();
+ }
+
+ let data = encode(&output);
+ Screenshot {
+ kind: "screenshot",
+ data
+ }
+ }
+}
+
+// A serializable list of debug information about spatial trees
+// that can be sent to the client
+
+#[derive(Serialize)]
+pub struct SpatialTreeList {
+ kind: &'static str,
+ root: TreeNode,
+}
+
+impl SpatialTreeList {
+ pub fn new() -> Self {
+ SpatialTreeList {
+ kind: "spatial_tree",
+ root: TreeNode::new("root"),
+ }
+ }
+
+ pub fn add(&mut self, item: TreeNode) {
+ self.root.add_child(item);
+ }
+}
+
+#[derive(Serialize)]
+pub struct RenderTaskList {
+ kind: &'static str,
+ root: TreeNode,
+}
+
+impl RenderTaskList {
+ pub fn new() -> Self {
+ RenderTaskList {
+ kind: "render_tasks",
+ root: TreeNode::new("root"),
+ }
+ }
+
+ pub fn add(&mut self, item: TreeNode) {
+ self.root.add_child(item);
+ }
+}
+
+// A TreeNode-based PrintTreePrinter to serialize pretty-printed
+// trees as json
+pub struct TreeNodeBuilder {
+ levels: Vec<TreeNode>,
+}
+
+impl TreeNodeBuilder {
+ pub fn new(root: TreeNode) -> TreeNodeBuilder {
+ TreeNodeBuilder { levels: vec![root] }
+ }
+
+ fn current_level_mut(&mut self) -> &mut TreeNode {
+ assert!(!self.levels.is_empty());
+ self.levels.last_mut().unwrap()
+ }
+
+ pub fn build(mut self) -> TreeNode {
+ assert!(self.levels.len() == 1);
+ self.levels.pop().unwrap()
+ }
+}
+
+impl PrintTreePrinter for TreeNodeBuilder {
+ fn new_level(&mut self, title: String) {
+ let level = TreeNode::new(&title);
+ self.levels.push(level);
+ }
+
+ fn end_level(&mut self) {
+ assert!(!self.levels.is_empty());
+ let last_level = self.levels.pop().unwrap();
+ self.current_level_mut().add_child(last_level);
+ }
+
+ fn add_item(&mut self, text: String) {
+ self.current_level_mut().add_item(&text);
+ }
+}
diff --git a/gfx/wr/webrender/src/device/gl.rs b/gfx/wr/webrender/src/device/gl.rs
new file mode 100644
index 0000000000..297f2732a3
--- /dev/null
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -0,0 +1,4589 @@
+/* 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 super::super::shader_source::{OPTIMIZED_SHADERS, UNOPTIMIZED_SHADERS};
+use api::{ColorF, ImageDescriptor, ImageFormat};
+use api::{MixBlendMode, ImageBufferKind, VoidPtrToSizeFn};
+use api::{CrashAnnotator, CrashAnnotation, CrashAnnotatorGuard};
+use api::units::*;
+use euclid::default::Transform3D;
+use gleam::gl;
+use crate::render_api::MemoryReport;
+use crate::internal_types::{FastHashMap, LayerIndex, RenderTargetInfo, Swizzle, SwizzleSettings};
+use crate::util::round_up_to_multiple;
+use crate::profiler;
+use log::Level;
+use smallvec::SmallVec;
+use std::{
+ borrow::Cow,
+ cell::{Cell, RefCell},
+ cmp,
+ collections::hash_map::Entry,
+ fmt::Write,
+ marker::PhantomData,
+ mem,
+ num::NonZeroUsize,
+ os::raw::c_void,
+ ops::Add,
+ path::PathBuf,
+ ptr,
+ rc::Rc,
+ slice,
+ sync::Arc,
+ thread,
+ time::Duration,
+};
+use uuid::Uuid;
+use webrender_build::shader::{
+ ProgramSourceDigest, ShaderKind, ShaderVersion, build_shader_main_string,
+ build_shader_prefix_string, do_build_shader_string, shader_source_from_file,
+};
+
+/// Sequence number for frames, as tracked by the device layer.
+#[derive(Debug, Copy, Clone, PartialEq, Ord, Eq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuFrameId(usize);
+
+impl GpuFrameId {
+ pub fn new(value: usize) -> Self {
+ GpuFrameId(value)
+ }
+}
+
+impl Add<usize> for GpuFrameId {
+ type Output = GpuFrameId;
+
+ fn add(self, other: usize) -> GpuFrameId {
+ GpuFrameId(self.0 + other)
+ }
+}
+
+pub struct TextureSlot(pub usize);
+
+// In some places we need to temporarily bind a texture to any slot.
+const DEFAULT_TEXTURE: TextureSlot = TextureSlot(0);
+
+#[repr(u32)]
+pub enum DepthFunction {
+ Always = gl::ALWAYS,
+ Less = gl::LESS,
+ LessEqual = gl::LEQUAL,
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureFilter {
+ Nearest,
+ Linear,
+ Trilinear,
+}
+
+/// A structure defining a particular workflow of texture transfers.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureFormatPair<T> {
+ /// Format the GPU natively stores texels in.
+ pub internal: T,
+ /// Format we expect the users to provide the texels in.
+ pub external: T,
+}
+
+impl<T: Copy> From<T> for TextureFormatPair<T> {
+ fn from(value: T) -> Self {
+ TextureFormatPair {
+ internal: value,
+ external: value,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum VertexAttributeKind {
+ F32,
+ U8Norm,
+ U16Norm,
+ I32,
+ U16,
+}
+
+#[derive(Debug)]
+pub struct VertexAttribute {
+ pub name: &'static str,
+ pub count: u32,
+ pub kind: VertexAttributeKind,
+}
+
+#[derive(Debug)]
+pub struct VertexDescriptor {
+ pub vertex_attributes: &'static [VertexAttribute],
+ pub instance_attributes: &'static [VertexAttribute],
+}
+
+enum FBOTarget {
+ Read,
+ Draw,
+}
+
+/// Method of uploading texel data from CPU to GPU.
+#[derive(Debug, Clone)]
+pub enum UploadMethod {
+ /// Just call `glTexSubImage` directly with the CPU data pointer
+ Immediate,
+ /// Accumulate the changes in PBO first before transferring to a texture.
+ PixelBuffer(VertexUsageHint),
+}
+
+/// Plain old data that can be used to initialize a texture.
+pub unsafe trait Texel: Copy {}
+unsafe impl Texel for u8 {}
+unsafe impl Texel for f32 {}
+
+/// Returns the size in bytes of a depth target with the given dimensions.
+fn depth_target_size_in_bytes(dimensions: &DeviceIntSize) -> usize {
+ // DEPTH24 textures generally reserve 3 bytes for depth and 1 byte
+ // for stencil, so we measure them as 32 bits.
+ let pixels = dimensions.width * dimensions.height;
+ (pixels as usize) * 4
+}
+
+pub fn get_gl_target(target: ImageBufferKind) -> gl::GLuint {
+ match target {
+ ImageBufferKind::Texture2D => gl::TEXTURE_2D,
+ ImageBufferKind::Texture2DArray => gl::TEXTURE_2D_ARRAY,
+ ImageBufferKind::TextureRect => gl::TEXTURE_RECTANGLE,
+ ImageBufferKind::TextureExternal => gl::TEXTURE_EXTERNAL_OES,
+ }
+}
+
+fn supports_extension(extensions: &[String], extension: &str) -> bool {
+ extensions.iter().any(|s| s == extension)
+}
+
+fn get_shader_version(gl: &dyn gl::Gl) -> ShaderVersion {
+ match gl.get_type() {
+ gl::GlType::Gl => ShaderVersion::Gl,
+ gl::GlType::Gles => ShaderVersion::Gles,
+ }
+}
+
+// Get an unoptimized shader string by name, from the built in resources or
+// an override path, if supplied.
+pub fn get_unoptimized_shader_source(shader_name: &str, base_path: Option<&PathBuf>) -> Cow<'static, str> {
+ if let Some(ref base) = base_path {
+ let shader_path = base.join(&format!("{}.glsl", shader_name));
+ Cow::Owned(shader_source_from_file(&shader_path))
+ } else {
+ Cow::Borrowed(
+ UNOPTIMIZED_SHADERS
+ .get(shader_name)
+ .expect("Shader not found")
+ .source
+ )
+ }
+}
+
+pub trait FileWatcherHandler: Send {
+ fn file_changed(&self, path: PathBuf);
+}
+
+impl VertexAttributeKind {
+ fn size_in_bytes(&self) -> u32 {
+ match *self {
+ VertexAttributeKind::F32 => 4,
+ VertexAttributeKind::U8Norm => 1,
+ VertexAttributeKind::U16Norm => 2,
+ VertexAttributeKind::I32 => 4,
+ VertexAttributeKind::U16 => 2,
+ }
+ }
+}
+
+impl VertexAttribute {
+ fn size_in_bytes(&self) -> u32 {
+ self.count * self.kind.size_in_bytes()
+ }
+
+ fn bind_to_vao(
+ &self,
+ attr_index: gl::GLuint,
+ divisor: gl::GLuint,
+ stride: gl::GLint,
+ offset: gl::GLuint,
+ gl: &dyn gl::Gl,
+ ) {
+ gl.enable_vertex_attrib_array(attr_index);
+ gl.vertex_attrib_divisor(attr_index, divisor);
+
+ match self.kind {
+ VertexAttributeKind::F32 => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::FLOAT,
+ false,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U8Norm => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_BYTE,
+ true,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U16Norm => {
+ gl.vertex_attrib_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_SHORT,
+ true,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::I32 => {
+ gl.vertex_attrib_i_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::INT,
+ stride,
+ offset,
+ );
+ }
+ VertexAttributeKind::U16 => {
+ gl.vertex_attrib_i_pointer(
+ attr_index,
+ self.count as gl::GLint,
+ gl::UNSIGNED_SHORT,
+ stride,
+ offset,
+ );
+ }
+ }
+ }
+}
+
+impl VertexDescriptor {
+ fn instance_stride(&self) -> u32 {
+ self.instance_attributes
+ .iter()
+ .map(|attr| attr.size_in_bytes())
+ .sum()
+ }
+
+ fn bind_attributes(
+ attributes: &[VertexAttribute],
+ start_index: usize,
+ divisor: u32,
+ gl: &dyn gl::Gl,
+ vbo: VBOId,
+ ) {
+ vbo.bind(gl);
+
+ let stride: u32 = attributes
+ .iter()
+ .map(|attr| attr.size_in_bytes())
+ .sum();
+
+ let mut offset = 0;
+ for (i, attr) in attributes.iter().enumerate() {
+ let attr_index = (start_index + i) as gl::GLuint;
+ attr.bind_to_vao(attr_index, divisor, stride as _, offset, gl);
+ offset += attr.size_in_bytes();
+ }
+ }
+
+ fn bind(&self, gl: &dyn gl::Gl, main: VBOId, instance: VBOId, instance_divisor: u32) {
+ Self::bind_attributes(self.vertex_attributes, 0, 0, gl, main);
+
+ if !self.instance_attributes.is_empty() {
+ Self::bind_attributes(
+ self.instance_attributes,
+ self.vertex_attributes.len(),
+ instance_divisor,
+ gl,
+ instance,
+ );
+ }
+ }
+}
+
+impl VBOId {
+ fn bind(&self, gl: &dyn gl::Gl) {
+ gl.bind_buffer(gl::ARRAY_BUFFER, self.0);
+ }
+}
+
+impl IBOId {
+ fn bind(&self, gl: &dyn gl::Gl) {
+ gl.bind_buffer(gl::ELEMENT_ARRAY_BUFFER, self.0);
+ }
+}
+
+impl FBOId {
+ fn bind(&self, gl: &dyn gl::Gl, target: FBOTarget) {
+ let target = match target {
+ FBOTarget::Read => gl::READ_FRAMEBUFFER,
+ FBOTarget::Draw => gl::DRAW_FRAMEBUFFER,
+ };
+ gl.bind_framebuffer(target, self.0);
+ }
+}
+
+pub struct Stream<'a> {
+ attributes: &'a [VertexAttribute],
+ vbo: VBOId,
+}
+
+pub struct VBO<V> {
+ id: gl::GLuint,
+ target: gl::GLenum,
+ allocated_count: usize,
+ marker: PhantomData<V>,
+}
+
+impl<V> VBO<V> {
+ pub fn allocated_count(&self) -> usize {
+ self.allocated_count
+ }
+
+ pub fn stream_with<'a>(&self, attributes: &'a [VertexAttribute]) -> Stream<'a> {
+ debug_assert_eq!(
+ mem::size_of::<V>(),
+ attributes.iter().map(|a| a.size_in_bytes() as usize).sum::<usize>()
+ );
+ Stream {
+ attributes,
+ vbo: VBOId(self.id),
+ }
+ }
+}
+
+impl<T> Drop for VBO<T> {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.id == 0);
+ }
+}
+
+#[cfg_attr(feature = "replay", derive(Clone))]
+#[derive(Debug)]
+pub struct ExternalTexture {
+ id: gl::GLuint,
+ target: gl::GLuint,
+ swizzle: Swizzle,
+ uv_rect: TexelRect,
+}
+
+impl ExternalTexture {
+ pub fn new(
+ id: u32,
+ target: ImageBufferKind,
+ swizzle: Swizzle,
+ uv_rect: TexelRect,
+ ) -> Self {
+ ExternalTexture {
+ id,
+ target: get_gl_target(target),
+ swizzle,
+ uv_rect,
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn internal_id(&self) -> gl::GLuint {
+ self.id
+ }
+
+ pub fn get_uv_rect(&self) -> TexelRect {
+ self.uv_rect
+ }
+}
+
+bitflags! {
+ #[derive(Default)]
+ pub struct TextureFlags: u32 {
+ /// This texture corresponds to one of the shared texture caches.
+ const IS_SHARED_TEXTURE_CACHE = 1 << 0;
+ }
+}
+
+/// WebRender interface to an OpenGL texture.
+///
+/// Because freeing a texture requires various device handles that are not
+/// reachable from this struct, manual destruction via `Device` is required.
+/// Our `Drop` implementation asserts that this has happened.
+#[derive(Debug)]
+pub struct Texture {
+ id: gl::GLuint,
+ target: gl::GLuint,
+ layer_count: i32,
+ format: ImageFormat,
+ size: DeviceIntSize,
+ filter: TextureFilter,
+ flags: TextureFlags,
+ /// An internally mutable swizzling state that may change between batches.
+ active_swizzle: Cell<Swizzle>,
+ /// Framebuffer Objects, one for each layer of the texture, allowing this
+ /// texture to be rendered to. Empty if this texture is not used as a render
+ /// target.
+ fbos: Vec<FBOId>,
+ /// Same as the above, but with a depth buffer attached.
+ ///
+ /// FBOs are cheap to create but expensive to reconfigure (since doing so
+ /// invalidates framebuffer completeness caching). Moreover, rendering with
+ /// a depth buffer attached but the depth write+test disabled relies on the
+ /// driver to optimize it out of the rendering pass, which most drivers
+ /// probably do but, according to jgilbert, is best not to rely on.
+ ///
+ /// So we lazily generate a second list of FBOs with depth. This list is
+ /// empty if this texture is not used as a render target _or_ if it is, but
+ /// the depth buffer has never been requested.
+ ///
+ /// Note that we always fill fbos, and then lazily create fbos_with_depth
+ /// when needed. We could make both lazy (i.e. render targets would have one
+ /// or the other, but not both, unless they were actually used in both
+ /// configurations). But that would complicate a lot of logic in this module,
+ /// and FBOs are cheap enough to create.
+ fbos_with_depth: Vec<FBOId>,
+ /// If we are unable to blit directly to a texture array then we need
+ /// an intermediate renderbuffer.
+ blit_workaround_buffer: Option<(RBOId, FBOId)>,
+ last_frame_used: GpuFrameId,
+}
+
+impl Texture {
+ pub fn get_dimensions(&self) -> DeviceIntSize {
+ self.size
+ }
+
+ pub fn get_layer_count(&self) -> i32 {
+ self.layer_count
+ }
+
+ pub fn get_format(&self) -> ImageFormat {
+ self.format
+ }
+
+ pub fn get_filter(&self) -> TextureFilter {
+ self.filter
+ }
+
+ pub fn is_array(&self) -> bool {
+ match self.target {
+ gl::TEXTURE_2D_ARRAY => true,
+ _ => false
+ }
+ }
+
+ pub fn supports_depth(&self) -> bool {
+ !self.fbos_with_depth.is_empty()
+ }
+
+ pub fn last_frame_used(&self) -> GpuFrameId {
+ self.last_frame_used
+ }
+
+ pub fn used_in_frame(&self, frame_id: GpuFrameId) -> bool {
+ self.last_frame_used == frame_id
+ }
+
+ pub fn is_render_target(&self) -> bool {
+ !self.fbos.is_empty()
+ }
+
+ /// Returns true if this texture was used within `threshold` frames of
+ /// the current frame.
+ pub fn used_recently(&self, current_frame_id: GpuFrameId, threshold: usize) -> bool {
+ self.last_frame_used + threshold >= current_frame_id
+ }
+
+ /// Returns the flags for this texture.
+ pub fn flags(&self) -> &TextureFlags {
+ &self.flags
+ }
+
+ /// Returns a mutable borrow of the flags for this texture.
+ pub fn flags_mut(&mut self) -> &mut TextureFlags {
+ &mut self.flags
+ }
+
+ /// Returns the number of bytes (generally in GPU memory) that each layer of
+ /// this texture consumes.
+ pub fn layer_size_in_bytes(&self) -> usize {
+ assert!(self.layer_count > 0 || self.size.width + self.size.height == 0);
+ let bpp = self.format.bytes_per_pixel() as usize;
+ let w = self.size.width as usize;
+ let h = self.size.height as usize;
+ bpp * w * h
+ }
+
+ /// Returns the number of bytes (generally in GPU memory) that this texture
+ /// consumes.
+ pub fn size_in_bytes(&self) -> usize {
+ self.layer_size_in_bytes() * (self.layer_count as usize)
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn into_external(mut self) -> ExternalTexture {
+ let ext = ExternalTexture {
+ id: self.id,
+ target: self.target,
+ swizzle: Swizzle::default(),
+ // TODO(gw): Support custom UV rect for external textures during captures
+ uv_rect: TexelRect::new(
+ 0.0,
+ 0.0,
+ self.size.width as f32,
+ self.size.height as f32,
+ ),
+ };
+ self.id = 0; // don't complain, moved out
+ ext
+ }
+}
+
+impl Drop for Texture {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.id == 0);
+ }
+}
+
+pub struct Program {
+ id: gl::GLuint,
+ u_transform: gl::GLint,
+ u_mode: gl::GLint,
+ source_info: ProgramSourceInfo,
+ is_initialized: bool,
+}
+
+impl Program {
+ pub fn is_initialized(&self) -> bool {
+ self.is_initialized
+ }
+}
+
+impl Drop for Program {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+pub struct CustomVAO {
+ id: gl::GLuint,
+}
+
+impl Drop for CustomVAO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+pub struct VAO {
+ id: gl::GLuint,
+ ibo_id: IBOId,
+ main_vbo_id: VBOId,
+ instance_vbo_id: VBOId,
+ instance_stride: usize,
+ instance_divisor: u32,
+ owns_vertices_and_indices: bool,
+}
+
+impl Drop for VAO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called"
+ );
+ }
+}
+
+#[derive(Debug)]
+pub struct PBO {
+ id: gl::GLuint,
+ reserved_size: usize,
+}
+
+impl PBO {
+ pub fn get_reserved_size(&self) -> usize {
+ self.reserved_size
+ }
+}
+
+impl Drop for PBO {
+ fn drop(&mut self) {
+ debug_assert!(
+ thread::panicking() || self.id == 0,
+ "renderer::deinit not called or PBO not returned to pool"
+ );
+ }
+}
+
+pub struct BoundPBO<'a> {
+ device: &'a mut Device,
+ pub data: &'a [u8]
+}
+
+impl<'a> Drop for BoundPBO<'a> {
+ fn drop(&mut self) {
+ self.device.gl.unmap_buffer(gl::PIXEL_PACK_BUFFER);
+ self.device.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, 0);
+ }
+}
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct FBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct RBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+pub struct VBOId(gl::GLuint);
+
+#[derive(PartialEq, Eq, Hash, Debug, Copy, Clone)]
+struct IBOId(gl::GLuint);
+
+#[derive(Clone, Debug)]
+enum ProgramSourceType {
+ Unoptimized,
+ Optimized(ShaderVersion),
+}
+
+#[derive(Clone, Debug)]
+pub struct ProgramSourceInfo {
+ base_filename: &'static str,
+ features: Vec<&'static str>,
+ source_type: ProgramSourceType,
+ digest: ProgramSourceDigest,
+}
+
+impl ProgramSourceInfo {
+ fn new(
+ device: &Device,
+ name: &'static str,
+ features: &[&'static str],
+ ) -> Self {
+
+ // Compute the digest. Assuming the device has a `ProgramCache`, this
+ // will always be needed, whereas the source is rarely needed.
+
+ use std::collections::hash_map::DefaultHasher;
+ use std::hash::Hasher;
+
+ // Setup.
+ let mut hasher = DefaultHasher::new();
+ let gl_version = get_shader_version(&*device.gl());
+
+ // Hash the renderer name.
+ hasher.write(device.capabilities.renderer_name.as_bytes());
+
+ let full_name = &Self::make_full_name(name, features);
+
+ let optimized_source = if device.use_optimized_shaders {
+ OPTIMIZED_SHADERS.get(&(gl_version, full_name)).or_else(|| {
+ warn!("Missing optimized shader source for {}", full_name);
+ None
+ })
+ } else {
+ None
+ };
+
+ let source_type = match optimized_source {
+ Some(source_and_digest) => {
+ // Optimized shader sources are used as-is, without any run-time processing.
+ // The vertex and fragment shaders are different, so must both be hashed.
+ // We use the hashes that were computed at build time, and verify it in debug builds.
+ if cfg!(debug_assertions) {
+ let mut h = DefaultHasher::new();
+ h.write(source_and_digest.vert_source.as_bytes());
+ h.write(source_and_digest.frag_source.as_bytes());
+ let d: ProgramSourceDigest = h.into();
+ let digest = d.to_string();
+ debug_assert_eq!(digest, source_and_digest.digest);
+ hasher.write(digest.as_bytes());
+ } else {
+ hasher.write(source_and_digest.digest.as_bytes());
+ }
+
+ ProgramSourceType::Optimized(gl_version)
+ }
+ None => {
+ // For non-optimized sources we compute the hash by walking the static strings
+ // in the same order as we would when concatenating the source, to avoid
+ // heap-allocating in the common case.
+ //
+ // Note that we cheat a bit to make the hashing more efficient. First, the only
+ // difference between the vertex and fragment shader is a single deterministic
+ // define, so we don't need to hash both. Second, we precompute the digest of the
+ // expanded source file at build time, and then just hash that digest here.
+ let override_path = device.resource_override_path.as_ref();
+ let source_and_digest = UNOPTIMIZED_SHADERS.get(&name).expect("Shader not found");
+
+ // Hash the prefix string.
+ build_shader_prefix_string(
+ gl_version,
+ &features,
+ ShaderKind::Vertex,
+ &name,
+ &mut |s| hasher.write(s.as_bytes()),
+ );
+
+ // Hash the shader file contents. We use a precomputed digest, and
+ // verify it in debug builds.
+ if override_path.is_some() || cfg!(debug_assertions) {
+ let mut h = DefaultHasher::new();
+ build_shader_main_string(
+ &name,
+ &|f| get_unoptimized_shader_source(f, override_path),
+ &mut |s| h.write(s.as_bytes())
+ );
+ let d: ProgramSourceDigest = h.into();
+ let digest = format!("{}", d);
+ debug_assert!(override_path.is_some() || digest == source_and_digest.digest);
+ hasher.write(digest.as_bytes());
+ } else {
+ hasher.write(source_and_digest.digest.as_bytes());
+ }
+
+ ProgramSourceType::Unoptimized
+ }
+ };
+
+ // Finish.
+ ProgramSourceInfo {
+ base_filename: name,
+ features: features.to_vec(),
+ source_type,
+ digest: hasher.into(),
+ }
+ }
+
+ fn compute_source(&self, device: &Device, kind: ShaderKind) -> String {
+ let full_name = self.full_name();
+ match self.source_type {
+ ProgramSourceType::Optimized(gl_version) => {
+ let shader = OPTIMIZED_SHADERS
+ .get(&(gl_version, &full_name))
+ .unwrap_or_else(|| panic!("Missing optimized shader source for {}", full_name));
+
+ match kind {
+ ShaderKind::Vertex => shader.vert_source.to_string(),
+ ShaderKind::Fragment => shader.frag_source.to_string(),
+ }
+ },
+ ProgramSourceType::Unoptimized => {
+ let mut src = String::new();
+ device.build_shader_string(
+ &self.features,
+ kind,
+ self.base_filename,
+ |s| src.push_str(s),
+ );
+ src
+ }
+ }
+ }
+
+ fn make_full_name(base_filename: &'static str, features: &[&'static str]) -> String {
+ if features.is_empty() {
+ base_filename.to_string()
+ } else {
+ format!("{}_{}", base_filename, features.join("_"))
+ }
+ }
+
+ fn full_name(&self) -> String {
+ Self::make_full_name(self.base_filename, &self.features)
+ }
+}
+
+#[cfg_attr(feature = "serialize_program", derive(Deserialize, Serialize))]
+pub struct ProgramBinary {
+ bytes: Vec<u8>,
+ format: gl::GLenum,
+ source_digest: ProgramSourceDigest,
+}
+
+impl ProgramBinary {
+ fn new(bytes: Vec<u8>,
+ format: gl::GLenum,
+ source_digest: ProgramSourceDigest) -> Self {
+ ProgramBinary {
+ bytes,
+ format,
+ source_digest,
+ }
+ }
+
+ /// Returns a reference to the source digest hash.
+ pub fn source_digest(&self) -> &ProgramSourceDigest {
+ &self.source_digest
+ }
+}
+
+/// The interfaces that an application can implement to handle ProgramCache update
+pub trait ProgramCacheObserver {
+ fn save_shaders_to_disk(&self, entries: Vec<Arc<ProgramBinary>>);
+ fn set_startup_shaders(&self, entries: Vec<Arc<ProgramBinary>>);
+ fn try_load_shader_from_disk(&self, digest: &ProgramSourceDigest, program_cache: &Rc<ProgramCache>);
+ fn notify_program_binary_failed(&self, program_binary: &Arc<ProgramBinary>);
+}
+
+struct ProgramCacheEntry {
+ /// The binary.
+ binary: Arc<ProgramBinary>,
+ /// True if the binary has been linked, i.e. used for rendering.
+ linked: bool,
+}
+
+pub struct ProgramCache {
+ entries: RefCell<FastHashMap<ProgramSourceDigest, ProgramCacheEntry>>,
+
+ /// Optional trait object that allows the client
+ /// application to handle ProgramCache updating
+ program_cache_handler: Option<Box<dyn ProgramCacheObserver>>,
+
+ /// Programs that have not yet been cached to disk (by program_cache_handler)
+ pending_entries: RefCell<Vec<Arc<ProgramBinary>>>,
+}
+
+impl ProgramCache {
+ pub fn new(program_cache_observer: Option<Box<dyn ProgramCacheObserver>>) -> Rc<Self> {
+ Rc::new(
+ ProgramCache {
+ entries: RefCell::new(FastHashMap::default()),
+ program_cache_handler: program_cache_observer,
+ pending_entries: RefCell::new(Vec::default()),
+ }
+ )
+ }
+
+ /// Save any new program binaries to the disk cache, and if startup has
+ /// just completed then write the list of shaders to load on next startup.
+ fn update_disk_cache(&self, startup_complete: bool) {
+ if let Some(ref handler) = self.program_cache_handler {
+ if !self.pending_entries.borrow().is_empty() {
+ let pending_entries = self.pending_entries.replace(Vec::default());
+ handler.save_shaders_to_disk(pending_entries);
+ }
+
+ if startup_complete {
+ let startup_shaders = self.entries.borrow().values()
+ .filter(|e| e.linked).map(|e| e.binary.clone())
+ .collect::<Vec<_>>();
+ handler.set_startup_shaders(startup_shaders);
+ }
+ }
+ }
+
+ /// Add a new ProgramBinary to the cache.
+ /// This function is typically used after compiling and linking a new program.
+ /// The binary will be saved to disk the next time update_disk_cache() is called.
+ fn add_new_program_binary(&self, program_binary: Arc<ProgramBinary>) {
+ self.pending_entries.borrow_mut().push(program_binary.clone());
+
+ let digest = program_binary.source_digest.clone();
+ let entry = ProgramCacheEntry {
+ binary: program_binary,
+ linked: true,
+ };
+ self.entries.borrow_mut().insert(digest, entry);
+ }
+
+ /// Load ProgramBinary to ProgramCache.
+ /// The function is typically used to load ProgramBinary from disk.
+ #[cfg(feature = "serialize_program")]
+ pub fn load_program_binary(&self, program_binary: Arc<ProgramBinary>) {
+ let digest = program_binary.source_digest.clone();
+ let entry = ProgramCacheEntry {
+ binary: program_binary,
+ linked: false,
+ };
+ self.entries.borrow_mut().insert(digest, entry);
+ }
+
+ /// Returns the number of bytes allocated for shaders in the cache.
+ pub fn report_memory(&self, op: VoidPtrToSizeFn) -> usize {
+ self.entries.borrow().values()
+ .map(|e| unsafe { op(e.binary.bytes.as_ptr() as *const c_void ) })
+ .sum()
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum VertexUsageHint {
+ Static,
+ Dynamic,
+ Stream,
+}
+
+impl VertexUsageHint {
+ fn to_gl(&self) -> gl::GLuint {
+ match *self {
+ VertexUsageHint::Static => gl::STATIC_DRAW,
+ VertexUsageHint::Dynamic => gl::DYNAMIC_DRAW,
+ VertexUsageHint::Stream => gl::STREAM_DRAW,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct UniformLocation(gl::GLint);
+
+impl UniformLocation {
+ pub const INVALID: Self = UniformLocation(-1);
+}
+
+#[derive(Debug)]
+pub struct Capabilities {
+ /// Whether multisampled render targets are supported.
+ pub supports_multisampling: bool,
+ /// Whether the function `glCopyImageSubData` is available.
+ pub supports_copy_image_sub_data: bool,
+ /// Whether the device supports persistently mapped buffers, via glBufferStorage.
+ pub supports_buffer_storage: bool,
+ /// Whether we are able to use `glBlitFramebuffers` with the draw fbo
+ /// bound to a non-0th layer of a texture array. This is buggy on
+ /// Adreno devices.
+ pub supports_blit_to_texture_array: bool,
+ /// Whether advanced blend equations are supported.
+ pub supports_advanced_blend_equation: bool,
+ /// Whether dual-source blending is supported.
+ pub supports_dual_source_blending: bool,
+ /// Whether KHR_debug is supported for getting debug messages from
+ /// the driver.
+ pub supports_khr_debug: bool,
+ /// Whether we can configure texture units to do swizzling on sampling.
+ pub supports_texture_swizzle: bool,
+ /// Whether the driver supports uploading to textures from a non-zero
+ /// offset within a PBO.
+ pub supports_nonzero_pbo_offsets: bool,
+ /// Whether the driver supports specifying the texture usage up front.
+ pub supports_texture_usage: bool,
+ /// Whether offscreen render targets can be partially updated.
+ pub supports_render_target_partial_update: bool,
+ /// Whether we can use SSBOs.
+ pub supports_shader_storage_object: bool,
+ /// Whether the driver prefers fewer and larger texture uploads
+ /// over many smaller updates.
+ pub prefers_batched_texture_uploads: bool,
+ /// The name of the renderer, as reported by GL
+ pub renderer_name: String,
+}
+
+#[derive(Clone, Debug)]
+pub enum ShaderError {
+ Compilation(String, String), // name, error message
+ Link(String, String), // name, error message
+}
+
+/// A refcounted depth target, which may be shared by multiple textures across
+/// the device.
+struct SharedDepthTarget {
+ /// The Render Buffer Object representing the depth target.
+ rbo_id: RBOId,
+ /// Reference count. When this drops to zero, the RBO is deleted.
+ refcount: usize,
+}
+
+#[cfg(debug_assertions)]
+impl Drop for SharedDepthTarget {
+ fn drop(&mut self) {
+ debug_assert!(thread::panicking() || self.refcount == 0);
+ }
+}
+
+/// Describes for which texture formats to use the glTexStorage*
+/// family of functions.
+#[derive(PartialEq, Debug)]
+enum TexStorageUsage {
+ Never,
+ NonBGRA8,
+ Always,
+}
+
+/// Describes a required alignment for a stride,
+/// which can either be represented in bytes or pixels.
+#[derive(Copy, Clone, Debug)]
+pub enum StrideAlignment {
+ Bytes(NonZeroUsize),
+ Pixels(NonZeroUsize),
+}
+
+impl StrideAlignment {
+ pub fn num_bytes(&self, format: ImageFormat) -> NonZeroUsize {
+ match *self {
+ Self::Bytes(bytes) => bytes,
+ Self::Pixels(pixels) => {
+ assert!(format.bytes_per_pixel() > 0);
+ NonZeroUsize::new(pixels.get() * format.bytes_per_pixel() as usize).unwrap()
+ }
+ }
+ }
+}
+
+// We get 24 bits of Z value - use up 22 bits of it to give us
+// 4 bits to account for GPU issues. This seems to manifest on
+// some GPUs under certain perspectives due to z interpolation
+// precision problems.
+const RESERVE_DEPTH_BITS: i32 = 2;
+
+pub struct Device {
+ gl: Rc<dyn gl::Gl>,
+
+ /// If non-None, |gl| points to a profiling wrapper, and this points to the
+ /// underling Gl instance.
+ base_gl: Option<Rc<dyn gl::Gl>>,
+
+ // device state
+ bound_textures: [gl::GLuint; 16],
+ bound_program: gl::GLuint,
+ bound_vao: gl::GLuint,
+ bound_read_fbo: FBOId,
+ bound_draw_fbo: FBOId,
+ program_mode_id: UniformLocation,
+ default_read_fbo: FBOId,
+ default_draw_fbo: FBOId,
+
+ /// Track depth state for assertions. Note that the default FBO has depth,
+ /// so this defaults to true.
+ depth_available: bool,
+
+ upload_method: UploadMethod,
+
+ // HW or API capabilities
+ capabilities: Capabilities,
+
+ color_formats: TextureFormatPair<ImageFormat>,
+ bgra_formats: TextureFormatPair<gl::GLuint>,
+ bgra_pixel_type: gl::GLuint,
+ swizzle_settings: SwizzleSettings,
+ depth_format: gl::GLuint,
+
+ /// Map from texture dimensions to shared depth buffers for render targets.
+ ///
+ /// Render targets often have the same width/height, so we can save memory
+ /// by sharing these across targets.
+ depth_targets: FastHashMap<DeviceIntSize, SharedDepthTarget>,
+
+ // debug
+ inside_frame: bool,
+ crash_annotator: Option<Box<dyn CrashAnnotator>>,
+
+ // resources
+ resource_override_path: Option<PathBuf>,
+
+ /// Whether to use shaders that have been optimized at build time.
+ use_optimized_shaders: bool,
+
+ max_texture_size: i32,
+ max_texture_layers: u32,
+ cached_programs: Option<Rc<ProgramCache>>,
+
+ // Frame counter. This is used to map between CPU
+ // frames and GPU frames.
+ frame_id: GpuFrameId,
+
+ /// When to use glTexStorage*. We prefer this over glTexImage* because it
+ /// guarantees that mipmaps won't be generated (which they otherwise are on
+ /// some drivers, particularly ANGLE). However, it is not always supported
+ /// at all, or for BGRA8 format. If it's not supported for the required
+ /// format, we fall back to glTexImage*.
+ texture_storage_usage: TexStorageUsage,
+
+ optimal_pbo_stride: StrideAlignment,
+
+ /// Whether we must ensure the source strings passed to glShaderSource()
+ /// are null-terminated, to work around driver bugs.
+ requires_null_terminated_shader_source: bool,
+
+ /// Whether we must ensure the source strings passed to glShaderSource()
+ /// are unique, to work around driver bugs.
+ requires_unique_shader_source: bool,
+
+ /// Whether we must unbind any texture from GL_TEXTURE_EXTERNAL_OES before
+ /// binding to GL_TEXTURE_2D, to work around an android emulator bug.
+ requires_texture_external_unbind: bool,
+
+ // GL extensions
+ extensions: Vec<String>,
+
+ /// Dumps the source of the shader with the given name
+ dump_shader_source: Option<String>,
+
+ surface_origin_is_top_left: bool,
+
+ /// A debug boolean for tracking if the shader program has been set after
+ /// a blend mode change.
+ ///
+ /// This is needed for compatibility with next-gen
+ /// GPU APIs that switch states using "pipeline object" that bundles
+ /// together the blending state with the shader.
+ ///
+ /// Having the constraint of always binding the shader last would allow
+ /// us to have the "pipeline object" bound at that time. Without this
+ /// constraint, we'd either have to eagerly bind the "pipeline object"
+ /// on changing either the shader or the blend more, or lazily bind it
+ /// at draw call time, neither of which is desirable.
+ #[cfg(debug_assertions)]
+ shader_is_ready: bool,
+}
+
+/// Contains the parameters necessary to bind a draw target.
+#[derive(Clone, Copy, Debug)]
+pub enum DrawTarget {
+ /// Use the device's default draw target, with the provided dimensions,
+ /// which are used to set the viewport.
+ Default {
+ /// Target rectangle to draw.
+ rect: FramebufferIntRect,
+ /// Total size of the target.
+ total_size: FramebufferIntSize,
+ surface_origin_is_top_left: bool,
+ },
+ /// Use the provided texture.
+ Texture {
+ /// Size of the texture in pixels
+ dimensions: DeviceIntSize,
+ /// The slice within the texture array to draw to
+ layer: LayerIndex,
+ /// Whether to draw with the texture's associated depth target
+ with_depth: bool,
+ /// Workaround buffers for devices with broken texture array copy implementation
+ blit_workaround_buffer: Option<(RBOId, FBOId)>,
+ /// FBO that corresponds to the selected layer / depth mode
+ fbo_id: FBOId,
+ /// Native GL texture ID
+ id: gl::GLuint,
+ /// Native GL texture target
+ target: gl::GLuint,
+ },
+ /// Use an FBO attached to an external texture.
+ External {
+ fbo: FBOId,
+ size: FramebufferIntSize,
+ },
+ /// An OS compositor surface
+ NativeSurface {
+ offset: DeviceIntPoint,
+ external_fbo_id: u32,
+ dimensions: DeviceIntSize,
+ },
+}
+
+impl DrawTarget {
+ pub fn new_default(size: DeviceIntSize, surface_origin_is_top_left: bool) -> Self {
+ let total_size = device_size_as_framebuffer_size(size);
+ DrawTarget::Default {
+ rect: total_size.into(),
+ total_size,
+ surface_origin_is_top_left,
+ }
+ }
+
+ /// Returns true if this draw target corresponds to the default framebuffer.
+ pub fn is_default(&self) -> bool {
+ match *self {
+ DrawTarget::Default {..} => true,
+ _ => false,
+ }
+ }
+
+ pub fn from_texture(
+ texture: &Texture,
+ layer: usize,
+ with_depth: bool,
+ ) -> Self {
+ let fbo_id = if with_depth {
+ texture.fbos_with_depth[layer]
+ } else {
+ texture.fbos[layer]
+ };
+
+ DrawTarget::Texture {
+ dimensions: texture.get_dimensions(),
+ fbo_id,
+ with_depth,
+ layer,
+ blit_workaround_buffer: texture.blit_workaround_buffer,
+ id: texture.id,
+ target: texture.target,
+ }
+ }
+
+ /// Returns the dimensions of this draw-target.
+ pub fn dimensions(&self) -> DeviceIntSize {
+ match *self {
+ DrawTarget::Default { total_size, .. } => total_size.cast_unit(),
+ DrawTarget::Texture { dimensions, .. } => dimensions,
+ DrawTarget::External { size, .. } => size.cast_unit(),
+ DrawTarget::NativeSurface { dimensions, .. } => dimensions,
+ }
+ }
+
+ pub fn to_framebuffer_rect(&self, device_rect: DeviceIntRect) -> FramebufferIntRect {
+ let mut fb_rect = device_rect_as_framebuffer_rect(&device_rect);
+ match *self {
+ DrawTarget::Default { ref rect, surface_origin_is_top_left, .. } => {
+ // perform a Y-flip here
+ if !surface_origin_is_top_left {
+ fb_rect.origin.y = rect.origin.y + rect.size.height - fb_rect.origin.y - fb_rect.size.height;
+ fb_rect.origin.x += rect.origin.x;
+ }
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } | DrawTarget::NativeSurface { .. } => (),
+ }
+ fb_rect
+ }
+
+ pub fn surface_origin_is_top_left(&self) -> bool {
+ match *self {
+ DrawTarget::Default { surface_origin_is_top_left, .. } => surface_origin_is_top_left,
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } | DrawTarget::NativeSurface { .. } => true,
+ }
+ }
+
+ /// Given a scissor rect, convert it to the right coordinate space
+ /// depending on the draw target kind. If no scissor rect was supplied,
+ /// returns a scissor rect that encloses the entire render target.
+ pub fn build_scissor_rect(
+ &self,
+ scissor_rect: Option<DeviceIntRect>,
+ ) -> FramebufferIntRect {
+ let dimensions = self.dimensions();
+
+ match scissor_rect {
+ Some(scissor_rect) => match *self {
+ DrawTarget::Default { ref rect, .. } => {
+ self.to_framebuffer_rect(scissor_rect)
+ .intersection(rect)
+ .unwrap_or_else(FramebufferIntRect::zero)
+ }
+ DrawTarget::NativeSurface { offset, .. } => {
+ device_rect_as_framebuffer_rect(&scissor_rect.translate(offset.to_vector()))
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } => {
+ device_rect_as_framebuffer_rect(&scissor_rect)
+ }
+ }
+ None => {
+ FramebufferIntRect::new(
+ FramebufferIntPoint::zero(),
+ device_size_as_framebuffer_size(dimensions),
+ )
+ }
+ }
+ }
+}
+
+/// Contains the parameters necessary to bind a texture-backed read target.
+#[derive(Clone, Copy, Debug)]
+pub enum ReadTarget {
+ /// Use the device's default draw target.
+ Default,
+ /// Use the provided texture,
+ Texture {
+ /// ID of the FBO to read from.
+ fbo_id: FBOId,
+ },
+ /// Use an FBO attached to an external texture.
+ External {
+ fbo: FBOId,
+ },
+}
+
+impl ReadTarget {
+ pub fn from_texture(
+ texture: &Texture,
+ layer: usize,
+ ) -> Self {
+ ReadTarget::Texture {
+ fbo_id: texture.fbos[layer],
+ }
+ }
+}
+
+impl From<DrawTarget> for ReadTarget {
+ fn from(t: DrawTarget) -> Self {
+ match t {
+ DrawTarget::Default { .. } => ReadTarget::Default,
+ DrawTarget::NativeSurface { .. } => {
+ unreachable!("bug: native surfaces cannot be read targets");
+ }
+ DrawTarget::Texture { fbo_id, .. } =>
+ ReadTarget::Texture { fbo_id },
+ DrawTarget::External { fbo, .. } =>
+ ReadTarget::External { fbo },
+ }
+ }
+}
+
+impl Device {
+ pub fn new(
+ mut gl: Rc<dyn gl::Gl>,
+ crash_annotator: Option<Box<dyn CrashAnnotator>>,
+ resource_override_path: Option<PathBuf>,
+ use_optimized_shaders: bool,
+ upload_method: UploadMethod,
+ cached_programs: Option<Rc<ProgramCache>>,
+ allow_texture_storage_support: bool,
+ allow_texture_swizzling: bool,
+ dump_shader_source: Option<String>,
+ surface_origin_is_top_left: bool,
+ panic_on_gl_error: bool,
+ ) -> Device {
+ let mut max_texture_size = [0];
+ let mut max_texture_layers = [0];
+ unsafe {
+ gl.get_integer_v(gl::MAX_TEXTURE_SIZE, &mut max_texture_size);
+ gl.get_integer_v(gl::MAX_ARRAY_TEXTURE_LAYERS, &mut max_texture_layers);
+ }
+
+ let max_texture_size = max_texture_size[0];
+ let max_texture_layers = max_texture_layers[0] as u32;
+ let renderer_name = gl.get_string(gl::RENDERER);
+ info!("Renderer: {}", renderer_name);
+ info!("Max texture size: {}", max_texture_size);
+
+ let mut extension_count = [0];
+ unsafe {
+ gl.get_integer_v(gl::NUM_EXTENSIONS, &mut extension_count);
+ }
+ let extension_count = extension_count[0] as gl::GLuint;
+ let mut extensions = Vec::new();
+ for i in 0 .. extension_count {
+ extensions.push(gl.get_string_i(gl::EXTENSIONS, i));
+ }
+
+ // On debug builds, assert that each GL call is error-free. We don't do
+ // this on release builds because the synchronous call can stall the
+ // pipeline.
+ let supports_khr_debug = supports_extension(&extensions, "GL_KHR_debug");
+ if panic_on_gl_error || cfg!(debug_assertions) {
+ gl = gl::ErrorReactingGl::wrap(gl, move |gl, name, code| {
+ if supports_khr_debug {
+ Self::log_driver_messages(gl);
+ }
+ println!("Caught GL error {:x} at {}", code, name);
+ panic!("Caught GL error {:x} at {}", code, name);
+ });
+ }
+
+ if supports_extension(&extensions, "GL_ANGLE_provoking_vertex") {
+ gl.provoking_vertex_angle(gl::FIRST_VERTEX_CONVENTION);
+ }
+
+ let supports_texture_usage = supports_extension(&extensions, "GL_ANGLE_texture_usage");
+
+ // Our common-case image data in Firefox is BGRA, so we make an effort
+ // to use BGRA as the internal texture storage format to avoid the need
+ // to swizzle during upload. Currently we only do this on GLES (and thus
+ // for Windows, via ANGLE).
+ //
+ // On Mac, Apple docs [1] claim that BGRA is a more efficient internal
+ // format, but they don't support it with glTextureStorage. As a workaround,
+ // we pretend that it's RGBA8 for the purposes of texture transfers,
+ // but swizzle R with B for the texture sampling.
+ //
+ // We also need our internal format types to be sized, since glTexStorage*
+ // will reject non-sized internal format types.
+ //
+ // Unfortunately, with GL_EXT_texture_format_BGRA8888, BGRA8 is not a
+ // valid internal format (for glTexImage* or glTexStorage*) unless
+ // GL_EXT_texture_storage is also available [2][3], which is usually
+ // not the case on GLES 3 as the latter's functionality has been
+ // included by default but the former has not been updated.
+ // The extension is available on ANGLE, but on Android this usually
+ // means we must fall back to using unsized BGRA and glTexImage*.
+ //
+ // Overall, we have the following factors in play when choosing the formats:
+ // - with glTexStorage, the internal format needs to match the external format,
+ // or the driver would have to do the conversion, which is slow
+ // - on desktop GL, there is no BGRA internal format. However, initializing
+ // the textures with glTexImage as RGBA appears to use BGRA internally,
+ // preferring BGRA external data [4].
+ // - when glTexStorage + BGRA internal format is not supported,
+ // and the external data is BGRA, we have the following options:
+ // 1. use glTexImage with RGBA internal format, this costs us VRAM for mipmaps
+ // 2. use glTexStorage with RGBA internal format, this costs us the conversion by the driver
+ // 3. pretend we are uploading RGBA and set up the swizzling of the texture unit - this costs us batch breaks
+ //
+ // [1] https://developer.apple.com/library/archive/documentation/
+ // GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/
+ // opengl_texturedata.html#//apple_ref/doc/uid/TP40001987-CH407-SW22
+ // [2] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_format_BGRA8888.txt
+ // [3] https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_storage.txt
+ // [4] http://http.download.nvidia.com/developer/Papers/2005/Fast_Texture_Transfers/Fast_Texture_Transfers.pdf
+
+ // To support BGRA8 with glTexStorage* we specifically need
+ // GL_EXT_texture_storage and GL_EXT_texture_format_BGRA8888.
+ let supports_gles_bgra = supports_extension(&extensions, "GL_EXT_texture_format_BGRA8888");
+
+ // On the android emulator glTexImage fails to create textures larger than 3379.
+ // So we must use glTexStorage instead. See bug 1591436.
+ let is_emulator = renderer_name.starts_with("Android Emulator");
+ let avoid_tex_image = is_emulator;
+ let mut gl_version = [0; 2];
+ unsafe {
+ gl.get_integer_v(gl::MAJOR_VERSION, &mut gl_version[0..1]);
+ gl.get_integer_v(gl::MINOR_VERSION, &mut gl_version[1..2]);
+ }
+ info!("GL context {:?} {}.{}", gl.get_type(), gl_version[0], gl_version[1]);
+
+ // We block texture storage on mac because it doesn't support BGRA
+ let supports_texture_storage = allow_texture_storage_support && !cfg!(target_os = "macos") &&
+ match gl.get_type() {
+ gl::GlType::Gl => supports_extension(&extensions, "GL_ARB_texture_storage"),
+ // ES 3 technically always supports glTexStorage, but only check here for the extension
+ // necessary to interact with BGRA.
+ gl::GlType::Gles => supports_extension(&extensions, "GL_EXT_texture_storage"),
+ };
+ let supports_texture_swizzle = allow_texture_swizzling &&
+ match gl.get_type() {
+ // see https://www.g-truc.net/post-0734.html
+ gl::GlType::Gl => gl_version >= [3, 3] ||
+ supports_extension(&extensions, "GL_ARB_texture_swizzle"),
+ gl::GlType::Gles => true,
+ };
+
+ let (color_formats, bgra_formats, bgra_pixel_type, bgra8_sampling_swizzle, texture_storage_usage) = match gl.get_type() {
+ // There is `glTexStorage`, use it and expect RGBA on the input.
+ gl::GlType::Gl if supports_texture_storage && supports_texture_swizzle => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::RGBA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Bgra, // pretend it's RGBA, rely on swizzling
+ TexStorageUsage::Always
+ ),
+ // There is no `glTexStorage`, upload as `glTexImage` with BGRA input.
+ gl::GlType::Gl => (
+ TextureFormatPair { internal: ImageFormat::BGRA8, external: ImageFormat::BGRA8 },
+ TextureFormatPair { internal: gl::RGBA, external: gl::BGRA },
+ gl::UNSIGNED_INT_8_8_8_8_REV,
+ Swizzle::Rgba, // converted on uploads by the driver, no swizzling needed
+ TexStorageUsage::Never
+ ),
+ // We can use glTexStorage with BGRA8 as the internal format.
+ gl::GlType::Gles if supports_gles_bgra && supports_texture_storage => (
+ TextureFormatPair::from(ImageFormat::BGRA8),
+ TextureFormatPair { internal: gl::BGRA8_EXT, external: gl::BGRA_EXT },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba, // no conversion needed
+ TexStorageUsage::Always,
+ ),
+ // For BGRA8 textures we must use the unsized BGRA internal
+ // format and glTexImage. If texture storage is supported we can
+ // use it for other formats, which is always the case for ES 3.
+ // We can't use glTexStorage with BGRA8 as the internal format.
+ gl::GlType::Gles if supports_gles_bgra && !avoid_tex_image => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair::from(gl::BGRA_EXT),
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba, // no conversion needed
+ TexStorageUsage::NonBGRA8,
+ ),
+ // BGRA is not supported as an internal format, therefore we will
+ // use RGBA. The swizzling will happen at the texture unit.
+ gl::GlType::Gles if supports_texture_swizzle => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::RGBA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Bgra, // pretend it's RGBA, rely on swizzling
+ TexStorageUsage::Always,
+ ),
+ // BGRA and swizzling are not supported. We force the conversion done by the driver.
+ gl::GlType::Gles => (
+ TextureFormatPair::from(ImageFormat::RGBA8),
+ TextureFormatPair { internal: gl::RGBA8, external: gl::BGRA },
+ gl::UNSIGNED_BYTE,
+ Swizzle::Rgba,
+ TexStorageUsage::Always,
+ ),
+ };
+
+ let is_software_webrender = renderer_name.starts_with("Software WebRender");
+ let (depth_format, upload_method) = if is_software_webrender {
+ (gl::DEPTH_COMPONENT16, UploadMethod::Immediate)
+ } else {
+ (gl::DEPTH_COMPONENT24, upload_method)
+ };
+
+ info!("GL texture cache {:?}, bgra {:?} swizzle {:?}, texture storage {:?}, depth {:?}",
+ color_formats, bgra_formats, bgra8_sampling_swizzle, texture_storage_usage, depth_format);
+
+ // On Mali-T devices glCopyImageSubData appears to stall the pipeline until any pending
+ // renders to the source texture have completed. On Mali-G, it has been observed to
+ // indefinitely hang in some circumstances. Using an alternative such as glBlitFramebuffer
+ // is preferable on such devices, so pretend we don't support glCopyImageSubData.
+ // See bugs 1669494 and 1677757.
+ let supports_copy_image_sub_data = if renderer_name.starts_with("Mali") {
+ false
+ } else {
+ supports_extension(&extensions, "GL_EXT_copy_image") ||
+ supports_extension(&extensions, "GL_ARB_copy_image")
+ };
+
+ let is_adreno = renderer_name.starts_with("Adreno");
+
+ // There appears to be a driver bug on older versions of the Adreno
+ // driver which prevents usage of persistenly mapped buffers.
+ // See bugs 1678585 and 1683936.
+ // TODO: only disable feature for affected driver versions.
+ let supports_buffer_storage = if is_adreno {
+ false
+ } else {
+ supports_extension(&extensions, "GL_EXT_buffer_storage") ||
+ supports_extension(&extensions, "GL_ARB_buffer_storage")
+ };
+
+ // Due to a bug on Adreno devices, blitting to an fbo bound to
+ // a non-0th layer of a texture array is not supported.
+ let supports_blit_to_texture_array = !renderer_name.starts_with("Adreno");
+
+ // KHR_blend_equation_advanced renders incorrectly on Adreno
+ // devices. This has only been confirmed up to Adreno 5xx, and has been
+ // fixed for Android 9, so this condition could be made more specific.
+ let supports_advanced_blend_equation =
+ supports_extension(&extensions, "GL_KHR_blend_equation_advanced") &&
+ !is_adreno;
+
+ let supports_dual_source_blending = match gl.get_type() {
+ gl::GlType::Gl => supports_extension(&extensions,"GL_ARB_blend_func_extended") &&
+ supports_extension(&extensions,"GL_ARB_explicit_attrib_location"),
+ gl::GlType::Gles => supports_extension(&extensions,"GL_EXT_blend_func_extended"),
+ };
+
+ // Software webrender relies on the unoptimized shader source.
+ let use_optimized_shaders = use_optimized_shaders && !is_software_webrender;
+
+ // On the android emulator, glShaderSource can crash if the source
+ // strings are not null-terminated. See bug 1591945.
+ let requires_null_terminated_shader_source = is_emulator;
+
+ // On Adreno 505 and 506 we encounter crashes during glLinkProgram(), which
+ // appear to be caused by some driver-internal caching of shader strings.
+ // We attempt to workaround this by appending unique comments to each source.
+ // See bug 1609191.
+ let requires_unique_shader_source = renderer_name == "Adreno (TM) 505" || renderer_name == "Adreno (TM) 506";
+
+ // The android emulator gets confused if you don't explicitly unbind any texture
+ // from GL_TEXTURE_EXTERNAL_OES before binding another to GL_TEXTURE_2D. See bug 1636085.
+ let requires_texture_external_unbind = is_emulator;
+
+ let is_macos = cfg!(target_os = "macos");
+ // && renderer_name.starts_with("AMD");
+ // (XXX: we apply this restriction to all GPUs to handle switching)
+
+ let is_angle = renderer_name.starts_with("ANGLE");
+
+ // On certain GPUs PBO texture upload is only performed asynchronously
+ // if the stride of the data is a multiple of a certain value.
+ let optimal_pbo_stride = if is_adreno {
+ // On Adreno it must be a multiple of 64 pixels, meaning value in bytes
+ // varies with the texture format.
+ StrideAlignment::Pixels(NonZeroUsize::new(64).unwrap())
+ } else if is_macos {
+ // On AMD Mac, it must always be a multiple of 256 bytes.
+ // We apply this restriction to all GPUs to handle switching
+ StrideAlignment::Bytes(NonZeroUsize::new(256).unwrap())
+ } else if is_angle {
+ // On ANGLE, PBO texture uploads get incorrectly truncated if
+ // the stride is greater than the width * bpp.
+ StrideAlignment::Bytes(NonZeroUsize::new(1).unwrap())
+ } else {
+ // Other platforms may have similar requirements and should be added
+ // here. The default value should be 4 bytes.
+ StrideAlignment::Bytes(NonZeroUsize::new(4).unwrap())
+ };
+
+ // On AMD Macs there is a driver bug which causes some texture uploads
+ // from a non-zero offset within a PBO to fail. See bug 1603783.
+ let supports_nonzero_pbo_offsets = !is_macos;
+
+ let is_mali_g = renderer_name.starts_with("Mali-G");
+
+ // On Mali-Gxx there is a driver bug when rendering partial updates to
+ // offscreen render targets, so we must ensure we render to the entire target.
+ // See bug 1663355.
+ let supports_render_target_partial_update = !is_mali_g;
+
+ let supports_shader_storage_object = match gl.get_type() {
+ // see https://www.g-truc.net/post-0734.html
+ gl::GlType::Gl => supports_extension(&extensions, "GL_ARB_shader_storage_buffer_object"),
+ gl::GlType::Gles => gl_version >= [3, 1],
+ };
+
+ // On Mali-Gxx the driver really struggles with many small texture uploads,
+ // and handles fewer, larger uploads better.
+ let prefers_batched_texture_uploads = is_mali_g;
+
+ Device {
+ gl,
+ base_gl: None,
+ crash_annotator,
+ resource_override_path,
+ use_optimized_shaders,
+ upload_method,
+ inside_frame: false,
+
+ capabilities: Capabilities {
+ supports_multisampling: false, //TODO
+ supports_copy_image_sub_data,
+ supports_buffer_storage,
+ supports_blit_to_texture_array,
+ supports_advanced_blend_equation,
+ supports_dual_source_blending,
+ supports_khr_debug,
+ supports_texture_swizzle,
+ supports_nonzero_pbo_offsets,
+ supports_texture_usage,
+ supports_render_target_partial_update,
+ supports_shader_storage_object,
+ prefers_batched_texture_uploads,
+ renderer_name,
+ },
+
+ color_formats,
+ bgra_formats,
+ bgra_pixel_type,
+ swizzle_settings: SwizzleSettings {
+ bgra8_sampling_swizzle,
+ },
+ depth_format,
+
+ depth_targets: FastHashMap::default(),
+
+ bound_textures: [0; 16],
+ bound_program: 0,
+ bound_vao: 0,
+ bound_read_fbo: FBOId(0),
+ bound_draw_fbo: FBOId(0),
+ program_mode_id: UniformLocation::INVALID,
+ default_read_fbo: FBOId(0),
+ default_draw_fbo: FBOId(0),
+
+ depth_available: true,
+
+ max_texture_size,
+ max_texture_layers,
+ cached_programs,
+ frame_id: GpuFrameId(0),
+ extensions,
+ texture_storage_usage,
+ requires_null_terminated_shader_source,
+ requires_unique_shader_source,
+ requires_texture_external_unbind,
+ optimal_pbo_stride,
+ dump_shader_source,
+ surface_origin_is_top_left,
+
+ #[cfg(debug_assertions)]
+ shader_is_ready: false,
+ }
+ }
+
+ pub fn gl(&self) -> &dyn gl::Gl {
+ &*self.gl
+ }
+
+ pub fn rc_gl(&self) -> &Rc<dyn gl::Gl> {
+ &self.gl
+ }
+
+ /// Ensures that the maximum texture size is less than or equal to the
+ /// provided value. If the provided value is less than the value supported
+ /// by the driver, the latter is used.
+ pub fn clamp_max_texture_size(&mut self, size: i32) {
+ self.max_texture_size = self.max_texture_size.min(size);
+ }
+
+ /// Returns the limit on texture dimensions (width or height).
+ pub fn max_texture_size(&self) -> i32 {
+ self.max_texture_size
+ }
+
+ pub fn surface_origin_is_top_left(&self) -> bool {
+ self.surface_origin_is_top_left
+ }
+
+ /// Returns the limit on texture array layers.
+ pub fn max_texture_layers(&self) -> usize {
+ self.max_texture_layers as usize
+ }
+
+ pub fn get_capabilities(&self) -> &Capabilities {
+ &self.capabilities
+ }
+
+ pub fn preferred_color_formats(&self) -> TextureFormatPair<ImageFormat> {
+ self.color_formats.clone()
+ }
+
+ pub fn swizzle_settings(&self) -> Option<SwizzleSettings> {
+ if self.capabilities.supports_texture_swizzle {
+ Some(self.swizzle_settings)
+ } else {
+ None
+ }
+ }
+
+ pub fn depth_bits(&self) -> i32 {
+ match self.depth_format {
+ gl::DEPTH_COMPONENT16 => 16,
+ gl::DEPTH_COMPONENT24 => 24,
+ _ => panic!("Unknown depth format {:?}", self.depth_format),
+ }
+ }
+
+ // See gpu_types.rs where we declare the number of possible documents and
+ // number of items per document. This should match up with that.
+ pub fn max_depth_ids(&self) -> i32 {
+ return 1 << (self.depth_bits() - RESERVE_DEPTH_BITS);
+ }
+
+ pub fn ortho_near_plane(&self) -> f32 {
+ return -self.max_depth_ids() as f32;
+ }
+
+ pub fn ortho_far_plane(&self) -> f32 {
+ return (self.max_depth_ids() - 1) as f32;
+ }
+
+ pub fn optimal_pbo_stride(&self) -> StrideAlignment {
+ self.optimal_pbo_stride
+ }
+
+ pub fn reset_state(&mut self) {
+ for i in 0 .. self.bound_textures.len() {
+ self.bound_textures[i] = 0;
+ self.gl.active_texture(gl::TEXTURE0 + i as gl::GLuint);
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ }
+
+ self.bound_vao = 0;
+ self.gl.bind_vertex_array(0);
+
+ self.bound_read_fbo = self.default_read_fbo;
+ self.gl.bind_framebuffer(gl::READ_FRAMEBUFFER, self.bound_read_fbo.0);
+
+ self.bound_draw_fbo = self.default_draw_fbo;
+ self.gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, self.bound_draw_fbo.0);
+ }
+
+ #[cfg(debug_assertions)]
+ fn print_shader_errors(source: &str, log: &str) {
+ // hacky way to extract the offending lines
+ if !log.starts_with("0:") && !log.starts_with("0(") {
+ return;
+ }
+ let end_pos = match log[2..].chars().position(|c| !c.is_digit(10)) {
+ Some(pos) => 2 + pos,
+ None => return,
+ };
+ let base_line_number = match log[2 .. end_pos].parse::<usize>() {
+ Ok(number) if number >= 2 => number - 2,
+ _ => return,
+ };
+ for (line, prefix) in source.lines().skip(base_line_number).zip(&["|",">","|"]) {
+ error!("{}\t{}", prefix, line);
+ }
+ }
+
+ pub fn compile_shader(
+ &self,
+ name: &str,
+ shader_type: gl::GLenum,
+ source: &String,
+ ) -> Result<gl::GLuint, ShaderError> {
+ debug!("compile {}", name);
+ let id = self.gl.create_shader(shader_type);
+
+ let mut new_source = Cow::from(source.as_str());
+ // On some drivers we encounter crashes during glLinkProgram(), which
+ // appear to be caused by some driver-internal caching of shader strings.
+ // We attempt to workaround this by appending unique comments to each source.
+ if self.requires_unique_shader_source {
+ let uuid = Uuid::new_v4().to_hyphenated();
+ write!(new_source.to_mut(), "\n//{}\n", uuid).unwrap();
+ }
+ // Ensure the source strings we pass to glShaderSource are
+ // null-terminated on buggy platforms.
+ if self.requires_null_terminated_shader_source {
+ new_source.to_mut().push('\0');
+ }
+
+ self.gl.shader_source(id, &[new_source.as_bytes()]);
+ self.gl.compile_shader(id);
+ let log = self.gl.get_shader_info_log(id);
+ let mut status = [0];
+ unsafe {
+ self.gl.get_shader_iv(id, gl::COMPILE_STATUS, &mut status);
+ }
+ if status[0] == 0 {
+ error!("Failed to compile shader: {}\n{}", name, log);
+ #[cfg(debug_assertions)]
+ Self::print_shader_errors(source, &log);
+ Err(ShaderError::Compilation(name.to_string(), log))
+ } else {
+ if !log.is_empty() {
+ warn!("Warnings detected on shader: {}\n{}", name, log);
+ }
+ Ok(id)
+ }
+ }
+
+ pub fn begin_frame(&mut self) -> GpuFrameId {
+ debug_assert!(!self.inside_frame);
+ self.inside_frame = true;
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+
+ // If our profiler state has changed, apply or remove the profiling
+ // wrapper from our GL context.
+ let being_profiled = profiler::thread_is_being_profiled();
+ let using_wrapper = self.base_gl.is_some();
+ if being_profiled && !using_wrapper {
+ fn note(name: &str, duration: Duration) {
+ profiler::add_text_marker(cstr!("OpenGL Calls"), name, duration);
+ }
+ let threshold = Duration::from_millis(1);
+ let wrapped = gl::ProfilingGl::wrap(self.gl.clone(), threshold, note);
+ let base = mem::replace(&mut self.gl, wrapped);
+ self.base_gl = Some(base);
+ } else if !being_profiled && using_wrapper {
+ self.gl = self.base_gl.take().unwrap();
+ }
+
+ // Retrieve the currently set FBO.
+ let mut default_read_fbo = [0];
+ unsafe {
+ self.gl.get_integer_v(gl::READ_FRAMEBUFFER_BINDING, &mut default_read_fbo);
+ }
+ self.default_read_fbo = FBOId(default_read_fbo[0] as gl::GLuint);
+ let mut default_draw_fbo = [0];
+ unsafe {
+ self.gl.get_integer_v(gl::DRAW_FRAMEBUFFER_BINDING, &mut default_draw_fbo);
+ }
+ self.default_draw_fbo = FBOId(default_draw_fbo[0] as gl::GLuint);
+
+ // Shader state
+ self.bound_program = 0;
+ self.program_mode_id = UniformLocation::INVALID;
+ self.gl.use_program(0);
+
+ // Reset common state
+ self.reset_state();
+
+ // Pixel op state
+ self.gl.pixel_store_i(gl::UNPACK_ALIGNMENT, 1);
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ // Default is sampler 0, always
+ self.gl.active_texture(gl::TEXTURE0);
+
+ self.frame_id
+ }
+
+ fn bind_texture_impl(
+ &mut self, slot: TextureSlot, id: gl::GLuint, target: gl::GLenum, set_swizzle: Option<Swizzle>
+ ) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_textures[slot.0] != id || set_swizzle.is_some() {
+ self.gl.active_texture(gl::TEXTURE0 + slot.0 as gl::GLuint);
+ // The android emulator gets confused if you don't explicitly unbind any texture
+ // from GL_TEXTURE_EXTERNAL_OES before binding to GL_TEXTURE_2D. See bug 1636085.
+ if target == gl::TEXTURE_2D && self.requires_texture_external_unbind {
+ self.gl.bind_texture(gl::TEXTURE_EXTERNAL_OES, 0);
+ }
+ self.gl.bind_texture(target, id);
+ if let Some(swizzle) = set_swizzle {
+ if self.capabilities.supports_texture_swizzle {
+ let components = match swizzle {
+ Swizzle::Rgba => [gl::RED, gl::GREEN, gl::BLUE, gl::ALPHA],
+ Swizzle::Bgra => [gl::BLUE, gl::GREEN, gl::RED, gl::ALPHA],
+ };
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_R, components[0] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_G, components[1] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_B, components[2] as i32);
+ self.gl.tex_parameter_i(target, gl::TEXTURE_SWIZZLE_A, components[3] as i32);
+ } else {
+ debug_assert_eq!(swizzle, Swizzle::default());
+ }
+ }
+ self.gl.active_texture(gl::TEXTURE0);
+ self.bound_textures[slot.0] = id;
+ }
+ }
+
+ pub fn bind_texture<S>(&mut self, slot: S, texture: &Texture, swizzle: Swizzle)
+ where
+ S: Into<TextureSlot>,
+ {
+ let old_swizzle = texture.active_swizzle.replace(swizzle);
+ let set_swizzle = if old_swizzle != swizzle {
+ Some(swizzle)
+ } else {
+ None
+ };
+ self.bind_texture_impl(slot.into(), texture.id, texture.target, set_swizzle);
+ }
+
+ pub fn bind_external_texture<S>(&mut self, slot: S, external_texture: &ExternalTexture)
+ where
+ S: Into<TextureSlot>,
+ {
+ self.bind_texture_impl(slot.into(), external_texture.id, external_texture.target, None);
+ }
+
+ pub fn bind_read_target_impl(&mut self, fbo_id: FBOId) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_read_fbo != fbo_id {
+ self.bound_read_fbo = fbo_id;
+ fbo_id.bind(self.gl(), FBOTarget::Read);
+ }
+ }
+
+ pub fn bind_read_target(&mut self, target: ReadTarget) {
+ let fbo_id = match target {
+ ReadTarget::Default => self.default_read_fbo,
+ ReadTarget::Texture { fbo_id } => fbo_id,
+ ReadTarget::External { fbo } => fbo,
+ };
+
+ self.bind_read_target_impl(fbo_id)
+ }
+
+ fn bind_draw_target_impl(&mut self, fbo_id: FBOId) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_draw_fbo != fbo_id {
+ self.bound_draw_fbo = fbo_id;
+ fbo_id.bind(self.gl(), FBOTarget::Draw);
+ }
+ }
+
+ pub fn reset_read_target(&mut self) {
+ let fbo = self.default_read_fbo;
+ self.bind_read_target_impl(fbo);
+ }
+
+
+ pub fn reset_draw_target(&mut self) {
+ let fbo = self.default_draw_fbo;
+ self.bind_draw_target_impl(fbo);
+ self.depth_available = true;
+ }
+
+ pub fn bind_draw_target(
+ &mut self,
+ target: DrawTarget,
+ ) {
+ let (fbo_id, rect, depth_available) = match target {
+ DrawTarget::Default { rect, .. } => {
+ (self.default_draw_fbo, rect, true)
+ }
+ DrawTarget::Texture { dimensions, fbo_id, with_depth, .. } => {
+ let rect = FramebufferIntRect::new(
+ FramebufferIntPoint::zero(),
+ device_size_as_framebuffer_size(dimensions),
+ );
+ (fbo_id, rect, with_depth)
+ },
+ DrawTarget::External { fbo, size } => {
+ (fbo, size.into(), false)
+ }
+ DrawTarget::NativeSurface { external_fbo_id, offset, dimensions, .. } => {
+ (
+ FBOId(external_fbo_id),
+ device_rect_as_framebuffer_rect(&DeviceIntRect::new(offset, dimensions)),
+ true
+ )
+ }
+ };
+
+ self.depth_available = depth_available;
+ self.bind_draw_target_impl(fbo_id);
+ self.gl.viewport(
+ rect.origin.x,
+ rect.origin.y,
+ rect.size.width,
+ rect.size.height,
+ );
+ }
+
+ /// Creates an unbound FBO object. Additional attachment API calls are
+ /// required to make it complete.
+ pub fn create_fbo(&mut self) -> FBOId {
+ FBOId(self.gl.gen_framebuffers(1)[0])
+ }
+
+ /// Creates an FBO with the given texture bound as the color attachment.
+ pub fn create_fbo_for_external_texture(&mut self, texture_id: u32) -> FBOId {
+ let fbo = self.create_fbo();
+ fbo.bind(self.gl(), FBOTarget::Draw);
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::TEXTURE_2D,
+ texture_id,
+ 0,
+ );
+ debug_assert_eq!(
+ self.gl.check_frame_buffer_status(gl::DRAW_FRAMEBUFFER),
+ gl::FRAMEBUFFER_COMPLETE,
+ "Incomplete framebuffer",
+ );
+ self.bound_draw_fbo.bind(self.gl(), FBOTarget::Draw);
+ fbo
+ }
+
+ pub fn delete_fbo(&mut self, fbo: FBOId) {
+ self.gl.delete_framebuffers(&[fbo.0]);
+ }
+
+ pub fn bind_external_draw_target(&mut self, fbo_id: FBOId) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_draw_fbo != fbo_id {
+ self.bound_draw_fbo = fbo_id;
+ fbo_id.bind(self.gl(), FBOTarget::Draw);
+ }
+ }
+
+ /// Link a program, attaching the supplied vertex format.
+ ///
+ /// If `create_program()` finds a binary shader on disk, it will kick
+ /// off linking immediately, which some drivers (notably ANGLE) run
+ /// in parallel on background threads. As such, this function should
+ /// ideally be run sometime later, to give the driver time to do that
+ /// before blocking due to an API call accessing the shader.
+ ///
+ /// This generally means that the first run of the application will have
+ /// to do a bunch of blocking work to compile the shader from source, but
+ /// subsequent runs should load quickly.
+ pub fn link_program(
+ &mut self,
+ program: &mut Program,
+ descriptor: &VertexDescriptor,
+ ) -> Result<(), ShaderError> {
+ let _guard = CrashAnnotatorGuard::new(
+ &self.crash_annotator,
+ CrashAnnotation::CompileShader,
+ &program.source_info.full_name()
+ );
+
+ assert!(!program.is_initialized());
+ let mut build_program = true;
+ let info = &program.source_info;
+
+ // See if we hit the binary shader cache
+ if let Some(ref cached_programs) = self.cached_programs {
+ // If the shader is not in the cache, attempt to load it from disk
+ if cached_programs.entries.borrow().get(&program.source_info.digest).is_none() {
+ if let Some(ref handler) = cached_programs.program_cache_handler {
+ handler.try_load_shader_from_disk(&program.source_info.digest, cached_programs);
+ if let Some(entry) = cached_programs.entries.borrow().get(&program.source_info.digest) {
+ self.gl.program_binary(program.id, entry.binary.format, &entry.binary.bytes);
+ }
+ }
+ }
+
+ if let Some(entry) = cached_programs.entries.borrow_mut().get_mut(&info.digest) {
+ let mut link_status = [0];
+ unsafe {
+ self.gl.get_program_iv(program.id, gl::LINK_STATUS, &mut link_status);
+ }
+ if link_status[0] == 0 {
+ let error_log = self.gl.get_program_info_log(program.id);
+ error!(
+ "Failed to load a program object with a program binary: {} renderer {}\n{}",
+ &info.base_filename,
+ self.capabilities.renderer_name,
+ error_log
+ );
+ if let Some(ref program_cache_handler) = cached_programs.program_cache_handler {
+ program_cache_handler.notify_program_binary_failed(&entry.binary);
+ }
+ } else {
+ entry.linked = true;
+ build_program = false;
+ }
+ }
+ }
+
+ // If not, we need to do a normal compile + link pass.
+ if build_program {
+ // Compile the vertex shader
+ let vs_source = info.compute_source(self, ShaderKind::Vertex);
+ let vs_id = match self.compile_shader(&info.base_filename, gl::VERTEX_SHADER, &vs_source) {
+ Ok(vs_id) => vs_id,
+ Err(err) => return Err(err),
+ };
+
+ // Compile the fragment shader
+ let fs_source = info.compute_source(self, ShaderKind::Fragment);
+ let fs_id =
+ match self.compile_shader(&info.base_filename, gl::FRAGMENT_SHADER, &fs_source) {
+ Ok(fs_id) => fs_id,
+ Err(err) => {
+ self.gl.delete_shader(vs_id);
+ return Err(err);
+ }
+ };
+
+ // Check if shader source should be dumped
+ if Some(info.base_filename) == self.dump_shader_source.as_ref().map(String::as_ref) {
+ let path = std::path::Path::new(info.base_filename);
+ std::fs::write(path.with_extension("vert"), vs_source).unwrap();
+ std::fs::write(path.with_extension("frag"), fs_source).unwrap();
+ }
+
+ // Attach shaders
+ self.gl.attach_shader(program.id, vs_id);
+ self.gl.attach_shader(program.id, fs_id);
+
+ // Bind vertex attributes
+ for (i, attr) in descriptor
+ .vertex_attributes
+ .iter()
+ .chain(descriptor.instance_attributes.iter())
+ .enumerate()
+ {
+ self.gl
+ .bind_attrib_location(program.id, i as gl::GLuint, attr.name);
+ }
+
+ if self.cached_programs.is_some() {
+ self.gl.program_parameter_i(program.id, gl::PROGRAM_BINARY_RETRIEVABLE_HINT, gl::TRUE as gl::GLint);
+ }
+
+ // Link!
+ self.gl.link_program(program.id);
+
+ if cfg!(debug_assertions) {
+ // Check that all our overrides worked
+ for (i, attr) in descriptor
+ .vertex_attributes
+ .iter()
+ .chain(descriptor.instance_attributes.iter())
+ .enumerate()
+ {
+ //Note: we can't assert here because the driver may optimize out some of the
+ // vertex attributes legitimately, returning their location to be -1.
+ let location = self.gl.get_attrib_location(program.id, attr.name);
+ if location != i as gl::GLint {
+ warn!("Attribute {:?} is not found in the shader {}. Expected at {}, found at {}",
+ attr, program.source_info.base_filename, i, location);
+ }
+ }
+ }
+
+ // GL recommends detaching and deleting shaders once the link
+ // is complete (whether successful or not). This allows the driver
+ // to free any memory associated with the parsing and compilation.
+ self.gl.detach_shader(program.id, vs_id);
+ self.gl.detach_shader(program.id, fs_id);
+ self.gl.delete_shader(vs_id);
+ self.gl.delete_shader(fs_id);
+
+ let mut link_status = [0];
+ unsafe {
+ self.gl.get_program_iv(program.id, gl::LINK_STATUS, &mut link_status);
+ }
+ if link_status[0] == 0 {
+ let error_log = self.gl.get_program_info_log(program.id);
+ error!(
+ "Failed to link shader program: {}\n{}",
+ &info.base_filename,
+ error_log
+ );
+ self.gl.delete_program(program.id);
+ return Err(ShaderError::Link(info.base_filename.to_owned(), error_log));
+ }
+
+ if let Some(ref cached_programs) = self.cached_programs {
+ if !cached_programs.entries.borrow().contains_key(&info.digest) {
+ let (buffer, format) = self.gl.get_program_binary(program.id);
+ if buffer.len() > 0 {
+ let binary = Arc::new(ProgramBinary::new(buffer, format, info.digest.clone()));
+ cached_programs.add_new_program_binary(binary);
+ }
+ }
+ }
+ }
+
+ // If we get here, the link succeeded, so get the uniforms.
+ program.is_initialized = true;
+ program.u_transform = self.gl.get_uniform_location(program.id, "uTransform");
+ program.u_mode = self.gl.get_uniform_location(program.id, "uMode");
+
+ Ok(())
+ }
+
+ pub fn bind_program(&mut self, program: &Program) -> bool {
+ debug_assert!(self.inside_frame);
+ debug_assert!(program.is_initialized());
+ if !program.is_initialized() {
+ return false;
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = true;
+ }
+
+ if self.bound_program != program.id {
+ self.gl.use_program(program.id);
+ self.bound_program = program.id;
+ self.program_mode_id = UniformLocation(program.u_mode);
+ }
+ true
+ }
+
+ pub fn create_texture(
+ &mut self,
+ target: ImageBufferKind,
+ format: ImageFormat,
+ mut width: i32,
+ mut height: i32,
+ filter: TextureFilter,
+ render_target: Option<RenderTargetInfo>,
+ layer_count: i32,
+ ) -> Texture {
+ debug_assert!(self.inside_frame);
+
+ if width > self.max_texture_size || height > self.max_texture_size {
+ error!("Attempting to allocate a texture of size {}x{} above the limit, trimming", width, height);
+ width = width.min(self.max_texture_size);
+ height = height.min(self.max_texture_size);
+ }
+
+ // Set up the texture book-keeping.
+ let mut texture = Texture {
+ id: self.gl.gen_textures(1)[0],
+ target: get_gl_target(target),
+ size: DeviceIntSize::new(width, height),
+ layer_count,
+ format,
+ filter,
+ active_swizzle: Cell::default(),
+ fbos: vec![],
+ fbos_with_depth: vec![],
+ blit_workaround_buffer: None,
+ last_frame_used: self.frame_id,
+ flags: TextureFlags::default(),
+ };
+ self.bind_texture(DEFAULT_TEXTURE, &texture, Swizzle::default());
+ self.set_texture_parameters(texture.target, filter);
+
+ if self.capabilities.supports_texture_usage && render_target.is_some() {
+ self.gl.tex_parameter_i(texture.target, gl::TEXTURE_USAGE_ANGLE, gl::FRAMEBUFFER_ATTACHMENT_ANGLE as gl::GLint);
+ }
+
+ // Allocate storage.
+ let desc = self.gl_describe_format(texture.format);
+ let is_array = match texture.target {
+ gl::TEXTURE_2D_ARRAY => true,
+ gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES => false,
+ _ => panic!("BUG: Unexpected texture target!"),
+ };
+ assert!(is_array || texture.layer_count == 1);
+
+ // Firefox doesn't use mipmaps, but Servo uses them for standalone image
+ // textures images larger than 512 pixels. This is the only case where
+ // we set the filter to trilinear.
+ let mipmap_levels = if texture.filter == TextureFilter::Trilinear {
+ let max_dimension = cmp::max(width, height);
+ ((max_dimension) as f64).log2() as gl::GLint + 1
+ } else {
+ 1
+ };
+
+ // We never want to upload texture data at the same time as allocating the texture.
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ // Use glTexStorage where available, since it avoids allocating
+ // unnecessary mipmap storage and generally improves performance with
+ // stronger invariants.
+ let use_texture_storage = match self.texture_storage_usage {
+ TexStorageUsage::Always => true,
+ TexStorageUsage::NonBGRA8 => texture.format != ImageFormat::BGRA8,
+ TexStorageUsage::Never => false,
+ };
+ match (use_texture_storage, is_array) {
+ (true, true) =>
+ self.gl.tex_storage_3d(
+ gl::TEXTURE_2D_ARRAY,
+ mipmap_levels,
+ desc.internal,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ texture.layer_count,
+ ),
+ (true, false) =>
+ self.gl.tex_storage_2d(
+ texture.target,
+ mipmap_levels,
+ desc.internal,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ ),
+ (false, true) =>
+ self.gl.tex_image_3d(
+ gl::TEXTURE_2D_ARRAY,
+ 0,
+ desc.internal as gl::GLint,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ texture.layer_count,
+ 0,
+ desc.external,
+ desc.pixel_type,
+ None,
+ ),
+ (false, false) =>
+ self.gl.tex_image_2d(
+ texture.target,
+ 0,
+ desc.internal as gl::GLint,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ 0,
+ desc.external,
+ desc.pixel_type,
+ None,
+ ),
+ }
+
+ // Set up FBOs, if required.
+ if let Some(rt_info) = render_target {
+ self.init_fbos(&mut texture, false);
+ if rt_info.has_depth {
+ self.init_fbos(&mut texture, true);
+ }
+ }
+
+ // Set up intermediate buffer for blitting to texture, if required.
+ if texture.layer_count > 1 && !self.capabilities.supports_blit_to_texture_array {
+ let rbo = RBOId(self.gl.gen_renderbuffers(1)[0]);
+ let fbo = FBOId(self.gl.gen_framebuffers(1)[0]);
+ self.gl.bind_renderbuffer(gl::RENDERBUFFER, rbo.0);
+ self.gl.renderbuffer_storage(
+ gl::RENDERBUFFER,
+ self.matching_renderbuffer_format(texture.format),
+ texture.size.width as _,
+ texture.size.height as _
+ );
+
+ self.bind_draw_target_impl(fbo);
+ self.gl.framebuffer_renderbuffer(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::RENDERBUFFER,
+ rbo.0
+ );
+ texture.blit_workaround_buffer = Some((rbo, fbo));
+ }
+
+ texture
+ }
+
+ fn set_texture_parameters(&mut self, target: gl::GLuint, filter: TextureFilter) {
+ let mag_filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
+ };
+
+ let min_filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear => gl::LINEAR,
+ TextureFilter::Trilinear => gl::LINEAR_MIPMAP_LINEAR,
+ };
+
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_MAG_FILTER, mag_filter as gl::GLint);
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_MIN_FILTER, min_filter as gl::GLint);
+
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
+ self.gl
+ .tex_parameter_i(target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
+ }
+
+ /// Copies the entire contents of one texture to another. The dest texture must be at least
+ /// as large as the source texture in each dimension. No scaling is performed, so if the dest
+ /// texture is larger than the source texture then some of its pixels will not be written to.
+ pub fn copy_entire_texture(
+ &mut self,
+ dst: &mut Texture,
+ src: &Texture,
+ ) {
+ debug_assert!(self.inside_frame);
+ debug_assert!(dst.size.width >= src.size.width);
+ debug_assert!(dst.size.height >= src.size.height);
+ debug_assert!(dst.layer_count >= src.layer_count);
+
+ self.copy_texture_sub_region(
+ src,
+ 0,
+ 0,
+ 0,
+ dst,
+ 0,
+ 0,
+ 0,
+ src.size.width as _,
+ src.size.height as _,
+ src.layer_count as _,
+ );
+ }
+
+ /// Copies the specified subregion from src_texture to dest_texture.
+ pub fn copy_texture_sub_region(
+ &mut self,
+ src_texture: &Texture,
+ src_x: usize,
+ src_y: usize,
+ src_z: LayerIndex,
+ dest_texture: &Texture,
+ dest_x: usize,
+ dest_y: usize,
+ dest_z: LayerIndex,
+ width: usize,
+ height: usize,
+ depth: LayerIndex,
+ ) {
+ if self.capabilities.supports_copy_image_sub_data {
+ assert_ne!(
+ src_texture.id, dest_texture.id,
+ "glCopyImageSubData's behaviour is undefined if src and dst images are identical and the rectangles overlap."
+ );
+ unsafe {
+ self.gl.copy_image_sub_data(
+ src_texture.id,
+ src_texture.target,
+ 0,
+ src_x as _,
+ src_y as _,
+ src_z as _,
+ dest_texture.id,
+ dest_texture.target,
+ 0,
+ dest_x as _,
+ dest_y as _,
+ dest_z as _,
+ width as _,
+ height as _,
+ depth as _,
+ );
+ }
+ } else {
+ for i in 0..depth as LayerIndex {
+ let src_offset = FramebufferIntPoint::new(src_x as i32, src_y as i32);
+ let dest_offset = FramebufferIntPoint::new(dest_x as i32, dest_y as i32);
+ let size = FramebufferIntSize::new(width as i32, height as i32);
+
+ self.blit_render_target(
+ ReadTarget::from_texture(src_texture, src_z + i),
+ FramebufferIntRect::new(src_offset, size),
+ DrawTarget::from_texture(dest_texture, dest_z + i, false),
+ FramebufferIntRect::new(dest_offset, size),
+ // In most cases the filter shouldn't matter, as there is no scaling involved
+ // in the blit. We were previously using Linear, but this caused issues when
+ // blitting RGBAF32 textures on Mali, so use Nearest to be safe.
+ TextureFilter::Nearest,
+ );
+ }
+ }
+ }
+
+ /// Notifies the device that the contents of a render target are no longer
+ /// needed.
+ pub fn invalidate_render_target(&mut self, texture: &Texture) {
+ let (fbos, attachments) = if texture.supports_depth() {
+ (&texture.fbos_with_depth,
+ &[gl::COLOR_ATTACHMENT0, gl::DEPTH_ATTACHMENT] as &[gl::GLenum])
+ } else {
+ (&texture.fbos, &[gl::COLOR_ATTACHMENT0] as &[gl::GLenum])
+ };
+
+ let original_bound_fbo = self.bound_draw_fbo;
+ for fbo_id in fbos.iter() {
+ // Note: The invalidate extension may not be supported, in which
+ // case this is a no-op. That's ok though, because it's just a
+ // hint.
+ self.bind_external_draw_target(*fbo_id);
+ self.gl.invalidate_framebuffer(gl::FRAMEBUFFER, attachments);
+ }
+ self.bind_external_draw_target(original_bound_fbo);
+ }
+
+ /// Notifies the device that the contents of the current framebuffer's depth
+ /// attachment is no longer needed. Unlike invalidate_render_target, this can
+ /// be called even when the contents of the colour attachment is still required.
+ /// This should be called before unbinding the framebuffer at the end of a pass,
+ /// to allow tiled GPUs to avoid writing the contents back to memory.
+ pub fn invalidate_depth_target(&mut self) {
+ assert!(self.depth_available);
+ self.gl.invalidate_framebuffer(gl::DRAW_FRAMEBUFFER, &[gl::DEPTH_ATTACHMENT]);
+ }
+
+ /// Notifies the device that a render target is about to be reused.
+ ///
+ /// This method adds or removes a depth target as necessary.
+ pub fn reuse_render_target<T: Texel>(
+ &mut self,
+ texture: &mut Texture,
+ rt_info: RenderTargetInfo,
+ ) {
+ texture.last_frame_used = self.frame_id;
+
+ // Add depth support if needed.
+ if rt_info.has_depth && !texture.supports_depth() {
+ self.init_fbos(texture, true);
+ }
+ }
+
+ fn init_fbos(&mut self, texture: &mut Texture, with_depth: bool) {
+ let (fbos, depth_rb) = if with_depth {
+ let depth_target = self.acquire_depth_target(texture.get_dimensions());
+ (&mut texture.fbos_with_depth, Some(depth_target))
+ } else {
+ (&mut texture.fbos, None)
+ };
+
+ // Generate the FBOs.
+ assert!(fbos.is_empty());
+ fbos.extend(self.gl.gen_framebuffers(texture.layer_count).into_iter().map(FBOId));
+
+ // Bind the FBOs.
+ let original_bound_fbo = self.bound_draw_fbo;
+ for (fbo_index, &fbo_id) in fbos.iter().enumerate() {
+ self.bind_external_draw_target(fbo_id);
+ match texture.target {
+ gl::TEXTURE_2D_ARRAY => {
+ self.gl.framebuffer_texture_layer(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ texture.id,
+ 0,
+ fbo_index as _,
+ )
+ }
+ _ => {
+ assert_eq!(fbo_index, 0);
+ self.gl.framebuffer_texture_2d(
+ gl::DRAW_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ texture.target,
+ texture.id,
+ 0,
+ )
+ }
+ }
+
+ if let Some(depth_rb) = depth_rb {
+ self.gl.framebuffer_renderbuffer(
+ gl::DRAW_FRAMEBUFFER,
+ gl::DEPTH_ATTACHMENT,
+ gl::RENDERBUFFER,
+ depth_rb.0,
+ );
+ }
+
+ debug_assert_eq!(
+ self.gl.check_frame_buffer_status(gl::DRAW_FRAMEBUFFER),
+ gl::FRAMEBUFFER_COMPLETE,
+ "Incomplete framebuffer",
+ );
+ }
+ self.bind_external_draw_target(original_bound_fbo);
+ }
+
+ fn deinit_fbos(&mut self, fbos: &mut Vec<FBOId>) {
+ if !fbos.is_empty() {
+ let fbo_ids: SmallVec<[gl::GLuint; 8]> = fbos
+ .drain(..)
+ .map(|FBOId(fbo_id)| fbo_id)
+ .collect();
+ self.gl.delete_framebuffers(&fbo_ids[..]);
+ }
+ }
+
+ fn acquire_depth_target(&mut self, dimensions: DeviceIntSize) -> RBOId {
+ let gl = &self.gl;
+ let depth_format = self.depth_format;
+ let target = self.depth_targets.entry(dimensions).or_insert_with(|| {
+ let renderbuffer_ids = gl.gen_renderbuffers(1);
+ let depth_rb = renderbuffer_ids[0];
+ gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+ gl.renderbuffer_storage(
+ gl::RENDERBUFFER,
+ depth_format,
+ dimensions.width as _,
+ dimensions.height as _,
+ );
+ SharedDepthTarget {
+ rbo_id: RBOId(depth_rb),
+ refcount: 0,
+ }
+ });
+ target.refcount += 1;
+ target.rbo_id
+ }
+
+ fn release_depth_target(&mut self, dimensions: DeviceIntSize) {
+ let mut entry = match self.depth_targets.entry(dimensions) {
+ Entry::Occupied(x) => x,
+ Entry::Vacant(..) => panic!("Releasing unknown depth target"),
+ };
+ debug_assert!(entry.get().refcount != 0);
+ entry.get_mut().refcount -= 1;
+ if entry.get().refcount == 0 {
+ let (_, target) = entry.remove_entry();
+ self.gl.delete_renderbuffers(&[target.rbo_id.0]);
+ }
+ }
+
+ /// Perform a blit between self.bound_read_fbo and self.bound_draw_fbo.
+ fn blit_render_target_impl(
+ &mut self,
+ src_rect: FramebufferIntRect,
+ dest_rect: FramebufferIntRect,
+ filter: TextureFilter,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ let filter = match filter {
+ TextureFilter::Nearest => gl::NEAREST,
+ TextureFilter::Linear | TextureFilter::Trilinear => gl::LINEAR,
+ };
+
+ self.gl.blit_framebuffer(
+ src_rect.origin.x,
+ src_rect.origin.y,
+ src_rect.origin.x + src_rect.size.width,
+ src_rect.origin.y + src_rect.size.height,
+ dest_rect.origin.x,
+ dest_rect.origin.y,
+ dest_rect.origin.x + dest_rect.size.width,
+ dest_rect.origin.y + dest_rect.size.height,
+ gl::COLOR_BUFFER_BIT,
+ filter,
+ );
+ }
+
+ /// Perform a blit between src_target and dest_target.
+ /// This will overwrite self.bound_read_fbo and self.bound_draw_fbo.
+ pub fn blit_render_target(
+ &mut self,
+ src_target: ReadTarget,
+ src_rect: FramebufferIntRect,
+ dest_target: DrawTarget,
+ dest_rect: FramebufferIntRect,
+ filter: TextureFilter,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ self.bind_read_target(src_target);
+
+ match dest_target {
+ DrawTarget::Texture { layer, blit_workaround_buffer, dimensions, id, target, .. } if layer != 0 &&
+ !self.capabilities.supports_blit_to_texture_array =>
+ {
+ // This should have been initialized in create_texture().
+ let (_rbo, fbo) = blit_workaround_buffer.expect("Blit workaround buffer has not been initialized.");
+
+ // Blit from read target to intermediate buffer.
+ self.bind_draw_target_impl(fbo);
+ self.blit_render_target_impl(
+ src_rect,
+ dest_rect,
+ filter
+ );
+
+ // dest_rect may be inverted, so min_x/y() might actually be the
+ // bottom-right, max_x/y() might actually be the top-left,
+ // and width/height might be negative. See servo/euclid#321.
+ // Calculate the non-inverted rect here.
+ let dest_bounds = DeviceIntRect::new(
+ DeviceIntPoint::new(
+ dest_rect.min_x().min(dest_rect.max_x()),
+ dest_rect.min_y().min(dest_rect.max_y()),
+ ),
+ DeviceIntSize::new(
+ dest_rect.size.width.abs(),
+ dest_rect.size.height.abs(),
+ ),
+ ).intersection(&dimensions.into()).unwrap_or_else(DeviceIntRect::zero);
+
+ self.bind_read_target_impl(fbo);
+ self.bind_texture_impl(
+ DEFAULT_TEXTURE,
+ id,
+ target,
+ None, // not depending on swizzle
+ );
+
+ // Copy from intermediate buffer to the texture layer.
+ self.gl.copy_tex_sub_image_3d(
+ target, 0,
+ dest_bounds.origin.x, dest_bounds.origin.y,
+ layer as _,
+ dest_bounds.origin.x, dest_bounds.origin.y,
+ dest_bounds.size.width, dest_bounds.size.height,
+ );
+
+ }
+ _ => {
+ self.bind_draw_target(dest_target);
+
+ self.blit_render_target_impl(src_rect, dest_rect, filter);
+ }
+ }
+ }
+
+ /// Performs a blit while flipping vertically. Useful for blitting textures
+ /// (which use origin-bottom-left) to the main framebuffer (which uses
+ /// origin-top-left).
+ pub fn blit_render_target_invert_y(
+ &mut self,
+ src_target: ReadTarget,
+ src_rect: FramebufferIntRect,
+ dest_target: DrawTarget,
+ dest_rect: FramebufferIntRect,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ let mut inverted_dest_rect = dest_rect;
+ inverted_dest_rect.origin.y = dest_rect.max_y();
+ inverted_dest_rect.size.height *= -1;
+
+ self.blit_render_target(
+ src_target,
+ src_rect,
+ dest_target,
+ inverted_dest_rect,
+ TextureFilter::Linear,
+ );
+ }
+
+ pub fn delete_texture(&mut self, mut texture: Texture) {
+ debug_assert!(self.inside_frame);
+ let had_depth = texture.supports_depth();
+ self.deinit_fbos(&mut texture.fbos);
+ self.deinit_fbos(&mut texture.fbos_with_depth);
+ if had_depth {
+ self.release_depth_target(texture.get_dimensions());
+ }
+ if let Some((rbo, fbo)) = texture.blit_workaround_buffer {
+ self.gl.delete_framebuffers(&[fbo.0]);
+ self.gl.delete_renderbuffers(&[rbo.0]);
+ }
+
+ self.gl.delete_textures(&[texture.id]);
+
+ for bound_texture in &mut self.bound_textures {
+ if *bound_texture == texture.id {
+ *bound_texture = 0;
+ }
+ }
+
+ // Disarm the assert in Texture::drop().
+ texture.id = 0;
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn delete_external_texture(&mut self, mut external: ExternalTexture) {
+ self.gl.delete_textures(&[external.id]);
+ external.id = 0;
+ }
+
+ pub fn delete_program(&mut self, mut program: Program) {
+ self.gl.delete_program(program.id);
+ program.id = 0;
+ }
+
+ /// Create a shader program and link it immediately.
+ pub fn create_program_linked(
+ &mut self,
+ base_filename: &'static str,
+ features: &[&'static str],
+ descriptor: &VertexDescriptor,
+ ) -> Result<Program, ShaderError> {
+ let mut program = self.create_program(base_filename, features)?;
+ self.link_program(&mut program, descriptor)?;
+ Ok(program)
+ }
+
+ /// Create a shader program. This does minimal amount of work to start
+ /// loading a binary shader. If a binary shader is found, we invoke
+ /// glProgramBinary, which, at least on ANGLE, will load and link the
+ /// binary on a background thread. This can speed things up later when
+ /// we invoke `link_program()`.
+ pub fn create_program(
+ &mut self,
+ base_filename: &'static str,
+ features: &[&'static str],
+ ) -> Result<Program, ShaderError> {
+ debug_assert!(self.inside_frame);
+
+ let source_info = ProgramSourceInfo::new(self, base_filename, features);
+
+ // Create program
+ let pid = self.gl.create_program();
+
+ // Attempt to load a cached binary if possible.
+ if let Some(ref cached_programs) = self.cached_programs {
+ if let Some(entry) = cached_programs.entries.borrow().get(&source_info.digest) {
+ self.gl.program_binary(pid, entry.binary.format, &entry.binary.bytes);
+ }
+ }
+
+ // Use 0 for the uniforms as they are initialized by link_program.
+ let program = Program {
+ id: pid,
+ u_transform: 0,
+ u_mode: 0,
+ source_info,
+ is_initialized: false,
+ };
+
+ Ok(program)
+ }
+
+ fn build_shader_string<F: FnMut(&str)>(
+ &self,
+ features: &[&'static str],
+ kind: ShaderKind,
+ base_filename: &str,
+ output: F,
+ ) {
+ do_build_shader_string(
+ get_shader_version(&*self.gl),
+ features,
+ kind,
+ base_filename,
+ &|f| get_unoptimized_shader_source(f, self.resource_override_path.as_ref()),
+ output,
+ )
+ }
+
+ pub fn bind_shader_samplers<S>(&mut self, program: &Program, bindings: &[(&'static str, S)])
+ where
+ S: Into<TextureSlot> + Copy,
+ {
+ // bind_program() must be called before calling bind_shader_samplers
+ assert_eq!(self.bound_program, program.id);
+
+ for binding in bindings {
+ let u_location = self.gl.get_uniform_location(program.id, binding.0);
+ if u_location != -1 {
+ self.bind_program(program);
+ self.gl
+ .uniform_1i(u_location, binding.1.into().0 as gl::GLint);
+ }
+ }
+ }
+
+ pub fn get_uniform_location(&self, program: &Program, name: &str) -> UniformLocation {
+ UniformLocation(self.gl.get_uniform_location(program.id, name))
+ }
+
+ pub fn set_uniforms(
+ &self,
+ program: &Program,
+ transform: &Transform3D<f32>,
+ ) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl
+ .uniform_matrix_4fv(program.u_transform, false, &transform.to_array());
+ }
+
+ pub fn switch_mode(&self, mode: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.uniform_1i(self.program_mode_id.0, mode);
+ }
+
+ pub fn create_pbo(&mut self) -> PBO {
+ let id = self.gl.gen_buffers(1)[0];
+ PBO {
+ id,
+ reserved_size: 0,
+ }
+ }
+
+ pub fn create_pbo_with_size(&mut self, size: usize) -> PBO {
+ let mut pbo = self.create_pbo();
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+ self.gl.pixel_store_i(gl::PACK_ALIGNMENT, 1);
+ self.gl.buffer_data_untyped(
+ gl::PIXEL_PACK_BUFFER,
+ size as _,
+ ptr::null(),
+ gl::STREAM_READ,
+ );
+ self.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+
+ pbo.reserved_size = size;
+ pbo
+ }
+
+ pub fn read_pixels_into_pbo(
+ &mut self,
+ read_target: ReadTarget,
+ rect: DeviceIntRect,
+ format: ImageFormat,
+ pbo: &PBO,
+ ) {
+ let byte_size = rect.size.area() as usize * format.bytes_per_pixel() as usize;
+
+ assert!(byte_size <= pbo.reserved_size);
+
+ self.bind_read_target(read_target);
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+ self.gl.pixel_store_i(gl::PACK_ALIGNMENT, 1);
+
+ let gl_format = self.gl_describe_format(format);
+
+ unsafe {
+ self.gl.read_pixels_into_pbo(
+ rect.origin.x as _,
+ rect.origin.y as _,
+ rect.size.width as _,
+ rect.size.height as _,
+ gl_format.read,
+ gl_format.pixel_type,
+ );
+ }
+
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, 0);
+ }
+
+ pub fn map_pbo_for_readback<'a>(&'a mut self, pbo: &'a PBO) -> Option<BoundPBO<'a>> {
+ self.gl.bind_buffer(gl::PIXEL_PACK_BUFFER, pbo.id);
+
+ let buf_ptr = match self.gl.get_type() {
+ gl::GlType::Gl => {
+ self.gl.map_buffer(gl::PIXEL_PACK_BUFFER, gl::READ_ONLY)
+ }
+
+ gl::GlType::Gles => {
+ self.gl.map_buffer_range(
+ gl::PIXEL_PACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ gl::MAP_READ_BIT)
+ }
+ };
+
+ if buf_ptr.is_null() {
+ return None;
+ }
+
+ let buffer = unsafe { slice::from_raw_parts(buf_ptr as *const u8, pbo.reserved_size) };
+
+ Some(BoundPBO {
+ device: self,
+ data: buffer,
+ })
+ }
+
+ pub fn delete_pbo(&mut self, mut pbo: PBO) {
+ self.gl.delete_buffers(&[pbo.id]);
+ pbo.id = 0;
+ pbo.reserved_size = 0
+ }
+
+ /// Returns the size and stride in bytes required to upload an area of pixels
+ /// of the specified size, to a texture of the specified format.
+ pub fn required_upload_size_and_stride(&self, size: DeviceIntSize, format: ImageFormat) -> (usize, usize) {
+ assert!(size.width >= 0);
+ assert!(size.height >= 0);
+
+ let bytes_pp = format.bytes_per_pixel() as usize;
+ let width_bytes = size.width as usize * bytes_pp;
+
+ let dst_stride = round_up_to_multiple(width_bytes, self.optimal_pbo_stride.num_bytes(format));
+
+ // The size of the chunk should only need to be (height - 1) * dst_stride + width_bytes,
+ // however, the android emulator will error unless it is height * dst_stride.
+ // See bug 1587047 for details.
+ // Using the full final row also ensures that the offset of the next chunk is
+ // optimally aligned.
+ let dst_size = dst_stride * size.height as usize;
+
+ (dst_size, dst_stride)
+ }
+
+ /// Returns a `TextureUploader` which can be used to upload texture data to `texture`.
+ /// Once uploads have been performed the uploader must be flushed with `TextureUploader::flush()`.
+ pub fn upload_texture<'a>(
+ &mut self,
+ pbo_pool: &'a mut UploadPBOPool,
+ ) -> TextureUploader<'a> {
+ debug_assert!(self.inside_frame);
+
+ pbo_pool.begin_frame(self);
+
+ TextureUploader {
+ buffers: Vec::new(),
+ pbo_pool,
+ }
+ }
+
+ /// Performs an immediate (non-PBO) texture upload.
+ pub fn upload_texture_immediate<T: Texel>(
+ &mut self,
+ texture: &Texture,
+ pixels: &[T]
+ ) {
+ self.bind_texture(DEFAULT_TEXTURE, texture, Swizzle::default());
+ let desc = self.gl_describe_format(texture.format);
+ match texture.target {
+ gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES =>
+ self.gl.tex_sub_image_2d(
+ texture.target,
+ 0,
+ 0,
+ 0,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ desc.external,
+ desc.pixel_type,
+ texels_to_u8_slice(pixels),
+ ),
+ gl::TEXTURE_2D_ARRAY =>
+ self.gl.tex_sub_image_3d(
+ texture.target,
+ 0,
+ 0,
+ 0,
+ 0,
+ texture.size.width as gl::GLint,
+ texture.size.height as gl::GLint,
+ texture.layer_count as gl::GLint,
+ desc.external,
+ desc.pixel_type,
+ texels_to_u8_slice(pixels),
+ ),
+ _ => panic!("BUG: Unexpected texture target!"),
+ }
+ }
+
+ pub fn read_pixels(&mut self, img_desc: &ImageDescriptor) -> Vec<u8> {
+ let desc = self.gl_describe_format(img_desc.format);
+ self.gl.read_pixels(
+ 0, 0,
+ img_desc.size.width as i32,
+ img_desc.size.height as i32,
+ desc.read,
+ desc.pixel_type,
+ )
+ }
+
+ /// Read rectangle of pixels into the specified output slice.
+ pub fn read_pixels_into(
+ &mut self,
+ rect: FramebufferIntRect,
+ format: ImageFormat,
+ output: &mut [u8],
+ ) {
+ let bytes_per_pixel = format.bytes_per_pixel();
+ let desc = self.gl_describe_format(format);
+ let size_in_bytes = (bytes_per_pixel * rect.size.width * rect.size.height) as usize;
+ assert_eq!(output.len(), size_in_bytes);
+
+ self.gl.flush();
+ self.gl.read_pixels_into_buffer(
+ rect.origin.x as _,
+ rect.origin.y as _,
+ rect.size.width as _,
+ rect.size.height as _,
+ desc.read,
+ desc.pixel_type,
+ output,
+ );
+ }
+
+ /// Get texels of a texture into the specified output slice.
+ pub fn get_tex_image_into(
+ &mut self,
+ texture: &Texture,
+ format: ImageFormat,
+ output: &mut [u8],
+ ) {
+ self.bind_texture(DEFAULT_TEXTURE, texture, Swizzle::default());
+ let desc = self.gl_describe_format(format);
+ self.gl.get_tex_image_into_buffer(
+ texture.target,
+ 0,
+ desc.external,
+ desc.pixel_type,
+ output,
+ );
+ }
+
+ /// Attaches the provided texture to the current Read FBO binding.
+ fn attach_read_texture_raw(
+ &mut self, texture_id: gl::GLuint, target: gl::GLuint, layer_id: i32
+ ) {
+ match target {
+ gl::TEXTURE_2D_ARRAY => {
+ self.gl.framebuffer_texture_layer(
+ gl::READ_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ texture_id,
+ 0,
+ layer_id,
+ )
+ }
+ _ => {
+ assert_eq!(layer_id, 0);
+ self.gl.framebuffer_texture_2d(
+ gl::READ_FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ target,
+ texture_id,
+ 0,
+ )
+ }
+ }
+ }
+
+ pub fn attach_read_texture_external(
+ &mut self, texture_id: gl::GLuint, target: ImageBufferKind, layer_id: i32
+ ) {
+ self.attach_read_texture_raw(texture_id, get_gl_target(target), layer_id)
+ }
+
+ pub fn attach_read_texture(&mut self, texture: &Texture, layer_id: i32) {
+ self.attach_read_texture_raw(texture.id, texture.target, layer_id)
+ }
+
+ fn bind_vao_impl(&mut self, id: gl::GLuint) {
+ debug_assert!(self.inside_frame);
+
+ if self.bound_vao != id {
+ self.bound_vao = id;
+ self.gl.bind_vertex_array(id);
+ }
+ }
+
+ pub fn bind_vao(&mut self, vao: &VAO) {
+ self.bind_vao_impl(vao.id)
+ }
+
+ pub fn bind_custom_vao(&mut self, vao: &CustomVAO) {
+ self.bind_vao_impl(vao.id)
+ }
+
+ fn create_vao_with_vbos(
+ &mut self,
+ descriptor: &VertexDescriptor,
+ main_vbo_id: VBOId,
+ instance_vbo_id: VBOId,
+ instance_divisor: u32,
+ ibo_id: IBOId,
+ owns_vertices_and_indices: bool,
+ ) -> VAO {
+ let instance_stride = descriptor.instance_stride() as usize;
+ let vao_id = self.gl.gen_vertex_arrays(1)[0];
+
+ self.bind_vao_impl(vao_id);
+
+ descriptor.bind(self.gl(), main_vbo_id, instance_vbo_id, instance_divisor);
+ ibo_id.bind(self.gl()); // force it to be a part of VAO
+
+ VAO {
+ id: vao_id,
+ ibo_id,
+ main_vbo_id,
+ instance_vbo_id,
+ instance_stride,
+ instance_divisor,
+ owns_vertices_and_indices,
+ }
+ }
+
+ pub fn create_custom_vao(
+ &mut self,
+ streams: &[Stream],
+ ) -> CustomVAO {
+ debug_assert!(self.inside_frame);
+
+ let vao_id = self.gl.gen_vertex_arrays(1)[0];
+ self.bind_vao_impl(vao_id);
+
+ let mut attrib_index = 0;
+ for stream in streams {
+ VertexDescriptor::bind_attributes(
+ stream.attributes,
+ attrib_index,
+ 0,
+ self.gl(),
+ stream.vbo,
+ );
+ attrib_index += stream.attributes.len();
+ }
+
+ CustomVAO {
+ id: vao_id,
+ }
+ }
+
+ pub fn delete_custom_vao(&mut self, mut vao: CustomVAO) {
+ self.gl.delete_vertex_arrays(&[vao.id]);
+ vao.id = 0;
+ }
+
+ pub fn create_vbo<T>(&mut self) -> VBO<T> {
+ let ids = self.gl.gen_buffers(1);
+ VBO {
+ id: ids[0],
+ target: gl::ARRAY_BUFFER,
+ allocated_count: 0,
+ marker: PhantomData,
+ }
+ }
+
+ pub fn delete_vbo<T>(&mut self, mut vbo: VBO<T>) {
+ self.gl.delete_buffers(&[vbo.id]);
+ vbo.id = 0;
+ }
+
+ pub fn create_vao(&mut self, descriptor: &VertexDescriptor, instance_divisor: u32) -> VAO {
+ debug_assert!(self.inside_frame);
+
+ let buffer_ids = self.gl.gen_buffers(3);
+ let ibo_id = IBOId(buffer_ids[0]);
+ let main_vbo_id = VBOId(buffer_ids[1]);
+ let intance_vbo_id = VBOId(buffer_ids[2]);
+
+ self.create_vao_with_vbos(descriptor, main_vbo_id, intance_vbo_id, instance_divisor, ibo_id, true)
+ }
+
+ pub fn delete_vao(&mut self, mut vao: VAO) {
+ self.gl.delete_vertex_arrays(&[vao.id]);
+ vao.id = 0;
+
+ if vao.owns_vertices_and_indices {
+ self.gl.delete_buffers(&[vao.ibo_id.0]);
+ self.gl.delete_buffers(&[vao.main_vbo_id.0]);
+ }
+
+ self.gl.delete_buffers(&[vao.instance_vbo_id.0])
+ }
+
+ pub fn allocate_vbo<V>(
+ &mut self,
+ vbo: &mut VBO<V>,
+ count: usize,
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert!(self.inside_frame);
+ vbo.allocated_count = count;
+
+ self.gl.bind_buffer(vbo.target, vbo.id);
+ self.gl.buffer_data_untyped(
+ vbo.target,
+ (count * mem::size_of::<V>()) as _,
+ ptr::null(),
+ usage_hint.to_gl(),
+ );
+ }
+
+ pub fn fill_vbo<V>(
+ &mut self,
+ vbo: &VBO<V>,
+ data: &[V],
+ offset: usize,
+ ) {
+ debug_assert!(self.inside_frame);
+ assert!(offset + data.len() <= vbo.allocated_count);
+ let stride = mem::size_of::<V>();
+
+ self.gl.bind_buffer(vbo.target, vbo.id);
+ self.gl.buffer_sub_data_untyped(
+ vbo.target,
+ (offset * stride) as _,
+ (data.len() * stride) as _,
+ data.as_ptr() as _,
+ );
+ }
+
+ fn update_vbo_data<V>(
+ &mut self,
+ vbo: VBOId,
+ vertices: &[V],
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert!(self.inside_frame);
+
+ vbo.bind(self.gl());
+ gl::buffer_data(self.gl(), gl::ARRAY_BUFFER, vertices, usage_hint.to_gl());
+ }
+
+ pub fn create_vao_with_new_instances(
+ &mut self,
+ descriptor: &VertexDescriptor,
+ base_vao: &VAO,
+ ) -> VAO {
+ debug_assert!(self.inside_frame);
+
+ let buffer_ids = self.gl.gen_buffers(1);
+ let intance_vbo_id = VBOId(buffer_ids[0]);
+
+ self.create_vao_with_vbos(
+ descriptor,
+ base_vao.main_vbo_id,
+ intance_vbo_id,
+ base_vao.instance_divisor,
+ base_vao.ibo_id,
+ false,
+ )
+ }
+
+ pub fn update_vao_main_vertices<V>(
+ &mut self,
+ vao: &VAO,
+ vertices: &[V],
+ usage_hint: VertexUsageHint,
+ ) {
+ debug_assert_eq!(self.bound_vao, vao.id);
+ self.update_vbo_data(vao.main_vbo_id, vertices, usage_hint)
+ }
+
+ pub fn update_vao_instances<V: Clone>(
+ &mut self,
+ vao: &VAO,
+ instances: &[V],
+ usage_hint: VertexUsageHint,
+ // if `Some(count)`, each instance is repeated `count` times
+ repeat: Option<NonZeroUsize>,
+ ) {
+ debug_assert_eq!(self.bound_vao, vao.id);
+ debug_assert_eq!(vao.instance_stride as usize, mem::size_of::<V>());
+
+ match repeat {
+ Some(count) => {
+ let target = gl::ARRAY_BUFFER;
+ self.gl.bind_buffer(target, vao.instance_vbo_id.0);
+ let size = instances.len() * count.get() * mem::size_of::<V>();
+ self.gl.buffer_data_untyped(
+ target,
+ size as _,
+ ptr::null(),
+ usage_hint.to_gl(),
+ );
+
+ let ptr = match self.gl.get_type() {
+ gl::GlType::Gl => {
+ self.gl.map_buffer(target, gl::WRITE_ONLY)
+ }
+ gl::GlType::Gles => {
+ self.gl.map_buffer_range(target, 0, size as _, gl::MAP_WRITE_BIT)
+ }
+ };
+ assert!(!ptr.is_null());
+
+ let buffer_slice = unsafe {
+ slice::from_raw_parts_mut(ptr as *mut V, instances.len() * count.get())
+ };
+ for (quad, instance) in buffer_slice.chunks_mut(4).zip(instances) {
+ quad[0] = instance.clone();
+ quad[1] = instance.clone();
+ quad[2] = instance.clone();
+ quad[3] = instance.clone();
+ }
+ self.gl.unmap_buffer(target);
+ }
+ None => {
+ self.update_vbo_data(vao.instance_vbo_id, instances, usage_hint);
+ }
+ }
+ }
+
+ pub fn update_vao_indices<I>(&mut self, vao: &VAO, indices: &[I], usage_hint: VertexUsageHint) {
+ debug_assert!(self.inside_frame);
+ debug_assert_eq!(self.bound_vao, vao.id);
+
+ vao.ibo_id.bind(self.gl());
+ gl::buffer_data(
+ self.gl(),
+ gl::ELEMENT_ARRAY_BUFFER,
+ indices,
+ usage_hint.to_gl(),
+ );
+ }
+
+ pub fn draw_triangles_u16(&mut self, first_vertex: i32, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ first_vertex as u32 * 2,
+ );
+ }
+
+ pub fn draw_triangles_u32(&mut self, first_vertex: i32, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_INT,
+ first_vertex as u32 * 4,
+ );
+ }
+
+ pub fn draw_nonindexed_points(&mut self, first_vertex: i32, vertex_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_arrays(gl::POINTS, first_vertex, vertex_count);
+ }
+
+ pub fn draw_nonindexed_lines(&mut self, first_vertex: i32, vertex_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_arrays(gl::LINES, first_vertex, vertex_count);
+ }
+
+ pub fn draw_indexed_triangles(&mut self, index_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_elements(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ 0,
+ );
+ }
+
+ pub fn draw_indexed_triangles_instanced_u16(&mut self, index_count: i32, instance_count: i32) {
+ debug_assert!(self.inside_frame);
+ #[cfg(debug_assertions)]
+ debug_assert!(self.shader_is_ready);
+
+ self.gl.draw_elements_instanced(
+ gl::TRIANGLES,
+ index_count,
+ gl::UNSIGNED_SHORT,
+ 0,
+ instance_count,
+ );
+ }
+
+ pub fn end_frame(&mut self) {
+ self.reset_draw_target();
+ self.reset_read_target();
+
+ debug_assert!(self.inside_frame);
+ self.inside_frame = false;
+
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ self.gl.use_program(0);
+
+ for i in 0 .. self.bound_textures.len() {
+ self.gl.active_texture(gl::TEXTURE0 + i as gl::GLuint);
+ self.gl.bind_texture(gl::TEXTURE_2D, 0);
+ }
+
+ self.gl.active_texture(gl::TEXTURE0);
+
+ self.frame_id.0 += 1;
+
+ // Save any shaders compiled this frame to disk.
+ // If this is the tenth frame then treat startup as complete, meaning the
+ // current set of in-use shaders are the ones to load on the next startup.
+ if let Some(ref cache) = self.cached_programs {
+ cache.update_disk_cache(self.frame_id.0 == 10);
+ }
+ }
+
+ pub fn clear_target(
+ &self,
+ color: Option<[f32; 4]>,
+ depth: Option<f32>,
+ rect: Option<FramebufferIntRect>,
+ ) {
+ let mut clear_bits = 0;
+
+ if let Some(color) = color {
+ self.gl.clear_color(color[0], color[1], color[2], color[3]);
+ clear_bits |= gl::COLOR_BUFFER_BIT;
+ }
+
+ if let Some(depth) = depth {
+ if cfg!(debug_assertions) {
+ let mut mask = [0];
+ unsafe {
+ self.gl.get_boolean_v(gl::DEPTH_WRITEMASK, &mut mask);
+ }
+ assert_ne!(mask[0], 0);
+ }
+ self.gl.clear_depth(depth as f64);
+ clear_bits |= gl::DEPTH_BUFFER_BIT;
+ }
+
+ if clear_bits != 0 {
+ match rect {
+ Some(rect) => {
+ self.gl.enable(gl::SCISSOR_TEST);
+ self.gl.scissor(
+ rect.origin.x,
+ rect.origin.y,
+ rect.size.width,
+ rect.size.height,
+ );
+ self.gl.clear(clear_bits);
+ self.gl.disable(gl::SCISSOR_TEST);
+ }
+ None => {
+ self.gl.clear(clear_bits);
+ }
+ }
+ }
+ }
+
+ pub fn enable_depth(&self, depth_func: DepthFunction) {
+ assert!(self.depth_available, "Enabling depth test without depth target");
+ self.gl.enable(gl::DEPTH_TEST);
+ self.gl.depth_func(depth_func as gl::GLuint);
+ }
+
+ pub fn disable_depth(&self) {
+ self.gl.disable(gl::DEPTH_TEST);
+ }
+
+ pub fn enable_depth_write(&self) {
+ assert!(self.depth_available, "Enabling depth write without depth target");
+ self.gl.depth_mask(true);
+ }
+
+ pub fn disable_depth_write(&self) {
+ self.gl.depth_mask(false);
+ }
+
+ pub fn disable_stencil(&self) {
+ self.gl.disable(gl::STENCIL_TEST);
+ }
+
+ pub fn set_scissor_rect(&self, rect: FramebufferIntRect) {
+ self.gl.scissor(
+ rect.origin.x,
+ rect.origin.y,
+ rect.size.width,
+ rect.size.height,
+ );
+ }
+
+ pub fn enable_scissor(&self) {
+ self.gl.enable(gl::SCISSOR_TEST);
+ }
+
+ pub fn disable_scissor(&self) {
+ self.gl.disable(gl::SCISSOR_TEST);
+ }
+
+ pub fn enable_color_write(&self) {
+ self.gl.color_mask(true, true, true, true);
+ }
+
+ pub fn disable_color_write(&self) {
+ self.gl.color_mask(false, false, false, false);
+ }
+
+ pub fn set_blend(&mut self, enable: bool) {
+ if enable {
+ self.gl.enable(gl::BLEND);
+ } else {
+ self.gl.disable(gl::BLEND);
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ fn set_blend_factors(
+ &mut self,
+ color: (gl::GLenum, gl::GLenum),
+ alpha: (gl::GLenum, gl::GLenum),
+ ) {
+ self.gl.blend_equation(gl::FUNC_ADD);
+ if color == alpha {
+ self.gl.blend_func(color.0, color.1);
+ } else {
+ self.gl.blend_func_separate(color.0, color.1, alpha.0, alpha.1);
+ }
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ pub fn set_blend_mode_alpha(&mut self) {
+ self.set_blend_factors(
+ (gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_premultiplied_alpha(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_premultiplied_dest_out(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_multiply(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::SRC_COLOR),
+ (gl::ZERO, gl::SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_pass0(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ZERO, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_pass1(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE),
+ (gl::ONE, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass0(&mut self) {
+ self.set_blend_factors(
+ (gl::ZERO, gl::ONE_MINUS_SRC_COLOR),
+ (gl::ZERO, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE_MINUS_DST_ALPHA, gl::ONE),
+ (gl::ZERO, gl::ONE),
+ );
+ }
+ pub fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_constant_text_color(&mut self, color: ColorF) {
+ // color is an unpremultiplied color.
+ self.gl.blend_color(color.r, color.g, color.b, 1.0);
+ self.set_blend_factors(
+ (gl::CONSTANT_COLOR, gl::ONE_MINUS_SRC_COLOR),
+ (gl::CONSTANT_ALPHA, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_subpixel_dual_source(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC1_COLOR),
+ (gl::ONE, gl::ONE_MINUS_SRC1_ALPHA),
+ );
+ }
+ pub fn set_blend_mode_show_overdraw(&mut self) {
+ self.set_blend_factors(
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ (gl::ONE, gl::ONE_MINUS_SRC_ALPHA),
+ );
+ }
+
+ pub fn set_blend_mode_max(&mut self) {
+ self.gl
+ .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
+ self.gl.blend_equation_separate(gl::MAX, gl::FUNC_ADD);
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+ pub fn set_blend_mode_min(&mut self) {
+ self.gl
+ .blend_func_separate(gl::ONE, gl::ONE, gl::ONE, gl::ONE);
+ self.gl.blend_equation_separate(gl::MIN, gl::FUNC_ADD);
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+ pub fn set_blend_mode_advanced(&mut self, mode: MixBlendMode) {
+ self.gl.blend_equation(match mode {
+ MixBlendMode::Normal => {
+ // blend factor only make sense for the normal mode
+ self.gl.blend_func_separate(gl::ZERO, gl::SRC_COLOR, gl::ZERO, gl::SRC_ALPHA);
+ gl::FUNC_ADD
+ },
+ MixBlendMode::Multiply => gl::MULTIPLY_KHR,
+ MixBlendMode::Screen => gl::SCREEN_KHR,
+ MixBlendMode::Overlay => gl::OVERLAY_KHR,
+ MixBlendMode::Darken => gl::DARKEN_KHR,
+ MixBlendMode::Lighten => gl::LIGHTEN_KHR,
+ MixBlendMode::ColorDodge => gl::COLORDODGE_KHR,
+ MixBlendMode::ColorBurn => gl::COLORBURN_KHR,
+ MixBlendMode::HardLight => gl::HARDLIGHT_KHR,
+ MixBlendMode::SoftLight => gl::SOFTLIGHT_KHR,
+ MixBlendMode::Difference => gl::DIFFERENCE_KHR,
+ MixBlendMode::Exclusion => gl::EXCLUSION_KHR,
+ MixBlendMode::Hue => gl::HSL_HUE_KHR,
+ MixBlendMode::Saturation => gl::HSL_SATURATION_KHR,
+ MixBlendMode::Color => gl::HSL_COLOR_KHR,
+ MixBlendMode::Luminosity => gl::HSL_LUMINOSITY_KHR,
+ });
+ #[cfg(debug_assertions)]
+ {
+ self.shader_is_ready = false;
+ }
+ }
+
+ pub fn supports_extension(&self, extension: &str) -> bool {
+ supports_extension(&self.extensions, extension)
+ }
+
+ pub fn echo_driver_messages(&self) {
+ if self.capabilities.supports_khr_debug {
+ Device::log_driver_messages(self.gl());
+ }
+ }
+
+ fn log_driver_messages(gl: &dyn gl::Gl) {
+ for msg in gl.get_debug_messages() {
+ let level = match msg.severity {
+ gl::DEBUG_SEVERITY_HIGH => Level::Error,
+ gl::DEBUG_SEVERITY_MEDIUM => Level::Warn,
+ gl::DEBUG_SEVERITY_LOW => Level::Info,
+ gl::DEBUG_SEVERITY_NOTIFICATION => Level::Debug,
+ _ => Level::Trace,
+ };
+ let ty = match msg.ty {
+ gl::DEBUG_TYPE_ERROR => "error",
+ gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR => "deprecated",
+ gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => "undefined",
+ gl::DEBUG_TYPE_PORTABILITY => "portability",
+ gl::DEBUG_TYPE_PERFORMANCE => "perf",
+ gl::DEBUG_TYPE_MARKER => "marker",
+ gl::DEBUG_TYPE_PUSH_GROUP => "group push",
+ gl::DEBUG_TYPE_POP_GROUP => "group pop",
+ gl::DEBUG_TYPE_OTHER => "other",
+ _ => "?",
+ };
+ log!(level, "({}) {}", ty, msg.message);
+ }
+ }
+
+ pub fn gl_describe_format(&self, format: ImageFormat) -> FormatDesc {
+ match format {
+ ImageFormat::R8 => FormatDesc {
+ internal: gl::R8,
+ external: gl::RED,
+ read: gl::RED,
+ pixel_type: gl::UNSIGNED_BYTE,
+ },
+ ImageFormat::R16 => FormatDesc {
+ internal: gl::R16,
+ external: gl::RED,
+ read: gl::RED,
+ pixel_type: gl::UNSIGNED_SHORT,
+ },
+ ImageFormat::BGRA8 => {
+ FormatDesc {
+ internal: self.bgra_formats.internal,
+ external: self.bgra_formats.external,
+ read: gl::BGRA,
+ pixel_type: self.bgra_pixel_type,
+ }
+ },
+ ImageFormat::RGBA8 => {
+ FormatDesc {
+ internal: gl::RGBA8,
+ external: gl::RGBA,
+ read: gl::RGBA,
+ pixel_type: gl::UNSIGNED_BYTE,
+ }
+ },
+ ImageFormat::RGBAF32 => FormatDesc {
+ internal: gl::RGBA32F,
+ external: gl::RGBA,
+ read: gl::RGBA,
+ pixel_type: gl::FLOAT,
+ },
+ ImageFormat::RGBAI32 => FormatDesc {
+ internal: gl::RGBA32I,
+ external: gl::RGBA_INTEGER,
+ read: gl::RGBA_INTEGER,
+ pixel_type: gl::INT,
+ },
+ ImageFormat::RG8 => FormatDesc {
+ internal: gl::RG8,
+ external: gl::RG,
+ read: gl::RG,
+ pixel_type: gl::UNSIGNED_BYTE,
+ },
+ ImageFormat::RG16 => FormatDesc {
+ internal: gl::RG16,
+ external: gl::RG,
+ read: gl::RG,
+ pixel_type: gl::UNSIGNED_SHORT,
+ },
+ }
+ }
+
+ /// Returns a GL format matching an ImageFormat suitable for a renderbuffer.
+ fn matching_renderbuffer_format(&self, format: ImageFormat) -> gl::GLenum {
+ match format {
+ ImageFormat::R8 => gl::R8,
+ ImageFormat::R16 => gl::R16UI,
+ // BGRA8 renderbuffers are not supported, so use RGBA8.
+ ImageFormat::BGRA8 => gl::RGBA8,
+ ImageFormat::RGBAF32 => gl::RGBA32F,
+ ImageFormat::RG8 => gl::RG8,
+ ImageFormat::RG16 => gl::RG16,
+ ImageFormat::RGBAI32 => gl::RGBA32I,
+ ImageFormat::RGBA8 => gl::RGBA8,
+ }
+ }
+
+ /// Generates a memory report for the resources managed by the device layer.
+ pub fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+ for dim in self.depth_targets.keys() {
+ report.depth_target_textures += depth_target_size_in_bytes(dim);
+ }
+ report
+ }
+}
+
+pub struct FormatDesc {
+ /// Format the texel data is internally stored in within a texture.
+ pub internal: gl::GLenum,
+ /// Format that we expect the data to be provided when filling the texture.
+ pub external: gl::GLuint,
+ /// Format to read the texels as, so that they can be uploaded as `external`
+ /// later on.
+ pub read: gl::GLuint,
+ /// Associated pixel type.
+ pub pixel_type: gl::GLuint,
+}
+
+#[derive(Debug)]
+struct UploadChunk<'a> {
+ rect: DeviceIntRect,
+ layer_index: i32,
+ stride: Option<i32>,
+ offset: usize,
+ format_override: Option<ImageFormat>,
+ texture: &'a Texture,
+}
+
+#[derive(Debug)]
+struct PixelBuffer<'a> {
+ size_used: usize,
+ // small vector avoids heap allocation for a single chunk
+ chunks: SmallVec<[UploadChunk<'a>; 1]>,
+ inner: UploadPBO,
+ mapping: &'a mut [mem::MaybeUninit<u8>],
+}
+
+impl<'a> PixelBuffer<'a> {
+ fn new(
+ pbo: UploadPBO,
+ ) -> Self {
+ let mapping = unsafe {
+ slice::from_raw_parts_mut(pbo.mapping.get_ptr().as_ptr(), pbo.pbo.reserved_size)
+ };
+ Self {
+ size_used: 0,
+ chunks: SmallVec::new(),
+ inner: pbo,
+ mapping,
+ }
+ }
+
+ fn flush_chunks(&mut self, device: &mut Device) {
+ for chunk in self.chunks.drain(..) {
+ TextureUploader::update_impl(device, chunk);
+ }
+ }
+}
+
+impl<'a> Drop for PixelBuffer<'a> {
+ fn drop(&mut self) {
+ assert_eq!(self.chunks.len(), 0, "PixelBuffer must be flushed before dropping.");
+ }
+}
+
+#[derive(Debug)]
+enum PBOMapping {
+ Unmapped,
+ Transient(ptr::NonNull<mem::MaybeUninit<u8>>),
+ Persistent(ptr::NonNull<mem::MaybeUninit<u8>>),
+}
+
+impl PBOMapping {
+ fn get_ptr(&self) -> ptr::NonNull<mem::MaybeUninit<u8>> {
+ match self {
+ PBOMapping::Unmapped => unreachable!("Cannot get pointer to unmapped PBO."),
+ PBOMapping::Transient(ptr) => *ptr,
+ PBOMapping::Persistent(ptr) => *ptr,
+ }
+ }
+}
+
+/// A PBO for uploading texture data, managed by UploadPBOPool.
+#[derive(Debug)]
+struct UploadPBO {
+ pbo: PBO,
+ mapping: PBOMapping,
+ can_recycle: bool,
+}
+
+impl UploadPBO {
+ fn empty() -> Self {
+ Self {
+ pbo: PBO {
+ id: 0,
+ reserved_size: 0,
+ },
+ mapping: PBOMapping::Unmapped,
+ can_recycle: false,
+ }
+ }
+}
+
+/// Allocates and recycles PBOs used for uploading texture data.
+/// Tries to allocate and recycle PBOs of a fixed size, but will make exceptions when
+/// a larger buffer is required or to work around driver bugs.
+pub struct UploadPBOPool {
+ /// Usage hint to provide to the driver for optimizations.
+ usage_hint: VertexUsageHint,
+ /// The preferred size, in bytes, of the buffers to allocate.
+ default_size: usize,
+ /// List of allocated PBOs ready to be re-used.
+ available_buffers: Vec<UploadPBO>,
+ /// PBOs which have been returned during the current frame,
+ /// and do not yet have an associated sync object.
+ returned_buffers: Vec<UploadPBO>,
+ /// PBOs which are waiting until their sync object is signalled,
+ /// indicating they can are ready to be re-used.
+ waiting_buffers: Vec<(gl::GLsync, Vec<UploadPBO>)>,
+ /// PBOs which have been orphaned.
+ /// We can recycle their IDs but must reallocate their storage.
+ orphaned_buffers: Vec<PBO>,
+}
+
+impl UploadPBOPool {
+ pub fn new(device: &mut Device, default_size: usize) -> Self {
+ let usage_hint = match device.upload_method {
+ UploadMethod::Immediate => VertexUsageHint::Stream,
+ UploadMethod::PixelBuffer(usage_hint) => usage_hint,
+ };
+ Self {
+ usage_hint,
+ default_size,
+ available_buffers: Vec::new(),
+ returned_buffers: Vec::new(),
+ waiting_buffers: Vec::new(),
+ orphaned_buffers: Vec::new(),
+ }
+ }
+
+ /// To be called at the beginning of a series of uploads.
+ /// Moves any buffers which are now ready to be used from the waiting list to the ready list.
+ pub fn begin_frame(&mut self, device: &mut Device) {
+ // Iterate through the waiting buffers and check if each fence has been signalled.
+ // If a fence is signalled, move its corresponding buffers to the available list.
+ // On error, delete the buffers. Stop when we find the first non-signalled fence,
+ // and clean up the signalled fences.
+ let mut first_not_signalled = self.waiting_buffers.len();
+ for (i, (sync, buffers)) in self.waiting_buffers.iter_mut().enumerate() {
+ match device.gl.client_wait_sync(*sync, 0, 0) {
+ gl::TIMEOUT_EXPIRED => {
+ first_not_signalled = i;
+ break;
+ },
+ gl::ALREADY_SIGNALED | gl::CONDITION_SATISFIED => {
+ self.available_buffers.extend(buffers.drain(..));
+ }
+ gl::WAIT_FAILED | _ => {
+ warn!("glClientWaitSync error in UploadPBOPool::begin_frame()");
+ for buffer in buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ }
+ }
+ }
+
+ // Delete signalled fences, and remove their now-empty Vecs from waiting_buffers.
+ for (sync, _) in self.waiting_buffers.drain(0..first_not_signalled) {
+ device.gl.delete_sync(sync);
+ }
+ }
+
+ // To be called at the end of a series of uploads.
+ // Creates a sync object, and adds the buffers returned during this frame to waiting_buffers.
+ pub fn end_frame(&mut self, device: &mut Device) {
+ if !self.returned_buffers.is_empty() {
+ let sync = device.gl.fence_sync(gl::SYNC_GPU_COMMANDS_COMPLETE, 0);
+ if !sync.is_null() {
+ self.waiting_buffers.push((sync, mem::replace(&mut self.returned_buffers, Vec::new())))
+ } else {
+ warn!("glFenceSync error in UploadPBOPool::end_frame()");
+
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ }
+ }
+ }
+
+ /// Obtain a PBO, either by reusing an existing PBO or allocating a new one.
+ /// min_size specifies the minimum required size of the PBO. The returned PBO
+ /// may be larger than required.
+ fn get_pbo(&mut self, device: &mut Device, min_size: usize) -> Result<UploadPBO, ()> {
+
+ // If min_size is smaller than our default size, then use the default size.
+ // The exception to this is when due to driver bugs we cannot upload from
+ // offsets other than zero within a PBO. In this case, there is no point in
+ // allocating buffers larger than required, as they cannot be shared.
+ let (can_recycle, size) = if min_size <= self.default_size && device.capabilities.supports_nonzero_pbo_offsets {
+ (true, self.default_size)
+ } else {
+ (false, min_size)
+ };
+
+ // Try to recycle an already allocated PBO.
+ if can_recycle {
+ if let Some(mut buffer) = self.available_buffers.pop() {
+ assert_eq!(buffer.pbo.reserved_size, size);
+ assert!(buffer.can_recycle);
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.pbo.id);
+
+ match buffer.mapping {
+ PBOMapping::Unmapped => {
+ // If buffer was unmapped then transiently map it.
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ buffer.pbo.reserved_size as _,
+ gl::MAP_WRITE_BIT | gl::MAP_UNSYNCHRONIZED_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(|| {
+ error!("Failed to transiently map PBO of size {} bytes", buffer.pbo.reserved_size);
+ })?;
+
+ buffer.mapping = PBOMapping::Transient(ptr);
+ }
+ PBOMapping::Transient(_) => {
+ unreachable!("Transiently mapped UploadPBO must be unmapped before returning to pool.");
+ }
+ PBOMapping::Persistent(_) => {
+ }
+ }
+
+ return Ok(buffer);
+ }
+ }
+
+ // Try to recycle a PBO ID (but not its allocation) from a previously allocated PBO.
+ // If there are none available, create a new PBO.
+ let mut pbo = match self.orphaned_buffers.pop() {
+ Some(pbo) => pbo,
+ None => device.create_pbo(),
+ };
+
+ assert_eq!(pbo.reserved_size, 0);
+ pbo.reserved_size = size;
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, pbo.id);
+ let mapping = if device.capabilities.supports_buffer_storage && can_recycle {
+ device.gl.buffer_storage(
+ gl::PIXEL_UNPACK_BUFFER,
+ pbo.reserved_size as _,
+ ptr::null(),
+ gl::MAP_WRITE_BIT | gl::MAP_PERSISTENT_BIT,
+ );
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ // GL_MAP_COHERENT_BIT doesn't seem to work on Adreno, so use glFlushMappedBufferRange.
+ // kvark notes that coherent memory can be faster on some platforms, such as nvidia,
+ // so in the future we could choose which to use at run time.
+ gl::MAP_WRITE_BIT | gl::MAP_PERSISTENT_BIT | gl::MAP_FLUSH_EXPLICIT_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(|| {
+ error!("Failed to persistently map PBO of size {} bytes", pbo.reserved_size);
+ })?;
+
+ PBOMapping::Persistent(ptr)
+ } else {
+ device.gl.buffer_data_untyped(
+ gl::PIXEL_UNPACK_BUFFER,
+ pbo.reserved_size as _,
+ ptr::null(),
+ self.usage_hint.to_gl(),
+ );
+ let ptr = device.gl.map_buffer_range(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ pbo.reserved_size as _,
+ // Unlike the above code path, where we are re-mapping a buffer that has previously been unmapped,
+ // this buffer has just been created there is no need for GL_MAP_UNSYNCHRONIZED_BIT.
+ gl::MAP_WRITE_BIT,
+ ) as *mut _;
+
+ let ptr = ptr::NonNull::new(ptr).ok_or_else(|| {
+ error!("Failed to transiently map PBO of size {} bytes", pbo.reserved_size);
+ })?;
+
+ PBOMapping::Transient(ptr)
+ };
+
+ Ok(UploadPBO { pbo, mapping, can_recycle })
+ }
+
+ /// Returns a PBO to the pool. If the PBO is recyclable it is placed in the waiting list.
+ /// Otherwise we orphan the allocation immediately, and will subsequently reuse just the ID.
+ fn return_pbo(&mut self, device: &mut Device, mut buffer: UploadPBO) {
+ assert!(
+ !matches!(buffer.mapping, PBOMapping::Transient(_)),
+ "Transiently mapped UploadPBO must be unmapped before returning to pool.",
+ );
+
+ if buffer.can_recycle {
+ self.returned_buffers.push(buffer);
+ } else {
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.pbo.id);
+ device.gl.buffer_data_untyped(
+ gl::PIXEL_UNPACK_BUFFER,
+ 0,
+ ptr::null(),
+ gl::STREAM_DRAW,
+ );
+ buffer.pbo.reserved_size = 0;
+ self.orphaned_buffers.push(buffer.pbo);
+ }
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ /// Frees all allocated buffers in response to a memory pressure event.
+ pub fn on_memory_pressure(&mut self, device: &mut Device) {
+ for buffer in self.available_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo)
+ }
+ for (sync, buffers) in self.waiting_buffers.drain(..) {
+ device.gl.delete_sync(sync);
+ for buffer in buffers {
+ device.delete_pbo(buffer.pbo)
+ }
+ }
+ // There is no need to delete orphaned PBOs on memory pressure.
+ }
+
+ /// Generates a memory report.
+ pub fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+ for buffer in &self.available_buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ for buffer in &self.returned_buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ for (_, buffers) in &self.waiting_buffers {
+ for buffer in buffers {
+ report.texture_upload_pbos += buffer.pbo.reserved_size;
+ }
+ }
+ report
+ }
+
+ pub fn deinit(&mut self, device: &mut Device) {
+ for buffer in self.available_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo);
+ }
+ for buffer in self.returned_buffers.drain(..) {
+ device.delete_pbo(buffer.pbo)
+ }
+ for (sync, buffers) in self.waiting_buffers.drain(..) {
+ device.gl.delete_sync(sync);
+ for buffer in buffers {
+ device.delete_pbo(buffer.pbo)
+ }
+ }
+ for pbo in self.orphaned_buffers.drain(..) {
+ device.delete_pbo(pbo);
+ }
+ }
+}
+
+/// Used to perform a series of texture uploads.
+/// Create using Device::upload_texture(). Perform a series of uploads using either
+/// upload(), or stage() and upload_staged(), then call flush().
+pub struct TextureUploader<'a> {
+ /// A list of buffers containing uploads that need to be flushed.
+ buffers: Vec<PixelBuffer<'a>>,
+ /// Pool used to obtain PBOs to fill with texture data.
+ pbo_pool: &'a mut UploadPBOPool,
+}
+
+impl<'a> Drop for TextureUploader<'a> {
+ fn drop(&mut self) {
+ assert!(
+ thread::panicking() || self.buffers.is_empty(),
+ "TextureUploader must be flushed before it is dropped."
+ );
+ }
+}
+
+/// A buffer used to manually stage data to be uploaded to a texture.
+/// Created by calling TextureUploader::stage(), the data can then be written to via get_mapping().
+#[derive(Debug)]
+pub struct UploadStagingBuffer<'a> {
+ /// The PixelBuffer containing this upload.
+ buffer: PixelBuffer<'a>,
+ /// The offset of this upload within the PixelBuffer.
+ offset: usize,
+ /// The size of this upload.
+ size: usize,
+ /// The stride of the data within the buffer.
+ stride: usize,
+}
+
+impl<'a> UploadStagingBuffer<'a> {
+ /// Returns the required stride of the data to be written to the buffer.
+ pub fn get_stride(&self) -> usize {
+ self.stride
+ }
+
+ /// Returns a mapping of the data in the buffer, to be written to.
+ pub fn get_mapping(&mut self) -> &mut [mem::MaybeUninit<u8>] {
+ &mut self.buffer.mapping[self.offset..self.offset + self.size]
+ }
+}
+
+impl<'a> TextureUploader<'a> {
+ /// Returns an UploadStagingBuffer which can be used to manually stage data to be uploaded.
+ /// Once the data has been staged, it can be uploaded with upload_staged().
+ pub fn stage(
+ &mut self,
+ device: &mut Device,
+ format: ImageFormat,
+ size: DeviceIntSize,
+ ) -> Result<UploadStagingBuffer<'a>, ()> {
+ assert!(matches!(device.upload_method, UploadMethod::PixelBuffer(_)), "Texture uploads should only be staged when using pixel buffers.");
+
+ // for optimal PBO texture uploads the offset and stride of the data in
+ // the buffer may have to be a multiple of a certain value.
+ let (dst_size, dst_stride) = device.required_upload_size_and_stride(
+ size,
+ format,
+ );
+
+ // Find a pixel buffer with enough space remaining, creating a new one if required.
+ let buffer_index = self.buffers.iter().position(|buffer| {
+ buffer.size_used + dst_size <= buffer.inner.pbo.reserved_size
+ });
+ let buffer = match buffer_index {
+ Some(i) => self.buffers.swap_remove(i),
+ None => PixelBuffer::new(self.pbo_pool.get_pbo(device, dst_size)?),
+ };
+
+ if !device.capabilities.supports_nonzero_pbo_offsets {
+ assert_eq!(buffer.size_used, 0, "PBO uploads from non-zero offset are not supported.");
+ }
+ assert!(buffer.size_used + dst_size <= buffer.inner.pbo.reserved_size, "PixelBuffer is too small");
+
+ let offset = buffer.size_used;
+
+ Ok(UploadStagingBuffer {
+ buffer,
+ offset,
+ size: dst_size,
+ stride: dst_stride,
+ })
+ }
+
+ /// Uploads manually staged texture data to the specified texture.
+ pub fn upload_staged(
+ &mut self,
+ device: &mut Device,
+ texture: &'a Texture,
+ rect: DeviceIntRect,
+ layer_index: i32,
+ format_override: Option<ImageFormat>,
+ mut staging_buffer: UploadStagingBuffer<'a>,
+ ) -> usize {
+ let size = staging_buffer.size;
+
+ staging_buffer.buffer.chunks.push(UploadChunk {
+ rect,
+ layer_index,
+ stride: Some(staging_buffer.stride as i32),
+ offset: staging_buffer.offset,
+ format_override,
+ texture,
+ });
+ staging_buffer.buffer.size_used += staging_buffer.size;
+
+ // Flush the buffer if it is full, otherwise return it to the uploader for further use.
+ if staging_buffer.buffer.size_used < staging_buffer.buffer.inner.pbo.reserved_size {
+ self.buffers.push(staging_buffer.buffer);
+ } else {
+ Self::flush_buffer(device, self.pbo_pool, staging_buffer.buffer);
+ }
+
+ size
+ }
+
+ /// Uploads texture data to the specified texture.
+ pub fn upload<T>(
+ &mut self,
+ device: &mut Device,
+ texture: &'a Texture,
+ mut rect: DeviceIntRect,
+ layer_index: i32,
+ stride: Option<i32>,
+ format_override: Option<ImageFormat>,
+ data: *const T,
+ len: usize,
+ ) -> usize {
+ // Textures dimensions may have been clamped by the hardware. Crop the
+ // upload region to match.
+ let cropped = rect.intersection(
+ &DeviceIntRect::new(DeviceIntPoint::zero(), texture.get_dimensions())
+ );
+ if cfg!(debug_assertions) && cropped.map_or(true, |r| r != rect) {
+ warn!("Cropping texture upload {:?} to {:?}", rect, cropped);
+ }
+ rect = match cropped {
+ None => return 0,
+ Some(r) => r,
+ };
+
+ let bytes_pp = texture.format.bytes_per_pixel() as usize;
+ let width_bytes = rect.size.width as usize * bytes_pp;
+
+ let src_stride = stride.map_or(width_bytes, |stride| {
+ assert!(stride >= 0);
+ stride as usize
+ });
+ let src_size = (rect.size.height as usize - 1) * src_stride + width_bytes;
+ assert!(src_size <= len * mem::size_of::<T>());
+
+ match device.upload_method {
+ UploadMethod::Immediate => {
+ if cfg!(debug_assertions) {
+ let mut bound_buffer = [0];
+ unsafe {
+ device.gl.get_integer_v(gl::PIXEL_UNPACK_BUFFER_BINDING, &mut bound_buffer);
+ }
+ assert_eq!(bound_buffer[0], 0, "GL_PIXEL_UNPACK_BUFFER must not be bound for immediate uploads.");
+ }
+
+ Self::update_impl(device, UploadChunk {
+ rect,
+ layer_index,
+ stride: Some(src_stride as i32),
+ offset: data as _,
+ format_override,
+ texture,
+ });
+
+ width_bytes * rect.size.height as usize
+ }
+ UploadMethod::PixelBuffer(_) => {
+ let mut staging_buffer = match self.stage(device, texture.format, rect.size) {
+ Ok(staging_buffer) => staging_buffer,
+ Err(_) => return 0,
+ };
+ let dst_stride = staging_buffer.get_stride();
+
+ unsafe {
+ let src: &[mem::MaybeUninit<u8>] = slice::from_raw_parts(data as *const _, src_size);
+
+ if src_stride == dst_stride {
+ // the stride is already optimal, so simply copy
+ // the data as-is in to the buffer
+ staging_buffer.get_mapping()[..src_size].copy_from_slice(src);
+ } else {
+ // copy the data line-by-line in to the buffer so
+ // that it has an optimal stride
+ for y in 0..rect.size.height as usize {
+ let src_start = y * src_stride;
+ let src_end = src_start + width_bytes;
+ let dst_start = y * staging_buffer.get_stride();
+ let dst_end = dst_start + width_bytes;
+
+ staging_buffer.get_mapping()[dst_start..dst_end].copy_from_slice(&src[src_start..src_end])
+ }
+ }
+ }
+
+ self.upload_staged(device, texture, rect, layer_index, format_override, staging_buffer)
+ }
+ }
+ }
+
+ fn flush_buffer(device: &mut Device, pbo_pool: &mut UploadPBOPool, mut buffer: PixelBuffer) {
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, buffer.inner.pbo.id);
+ match buffer.inner.mapping {
+ PBOMapping::Unmapped => unreachable!("UploadPBO should be mapped at this stage."),
+ PBOMapping::Transient(_) => {
+ device.gl.unmap_buffer(gl::PIXEL_UNPACK_BUFFER);
+ buffer.inner.mapping = PBOMapping::Unmapped;
+ }
+ PBOMapping::Persistent(_) => {
+ device.gl.flush_mapped_buffer_range(gl::PIXEL_UNPACK_BUFFER, 0, buffer.size_used as _);
+ }
+ }
+ buffer.flush_chunks(device);
+ let pbo = mem::replace(&mut buffer.inner, UploadPBO::empty());
+ pbo_pool.return_pbo(device, pbo);
+ }
+
+ /// Flushes all pending texture uploads. Must be called after all
+ /// required upload() or upload_staged() calls have been made.
+ pub fn flush(mut self, device: &mut Device) {
+ for buffer in self.buffers.drain(..) {
+ Self::flush_buffer(device, self.pbo_pool, buffer);
+ }
+
+ device.gl.bind_buffer(gl::PIXEL_UNPACK_BUFFER, 0);
+ }
+
+ fn update_impl(device: &mut Device, chunk: UploadChunk) {
+ device.bind_texture(DEFAULT_TEXTURE, chunk.texture, Swizzle::default());
+
+ let format = chunk.format_override.unwrap_or(chunk.texture.format);
+ let (gl_format, bpp, data_type) = match format {
+ ImageFormat::R8 => (gl::RED, 1, gl::UNSIGNED_BYTE),
+ ImageFormat::R16 => (gl::RED, 2, gl::UNSIGNED_SHORT),
+ ImageFormat::BGRA8 => (device.bgra_formats.external, 4, device.bgra_pixel_type),
+ ImageFormat::RGBA8 => (gl::RGBA, 4, gl::UNSIGNED_BYTE),
+ ImageFormat::RG8 => (gl::RG, 2, gl::UNSIGNED_BYTE),
+ ImageFormat::RG16 => (gl::RG, 4, gl::UNSIGNED_SHORT),
+ ImageFormat::RGBAF32 => (gl::RGBA, 16, gl::FLOAT),
+ ImageFormat::RGBAI32 => (gl::RGBA_INTEGER, 16, gl::INT),
+ };
+
+ let row_length = match chunk.stride {
+ Some(value) => value / bpp,
+ None => chunk.texture.size.width,
+ };
+
+ if chunk.stride.is_some() {
+ device.gl.pixel_store_i(
+ gl::UNPACK_ROW_LENGTH,
+ row_length as _,
+ );
+ }
+
+ let pos = chunk.rect.origin;
+ let size = chunk.rect.size;
+
+ match chunk.texture.target {
+ gl::TEXTURE_2D_ARRAY => {
+ device.gl.tex_sub_image_3d_pbo(
+ chunk.texture.target,
+ 0,
+ pos.x as _,
+ pos.y as _,
+ chunk.layer_index,
+ size.width as _,
+ size.height as _,
+ 1,
+ gl_format,
+ data_type,
+ chunk.offset,
+ );
+ }
+ gl::TEXTURE_2D | gl::TEXTURE_RECTANGLE | gl::TEXTURE_EXTERNAL_OES => {
+ device.gl.tex_sub_image_2d_pbo(
+ chunk.texture.target,
+ 0,
+ pos.x as _,
+ pos.y as _,
+ size.width as _,
+ size.height as _,
+ gl_format,
+ data_type,
+ chunk.offset,
+ );
+ }
+ _ => panic!("BUG: Unexpected texture target!"),
+ }
+
+ // If using tri-linear filtering, build the mip-map chain for this texture.
+ if chunk.texture.filter == TextureFilter::Trilinear {
+ device.gl.generate_mipmap(chunk.texture.target);
+ }
+
+ // Reset row length to 0, otherwise the stride would apply to all texture uploads.
+ if chunk.stride.is_some() {
+ device.gl.pixel_store_i(gl::UNPACK_ROW_LENGTH, 0 as _);
+ }
+ }
+}
+
+fn texels_to_u8_slice<T: Texel>(texels: &[T]) -> &[u8] {
+ unsafe {
+ slice::from_raw_parts(texels.as_ptr() as *const u8, texels.len() * mem::size_of::<T>())
+ }
+}
diff --git a/gfx/wr/webrender/src/device/mod.rs b/gfx/wr/webrender/src/device/mod.rs
new file mode 100644
index 0000000000..21684dea3e
--- /dev/null
+++ b/gfx/wr/webrender/src/device/mod.rs
@@ -0,0 +1,9 @@
+/* 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/. */
+
+mod gl;
+pub mod query_gl;
+
+pub use self::gl::*;
+pub use self::query_gl as query;
diff --git a/gfx/wr/webrender/src/device/query_gl.rs b/gfx/wr/webrender/src/device/query_gl.rs
new file mode 100644
index 0000000000..c7fd9a9070
--- /dev/null
+++ b/gfx/wr/webrender/src/device/query_gl.rs
@@ -0,0 +1,318 @@
+/* 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 gleam::gl;
+use std::mem;
+use std::rc::Rc;
+
+use crate::device::GpuFrameId;
+use crate::profiler::GpuProfileTag;
+
+#[derive(Copy, Clone, Debug)]
+pub enum GpuDebugMethod {
+ None,
+ MarkerEXT,
+ KHR,
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuTimer {
+ pub tag: GpuProfileTag,
+ pub time_ns: u64,
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuSampler {
+ pub tag: GpuProfileTag,
+ pub count: u64,
+}
+
+pub struct QuerySet<T> {
+ set: Vec<gl::GLuint>,
+ data: Vec<T>,
+ pending: gl::GLuint,
+}
+
+impl<T> QuerySet<T> {
+ fn new() -> Self {
+ QuerySet {
+ set: Vec::new(),
+ data: Vec::new(),
+ pending: 0,
+ }
+ }
+
+ fn reset(&mut self) {
+ self.data.clear();
+ self.pending = 0;
+ }
+
+ fn add(&mut self, value: T) -> Option<gl::GLuint> {
+ assert_eq!(self.pending, 0);
+ self.set.get(self.data.len()).cloned().map(|query_id| {
+ self.data.push(value);
+ self.pending = query_id;
+ query_id
+ })
+ }
+
+ fn take<F: Fn(&mut T, gl::GLuint)>(&mut self, fun: F) -> Vec<T> {
+ let mut data = mem::replace(&mut self.data, Vec::new());
+ for (value, &query) in data.iter_mut().zip(self.set.iter()) {
+ fun(value, query)
+ }
+ data
+ }
+}
+
+pub struct GpuFrameProfile {
+ gl: Rc<dyn gl::Gl>,
+ timers: QuerySet<GpuTimer>,
+ samplers: QuerySet<GpuSampler>,
+ frame_id: GpuFrameId,
+ inside_frame: bool,
+ debug_method: GpuDebugMethod,
+}
+
+impl GpuFrameProfile {
+ fn new(gl: Rc<dyn gl::Gl>, debug_method: GpuDebugMethod) -> Self {
+ GpuFrameProfile {
+ gl,
+ timers: QuerySet::new(),
+ samplers: QuerySet::new(),
+ frame_id: GpuFrameId::new(0),
+ inside_frame: false,
+ debug_method
+ }
+ }
+
+ fn enable_timers(&mut self, count: i32) {
+ self.timers.set = self.gl.gen_queries(count);
+ }
+
+ fn disable_timers(&mut self) {
+ if !self.timers.set.is_empty() {
+ self.gl.delete_queries(&self.timers.set);
+ }
+ self.timers.set = Vec::new();
+ }
+
+ fn enable_samplers(&mut self, count: i32) {
+ self.samplers.set = self.gl.gen_queries(count);
+ }
+
+ fn disable_samplers(&mut self) {
+ if !self.samplers.set.is_empty() {
+ self.gl.delete_queries(&self.samplers.set);
+ }
+ self.samplers.set = Vec::new();
+ }
+
+ fn begin_frame(&mut self, frame_id: GpuFrameId) {
+ self.frame_id = frame_id;
+ self.timers.reset();
+ self.samplers.reset();
+ self.inside_frame = true;
+ }
+
+ fn end_frame(&mut self) {
+ self.finish_timer();
+ self.finish_sampler();
+ self.inside_frame = false;
+ }
+
+ fn finish_timer(&mut self) {
+ debug_assert!(self.inside_frame);
+ if self.timers.pending != 0 {
+ self.gl.end_query(gl::TIME_ELAPSED);
+ self.timers.pending = 0;
+ }
+ }
+
+ fn finish_sampler(&mut self) {
+ debug_assert!(self.inside_frame);
+ if self.samplers.pending != 0 {
+ self.gl.end_query(gl::SAMPLES_PASSED);
+ self.samplers.pending = 0;
+ }
+ }
+
+ fn start_timer(&mut self, tag: GpuProfileTag) -> GpuTimeQuery {
+ self.finish_timer();
+
+ let marker = GpuMarker::new(&self.gl, tag.label, self.debug_method);
+
+ if let Some(query) = self.timers.add(GpuTimer { tag, time_ns: 0 }) {
+ self.gl.begin_query(gl::TIME_ELAPSED, query);
+ }
+
+ GpuTimeQuery(marker)
+ }
+
+ fn start_sampler(&mut self, tag: GpuProfileTag) -> GpuSampleQuery {
+ self.finish_sampler();
+
+ if let Some(query) = self.samplers.add(GpuSampler { tag, count: 0 }) {
+ self.gl.begin_query(gl::SAMPLES_PASSED, query);
+ }
+
+ GpuSampleQuery
+ }
+
+ fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer>, Vec<GpuSampler>) {
+ debug_assert!(!self.inside_frame);
+ let gl = &self.gl;
+
+ (
+ self.frame_id,
+ self.timers.take(|timer, query| {
+ timer.time_ns = gl.get_query_object_ui64v(query, gl::QUERY_RESULT)
+ }),
+ self.samplers.take(|sampler, query| {
+ sampler.count = gl.get_query_object_ui64v(query, gl::QUERY_RESULT)
+ }),
+ )
+ }
+}
+
+impl Drop for GpuFrameProfile {
+ fn drop(&mut self) {
+ self.disable_timers();
+ self.disable_samplers();
+ }
+}
+
+const NUM_PROFILE_FRAMES: usize = 4;
+
+pub struct GpuProfiler {
+ gl: Rc<dyn gl::Gl>,
+ frames: [GpuFrameProfile; NUM_PROFILE_FRAMES],
+ next_frame: usize,
+ debug_method: GpuDebugMethod
+}
+
+impl GpuProfiler {
+ pub fn new(gl: Rc<dyn gl::Gl>, debug_method: GpuDebugMethod) -> Self {
+ let f = || GpuFrameProfile::new(Rc::clone(&gl), debug_method);
+
+ let frames = [f(), f(), f(), f()];
+ GpuProfiler {
+ gl,
+ next_frame: 0,
+ frames,
+ debug_method
+ }
+ }
+
+ pub fn enable_timers(&mut self) {
+ const MAX_TIMERS_PER_FRAME: i32 = 256;
+
+ for frame in &mut self.frames {
+ frame.enable_timers(MAX_TIMERS_PER_FRAME);
+ }
+ }
+
+ pub fn disable_timers(&mut self) {
+ for frame in &mut self.frames {
+ frame.disable_timers();
+ }
+ }
+
+ pub fn enable_samplers(&mut self) {
+ const MAX_SAMPLERS_PER_FRAME: i32 = 16;
+ if cfg!(target_os = "macos") {
+ warn!("Expect macOS driver bugs related to sample queries")
+ }
+
+ for frame in &mut self.frames {
+ frame.enable_samplers(MAX_SAMPLERS_PER_FRAME);
+ }
+ }
+
+ pub fn disable_samplers(&mut self) {
+ for frame in &mut self.frames {
+ frame.disable_samplers();
+ }
+ }
+
+ pub fn build_samples(&mut self) -> (GpuFrameId, Vec<GpuTimer>, Vec<GpuSampler>) {
+ self.frames[self.next_frame].build_samples()
+ }
+
+ pub fn begin_frame(&mut self, frame_id: GpuFrameId) {
+ self.frames[self.next_frame].begin_frame(frame_id);
+ }
+
+ pub fn end_frame(&mut self) {
+ self.frames[self.next_frame].end_frame();
+ self.next_frame = (self.next_frame + 1) % self.frames.len();
+ }
+
+ pub fn start_timer(&mut self, tag: GpuProfileTag) -> GpuTimeQuery {
+ self.frames[self.next_frame].start_timer(tag)
+ }
+
+ pub fn start_sampler(&mut self, tag: GpuProfileTag) -> GpuSampleQuery {
+ self.frames[self.next_frame].start_sampler(tag)
+ }
+
+ pub fn finish_sampler(&mut self, _sampler: GpuSampleQuery) {
+ self.frames[self.next_frame].finish_sampler()
+ }
+
+ pub fn start_marker(&mut self, label: &str) -> GpuMarker {
+ GpuMarker::new(&self.gl, label, self.debug_method)
+ }
+
+ pub fn place_marker(&mut self, label: &str) {
+ GpuMarker::fire(&self.gl, label, self.debug_method)
+ }
+}
+
+#[must_use]
+pub struct GpuMarker {
+ gl: Option<(Rc<dyn gl::Gl>, GpuDebugMethod)>,
+}
+
+impl GpuMarker {
+ fn new(gl: &Rc<dyn gl::Gl>, message: &str, debug_method: GpuDebugMethod) -> Self {
+ let gl = match debug_method {
+ GpuDebugMethod::KHR => {
+ gl.push_debug_group_khr(gl::DEBUG_SOURCE_APPLICATION, 0, message);
+ Some((Rc::clone(gl), debug_method))
+ },
+ GpuDebugMethod::MarkerEXT => {
+ gl.push_group_marker_ext(message);
+ Some((Rc::clone(gl), debug_method))
+ },
+ GpuDebugMethod::None => None,
+ };
+ GpuMarker { gl }
+ }
+
+ fn fire(gl: &Rc<dyn gl::Gl>, message: &str, debug_method: GpuDebugMethod) {
+ match debug_method {
+ GpuDebugMethod::KHR => gl.debug_message_insert_khr(gl::DEBUG_SOURCE_APPLICATION, gl::DEBUG_TYPE_MARKER, 0, gl::DEBUG_SEVERITY_NOTIFICATION, message),
+ GpuDebugMethod::MarkerEXT => gl.insert_event_marker_ext(message),
+ GpuDebugMethod::None => {}
+ };
+ }
+}
+
+impl Drop for GpuMarker {
+ fn drop(&mut self) {
+ if let Some((ref gl, debug_method)) = self.gl {
+ match debug_method {
+ GpuDebugMethod::KHR => gl.pop_debug_group_khr(),
+ GpuDebugMethod::MarkerEXT => gl.pop_group_marker_ext(),
+ GpuDebugMethod::None => {}
+ };
+ }
+ }
+}
+
+#[must_use]
+pub struct GpuTimeQuery(GpuMarker);
+#[must_use]
+pub struct GpuSampleQuery;
diff --git a/gfx/wr/webrender/src/ellipse.rs b/gfx/wr/webrender/src/ellipse.rs
new file mode 100644
index 0000000000..fac6765984
--- /dev/null
+++ b/gfx/wr/webrender/src/ellipse.rs
@@ -0,0 +1,187 @@
+/* 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 api::units::*;
+use euclid::Size2D;
+use std::f32::consts::FRAC_PI_2;
+
+
+/// Number of steps to integrate arc length over.
+const STEP_COUNT: usize = 20;
+
+/// Represents an ellipse centred at a local space origin.
+#[derive(Debug, Clone)]
+pub struct Ellipse<U> {
+ pub radius: Size2D<f32, U>,
+ pub total_arc_length: f32,
+}
+
+impl<U> Ellipse<U> {
+ pub fn new(radius: Size2D<f32, U>) -> Ellipse<U> {
+ // Approximate the total length of the first quadrant of this ellipse.
+ let total_arc_length = get_simpson_length(FRAC_PI_2, radius.width, radius.height);
+
+ Ellipse {
+ radius,
+ total_arc_length,
+ }
+ }
+
+ /// Binary search to estimate the angle of an ellipse
+ /// for a given arc length. This only searches over the
+ /// first quadrant of an ellipse.
+ pub fn find_angle_for_arc_length(&self, arc_length: f32) -> f32 {
+ // Clamp arc length to [0, pi].
+ let arc_length = arc_length.max(0.0).min(self.total_arc_length);
+
+ let epsilon = 0.01;
+ let mut low = 0.0;
+ let mut high = FRAC_PI_2;
+ let mut theta = 0.0;
+ let mut new_low = 0.0;
+ let mut new_high = FRAC_PI_2;
+
+ while low <= high {
+ theta = 0.5 * (low + high);
+ let length = get_simpson_length(theta, self.radius.width, self.radius.height);
+
+ if (length - arc_length).abs() < epsilon {
+ break;
+ } else if length < arc_length {
+ new_low = theta;
+ } else {
+ new_high = theta;
+ }
+
+ // If we have stopped moving down the arc, the answer that we have is as good as
+ // it is going to get. We break to avoid going into an infinite loop.
+ if new_low == low && new_high == high {
+ break;
+ }
+
+ high = new_high;
+ low = new_low;
+ }
+
+ theta
+ }
+
+ /// Get a point and tangent on this ellipse from a given angle.
+ /// This only works for the first quadrant of the ellipse.
+ pub fn get_point_and_tangent(&self, theta: f32) -> (LayoutPoint, LayoutPoint) {
+ let (sin_theta, cos_theta) = theta.sin_cos();
+ let point = LayoutPoint::new(
+ self.radius.width * cos_theta,
+ self.radius.height * sin_theta,
+ );
+ let tangent = LayoutPoint::new(
+ -self.radius.width * sin_theta,
+ self.radius.height * cos_theta,
+ );
+ (point, tangent)
+ }
+
+ pub fn contains(&self, point: LayoutPoint) -> bool {
+ self.signed_distance(point.to_vector()) <= 0.0
+ }
+
+ /// Find the signed distance from this ellipse given a point.
+ /// Taken from http://www.iquilezles.org/www/articles/ellipsedist/ellipsedist.htm
+ fn signed_distance(&self, point: LayoutVector2D) -> f32 {
+ // This algorithm fails for circles, so we handle them here.
+ if self.radius.width == self.radius.height {
+ return point.length() - self.radius.width;
+ }
+
+ let mut p = LayoutVector2D::new(point.x.abs(), point.y.abs());
+ let mut ab = self.radius.to_vector();
+ if p.x > p.y {
+ p = p.yx();
+ ab = ab.yx();
+ }
+
+ let l = ab.y * ab.y - ab.x * ab.x;
+
+ let m = ab.x * p.x / l;
+ let n = ab.y * p.y / l;
+ let m2 = m * m;
+ let n2 = n * n;
+
+ let c = (m2 + n2 - 1.0) / 3.0;
+ let c3 = c * c * c;
+
+ let q = c3 + m2 * n2 * 2.0;
+ let d = c3 + m2 * n2;
+ let g = m + m * n2;
+
+ let co = if d < 0.0 {
+ let p = (q / c3).acos() / 3.0;
+ let s = p.cos();
+ let t = p.sin() * (3.0_f32).sqrt();
+ let rx = (-c * (s + t + 2.0) + m2).sqrt();
+ let ry = (-c * (s - t + 2.0) + m2).sqrt();
+ (ry + l.signum() * rx + g.abs() / (rx * ry) - m) / 2.0
+ } else {
+ let h = 2.0 * m * n * d.sqrt();
+ let s = (q + h).signum() * (q + h).abs().powf(1.0 / 3.0);
+ let u = (q - h).signum() * (q - h).abs().powf(1.0 / 3.0);
+ let rx = -s - u - c * 4.0 + 2.0 * m2;
+ let ry = (s - u) * (3.0_f32).sqrt();
+ let rm = (rx * rx + ry * ry).sqrt();
+ let p = ry / (rm - rx).sqrt();
+ (p + 2.0 * g / rm - m) / 2.0
+ };
+
+ let si = (1.0 - co * co).sqrt();
+ let r = LayoutVector2D::new(ab.x * co, ab.y * si);
+ (r - p).length() * (p.y - r.y).signum()
+ }
+}
+
+/// Use Simpsons rule to approximate the arc length of
+/// part of an ellipse. Note that this only works over
+/// the range of [0, pi/2].
+// TODO(gw): This is a simplistic way to estimate the
+// arc length of an ellipse segment. We can probably use
+// a faster / more accurate method!
+fn get_simpson_length(theta: f32, rx: f32, ry: f32) -> f32 {
+ let df = theta / STEP_COUNT as f32;
+ let mut sum = 0.0;
+
+ for i in 0 .. (STEP_COUNT + 1) {
+ let (sin_theta, cos_theta) = (i as f32 * df).sin_cos();
+ let a = rx * sin_theta;
+ let b = ry * cos_theta;
+ let y = (a * a + b * b).sqrt();
+ let q = if i == 0 || i == STEP_COUNT {
+ 1.0
+ } else if i % 2 == 0 {
+ 2.0
+ } else {
+ 4.0
+ };
+
+ sum += q * y;
+ }
+
+ (df / 3.0) * sum
+}
+
+#[cfg(test)]
+pub mod test {
+ use super::*;
+
+ #[test]
+ fn find_angle_for_arc_length_for_long_eclipse() {
+ // Ensure that finding the angle on giant ellipses produces and answer and
+ // doesn't send us into an infinite loop.
+ let ellipse = Ellipse::new(LayoutSize::new(57500.0, 25.0));
+ let _ = ellipse.find_angle_for_arc_length(55674.53);
+ assert!(true);
+
+ let ellipse = Ellipse::new(LayoutSize::new(25.0, 57500.0));
+ let _ = ellipse.find_angle_for_arc_length(55674.53);
+ assert!(true);
+ }
+}
diff --git a/gfx/wr/webrender/src/filterdata.rs b/gfx/wr/webrender/src/filterdata.rs
new file mode 100644
index 0000000000..d399b2252e
--- /dev/null
+++ b/gfx/wr/webrender/src/filterdata.rs
@@ -0,0 +1,219 @@
+/* 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 std::{hash};
+use crate::gpu_cache::{GpuCacheHandle};
+use crate::frame_builder::FrameBuildingState;
+use crate::gpu_cache::GpuDataRequest;
+use crate::intern;
+use api::{ComponentTransferFuncType};
+
+
+pub type FilterDataHandle = intern::Handle<FilterDataIntern>;
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SFilterDataComponent {
+ Identity,
+ Table(Vec<f32>),
+ Discrete(Vec<f32>),
+ Linear(f32, f32),
+ Gamma(f32, f32, f32),
+}
+
+impl Eq for SFilterDataComponent {}
+
+impl hash::Hash for SFilterDataComponent {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ match self {
+ SFilterDataComponent::Identity => {
+ 0.hash(state);
+ }
+ SFilterDataComponent::Table(values) => {
+ 1.hash(state);
+ values.len().hash(state);
+ for val in values {
+ val.to_bits().hash(state);
+ }
+ }
+ SFilterDataComponent::Discrete(values) => {
+ 2.hash(state);
+ values.len().hash(state);
+ for val in values {
+ val.to_bits().hash(state);
+ }
+ }
+ SFilterDataComponent::Linear(a, b) => {
+ 3.hash(state);
+ a.to_bits().hash(state);
+ b.to_bits().hash(state);
+ }
+ SFilterDataComponent::Gamma(a, b, c) => {
+ 4.hash(state);
+ a.to_bits().hash(state);
+ b.to_bits().hash(state);
+ c.to_bits().hash(state);
+ }
+ }
+ }
+}
+
+impl SFilterDataComponent {
+ pub fn to_int(&self) -> u32 {
+ match self {
+ SFilterDataComponent::Identity => 0,
+ SFilterDataComponent::Table(_) => 1,
+ SFilterDataComponent::Discrete(_) => 2,
+ SFilterDataComponent::Linear(_, _) => 3,
+ SFilterDataComponent::Gamma(_, _, _) => 4,
+ }
+ }
+
+ pub fn from_functype_values(
+ func_type: ComponentTransferFuncType,
+ values: &[f32],
+ ) -> SFilterDataComponent {
+ match func_type {
+ ComponentTransferFuncType::Identity => SFilterDataComponent::Identity,
+ ComponentTransferFuncType::Table => SFilterDataComponent::Table(values.to_vec()),
+ ComponentTransferFuncType::Discrete => SFilterDataComponent::Discrete(values.to_vec()),
+ ComponentTransferFuncType::Linear => SFilterDataComponent::Linear(values[0], values[1]),
+ ComponentTransferFuncType::Gamma => SFilterDataComponent::Gamma(values[0], values[1], values[2]),
+ }
+ }
+}
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SFilterData {
+ pub r_func: SFilterDataComponent,
+ pub g_func: SFilterDataComponent,
+ pub b_func: SFilterDataComponent,
+ pub a_func: SFilterDataComponent,
+}
+
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SFilterDataKey {
+ pub data: SFilterData,
+}
+
+impl intern::InternDebug for SFilterDataKey {}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct SFilterDataTemplate {
+ pub data: SFilterData,
+ pub gpu_cache_handle: GpuCacheHandle,
+}
+
+impl From<SFilterDataKey> for SFilterDataTemplate {
+ fn from(item: SFilterDataKey) -> Self {
+ SFilterDataTemplate {
+ data: item.data,
+ gpu_cache_handle: GpuCacheHandle::new(),
+ }
+ }
+}
+
+impl SFilterData {
+ pub fn is_identity(&self) -> bool {
+ self.r_func == SFilterDataComponent::Identity
+ && self.g_func == SFilterDataComponent::Identity
+ && self.b_func == SFilterDataComponent::Identity
+ && self.a_func == SFilterDataComponent::Identity
+ }
+
+ pub fn update(&self, mut request: GpuDataRequest) {
+ push_component_transfer_data(&self.r_func, &mut request);
+ push_component_transfer_data(&self.g_func, &mut request);
+ push_component_transfer_data(&self.b_func, &mut request);
+ push_component_transfer_data(&self.a_func, &mut request);
+ assert!(!self.is_identity());
+ }
+}
+
+impl SFilterDataTemplate {
+ /// Update the GPU cache for a given filter data template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(request) = frame_state.gpu_cache.request(&mut self.gpu_cache_handle) {
+ self.data.update(request);
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum FilterDataIntern {}
+
+impl intern::Internable for FilterDataIntern {
+ type Key = SFilterDataKey;
+ type StoreData = SFilterDataTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_FILTER_DATA;
+}
+
+fn push_component_transfer_data(
+ func_comp: &SFilterDataComponent,
+ request: &mut GpuDataRequest,
+) {
+ match func_comp {
+ SFilterDataComponent::Identity => {}
+ SFilterDataComponent::Table(values) |
+ SFilterDataComponent::Discrete(values) => {
+ // Push a 256 entry lookup table.
+ assert!(values.len() > 0);
+ for i in 0 .. 64 {
+ let mut arr = [0.0 ; 4];
+ for j in 0 .. 4 {
+ if (values.len() == 1) || (i == 63 && j == 3) {
+ arr[j] = values[values.len()-1];
+ } else {
+ let c = ((4*i + j) as f32)/255.0;
+ match func_comp {
+ SFilterDataComponent::Table(_) => {
+ let n = (values.len()-1) as f32;
+ let k = (n * c).floor() as u32;
+ let ku = k as usize;
+ assert!(ku < values.len()-1);
+ arr[j] = values[ku] + (c*n - (k as f32)) * (values[ku+1] - values[ku]);
+ }
+ SFilterDataComponent::Discrete(_) => {
+ let n = values.len() as f32;
+ let k = (n * c).floor() as usize;
+ assert!(k < values.len());
+ arr[j] = values[k];
+ }
+ SFilterDataComponent::Identity |
+ SFilterDataComponent::Linear(_,_) |
+ SFilterDataComponent::Gamma(_,_,_) => {
+ unreachable!();
+ }
+ }
+
+ }
+ }
+
+ request.push(arr);
+ }
+ }
+ SFilterDataComponent::Linear(a, b) => {
+ request.push([*a, *b, 0.0, 0.0]);
+ }
+ SFilterDataComponent::Gamma(a, b, c) => {
+ request.push([*a, *b, *c, 0.0]);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs
new file mode 100644
index 0000000000..24a64a49c9
--- /dev/null
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -0,0 +1,1106 @@
+/* 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 api::{ColorF, DebugFlags, FontRenderMode, PremultipliedColorF};
+use api::units::*;
+use crate::batch::{BatchBuilder, AlphaBatchBuilder, AlphaBatchContainer};
+use crate::clip::{ClipStore, ClipChainStack};
+use crate::spatial_tree::{SpatialTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
+use crate::composite::{CompositorKind, CompositeState, CompositeStatePreallocator};
+use crate::debug_item::DebugItem;
+use crate::frame_graph::{Pass, SubPassSurface};
+use crate::gpu_cache::{GpuCache, GpuCacheHandle};
+use crate::gpu_types::{PrimitiveHeaders, TransformPalette, ZBufferIdGenerator};
+use crate::gpu_types::TransformData;
+use crate::internal_types::{FastHashMap, PlaneSplitter};
+use crate::picture::{DirtyRegion, PictureUpdateState, SliceId, TileCacheInstance};
+use crate::picture::{SurfaceInfo, SurfaceIndex, ROOT_SURFACE_INDEX, SurfaceRenderTasks};
+use crate::picture::{BackdropKind, SubpixelMode, TileCacheLogger, RasterConfig, PictureCompositeMode};
+use crate::prepare::prepare_primitives;
+use crate::prim_store::{PictureIndex, PrimitiveDebugId};
+use crate::prim_store::{DeferredResolve};
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::{DataStores, FrameStamp, FrameId, ScratchBuffer};
+use crate::render_target::{RenderTarget, PictureCacheTarget, TextureCacheRenderTarget};
+use crate::render_target::{RenderTargetContext, RenderTargetKind, AlphaRenderTarget, ColorRenderTarget};
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraph};
+use crate::render_task_graph::{RenderPass, RenderTaskGraphBuilder};
+use crate::render_task::{RenderTaskLocation, RenderTaskKind, StaticRenderTaskSurface};
+use crate::resource_cache::{ResourceCache};
+use crate::scene::{BuiltScene, SceneProperties};
+use crate::space::SpaceMapper;
+use crate::segment::SegmentBuilder;
+use std::{f32, mem};
+use crate::util::{VecHelper, Recycler, Preallocator};
+use crate::visibility::{update_primitive_visibility, FrameVisibilityState, FrameVisibilityContext};
+
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ChasePrimitive {
+ Nothing,
+ Id(PrimitiveDebugId),
+ LocalRect(LayoutRect),
+}
+
+impl Default for ChasePrimitive {
+ fn default() -> Self {
+ ChasePrimitive::Nothing
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameBuilderConfig {
+ pub default_font_render_mode: FontRenderMode,
+ pub dual_source_blending_is_supported: bool,
+ pub dual_source_blending_is_enabled: bool,
+ pub chase_primitive: ChasePrimitive,
+ /// True if we're running tests (i.e. via wrench).
+ pub testing: bool,
+ pub gpu_supports_fast_clears: bool,
+ pub gpu_supports_advanced_blend: bool,
+ pub advanced_blend_is_coherent: bool,
+ pub gpu_supports_render_target_partial_update: bool,
+ pub batch_lookback_count: usize,
+ pub background_color: Option<ColorF>,
+ pub compositor_kind: CompositorKind,
+ pub tile_size_override: Option<DeviceIntSize>,
+ pub max_depth_ids: i32,
+ pub max_target_size: i32,
+ pub force_invalidation: bool,
+}
+
+/// A set of common / global resources that are retained between
+/// new display lists, such that any GPU cache handles can be
+/// persisted even when a new display list arrives.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct FrameGlobalResources {
+ /// The image shader block for the most common / default
+ /// set of image parameters (color white, stretch == rect.size).
+ pub default_image_handle: GpuCacheHandle,
+
+ /// A GPU cache config for drawing transparent rectangle primitives.
+ /// This is used to 'cut out' overlay tiles where a compositor
+ /// surface exists.
+ pub default_transparent_rect_handle: GpuCacheHandle,
+}
+
+impl FrameGlobalResources {
+ pub fn empty() -> Self {
+ FrameGlobalResources {
+ default_image_handle: GpuCacheHandle::new(),
+ default_transparent_rect_handle: GpuCacheHandle::new(),
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ gpu_cache: &mut GpuCache,
+ ) {
+ if let Some(mut request) = gpu_cache.request(&mut self.default_image_handle) {
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ -1.0, // -ve means use prim rect for stretch size
+ 0.0,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ if let Some(mut request) = gpu_cache.request(&mut self.default_transparent_rect_handle) {
+ request.push(PremultipliedColorF::TRANSPARENT);
+ }
+ }
+}
+
+pub struct FrameScratchBuffer {
+ surfaces: Vec<SurfaceInfo>,
+ dirty_region_stack: Vec<DirtyRegion>,
+ surface_stack: Vec<SurfaceIndex>,
+ clip_chain_stack: ClipChainStack,
+}
+
+impl Default for FrameScratchBuffer {
+ fn default() -> Self {
+ FrameScratchBuffer {
+ surfaces: Vec::new(),
+ dirty_region_stack: Vec::new(),
+ surface_stack: Vec::new(),
+ clip_chain_stack: ClipChainStack::new(),
+ }
+ }
+}
+
+impl FrameScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.surfaces.clear();
+ self.dirty_region_stack.clear();
+ self.surface_stack.clear();
+ self.clip_chain_stack.clear();
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.surfaces);
+ // Don't call recycle on the stacks because the reycler's
+ // role is to get rid of allocations when the capacity
+ // is much larger than the lengths. with stacks the
+ // length varies through the frame but is supposedly
+ // back to zero by the end so we would always throw the
+ // allocation away.
+ }
+}
+
+/// Produces the frames that are sent to the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct FrameBuilder {
+ pub globals: FrameGlobalResources,
+ #[cfg_attr(feature = "capture", serde(skip))]
+ prim_headers_prealloc: Preallocator,
+ #[cfg_attr(feature = "capture", serde(skip))]
+ composite_state_prealloc: CompositeStatePreallocator,
+}
+
+pub struct FrameBuildingContext<'a> {
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub scene_properties: &'a SceneProperties,
+ pub global_screen_world_rect: WorldRect,
+ pub spatial_tree: &'a SpatialTree,
+ pub max_local_clip: LayoutRect,
+ pub debug_flags: DebugFlags,
+ pub fb_config: &'a FrameBuilderConfig,
+}
+
+pub struct FrameBuildingState<'a> {
+ pub rg_builder: &'a mut RenderTaskGraphBuilder,
+ pub clip_store: &'a mut ClipStore,
+ pub resource_cache: &'a mut ResourceCache,
+ pub gpu_cache: &'a mut GpuCache,
+ pub transforms: &'a mut TransformPalette,
+ pub segment_builder: SegmentBuilder,
+ pub surfaces: &'a mut Vec<SurfaceInfo>,
+ pub dirty_region_stack: Vec<DirtyRegion>,
+ pub composite_state: &'a mut CompositeState,
+ pub num_visible_primitives: u32,
+}
+
+impl<'a> FrameBuildingState<'a> {
+ /// Retrieve the current dirty region during primitive traversal.
+ pub fn current_dirty_region(&self) -> &DirtyRegion {
+ self.dirty_region_stack.last().unwrap()
+ }
+
+ /// Push a new dirty region for child primitives to cull / clip against.
+ pub fn push_dirty_region(&mut self, region: DirtyRegion) {
+ self.dirty_region_stack.push(region);
+ }
+
+ /// Pop the top dirty region from the stack.
+ pub fn pop_dirty_region(&mut self) {
+ self.dirty_region_stack.pop().unwrap();
+ }
+
+ /// Initialize render tasks for a surface that is tiled (currently applies
+ /// only to picture cache surfaces).
+ pub fn init_surface_tiled(
+ &mut self,
+ surface_index: SurfaceIndex,
+ tasks: Vec<RenderTaskId>,
+ ) {
+ let surface = &mut self.surfaces[surface_index.0];
+ assert!(surface.render_tasks.is_none());
+ surface.render_tasks = Some(SurfaceRenderTasks::Tiled(tasks));
+ }
+
+ /// Initialize render tasks for a simple surface, that contains only a
+ /// single render task.
+ pub fn init_surface(
+ &mut self,
+ surface_index: SurfaceIndex,
+ task_id: RenderTaskId,
+ parent_surface_index: SurfaceIndex,
+ ) {
+ let surface = &mut self.surfaces[surface_index.0];
+ assert!(surface.render_tasks.is_none());
+ surface.render_tasks = Some(SurfaceRenderTasks::Simple(task_id));
+
+ self.add_child_render_task(
+ parent_surface_index,
+ task_id,
+ );
+ }
+
+ /// Initialize render tasks for a surface that is made up of a chain of
+ /// render tasks, where the final output render task is different than the
+ /// input render task (for example, a blur pass on a picture).
+ pub fn init_surface_chain(
+ &mut self,
+ surface_index: SurfaceIndex,
+ root_task_id: RenderTaskId,
+ port_task_id: RenderTaskId,
+ parent_surface_index: SurfaceIndex,
+ ) {
+ let surface = &mut self.surfaces[surface_index.0];
+ assert!(surface.render_tasks.is_none());
+ surface.render_tasks = Some(SurfaceRenderTasks::Chained { root_task_id, port_task_id });
+
+ self.add_child_render_task(
+ parent_surface_index,
+ root_task_id,
+ );
+ }
+
+ /// Add a render task as a dependency of a given surface.
+ pub fn add_child_render_task(
+ &mut self,
+ surface_index: SurfaceIndex,
+ child_task_id: RenderTaskId,
+ ) {
+ add_child_render_task(
+ surface_index,
+ child_task_id,
+ self.surfaces,
+ self.rg_builder,
+ );
+ }
+}
+
+/// Immutable context of a picture when processing children.
+#[derive(Debug)]
+pub struct PictureContext {
+ pub pic_index: PictureIndex,
+ pub apply_local_clip_rect: bool,
+ pub is_passthrough: bool,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ pub raster_spatial_node_index: SpatialNodeIndex,
+ /// The surface that this picture will render on.
+ pub surface_index: SurfaceIndex,
+ pub dirty_region_count: usize,
+ pub subpixel_mode: SubpixelMode,
+}
+
+/// Mutable state of a picture that gets modified when
+/// the children are processed.
+pub struct PictureState {
+ pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
+ pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
+ pub map_pic_to_raster: SpaceMapper<PicturePixel, RasterPixel>,
+ pub map_raster_to_world: SpaceMapper<RasterPixel, WorldPixel>,
+ /// If the plane splitter, the primitives get added to it instead of
+ /// batching into their parent pictures.
+ pub plane_splitter: Option<PlaneSplitter>,
+}
+
+impl FrameBuilder {
+ pub fn new() -> Self {
+ FrameBuilder {
+ globals: FrameGlobalResources::empty(),
+ prim_headers_prealloc: Preallocator::new(0),
+ composite_state_prealloc: CompositeStatePreallocator::default(),
+ }
+ }
+
+ /// Compute the contribution (bounding rectangles, and resources) of layers and their
+ /// primitives in screen space.
+ fn build_layer_screen_rects_and_cull_layers(
+ &mut self,
+ scene: &mut BuiltScene,
+ global_screen_world_rect: WorldRect,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ transform_palette: &mut TransformPalette,
+ data_stores: &mut DataStores,
+ scratch: &mut ScratchBuffer,
+ debug_flags: DebugFlags,
+ composite_state: &mut CompositeState,
+ tile_cache_logger: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ profile: &mut TransactionProfile,
+ ) {
+ profile_scope!("build_layer_screen_rects_and_cull_layers");
+
+ scratch.begin_frame();
+
+ let root_spatial_node_index = scene.spatial_tree.root_reference_frame_index();
+
+ const MAX_CLIP_COORD: f32 = 1.0e9;
+
+ let frame_context = FrameBuildingContext {
+ global_device_pixel_scale,
+ scene_properties,
+ global_screen_world_rect,
+ spatial_tree: &scene.spatial_tree,
+ max_local_clip: LayoutRect::new(
+ LayoutPoint::new(-MAX_CLIP_COORD, -MAX_CLIP_COORD),
+ LayoutSize::new(2.0 * MAX_CLIP_COORD, 2.0 * MAX_CLIP_COORD),
+ ),
+ debug_flags,
+ fb_config: &scene.config,
+ };
+
+ // Construct a dummy root surface, that represents the
+ // main framebuffer surface.
+ let root_surface = SurfaceInfo::new(
+ ROOT_SPATIAL_NODE_INDEX,
+ ROOT_SPATIAL_NODE_INDEX,
+ 0.0,
+ global_screen_world_rect,
+ &scene.spatial_tree,
+ global_device_pixel_scale,
+ (1.0, 1.0),
+ );
+ let mut surfaces = scratch.frame.surfaces.take();
+ surfaces.push(root_surface);
+
+ // The first major pass of building a frame is to walk the picture
+ // tree. This pass must be quick (it should never touch individual
+ // primitives). For now, all we do here is determine which pictures
+ // will create surfaces. In the future, this will be expanded to
+ // set up render tasks, determine scaling of surfaces, and detect
+ // which surfaces have valid cached surfaces that don't need to
+ // be rendered this frame.
+ for pic_index in &scene.tile_cache_pictures {
+ PictureUpdateState::update_all(
+ &mut scratch.picture,
+ &mut surfaces,
+ *pic_index,
+ &mut scene.prim_store.pictures,
+ &frame_context,
+ gpu_cache,
+ &scene.clip_store,
+ data_stores,
+ );
+ }
+
+ {
+ profile_scope!("UpdateVisibility");
+ profile_marker!("UpdateVisibility");
+ profile.start_time(profiler::FRAME_VISIBILITY_TIME);
+
+ let visibility_context = FrameVisibilityContext {
+ global_device_pixel_scale,
+ spatial_tree: &scene.spatial_tree,
+ global_screen_world_rect,
+ surfaces: &mut surfaces,
+ debug_flags,
+ scene_properties,
+ config: scene.config,
+ };
+
+ let mut visibility_state = FrameVisibilityState {
+ clip_chain_stack: scratch.frame.clip_chain_stack.take(),
+ surface_stack: scratch.frame.surface_stack.take(),
+ resource_cache,
+ gpu_cache,
+ clip_store: &mut scene.clip_store,
+ scratch,
+ tile_cache: None,
+ data_stores,
+ composite_state,
+ };
+
+ for pic_index in &scene.tile_cache_pictures {
+ update_primitive_visibility(
+ &mut scene.prim_store,
+ *pic_index,
+ ROOT_SURFACE_INDEX,
+ &global_screen_world_rect,
+ &visibility_context,
+ &mut visibility_state,
+ tile_caches,
+ );
+ }
+
+ visibility_state.scratch.frame.clip_chain_stack = visibility_state.clip_chain_stack.take();
+ visibility_state.scratch.frame.surface_stack = visibility_state.surface_stack.take();
+
+ profile.end_time(profiler::FRAME_VISIBILITY_TIME);
+ }
+
+ profile.start_time(profiler::FRAME_PREPARE_TIME);
+
+ let mut frame_state = FrameBuildingState {
+ rg_builder,
+ clip_store: &mut scene.clip_store,
+ resource_cache,
+ gpu_cache,
+ transforms: transform_palette,
+ segment_builder: SegmentBuilder::new(),
+ surfaces: &mut surfaces,
+ dirty_region_stack: scratch.frame.dirty_region_stack.take(),
+ composite_state,
+ num_visible_primitives: 0,
+ };
+
+ // Push a default dirty region which culls primitives
+ // against the screen world rect, in absence of any
+ // other dirty regions.
+ let mut default_dirty_region = DirtyRegion::new(
+ ROOT_SPATIAL_NODE_INDEX,
+ );
+ default_dirty_region.add_dirty_region(
+ frame_context.global_screen_world_rect.cast_unit(),
+ frame_context.spatial_tree,
+ );
+ frame_state.push_dirty_region(default_dirty_region);
+
+ for pic_index in &scene.tile_cache_pictures {
+ if let Some((pic_context, mut pic_state, mut prim_list)) = scene
+ .prim_store
+ .pictures[pic_index.0]
+ .take_context(
+ *pic_index,
+ root_spatial_node_index,
+ root_spatial_node_index,
+ ROOT_SURFACE_INDEX,
+ &SubpixelMode::Allow,
+ &mut frame_state,
+ &frame_context,
+ &mut scratch.primitive,
+ tile_cache_logger,
+ tile_caches,
+ )
+ {
+ profile_marker!("PreparePrims");
+
+ prepare_primitives(
+ &mut scene.prim_store,
+ &mut prim_list,
+ &pic_context,
+ &mut pic_state,
+ &frame_context,
+ &mut frame_state,
+ data_stores,
+ &mut scratch.primitive,
+ tile_cache_logger,
+ tile_caches,
+ );
+
+ let pic = &mut scene.prim_store.pictures[pic_index.0];
+ pic.restore_context(
+ prim_list,
+ pic_context,
+ pic_state,
+ &mut frame_state,
+ );
+ }
+ }
+
+ tile_cache_logger.advance();
+ frame_state.pop_dirty_region();
+ profile.end_time(profiler::FRAME_PREPARE_TIME);
+ profile.set(profiler::VISIBLE_PRIMITIVES, frame_state.num_visible_primitives);
+
+ scratch.frame.dirty_region_stack = frame_state.dirty_region_stack.take();
+ scratch.frame.surfaces = surfaces.take();
+
+ {
+ profile_marker!("BlockOnResources");
+
+ resource_cache.block_until_all_resources_added(
+ gpu_cache,
+ profile,
+ );
+ }
+ }
+
+ pub fn build(
+ &mut self,
+ scene: &mut BuiltScene,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ stamp: FrameStamp,
+ global_device_pixel_scale: DevicePixelScale,
+ device_origin: DeviceIntPoint,
+ pan: WorldPoint,
+ scene_properties: &SceneProperties,
+ data_stores: &mut DataStores,
+ scratch: &mut ScratchBuffer,
+ debug_flags: DebugFlags,
+ tile_cache_logger: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ dirty_rects_are_valid: bool,
+ profile: &mut TransactionProfile,
+ ) -> Frame {
+ profile_scope!("build");
+ profile_marker!("BuildFrame");
+
+ profile.set(profiler::PRIMITIVES, scene.prim_store.prim_count());
+ profile.set(profiler::PICTURE_CACHE_SLICES, scene.tile_cache_config.picture_cache_slice_count);
+ resource_cache.begin_frame(stamp);
+ gpu_cache.begin_frame(stamp);
+
+ self.globals.update(gpu_cache);
+
+ scene.spatial_tree.update_tree(
+ pan,
+ global_device_pixel_scale,
+ scene_properties,
+ );
+ let mut transform_palette = scene.spatial_tree.build_transform_palette();
+ scene.clip_store.clear_old_instances();
+
+ rg_builder.begin_frame(stamp.frame_id());
+
+ let output_size = scene.output_rect.size.to_i32();
+ let screen_world_rect = (scene.output_rect.to_f32() / global_device_pixel_scale).round_out();
+
+ let mut composite_state = CompositeState::new(
+ scene.config.compositor_kind,
+ global_device_pixel_scale,
+ scene.config.max_depth_ids,
+ dirty_rects_are_valid,
+ );
+
+ self.composite_state_prealloc.preallocate(&mut composite_state);
+
+ self.build_layer_screen_rects_and_cull_layers(
+ scene,
+ screen_world_rect,
+ resource_cache,
+ gpu_cache,
+ rg_builder,
+ global_device_pixel_scale,
+ scene_properties,
+ &mut transform_palette,
+ data_stores,
+ scratch,
+ debug_flags,
+ &mut composite_state,
+ tile_cache_logger,
+ tile_caches,
+ profile,
+ );
+
+ profile.start_time(profiler::FRAME_BATCHING_TIME);
+
+ // Finish creating the frame graph and build it.
+ let render_tasks = rg_builder.end_frame(
+ resource_cache,
+ gpu_cache,
+ );
+
+ let mut passes = Vec::new();
+ let mut deferred_resolves = vec![];
+ let mut has_texture_cache_tasks = false;
+ let mut prim_headers = PrimitiveHeaders::new();
+ self.prim_headers_prealloc.preallocate_vec(&mut prim_headers.headers_int);
+ self.prim_headers_prealloc.preallocate_vec(&mut prim_headers.headers_float);
+
+ {
+ profile_marker!("Batching");
+
+ // Used to generated a unique z-buffer value per primitive.
+ let mut z_generator = ZBufferIdGenerator::new(scene.config.max_depth_ids);
+ let use_dual_source_blending = scene.config.dual_source_blending_is_enabled &&
+ scene.config.dual_source_blending_is_supported;
+
+ for pass in render_tasks.passes.iter().rev() {
+ let mut ctx = RenderTargetContext {
+ global_device_pixel_scale,
+ prim_store: &scene.prim_store,
+ resource_cache,
+ use_dual_source_blending,
+ use_advanced_blending: scene.config.gpu_supports_advanced_blend,
+ break_advanced_blend_batches: !scene.config.advanced_blend_is_coherent,
+ batch_lookback_count: scene.config.batch_lookback_count,
+ spatial_tree: &scene.spatial_tree,
+ data_stores,
+ surfaces: &scratch.frame.surfaces,
+ scratch: &mut scratch.primitive,
+ screen_world_rect,
+ globals: &self.globals,
+ tile_caches,
+ };
+
+ let pass = build_render_pass(
+ pass,
+ output_size,
+ &mut ctx,
+ gpu_cache,
+ &render_tasks,
+ &mut deferred_resolves,
+ &scene.clip_store,
+ &mut transform_palette,
+ &mut prim_headers,
+ &mut z_generator,
+ &mut composite_state,
+ scene.config.gpu_supports_fast_clears,
+ );
+
+ has_texture_cache_tasks |= !pass.texture_cache.is_empty();
+ has_texture_cache_tasks |= !pass.picture_cache.is_empty();
+
+ passes.push(pass);
+ }
+
+ let mut ctx = RenderTargetContext {
+ global_device_pixel_scale,
+ prim_store: &scene.prim_store,
+ resource_cache,
+ use_dual_source_blending,
+ use_advanced_blending: scene.config.gpu_supports_advanced_blend,
+ break_advanced_blend_batches: !scene.config.advanced_blend_is_coherent,
+ batch_lookback_count: scene.config.batch_lookback_count,
+ spatial_tree: &scene.spatial_tree,
+ data_stores,
+ surfaces: &scratch.frame.surfaces,
+ scratch: &mut scratch.primitive,
+ screen_world_rect,
+ globals: &self.globals,
+ tile_caches,
+ };
+
+ self.build_composite_pass(
+ scene,
+ &mut ctx,
+ gpu_cache,
+ &mut deferred_resolves,
+ &mut composite_state,
+ );
+ }
+
+ profile.end_time(profiler::FRAME_BATCHING_TIME);
+
+ let gpu_cache_frame_id = gpu_cache.end_frame(profile).frame_id();
+
+ resource_cache.end_frame(profile);
+
+ self.prim_headers_prealloc.record_vec(&mut prim_headers.headers_int);
+ self.composite_state_prealloc.record(&composite_state);
+
+ Frame {
+ device_rect: DeviceIntRect::new(
+ device_origin,
+ scene.output_rect.size,
+ ),
+ passes,
+ transform_palette: transform_palette.finish(),
+ render_tasks,
+ deferred_resolves,
+ gpu_cache_frame_id,
+ has_been_rendered: false,
+ has_texture_cache_tasks,
+ prim_headers,
+ debug_items: mem::replace(&mut scratch.primitive.debug_items, Vec::new()),
+ composite_state,
+ }
+ }
+
+ fn build_composite_pass(
+ &self,
+ scene: &BuiltScene,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ composite_state: &mut CompositeState,
+ ) {
+ for pic_index in &scene.tile_cache_pictures {
+ let pic = &ctx.prim_store.pictures[pic_index.0];
+
+ match pic.raster_config {
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ // Tile cache instances are added to the composite config, rather than
+ // directly added to batches. This allows them to be drawn with various
+ // present modes during render, such as partial present etc.
+ let tile_cache = &ctx.tile_caches[&slice_id];
+ let map_local_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ tile_cache.spatial_node_index,
+ ctx.screen_world_rect,
+ ctx.spatial_tree,
+ );
+ let world_clip_rect = map_local_to_world
+ .map(&tile_cache.local_clip_rect)
+ .expect("bug: unable to map clip rect");
+ let device_clip_rect = (world_clip_rect * ctx.global_device_pixel_scale).round();
+
+ composite_state.push_surface(
+ tile_cache,
+ device_clip_rect,
+ ctx.global_device_pixel_scale,
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ }
+ _ => {
+ panic!("bug: found a top-level prim that isn't a tile cache");
+ }
+ }
+ }
+ }
+}
+
+/// Processes this pass to prepare it for rendering.
+///
+/// Among other things, this allocates output regions for each of our tasks
+/// (added via `add_render_task`) in a RenderTarget and assigns it into that
+/// target.
+pub fn build_render_pass(
+ src_pass: &Pass,
+ screen_size: DeviceIntSize,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ prim_headers: &mut PrimitiveHeaders,
+ z_generator: &mut ZBufferIdGenerator,
+ composite_state: &mut CompositeState,
+ gpu_supports_fast_clears: bool,
+) -> RenderPass {
+ profile_scope!("build_render_pass");
+
+ // TODO(gw): In this initial frame graph work, we try to maintain the existing
+ // build_render_pass code as closely as possible, to make the review
+ // simpler and reduce chance of regressions. However, future work should
+ // include refactoring this to more closely match the built frame graph.
+
+ // Collect a list of picture cache tasks, keyed by picture index.
+ // This allows us to only walk that picture root once, adding the
+ // primitives to all relevant batches at the same time.
+ let mut picture_cache_tasks = FastHashMap::default();
+ let mut pass = RenderPass::new(src_pass);
+
+ for sub_pass in &src_pass.sub_passes {
+ match sub_pass.surface {
+ SubPassSurface::Dynamic { target_kind, texture_id, used_rect } => {
+ match target_kind {
+ RenderTargetKind::Color => {
+ let mut target = ColorRenderTarget::new(
+ texture_id,
+ screen_size,
+ gpu_supports_fast_clears,
+ used_rect,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ target.add_task(
+ *task_id,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ clip_store,
+ transforms,
+ deferred_resolves,
+ );
+ }
+
+ pass.color.targets.push(target);
+ }
+ RenderTargetKind::Alpha => {
+ let mut target = AlphaRenderTarget::new(
+ texture_id,
+ screen_size,
+ gpu_supports_fast_clears,
+ used_rect,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ target.add_task(
+ *task_id,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ clip_store,
+ transforms,
+ deferred_resolves,
+ );
+ }
+
+ pass.alpha.targets.push(target);
+ }
+ }
+ }
+ SubPassSurface::Persistent { surface: StaticRenderTaskSurface::PictureCache { .. }, .. } => {
+ assert_eq!(sub_pass.task_ids.len(), 1);
+ let task_id = sub_pass.task_ids[0];
+ let task = &render_tasks[task_id];
+
+ // For picture cache tiles, just store them in the map
+ // of picture cache tasks, to be handled below.
+ let pic_index = match task.kind {
+ RenderTaskKind::Picture(ref info) => {
+ info.pic_index
+ }
+ _ => {
+ unreachable!();
+ }
+ };
+
+ picture_cache_tasks
+ .entry(pic_index)
+ .or_insert_with(Vec::new)
+ .push(task_id);
+ }
+ SubPassSurface::Persistent { surface: StaticRenderTaskSurface::TextureCache { target_kind, texture, layer, .. } } => {
+ let texture = pass.texture_cache
+ .entry((texture, layer))
+ .or_insert_with(||
+ TextureCacheRenderTarget::new(target_kind)
+ );
+ for task_id in &sub_pass.task_ids {
+ texture.add_task(*task_id, render_tasks);
+ }
+ }
+ }
+ }
+
+ // For each picture in this pass that has picture cache tiles, create
+ // a batcher per task, and then build batches for each of the tasks
+ // at the same time.
+ for (pic_index, task_ids) in picture_cache_tasks {
+ profile_scope!("picture_cache_task");
+ let pic = &ctx.prim_store.pictures[pic_index.0];
+
+ // Extract raster/surface spatial nodes for this surface.
+ let (root_spatial_node_index, surface_spatial_node_index, tile_cache) = match pic.raster_config {
+ Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ let surface = &ctx.surfaces[surface_index.0];
+ (
+ surface.raster_spatial_node_index,
+ surface.surface_spatial_node_index,
+ &ctx.tile_caches[&slice_id],
+ )
+ }
+ _ => {
+ unreachable!();
+ }
+ };
+
+ // Determine the clear color for this picture cache.
+ // If the entire tile cache is opaque, we can skip clear completely.
+ // If it's the first layer, clear it to white to allow subpixel AA on that
+ // first layer even if it's technically transparent.
+ // Otherwise, clear to transparent and composite with alpha.
+ // TODO(gw): We can detect per-tile opacity for the clear color here
+ // which might be a significant win on some pages?
+ let forced_opaque = match tile_cache.background_color {
+ Some(color) => color.a >= 1.0,
+ None => false,
+ };
+ let mut clear_color = if forced_opaque {
+ Some(ColorF::WHITE)
+ } else {
+ Some(ColorF::TRANSPARENT)
+ };
+
+ // If this picture cache has a valid color backdrop, we will use
+ // that as the clear color, skipping the draw of the backdrop
+ // primitive (and anything prior to it) during batching.
+ if let Some(BackdropKind::Color { color }) = tile_cache.backdrop.kind {
+ clear_color = Some(color);
+ }
+
+ // Create an alpha batcher for each of the tasks of this picture.
+ let mut batchers = Vec::new();
+ for task_id in &task_ids {
+ let task_id = *task_id;
+ let vis_mask = match render_tasks[task_id].kind {
+ RenderTaskKind::Picture(ref info) => info.vis_mask,
+ _ => unreachable!(),
+ };
+ batchers.push(AlphaBatchBuilder::new(
+ screen_size,
+ ctx.break_advanced_blend_batches,
+ ctx.batch_lookback_count,
+ task_id,
+ task_id.into(),
+ vis_mask,
+ 0,
+ ));
+ }
+
+ // Run the batch creation code for this picture, adding items to
+ // all relevant per-task batchers.
+ let mut batch_builder = BatchBuilder::new(batchers);
+ {
+ profile_scope!("add_pic_to_batch");
+ batch_builder.add_pic_to_batch(
+ pic,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ root_spatial_node_index,
+ surface_spatial_node_index,
+ z_generator,
+ composite_state,
+ );
+ }
+
+ // Create picture cache targets, one per render task, and assign
+ // the correct batcher to them.
+ let batchers = batch_builder.finalize();
+ for (task_id, batcher) in task_ids.into_iter().zip(batchers.into_iter()) {
+ profile_scope!("task");
+ let task = &render_tasks[task_id];
+ let (target_rect, _) = task.get_target_rect();
+
+ match task.location {
+ RenderTaskLocation::Static { surface: StaticRenderTaskSurface::PictureCache { ref surface, .. }, .. } => {
+ // TODO(gw): The interface here is a bit untidy since it's
+ // designed to support batch merging, which isn't
+ // relevant for picture cache targets. We
+ // can restructure / tidy this up a bit.
+ let (scissor_rect, valid_rect) = match render_tasks[task_id].kind {
+ RenderTaskKind::Picture(ref info) => {
+ (
+ info.scissor_rect.expect("bug: must be set for cache tasks"),
+ info.valid_rect.expect("bug: must be set for cache tasks"),
+ )
+ }
+ _ => unreachable!(),
+ };
+ let mut batch_containers = Vec::new();
+ let mut alpha_batch_container = AlphaBatchContainer::new(Some(scissor_rect));
+ batcher.build(
+ &mut batch_containers,
+ &mut alpha_batch_container,
+ target_rect,
+ None,
+ );
+ debug_assert!(batch_containers.is_empty());
+
+ let target = PictureCacheTarget {
+ surface: surface.clone(),
+ clear_color,
+ alpha_batch_container,
+ dirty_rect: scissor_rect,
+ valid_rect,
+ };
+
+ pass.picture_cache.push(target);
+ }
+ _ => {
+ unreachable!()
+ }
+ }
+ }
+ }
+
+ pass.color.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ z_generator,
+ composite_state,
+ );
+ pass.alpha.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ z_generator,
+ composite_state,
+ );
+
+ pass
+}
+
+/// A rendering-oriented representation of the frame built by the render backend
+/// and presented to the renderer.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Frame {
+ /// The rectangle to show the frame in, on screen.
+ pub device_rect: DeviceIntRect,
+ pub passes: Vec<RenderPass>,
+
+ pub transform_palette: Vec<TransformData>,
+ pub render_tasks: RenderTaskGraph,
+ pub prim_headers: PrimitiveHeaders,
+
+ /// The GPU cache frame that the contents of Self depend on
+ pub gpu_cache_frame_id: FrameId,
+
+ /// List of textures that we don't know about yet
+ /// from the backend thread. The render thread
+ /// will use a callback to resolve these and
+ /// patch the data structures.
+ pub deferred_resolves: Vec<DeferredResolve>,
+
+ /// True if this frame contains any render tasks
+ /// that write to the texture cache.
+ pub has_texture_cache_tasks: bool,
+
+ /// True if this frame has been drawn by the
+ /// renderer.
+ pub has_been_rendered: bool,
+
+ /// Debugging information to overlay for this frame.
+ pub debug_items: Vec<DebugItem>,
+
+ /// Contains picture cache tiles, and associated information.
+ /// Used by the renderer to composite tiles into the framebuffer,
+ /// or hand them off to an OS compositor.
+ pub composite_state: CompositeState,
+}
+
+impl Frame {
+ // This frame must be flushed if it writes to the
+ // texture cache, and hasn't been drawn yet.
+ pub fn must_be_drawn(&self) -> bool {
+ self.has_texture_cache_tasks && !self.has_been_rendered
+ }
+
+ // Returns true if this frame doesn't alter what is on screen currently.
+ pub fn is_nop(&self) -> bool {
+ // If there are no off-screen passes, that implies that there are no
+ // picture cache tiles, and no texture cache tasks being updates. If this
+ // is the case, we can consider the frame a nop (higher level checks
+ // test if a composite is needed due to picture cache surfaces moving
+ // or external surfaces being updated).
+ self.passes.is_empty()
+ }
+}
+
+/// Add a child render task as a dependency to a surface. This is a free
+/// function for now as it's also used by the render task cache.
+// TODO(gw): Find a more appropriate place for this to live - probably clearer
+// once SurfaceInfo gets refactored.
+pub fn add_child_render_task(
+ surface_index: SurfaceIndex,
+ child_task_id: RenderTaskId,
+ surfaces: &[SurfaceInfo],
+ rg_builder: &mut RenderTaskGraphBuilder,
+) {
+ let surface_tasks = surfaces[surface_index.0]
+ .render_tasks
+ .as_ref()
+ .expect("bug: no task for surface");
+
+ match surface_tasks {
+ SurfaceRenderTasks::Tiled(ref tasks) => {
+ // For a tiled render task, add as a dependency to every tile.
+ for parent_id in tasks {
+ rg_builder.add_dependency(*parent_id, child_task_id);
+ }
+ }
+ SurfaceRenderTasks::Simple(parent_id) => {
+ rg_builder.add_dependency(*parent_id, child_task_id);
+ }
+ SurfaceRenderTasks::Chained { port_task_id, .. } => {
+ // For chained render tasks, add as a dependency of the lowest part of
+ // the chain (the picture content)
+ rg_builder.add_dependency(*port_task_id, child_task_id);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/frame_graph.rs b/gfx/wr/webrender/src/frame_graph.rs
new file mode 100644
index 0000000000..d4955984f4
--- /dev/null
+++ b/gfx/wr/webrender/src/frame_graph.rs
@@ -0,0 +1,950 @@
+// 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 api::units::*;
+use api::ImageFormat;
+use crate::gpu_cache::GpuCache;
+use crate::internal_types::{CacheTextureId, FastHashMap, FastHashSet};
+use crate::render_backend::FrameId;
+use crate::render_task_graph::{RenderTaskId};
+use crate::render_task::{StaticRenderTaskSurface, RenderTaskLocation, RenderTask};
+use crate::render_target::RenderTargetKind;
+use crate::render_task::RenderTaskData;
+use crate::render_task_graph::RenderTaskAllocation;
+use crate::resource_cache::ResourceCache;
+use crate::texture_pack::GuillotineAllocator;
+use crate::util::VecHelper;
+use smallvec::SmallVec;
+use std::mem;
+
+/// According to apitrace, textures larger than 2048 break fast clear
+/// optimizations on some intel drivers. We sometimes need to go larger, but
+/// we try to avoid it.
+const MAX_SHARED_SURFACE_SIZE: i32 = 2048;
+
+/// If we ever need a larger texture than the ideal, we better round it up to a
+/// reasonable number in order to have a bit of leeway in case the size of this
+/// this target is changing each frame.
+const TEXTURE_DIMENSION_MASK: i32 = 0xFF;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)]
+pub struct PassId(usize);
+
+impl PassId {
+ pub const MIN: PassId = PassId(0);
+ pub const MAX: PassId = PassId(!0);
+}
+
+/// An internal representation of a dynamic surface that tasks can be
+/// allocated into. Maintains some extra metadata about each surface
+/// during the graph build.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Surface {
+ /// Whether this is a color or alpha render target
+ kind: RenderTargetKind,
+ /// Allocator for this surface texture
+ allocator: GuillotineAllocator,
+ /// We can only allocate into this for reuse if it's a shared surface
+ is_shared: bool,
+}
+
+impl Surface {
+ /// Allocate a rect within a shared surfce. Returns None if the
+ /// format doesn't match, or allocation fails.
+ fn alloc_rect(
+ &mut self,
+ size: DeviceIntSize,
+ kind: RenderTargetKind,
+ is_shared: bool,
+ ) -> Option<DeviceIntPoint> {
+ if self.kind == kind && self.is_shared == is_shared {
+ self.allocator
+ .allocate(&size)
+ .map(|(_slice, origin)| origin)
+ } else {
+ None
+ }
+ }
+}
+
+/// A sub-pass can draw to either a dynamic (temporary render target) surface,
+/// or a persistent surface (texture or picture cache).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug)]
+pub enum SubPassSurface {
+ /// A temporary (intermediate) surface.
+ Dynamic {
+ /// The renderer texture id
+ texture_id: CacheTextureId,
+ /// Color / alpha render target
+ target_kind: RenderTargetKind,
+ /// The rectangle occupied by tasks in this surface. Used as a clear
+ /// optimization on some GPUs.
+ used_rect: DeviceIntRect,
+ },
+ Persistent {
+ /// Reference to the texture or picture cache surface being drawn to.
+ surface: StaticRenderTaskSurface,
+ },
+}
+
+/// A subpass is a specific render target, and a list of tasks to draw to it.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SubPass {
+ /// The surface this subpass draws to
+ pub surface: SubPassSurface,
+ /// The tasks assigned to this subpass.
+ pub task_ids: Vec<RenderTaskId>,
+}
+
+/// A pass expresses dependencies between tasks. Each pass consists of a number
+/// of subpasses.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Pass {
+ /// The tasks assigned to this render pass
+ pub task_ids: Vec<RenderTaskId>,
+ /// The subpasses that make up this dependency pass
+ pub sub_passes: Vec<SubPass>,
+ /// A list of intermediate surfaces that can be invalidated after
+ /// this pass completes.
+ pub textures_to_invalidate: Vec<CacheTextureId>,
+}
+
+/// The FrameGraph is the immutable representation of the render task graph. It is
+/// built by the FrameGraphBuilder, and is constructed once per frame.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameGraph {
+ /// List of tasks added to the graph
+ pub tasks: Vec<RenderTask>,
+
+ /// The passes that were created, based on dependencies between tasks
+ pub passes: Vec<Pass>,
+
+ /// Current frame id, used for debug validation
+ frame_id: FrameId,
+
+ /// GPU specific data for each task that is made available to shaders
+ pub task_data: Vec<RenderTaskData>,
+
+ /// Total number of intermediate surfaces that will be drawn to, used for test validation.
+ #[cfg(test)]
+ surface_count: usize,
+
+ /// Total number of real allocated textures that will be drawn to, used for test validation.
+ #[cfg(test)]
+ unique_surfaces: FastHashSet<CacheTextureId>,
+}
+
+/// The persistent interface that is used during frame building to construct the
+/// frame graph.
+pub struct FrameGraphBuilder {
+ /// List of tasks added to the builder
+ tasks: Vec<RenderTask>,
+
+ /// List of task roots
+ roots: FastHashSet<RenderTaskId>,
+
+ /// Input dependencies where the input is a persistent target,
+ /// rather than a specific render task id. Useful for expressing
+ /// when a task relies on a readback of a surface that is partially
+ /// drawn to.
+ target_inputs: Vec<(RenderTaskId, StaticRenderTaskSurface)>,
+
+ /// Current frame id, used for debug validation
+ frame_id: FrameId,
+
+ /// A list of texture surfaces that can be freed at the end of a pass. Retained
+ /// here to reduce heap allocations.
+ textures_to_free: FastHashSet<CacheTextureId>,
+
+ // Keep a map of `texture_id` to metadata about surfaces that are currently
+ // borrowed from the render target pool.
+ active_surfaces: FastHashMap<CacheTextureId, Surface>,
+
+ /// A temporary buffer used by assign_free_pass. Kept here to avoid heap reallocs
+ child_task_buffer: Vec<RenderTaskId>,
+}
+
+impl FrameGraphBuilder {
+ /// Construct a new graph builder. Typically constructed once and maintained
+ /// over many frames, to avoid extra heap allocations where possible.
+ pub fn new() -> Self {
+ FrameGraphBuilder {
+ tasks: Vec::new(),
+ roots: FastHashSet::default(),
+ target_inputs: Vec::new(),
+ frame_id: FrameId::INVALID,
+ textures_to_free: FastHashSet::default(),
+ active_surfaces: FastHashMap::default(),
+ child_task_buffer: Vec::new(),
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ pub fn frame_id(&self) -> FrameId {
+ self.frame_id
+ }
+
+ /// Begin a new frame
+ pub fn begin_frame(&mut self, frame_id: FrameId) {
+ self.frame_id = frame_id;
+ self.roots.clear();
+ }
+
+ /// Get immutable access to a task
+ // TODO(gw): There's only a couple of places that existing code needs to access
+ // a task during the building step. Perhaps we can remove this?
+ pub fn get_task(
+ &self,
+ task_id: RenderTaskId,
+ ) -> &RenderTask {
+ &self.tasks[task_id.index as usize]
+ }
+
+ /// Get mutable access to a task
+ // TODO(gw): There's only a couple of places that existing code needs to access
+ // a task during the building step. Perhaps we can remove this?
+ pub fn get_task_mut(
+ &mut self,
+ task_id: RenderTaskId,
+ ) -> &mut RenderTask {
+ &mut self.tasks[task_id.index as usize]
+ }
+
+ /// Add a new task to the graph.
+ pub fn add(&mut self) -> RenderTaskAllocation {
+ // Assume every task is a root to start with
+ self.roots.insert(
+ RenderTaskId { index: self.tasks.len() as u32 }
+ );
+
+ RenderTaskAllocation {
+ alloc: self.tasks.alloc(),
+ }
+ }
+
+ /// Express a dependency, such that `task_id` depends on `input` as a texture source.
+ pub fn add_dependency(
+ &mut self,
+ task_id: RenderTaskId,
+ input: RenderTaskId,
+ ) {
+ self.tasks[task_id.index as usize].children.push(input);
+
+ // Once a task is an input, it's no longer a root
+ self.roots.remove(&input);
+ }
+
+ /// Register a persistent surface as an input dependency of a task (readback).
+ pub fn add_target_input(
+ &mut self,
+ task_id: RenderTaskId,
+ target: StaticRenderTaskSurface,
+ ) {
+ self.target_inputs.push((task_id, target));
+ }
+
+ /// End the graph building phase and produce the immutable task graph for this frame
+ pub fn end_frame(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ ) -> FrameGraph {
+ // Copy the render tasks over to the immutable graph output
+ let task_count = self.tasks.len();
+ let tasks = mem::replace(
+ &mut self.tasks,
+ Vec::with_capacity(task_count),
+ );
+
+ let mut graph = FrameGraph {
+ tasks,
+ passes: Vec::new(),
+ task_data: Vec::with_capacity(task_count),
+ frame_id: self.frame_id,
+ #[cfg(test)]
+ surface_count: 0,
+ #[cfg(test)]
+ unique_surfaces: FastHashSet::default(),
+ };
+
+ // Handle late mapping of dependencies on a specific persistent target.
+ // NOTE: This functionality isn't used by current callers of the frame graph, but
+ // will be used in future (for example, to express readbacks of partially
+ // rendered picture tiles for mix-blend-mode etc).
+ if !self.target_inputs.is_empty() {
+ // Create a mapping from persistent surface id -> render task root (used below):
+ let mut roots = FastHashMap::default();
+ roots.reserve(self.roots.len());
+ for root_id in &self.roots {
+ let task = &graph.tasks[root_id.index as usize];
+ match task.location {
+ RenderTaskLocation::Static { ref surface, .. } => {
+ // We should never encounter a graph where the same surface is a
+ // render root more than one.
+ assert!(!roots.contains_key(surface));
+ roots.insert(surface.clone(), *root_id);
+ }
+ RenderTaskLocation::Dynamic { .. } | RenderTaskLocation::Unallocated { .. } => {
+ // Intermediate surfaces can't be render roots, they should always
+ // be a dependency of a render root.
+ panic!("bug: invalid root");
+ }
+ }
+ }
+ assert_eq!(roots.len(), self.roots.len());
+
+ // Now resolve those dependencies on persistent targets and add them
+ // as a render task dependency.
+ for (task_id, target_id) in self.target_inputs.drain(..) {
+ match roots.get(&target_id) {
+ Some(root_task_id) => {
+ graph.tasks[task_id.index as usize].children.push(*root_task_id);
+ self.roots.remove(root_task_id);
+ }
+ None => {
+ println!("WARN: {:?} depends on root {:?} but it has no tasks!",
+ task_id,
+ target_id,
+ );
+ }
+ }
+ }
+ }
+
+ // Two traversals of the graph are required. The first pass determines how many passes
+ // are required, and assigns render tasks a pass to be drawn on. The second pass determines
+ // when the last time a render task is used as an input, and assigns what pass the surface
+ // backing that render task can be freed (the surface is then returned to the render target
+ // pool and may be aliased / reused during subsequent passes).
+
+ let mut pass_count = 0;
+
+ // Traverse each root, and assign `render_on` for each task and count number of required passes
+ for root_id in &self.roots {
+ assign_render_pass(
+ *root_id,
+ PassId(0),
+ &mut graph,
+ &mut pass_count,
+ );
+ }
+
+ // Determine which pass each task can be freed on, which depends on which is
+ // the last task that has this as an input.
+ for i in 0 .. graph.tasks.len() {
+ let task_id = RenderTaskId { index: i as u32 };
+ assign_free_pass(
+ task_id,
+ &mut self.child_task_buffer,
+ &mut graph,
+ );
+ }
+
+ // Construct passes array for tasks to be assigned to below
+ for _ in 0 .. pass_count+1 {
+ graph.passes.push(Pass {
+ task_ids: Vec::new(),
+ sub_passes: Vec::new(),
+ textures_to_invalidate: Vec::new(),
+ });
+ }
+
+ // Assign tasks to each pass based on their `render_on` attribute
+ for (index, task) in graph.tasks.iter().enumerate() {
+ let id = RenderTaskId { index: index as u32 };
+ graph.passes[task.render_on.0].task_ids.push(id);
+ }
+
+ // At this point, tasks are assigned to each dependency pass. Now we
+ // can go through each pass and create sub-passes, assigning each task
+ // to a target and destination rect.
+ assert!(self.active_surfaces.is_empty());
+
+ for (pass_id, pass) in graph.passes.iter_mut().enumerate().rev() {
+ assert!(self.textures_to_free.is_empty());
+
+ for task_id in &pass.task_ids {
+ let task = &mut graph.tasks[task_id.index as usize];
+
+ match task.location {
+ RenderTaskLocation::Unallocated { size } => {
+ let mut location = None;
+ let kind = task.kind.target_kind();
+
+ // Allow this render task to use a shared surface target if it
+ // is freed straight after this pass. Tasks that must remain
+ // allocated for inputs on subsequent passes are always assigned
+ // to a standalone surface, to simplify lifetime management of
+ // render targets.
+
+ let can_use_shared_surface =
+ task.render_on == PassId(task.free_after.0 + 1);
+
+ if can_use_shared_surface {
+ // If we can use a shared surface, step through the existing shared
+ // surfaces for this subpass, and see if we can allocate the task
+ // to one of these targets.
+ for sub_pass in &mut pass.sub_passes {
+ if let SubPassSurface::Dynamic { texture_id, ref mut used_rect, .. } = sub_pass.surface {
+ let surface = self.active_surfaces.get_mut(&texture_id).unwrap();
+ if let Some(p) = surface.alloc_rect(size, kind, true) {
+ location = Some((texture_id, p));
+ *used_rect = used_rect.union(&DeviceIntRect::new(p, size));
+ sub_pass.task_ids.push(*task_id);
+ break;
+ }
+ }
+ }
+ }
+
+ if location.is_none() {
+ // If it wasn't possible to allocate the task to a shared surface, get a new
+ // render target from the resource cache pool/
+
+ // If this is a really large task, don't bother allocating it as a potential
+ // shared surface for other tasks.
+
+ let can_use_shared_surface = can_use_shared_surface &&
+ size.width <= MAX_SHARED_SURFACE_SIZE &&
+ size.height <= MAX_SHARED_SURFACE_SIZE;
+
+ let surface_size = if can_use_shared_surface {
+ DeviceIntSize::new(
+ MAX_SHARED_SURFACE_SIZE,
+ MAX_SHARED_SURFACE_SIZE,
+ )
+ } else {
+ // Round up size here to avoid constant re-allocs during resizing
+ DeviceIntSize::new(
+ (size.width + TEXTURE_DIMENSION_MASK) & !TEXTURE_DIMENSION_MASK,
+ (size.height + TEXTURE_DIMENSION_MASK) & !TEXTURE_DIMENSION_MASK,
+ )
+ };
+
+ let format = match kind {
+ RenderTargetKind::Color => ImageFormat::RGBA8,
+ RenderTargetKind::Alpha => ImageFormat::R8,
+ };
+
+ // Get render target of appropriate size and format from resource cache
+ let texture_id = resource_cache.get_or_create_render_target_from_pool(
+ surface_size,
+ format,
+ );
+
+ // Allocate metadata we need about this surface while it's active
+ let mut surface = Surface {
+ kind,
+ allocator: GuillotineAllocator::new(Some(surface_size)),
+ is_shared: can_use_shared_surface,
+ };
+
+ // Allocation of the task must fit in this new surface!
+ let p = surface.alloc_rect(
+ size,
+ kind,
+ can_use_shared_surface,
+ ).expect("bug: alloc must succeed!");
+
+ location = Some((texture_id, p));
+
+ // Store the metadata about this newly active surface. We should never
+ // get a target surface with the same texture_id as a currently active surface.
+ let _prev_surface = self.active_surfaces.insert(texture_id, surface);
+ assert!(_prev_surface.is_none());
+
+ // Store some information about surface allocations if in test mode
+ #[cfg(test)]
+ {
+ graph.surface_count += 1;
+ graph.unique_surfaces.insert(texture_id);
+ }
+
+ // Add the target as a new subpass for this render pass.
+ pass.sub_passes.push(SubPass {
+ surface: SubPassSurface::Dynamic {
+ texture_id,
+ target_kind: kind,
+ used_rect: DeviceIntRect::new(p, size),
+ },
+ task_ids: vec![*task_id],
+ });
+ }
+
+ // By now, we must have allocated a surface and rect for this task, so assign it!
+ assert!(location.is_some());
+ task.location = RenderTaskLocation::Dynamic {
+ texture_id: location.unwrap().0,
+ rect: DeviceIntRect::new(location.unwrap().1, size),
+ };
+ }
+ RenderTaskLocation::Static { ref surface, .. } => {
+ // No need to allocate for this surface, since it's a persistent
+ // target. Instead, just create a new sub-pass for it.
+ pass.sub_passes.push(SubPass {
+ surface: SubPassSurface::Persistent {
+ surface: surface.clone(),
+ },
+ task_ids: vec![*task_id],
+ });
+ }
+ RenderTaskLocation::Dynamic { .. } => {
+ // Dynamic tasks shouldn't be allocated by this point
+ panic!("bug: encountered an already allocated task");
+ }
+ }
+
+ // Return the shared surfaces from this pass
+ let task = &graph.tasks[task_id.index as usize];
+ for child_id in &task.children {
+ let child_task = &graph.tasks[child_id.index as usize];
+ match child_task.location {
+ RenderTaskLocation::Unallocated { .. } => panic!("bug: must be allocated"),
+ RenderTaskLocation::Dynamic { texture_id, .. } => {
+ // If this task can be freed after this pass, include it in the
+ // unique set of textures to be returned to the render target pool below.
+ if child_task.free_after == PassId(pass_id) {
+ self.textures_to_free.insert(texture_id);
+ }
+ }
+ RenderTaskLocation::Static { .. } => {}
+ }
+ }
+ }
+
+ // Return no longer used textures to the pool, so that they can be reused / aliased
+ // by later passes.
+ for texture_id in self.textures_to_free.drain() {
+ resource_cache.return_render_target_to_pool(texture_id);
+ self.active_surfaces.remove(&texture_id).unwrap();
+ pass.textures_to_invalidate.push(texture_id);
+ }
+ }
+
+ // By now, all surfaces that were borrowed from the render target pool must
+ // be returned to the resource cache, or we are leaking intermediate surfaces!
+ assert!(self.active_surfaces.is_empty());
+
+ // Each task is now allocated to a surface and target rect. Write that to the
+ // GPU blocks and task_data. After this point, the graph is returned and is
+ // considered to be immutable for the rest of the frame building process.
+
+ for task in &mut graph.tasks {
+ // Give the render task an opportunity to add any
+ // information to the GPU cache, if appropriate.
+ let (target_rect, target_index) = task.get_target_rect();
+
+ task.kind.write_gpu_blocks(
+ target_rect,
+ target_index,
+ gpu_cache,
+ );
+
+ graph.task_data.push(
+ task.kind.write_task_data(
+ target_rect,
+ target_index,
+ )
+ );
+ }
+
+ graph
+ }
+}
+
+impl FrameGraph {
+ /// Print the render task graph to console
+ #[allow(dead_code)]
+ pub fn print(
+ &self,
+ ) {
+ println!("-- FrameGraph --");
+
+ for (i, task) in self.tasks.iter().enumerate() {
+ println!("Task {}: render_on={} free_after={} {:?}",
+ i,
+ task.render_on.0,
+ task.free_after.0,
+ task.kind.as_str(),
+ );
+ }
+
+ for (p, pass) in self.passes.iter().enumerate() {
+ println!("Pass {}:", p);
+
+ for (s, sub_pass) in pass.sub_passes.iter().enumerate() {
+ println!("\tSubPass {}: {:?}",
+ s,
+ sub_pass.surface,
+ );
+
+ for task_id in &sub_pass.task_ids {
+ println!("\t\tTask {:?}", task_id.index);
+ }
+ }
+ }
+ }
+
+ /// Return the surface and texture counts, used for testing
+ #[cfg(test)]
+ pub fn surface_counts(&self) -> (usize, usize) {
+ (self.surface_count, self.unique_surfaces.len())
+ }
+
+ /// Return current frame id, used for validation
+ #[cfg(debug_assertions)]
+ pub fn frame_id(&self) -> FrameId {
+ self.frame_id
+ }
+}
+
+/// Batching uses index access to read information about tasks
+impl std::ops::Index<RenderTaskId> for FrameGraph {
+ type Output = RenderTask;
+ fn index(&self, id: RenderTaskId) -> &RenderTask {
+ &self.tasks[id.index as usize]
+ }
+}
+
+/// Recursive helper to assign pass that a task should render on
+fn assign_render_pass(
+ id: RenderTaskId,
+ pass: PassId,
+ graph: &mut FrameGraph,
+ pass_count: &mut usize,
+) {
+ let task = &mut graph.tasks[id.index as usize];
+
+ // Keep count of number of passes needed
+ *pass_count = pass.0.max(*pass_count);
+
+ // TODO(gw): Work around the borrowck - maybe we could structure the dependencies
+ // storage better, to avoid this?
+ let mut child_task_ids: SmallVec<[RenderTaskId; 8]> = SmallVec::new();
+ child_task_ids.extend_from_slice(&task.children);
+
+ // No point in recursing into paths in the graph if this task already
+ // has been set to draw after this pass.
+ if task.render_on > pass {
+ return;
+ }
+
+ // A task should be rendered on the earliest pass in the dependency
+ // graph that it's required. Using max here ensures the correct value
+ // in the presense of multiple paths to this task from the root(s).
+ task.render_on = task.render_on.max(pass);
+
+ let next_pass = PassId(pass.0 + 1);
+
+ for child_id in child_task_ids {
+ assign_render_pass(
+ child_id,
+ next_pass,
+ graph,
+ pass_count,
+ );
+ }
+}
+
+fn assign_free_pass(
+ id: RenderTaskId,
+ child_task_buffer: &mut Vec<RenderTaskId>,
+ graph: &mut FrameGraph,
+) {
+ let task = &graph.tasks[id.index as usize];
+ let render_on = task.render_on;
+ debug_assert!(child_task_buffer.is_empty());
+
+ // TODO(gw): Work around the borrowck - maybe we could structure the dependencies
+ // storage better, to avoid this?
+ child_task_buffer.extend_from_slice(&task.children);
+
+ for child_id in child_task_buffer.drain(..) {
+ let child_task = &mut graph.tasks[child_id.index as usize];
+
+ // Each dynamic child task can free its backing surface after the last
+ // task that references it as an input. Using min here ensures the
+ // safe time to free this surface in the presence of multiple paths
+ // to this task from the root(s).
+ match child_task.location {
+ RenderTaskLocation::Static { .. } => {
+ // never get freed anyway, so can leave untouched
+ // (could validate that they remain at PassId::MIN)
+ }
+ RenderTaskLocation::Unallocated { .. } => {
+ child_task.free_after = child_task.free_after.min(render_on);
+ }
+ RenderTaskLocation::Dynamic { .. } => {
+ panic!("bug: should not be allocated yet");
+ }
+ }
+ }
+}
+
+
+/// Construct a picture cache render task location for testing
+#[cfg(test)]
+fn pc_target(
+ surface_id: u64,
+ tile_x: i32,
+ tile_y: i32,
+) -> RenderTaskLocation {
+ use crate::{
+ composite::{NativeSurfaceId, NativeTileId},
+ picture::ResolvedSurfaceTexture,
+ };
+
+ let width = 512;
+ let height = 512;
+
+ RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::PictureCache {
+ surface: ResolvedSurfaceTexture::Native {
+ id: NativeTileId {
+ surface_id: NativeSurfaceId(surface_id),
+ x: tile_x,
+ y: tile_y,
+ },
+ size: DeviceIntSize::new(width, height),
+ },
+ },
+ rect: DeviceIntSize::new(width, height).into(),
+ }
+}
+
+#[cfg(test)]
+impl FrameGraphBuilder {
+ fn test_expect(
+ mut self,
+ pass_count: usize,
+ total_surface_count: usize,
+ unique_surfaces: &[(i32, i32, ImageFormat)],
+ ) {
+ let mut rc = ResourceCache::new_for_testing();
+ let mut gc = GpuCache::new();
+
+ let g = self.end_frame(&mut rc, &mut gc);
+ g.print();
+
+ assert_eq!(g.passes.len(), pass_count);
+ assert_eq!(g.surface_counts(), (total_surface_count, unique_surfaces.len()));
+
+ rc.validate_surfaces(unique_surfaces);
+ }
+}
+
+/// Construct a testing render task with given location
+#[cfg(test)]
+fn task_location(location: RenderTaskLocation) -> RenderTask {
+ RenderTask::new_test(
+ location,
+ RenderTargetKind::Color,
+ )
+}
+
+/// Construct a dynamic render task location for testing
+#[cfg(test)]
+fn task_dynamic(size: i32) -> RenderTask {
+ RenderTask::new_test(
+ RenderTaskLocation::Unallocated { size: DeviceIntSize::new(size, size) },
+ RenderTargetKind::Color,
+ )
+}
+
+#[test]
+fn fg_test_1() {
+ // Test that a root target can be used as an input for readbacks
+ // This functionality isn't currently used, but will be in future.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let root_target = pc_target(0, 0, 0);
+
+ let root = gb.add().init(task_location(root_target.clone()));
+
+ let readback = gb.add().init(task_dynamic(100));
+ gb.add_dependency(readback, root);
+
+ let mix_blend_content = gb.add().init(task_dynamic(50));
+
+ let content = gb.add().init(task_location(root_target));
+ gb.add_dependency(content, readback);
+ gb.add_dependency(content, mix_blend_content);
+
+ gb.test_expect(3, 1, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_2() {
+ // Test that texture cache tasks can be added and scheduled correctly as inputs
+ // to picture cache tasks. Ensure that no dynamic surfaces are allocated from the
+ // target pool in this case.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let tc_0 = StaticRenderTaskSurface::TextureCache {
+ texture: CacheTextureId(0),
+ layer: 0,
+ target_kind: RenderTargetKind::Color,
+ };
+
+ let tc_1 = StaticRenderTaskSurface::TextureCache {
+ texture: CacheTextureId(1),
+ layer: 0,
+ target_kind: RenderTargetKind::Color,
+ };
+
+ gb.add_target_input(
+ pc_root,
+ tc_0.clone(),
+ );
+
+ gb.add_target_input(
+ pc_root,
+ tc_1.clone(),
+ );
+
+ gb.add().init(
+ task_location(RenderTaskLocation::Static { surface: tc_0.clone(), rect: DeviceIntSize::new(128, 128).into() }),
+ );
+
+ gb.add().init(
+ task_location(RenderTaskLocation::Static { surface: tc_1.clone(), rect: DeviceIntSize::new(128, 128).into() }),
+ );
+
+ gb.test_expect(2, 0, &[]);
+}
+
+#[test]
+fn fg_test_3() {
+ // Test that small targets are allocated in a shared surface, and that large
+ // tasks are allocated in a rounded up texture size.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(3000));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(pc_root, child_pic_1);
+
+ gb.test_expect(2, 2, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (3072, 3072, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_4() {
+ // Test that for a simple dependency chain of tasks, that render
+ // target surfaces are aliased and reused between passes where possible.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(128));
+ let child_pic_2 = gb.add().init(task_dynamic(128));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(child_pic_0, child_pic_1);
+ gb.add_dependency(child_pic_1, child_pic_2);
+
+ gb.test_expect(4, 3, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_5() {
+ // Test that a task that is used as an input by direct parent and also
+ // distance ancestor are scheduled correctly, and allocates the correct
+ // number of passes, taking advantage of surface reuse / aliasing where feasible.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child_pic_0 = gb.add().init(task_dynamic(128));
+ let child_pic_1 = gb.add().init(task_dynamic(64));
+ let child_pic_2 = gb.add().init(task_dynamic(32));
+ let child_pic_3 = gb.add().init(task_dynamic(16));
+
+ gb.add_dependency(pc_root, child_pic_0);
+ gb.add_dependency(child_pic_0, child_pic_1);
+ gb.add_dependency(child_pic_1, child_pic_2);
+ gb.add_dependency(child_pic_2, child_pic_3);
+ gb.add_dependency(pc_root, child_pic_3);
+
+ gb.test_expect(5, 4, &[
+ (256, 256, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_6() {
+ // Test that a task that is used as an input dependency by two parent
+ // tasks is correctly allocated and freed.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root_1 = gb.add().init(task_location(pc_target(0, 0, 0)));
+ let pc_root_2 = gb.add().init(task_location(pc_target(0, 1, 0)));
+
+ let child_pic = gb.add().init(task_dynamic(128));
+
+ gb.add_dependency(pc_root_1, child_pic);
+ gb.add_dependency(pc_root_2, child_pic);
+
+ gb.test_expect(2, 1, &[
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
+
+#[test]
+fn fg_test_7() {
+ // Test that a standalone surface is not incorrectly used to
+ // allocate subsequent shared task rects.
+
+ let mut gb = FrameGraphBuilder::new();
+
+ let pc_root = gb.add().init(task_location(pc_target(0, 0, 0)));
+
+ let child0 = gb.add().init(task_dynamic(16));
+ let child1 = gb.add().init(task_dynamic(16));
+
+ let child2 = gb.add().init(task_dynamic(16));
+ let child3 = gb.add().init(task_dynamic(16));
+
+ gb.add_dependency(pc_root, child0);
+ gb.add_dependency(child0, child1);
+ gb.add_dependency(pc_root, child1);
+
+ gb.add_dependency(pc_root, child2);
+ gb.add_dependency(child2, child3);
+
+ gb.test_expect(3, 3, &[
+ (256, 256, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ (2048, 2048, ImageFormat::RGBA8),
+ ]);
+}
diff --git a/gfx/wr/webrender/src/freelist.rs b/gfx/wr/webrender/src/freelist.rs
new file mode 100644
index 0000000000..aa90aba03c
--- /dev/null
+++ b/gfx/wr/webrender/src/freelist.rs
@@ -0,0 +1,264 @@
+/* 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/. */
+
+//! A generic backing store for caches.
+//!
+//! `FreeList` is a simple vector-backed data structure where each entry in the
+//! vector contains an Option<T>. It maintains an index-based (rather than
+//! pointer-based) free list to efficiently locate the next unused entry. If all
+//! entries are occupied, insertion appends a new element to the vector.
+//!
+//! It also supports both strong and weak handle semantics. There is exactly one
+//! (non-Clonable) strong handle per occupied entry, which must be passed by
+//! value into `free()` to release an entry. Strong handles can produce an
+//! unlimited number of (Clonable) weak handles, which are used to perform
+//! lookups which may fail of the entry has been freed. A per-entry epoch ensures
+//! that weak handle lookups properly fail even if the entry has been freed and
+//! reused.
+//!
+//! TODO(gw): Add an occupied list head, for fast iteration of the occupied list
+//! to implement retain() style functionality.
+
+use std::{fmt, u32};
+use std::marker::PhantomData;
+
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Epoch(u32);
+
+impl Epoch {
+ /// Mints a new epoch.
+ ///
+ /// We start at 1 so that 0 is always an invalid epoch.
+ fn new() -> Self {
+ Epoch(1)
+ }
+
+ /// Returns an always-invalid epoch.
+ fn invalid() -> Self {
+ Epoch(0)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct FreeListHandle<M> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<M>,
+}
+
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for FreeListHandle<M> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("StrongHandle")
+ .field("index", &self.index)
+ .field("epoch", &self.epoch.0)
+ .finish()
+ }
+}
+
+impl<M> FreeListHandle<M> {
+ pub fn weak(&self) -> WeakFreeListHandle<M> {
+ WeakFreeListHandle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: PhantomData,
+ }
+ }
+
+ pub fn invalid() -> Self {
+ Self {
+ index: 0,
+ epoch: Epoch::invalid(),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Returns true if this handle and the supplied weak handle reference
+ /// the same underlying location in the freelist.
+ pub fn matches(&self, weak_handle: &WeakFreeListHandle<M>) -> bool {
+ self.index == weak_handle.index &&
+ self.epoch == weak_handle.epoch
+ }
+}
+
+impl<M> Clone for WeakFreeListHandle<M> {
+ fn clone(&self) -> Self {
+ WeakFreeListHandle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<M> PartialEq for WeakFreeListHandle<M> {
+ fn eq(&self, other: &Self) -> bool {
+ self.index == other.index && self.epoch == other.epoch
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct WeakFreeListHandle<M> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<M>,
+}
+
+/// More-compact textual representation for debug logging.
+impl<M> fmt::Debug for WeakFreeListHandle<M> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("WeakHandle")
+ .field("index", &self.index)
+ .field("epoch", &self.epoch.0)
+ .finish()
+ }
+}
+
+impl<M> WeakFreeListHandle<M> {
+ /// Returns an always-invalid handle.
+ pub fn invalid() -> Self {
+ Self {
+ index: 0,
+ epoch: Epoch::invalid(),
+ _marker: PhantomData,
+ }
+ }
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Slot<T> {
+ next: Option<u32>,
+ epoch: Epoch,
+ value: Option<T>,
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FreeList<T, M> {
+ slots: Vec<Slot<T>>,
+ free_list_head: Option<u32>,
+ active_count: usize,
+ _marker: PhantomData<M>,
+}
+
+impl<T, M> FreeList<T, M> {
+ /// Mints a new `FreeList` with no entries.
+ ///
+ /// Triggers a 1-entry allocation.
+ pub fn new() -> Self {
+ // We guarantee that we never have zero entries by starting with one
+ // free entry. This allows WeakFreeListHandle::invalid() to work
+ // without adding any additional branches.
+ let first_slot = Slot {
+ next: None,
+ epoch: Epoch::new(),
+ value: None,
+ };
+ FreeList {
+ slots: vec![first_slot],
+ free_list_head: Some(0),
+ active_count: 0,
+ _marker: PhantomData,
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.slots.truncate(1);
+ self.slots[0] = Slot {
+ next: None,
+ epoch: Epoch::new(),
+ value: None,
+ };
+ self.free_list_head = Some(0);
+ self.active_count = 0;
+ }
+
+ #[allow(dead_code)]
+ pub fn get(&self, id: &FreeListHandle<M>) -> &T {
+ self.slots[id.index as usize].value.as_ref().unwrap()
+ }
+
+ #[allow(dead_code)]
+ pub fn get_mut(&mut self, id: &FreeListHandle<M>) -> &mut T {
+ self.slots[id.index as usize].value.as_mut().unwrap()
+ }
+
+ pub fn get_opt(&self, id: &WeakFreeListHandle<M>) -> Option<&T> {
+ let slot = &self.slots[id.index as usize];
+ if slot.epoch == id.epoch {
+ slot.value.as_ref()
+ } else {
+ None
+ }
+ }
+
+ pub fn get_opt_mut(&mut self, id: &WeakFreeListHandle<M>) -> Option<&mut T> {
+ let slot = &mut self.slots[id.index as usize];
+ if slot.epoch == id.epoch {
+ slot.value.as_mut()
+ } else {
+ None
+ }
+ }
+
+ pub fn insert(&mut self, item: T) -> FreeListHandle<M> {
+ self.active_count += 1;
+
+ match self.free_list_head {
+ Some(free_index) => {
+ let slot = &mut self.slots[free_index as usize];
+
+ // Remove from free list.
+ self.free_list_head = slot.next;
+ slot.next = None;
+ slot.value = Some(item);
+
+ FreeListHandle {
+ index: free_index,
+ epoch: slot.epoch,
+ _marker: PhantomData,
+ }
+ }
+ None => {
+ let index = self.slots.len() as u32;
+ let epoch = Epoch::new();
+
+ self.slots.push(Slot {
+ next: None,
+ epoch,
+ value: Some(item),
+ });
+
+ FreeListHandle {
+ index,
+ epoch,
+ _marker: PhantomData,
+ }
+ }
+ }
+ }
+
+ pub fn free(&mut self, id: FreeListHandle<M>) -> T {
+ self.active_count -= 1;
+ let slot = &mut self.slots[id.index as usize];
+ slot.next = self.free_list_head;
+ slot.epoch = Epoch(slot.epoch.0 + 1);
+ self.free_list_head = Some(id.index);
+ slot.value.take().unwrap()
+ }
+
+ #[allow(dead_code)]
+ pub fn len(&self) -> usize {
+ self.active_count
+ }
+}
diff --git a/gfx/wr/webrender/src/gamma_lut.rs b/gfx/wr/webrender/src/gamma_lut.rs
new file mode 100644
index 0000000000..b5df7029e6
--- /dev/null
+++ b/gfx/wr/webrender/src/gamma_lut.rs
@@ -0,0 +1,414 @@
+/* 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/. */
+
+/*!
+Gamma correction lookup tables.
+
+This is a port of Skia gamma LUT logic into Rust, used by WebRender.
+*/
+//#![warn(missing_docs)] //TODO
+#![allow(dead_code)]
+
+use api::ColorU;
+use std::cmp::max;
+
+/// Color space responsible for converting between lumas and luminances.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LuminanceColorSpace {
+ /// Linear space - no conversion involved.
+ Linear,
+ /// Simple gamma space - uses the `luminance ^ gamma` function.
+ Gamma(f32),
+ /// Srgb space.
+ Srgb,
+}
+
+impl LuminanceColorSpace {
+ pub fn new(gamma: f32) -> LuminanceColorSpace {
+ if gamma == 1.0 {
+ LuminanceColorSpace::Linear
+ } else if gamma == 0.0 {
+ LuminanceColorSpace::Srgb
+ } else {
+ LuminanceColorSpace::Gamma(gamma)
+ }
+ }
+
+ pub fn to_luma(&self, luminance: f32) -> f32 {
+ match *self {
+ LuminanceColorSpace::Linear => luminance,
+ LuminanceColorSpace::Gamma(gamma) => luminance.powf(gamma),
+ LuminanceColorSpace::Srgb => {
+ //The magic numbers are derived from the sRGB specification.
+ //See http://www.color.org/chardata/rgb/srgb.xalter .
+ if luminance <= 0.04045 {
+ luminance / 12.92
+ } else {
+ ((luminance + 0.055) / 1.055).powf(2.4)
+ }
+ }
+ }
+ }
+
+ pub fn from_luma(&self, luma: f32) -> f32 {
+ match *self {
+ LuminanceColorSpace::Linear => luma,
+ LuminanceColorSpace::Gamma(gamma) => luma.powf(1. / gamma),
+ LuminanceColorSpace::Srgb => {
+ //The magic numbers are derived from the sRGB specification.
+ //See http://www.color.org/chardata/rgb/srgb.xalter .
+ if luma <= 0.0031308 {
+ luma * 12.92
+ } else {
+ 1.055 * luma.powf(1./2.4) - 0.055
+ }
+ }
+ }
+ }
+}
+
+//TODO: tests
+fn round_to_u8(x : f32) -> u8 {
+ let v = (x + 0.5).floor() as i32;
+ assert!(0 <= v && v < 0x100);
+ v as u8
+}
+
+//TODO: tests
+/*
+ * Scales base <= 2^N-1 to 2^8-1
+ * @param N [1, 8] the number of bits used by base.
+ * @param base the number to be scaled to [0, 255].
+ */
+fn scale255(n: u8, mut base: u8) -> u8 {
+ base <<= 8 - n;
+ let mut lum = base;
+ let mut i = n;
+
+ while i < 8 {
+ lum |= base >> i;
+ i += n;
+ }
+
+ lum
+}
+
+// Computes the luminance from the given r, g, and b in accordance with
+// SK_LUM_COEFF_X. For correct results, r, g, and b should be in linear space.
+fn compute_luminance(r: u8, g: u8, b: u8) -> u8 {
+ // The following is
+ // r * SK_LUM_COEFF_R + g * SK_LUM_COEFF_G + b * SK_LUM_COEFF_B
+ // with SK_LUM_COEFF_X in 1.8 fixed point (rounding adjusted to sum to 256).
+ let val: u32 = r as u32 * 54 + g as u32 * 183 + b as u32 * 19;
+ assert!(val < 0x10000);
+ (val >> 8) as u8
+}
+
+// Skia uses 3 bits per channel for luminance.
+const LUM_BITS: u8 = 3;
+// Mask of the highest used bits.
+const LUM_MASK: u8 = ((1 << LUM_BITS) - 1) << (8 - LUM_BITS);
+
+pub trait ColorLut {
+ fn quantize(&self) -> ColorU;
+ fn quantized_floor(&self) -> ColorU;
+ fn quantized_ceil(&self) -> ColorU;
+ fn luminance(&self) -> u8;
+ fn luminance_color(&self) -> ColorU;
+}
+
+impl ColorLut for ColorU {
+ // Compute a canonical color that is equivalent to the input color
+ // for preblend table lookups. The alpha channel is never used for
+ // preblending, so overwrite it with opaque.
+ fn quantize(&self) -> ColorU {
+ ColorU::new(
+ scale255(LUM_BITS, self.r >> (8 - LUM_BITS)),
+ scale255(LUM_BITS, self.g >> (8 - LUM_BITS)),
+ scale255(LUM_BITS, self.b >> (8 - LUM_BITS)),
+ 255,
+ )
+ }
+
+ // Quantize to the smallest value that yields the same table index.
+ fn quantized_floor(&self) -> ColorU {
+ ColorU::new(
+ self.r & LUM_MASK,
+ self.g & LUM_MASK,
+ self.b & LUM_MASK,
+ 255,
+ )
+ }
+
+ // Quantize to the largest value that yields the same table index.
+ fn quantized_ceil(&self) -> ColorU {
+ ColorU::new(
+ self.r | !LUM_MASK,
+ self.g | !LUM_MASK,
+ self.b | !LUM_MASK,
+ 255,
+ )
+ }
+
+ // Compute a luminance value suitable for grayscale preblend table
+ // lookups.
+ fn luminance(&self) -> u8 {
+ compute_luminance(self.r, self.g, self.b)
+ }
+
+ // Make a grayscale color from the computed luminance.
+ fn luminance_color(&self) -> ColorU {
+ let lum = self.luminance();
+ ColorU::new(lum, lum, lum, self.a)
+ }
+}
+
+// This will invert the gamma applied by CoreGraphics,
+// so we can get linear values.
+// CoreGraphics obscurely defaults to 2.0 as the smoothing gamma value.
+// The color space used does not appear to affect this choice.
+#[cfg(target_os="macos")]
+fn get_inverse_gamma_table_coregraphics_smoothing() -> [u8; 256] {
+ let mut table = [0u8; 256];
+
+ for (i, v) in table.iter_mut().enumerate() {
+ let x = i as f32 / 255.0;
+ *v = round_to_u8(x * x * 255.0);
+ }
+
+ table
+}
+
+// A value of 0.5 for SK_GAMMA_CONTRAST appears to be a good compromise.
+// With lower values small text appears washed out (though correctly so).
+// With higher values lcd fringing is worse and the smoothing effect of
+// partial coverage is diminished.
+fn apply_contrast(srca: f32, contrast: f32) -> f32 {
+ srca + ((1.0 - srca) * contrast * srca)
+}
+
+// The approach here is not necessarily the one with the lowest error
+// See https://bel.fi/alankila/lcd/alpcor.html for a similar kind of thing
+// that just search for the adjusted alpha value
+pub fn build_gamma_correcting_lut(table: &mut [u8; 256], src: u8, contrast: f32,
+ src_space: LuminanceColorSpace,
+ dst_convert: LuminanceColorSpace) {
+
+ let src = src as f32 / 255.0;
+ let lin_src = src_space.to_luma(src);
+ // Guess at the dst. The perceptual inverse provides smaller visual
+ // discontinuities when slight changes to desaturated colors cause a channel
+ // to map to a different correcting lut with neighboring srcI.
+ // See https://code.google.com/p/chromium/issues/detail?id=141425#c59 .
+ let dst = 1.0 - src;
+ let lin_dst = dst_convert.to_luma(dst);
+
+ // Contrast value tapers off to 0 as the src luminance becomes white
+ let adjusted_contrast = contrast * lin_dst;
+
+ // Remove discontinuity and instability when src is close to dst.
+ // The value 1/256 is arbitrary and appears to contain the instability.
+ if (src - dst).abs() < (1.0 / 256.0) {
+ let mut ii : f32 = 0.0;
+ for v in table.iter_mut() {
+ let raw_srca = ii / 255.0;
+ let srca = apply_contrast(raw_srca, adjusted_contrast);
+
+ *v = round_to_u8(255.0 * srca);
+ ii += 1.0;
+ }
+ } else {
+ // Avoid slow int to float conversion.
+ let mut ii : f32 = 0.0;
+ for v in table.iter_mut() {
+ // 'raw_srca += 1.0f / 255.0f' and even
+ // 'raw_srca = i * (1.0f / 255.0f)' can add up to more than 1.0f.
+ // When this happens the table[255] == 0x0 instead of 0xff.
+ // See http://code.google.com/p/chromium/issues/detail?id=146466
+ let raw_srca = ii / 255.0;
+ let srca = apply_contrast(raw_srca, adjusted_contrast);
+ assert!(srca <= 1.0);
+ let dsta = 1.0 - srca;
+
+ // Calculate the output we want.
+ let lin_out = lin_src * srca + dsta * lin_dst;
+ assert!(lin_out <= 1.0);
+ let out = dst_convert.from_luma(lin_out);
+
+ // Undo what the blit blend will do.
+ // i.e. given the formula for OVER: out = src * result + (1 - result) * dst
+ // solving for result gives:
+ let result = (out - dst) / (src - dst);
+
+ *v = round_to_u8(255.0 * result);
+ debug!("Setting {:?} to {:?}", ii as u8, *v);
+
+ ii += 1.0;
+ }
+ }
+}
+
+pub struct GammaLut {
+ tables: [[u8; 256]; 1 << LUM_BITS],
+ #[cfg(target_os="macos")]
+ cg_inverse_gamma: [u8; 256],
+}
+
+impl GammaLut {
+ // Skia actually makes 9 gamma tables, then based on the luminance color,
+ // fetches the RGB gamma table for that color.
+ fn generate_tables(&mut self, contrast: f32, paint_gamma: f32, device_gamma: f32) {
+ let paint_color_space = LuminanceColorSpace::new(paint_gamma);
+ let device_color_space = LuminanceColorSpace::new(device_gamma);
+
+ for (i, entry) in self.tables.iter_mut().enumerate() {
+ let luminance = scale255(LUM_BITS, i as u8);
+ build_gamma_correcting_lut(entry,
+ luminance,
+ contrast,
+ paint_color_space,
+ device_color_space);
+ }
+ }
+
+ pub fn table_count(&self) -> usize {
+ self.tables.len()
+ }
+
+ pub fn get_table(&self, color: u8) -> &[u8; 256] {
+ &self.tables[(color >> (8 - LUM_BITS)) as usize]
+ }
+
+ pub fn new(contrast: f32, paint_gamma: f32, device_gamma: f32) -> GammaLut {
+ #[cfg(target_os="macos")]
+ let mut table = GammaLut {
+ tables: [[0; 256]; 1 << LUM_BITS],
+ cg_inverse_gamma: get_inverse_gamma_table_coregraphics_smoothing(),
+ };
+ #[cfg(not(target_os="macos"))]
+ let mut table = GammaLut {
+ tables: [[0; 256]; 1 << LUM_BITS],
+ };
+
+ table.generate_tables(contrast, paint_gamma, device_gamma);
+
+ table
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend(&self, pixels: &mut [u8], color: ColorU) {
+ let table_r = self.get_table(color.r);
+ let table_g = self.get_table(color.g);
+ let table_b = self.get_table(color.b);
+
+ for pixel in pixels.chunks_mut(4) {
+ let (b, g, r) = (table_b[pixel[0] as usize], table_g[pixel[1] as usize], table_r[pixel[2] as usize]);
+ pixel[0] = b;
+ pixel[1] = g;
+ pixel[2] = r;
+ pixel[3] = max(max(b, g), r);
+ }
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend_scaled(&self, pixels: &mut [u8], color: ColorU, percent: u8) {
+ if percent >= 100 {
+ self.preblend(pixels, color);
+ return;
+ }
+
+ let table_r = self.get_table(color.r);
+ let table_g = self.get_table(color.g);
+ let table_b = self.get_table(color.b);
+ let scale = (percent as i32 * 256) / 100;
+
+ for pixel in pixels.chunks_mut(4) {
+ let (mut b, g, mut r) = (
+ table_b[pixel[0] as usize] as i32,
+ table_g[pixel[1] as usize] as i32,
+ table_r[pixel[2] as usize] as i32,
+ );
+ b = g + (((b - g) * scale) >> 8);
+ r = g + (((r - g) * scale) >> 8);
+ pixel[0] = b as u8;
+ pixel[1] = g as u8;
+ pixel[2] = r as u8;
+ pixel[3] = max(max(b, g), r) as u8;
+ }
+ }
+
+ #[cfg(target_os="macos")]
+ pub fn coregraphics_convert_to_linear(&self, pixels: &mut [u8]) {
+ for pixel in pixels.chunks_mut(4) {
+ pixel[0] = self.cg_inverse_gamma[pixel[0] as usize];
+ pixel[1] = self.cg_inverse_gamma[pixel[1] as usize];
+ pixel[2] = self.cg_inverse_gamma[pixel[2] as usize];
+ }
+ }
+
+ // Assumes pixels are in BGRA format. Assumes pixel values are in linear space already.
+ pub fn preblend_grayscale(&self, pixels: &mut [u8], color: ColorU) {
+ let table_g = self.get_table(color.g);
+
+ for pixel in pixels.chunks_mut(4) {
+ let luminance = compute_luminance(pixel[2], pixel[1], pixel[0]);
+ let alpha = table_g[luminance as usize];
+ pixel[0] = alpha;
+ pixel[1] = alpha;
+ pixel[2] = alpha;
+ pixel[3] = alpha;
+ }
+ }
+
+} // end impl GammaLut
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn over(dst: u32, src: u32, alpha: u32) -> u32 {
+ (src * alpha + dst * (255 - alpha))/255
+ }
+
+ fn overf(dst: f32, src: f32, alpha: f32) -> f32 {
+ ((src * alpha + dst * (255. - alpha))/255.) as f32
+ }
+
+
+ fn absdiff(a: u32, b: u32) -> u32 {
+ if a < b { b - a } else { a - b }
+ }
+
+ #[test]
+ fn gamma() {
+ let mut table = [0u8; 256];
+ let g = 2.0;
+ let space = LuminanceColorSpace::Gamma(g);
+ let mut src : u32 = 131;
+ while src < 256 {
+ build_gamma_correcting_lut(&mut table, src as u8, 0., space, space);
+ let mut max_diff = 0;
+ let mut dst = 0;
+ while dst < 256 {
+ for alpha in 0u32..256 {
+ let preblend = table[alpha as usize];
+ let lin_dst = (dst as f32 / 255.).powf(g) * 255.;
+ let lin_src = (src as f32 / 255.).powf(g) * 255.;
+
+ let preblend_result = over(dst, src, preblend as u32);
+ let true_result = ((overf(lin_dst, lin_src, alpha as f32) / 255.).powf(1. / g) * 255.) as u32;
+ let diff = absdiff(preblend_result, true_result);
+ //println!("{} -- {} {} = {}", alpha, preblend_result, true_result, diff);
+ max_diff = max(max_diff, diff);
+ }
+
+ //println!("{} {} max {}", src, dst, max_diff);
+ assert!(max_diff <= 33);
+ dst += 1;
+
+ }
+ src += 1;
+ }
+ }
+} // end mod
diff --git a/gfx/wr/webrender/src/glyph_cache.rs b/gfx/wr/webrender/src/glyph_cache.rs
new file mode 100644
index 0000000000..6f12276262
--- /dev/null
+++ b/gfx/wr/webrender/src/glyph_cache.rs
@@ -0,0 +1,173 @@
+/* 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 crate::glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer};
+use crate::internal_types::FastHashMap;
+use crate::render_backend::{FrameId, FrameStamp};
+use crate::resource_cache::ResourceClassCache;
+use std::sync::Arc;
+use crate::texture_cache::{EvictionNotice, TextureCache};
+use crate::texture_cache::TextureCacheHandle;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct CachedGlyphInfo {
+ pub format: GlyphFormat,
+ pub texture_cache_handle: TextureCacheHandle,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum GlyphCacheEntry {
+ // A glyph that has been successfully rasterized.
+ Cached(CachedGlyphInfo),
+ // A glyph that should not be rasterized (i.e. a space).
+ Blank,
+ // A glyph that has been submitted to the font backend for rasterization,
+ // but is still pending a result.
+ #[allow(dead_code)]
+ Pending,
+}
+
+impl GlyphCacheEntry {
+ fn has_been_evicted(&self, texture_cache: &TextureCache) -> bool {
+ match *self {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ !texture_cache.is_allocated(&glyph.texture_cache_handle)
+ }
+ GlyphCacheEntry::Pending | GlyphCacheEntry::Blank => false,
+ }
+ }
+}
+
+#[allow(dead_code)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub enum CachedGlyphData {
+ Memory(Arc<Vec<u8>>),
+ Gpu,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Default)]
+pub struct GlyphKeyCacheInfo {
+ eviction_notice: EvictionNotice,
+ last_frame_used: FrameId,
+}
+
+pub type GlyphKeyCache = ResourceClassCache<GlyphKey, GlyphCacheEntry, GlyphKeyCacheInfo>;
+
+impl GlyphKeyCache {
+ pub fn eviction_notice(&self) -> &EvictionNotice {
+ &self.user_data.eviction_notice
+ }
+
+ fn clear_glyphs(&mut self) {
+ self.clear();
+ }
+
+ pub fn add_glyph(&mut self, key: GlyphKey, value: GlyphCacheEntry) {
+ self.insert(key, value);
+ }
+
+ fn clear_evicted(&mut self, texture_cache: &TextureCache) {
+ if self.eviction_notice().check() {
+ // If there are evictions, filter out any glyphs evicted from the
+ // texture cache from the glyph key cache.
+ self.retain(|_, entry| !entry.has_been_evicted(texture_cache));
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphCache {
+ glyph_key_caches: FastHashMap<FontInstance, GlyphKeyCache>,
+ current_frame: FrameId,
+}
+
+impl GlyphCache {
+ pub fn new() -> Self {
+ GlyphCache {
+ glyph_key_caches: FastHashMap::default(),
+ current_frame: Default::default(),
+ }
+ }
+
+ pub fn get_glyph_key_cache_for_font_mut(&mut self, font: FontInstance) -> &mut GlyphKeyCache {
+ let cache = self.glyph_key_caches
+ .entry(font)
+ .or_insert_with(GlyphKeyCache::new);
+ cache.user_data.last_frame_used = self.current_frame;
+ cache
+ }
+
+ pub fn get_glyph_key_cache_for_font(&self, font: &FontInstance) -> &GlyphKeyCache {
+ self.glyph_key_caches
+ .get(font)
+ .expect("BUG: Unable to find glyph key cache!")
+ }
+
+ pub fn clear(&mut self) {
+ for (_, glyph_key_cache) in &mut self.glyph_key_caches {
+ glyph_key_cache.clear()
+ }
+ // We use this in on_memory_pressure where retaining memory allocations
+ // isn't desirable, so we completely remove the hash map instead of clearing it.
+ self.glyph_key_caches = FastHashMap::default();
+ }
+
+ pub fn clear_fonts<F>(&mut self, key_fun: F)
+ where
+ for<'r> F: Fn(&'r &FontInstance) -> bool,
+ {
+ self.glyph_key_caches.retain(|k, cache| {
+ let should_clear = key_fun(&k);
+ if !should_clear {
+ return true;
+ }
+
+ cache.clear_glyphs();
+ false
+ })
+ }
+
+ /// Clear out evicted entries from glyph key caches.
+ fn clear_evicted(&mut self, texture_cache: &TextureCache) {
+ for cache in self.glyph_key_caches.values_mut() {
+ // Scan for any glyph key caches that have evictions.
+ cache.clear_evicted(texture_cache);
+ }
+ }
+
+ /// If possible, remove entirely any empty glyph key caches.
+ fn clear_empty_caches(&mut self, glyph_rasterizer: &mut GlyphRasterizer) {
+ self.glyph_key_caches.retain(|key, cache| {
+ // Discard the glyph key cache if it has no valid glyphs.
+ if cache.is_empty() {
+ glyph_rasterizer.delete_font_instance(key);
+ false
+ } else {
+ true
+ }
+ });
+ }
+
+ pub fn begin_frame(
+ &mut self,
+ stamp: FrameStamp,
+ texture_cache: &mut TextureCache,
+ glyph_rasterizer: &mut GlyphRasterizer,
+ ) {
+ profile_scope!("begin_frame");
+ self.current_frame = stamp.frame_id();
+ self.clear_evicted(texture_cache);
+ // Clearing evicted glyphs and pruning excess usage might have produced empty caches,
+ // so get rid of them if possible.
+ self.clear_empty_caches(glyph_rasterizer);
+ }
+}
diff --git a/gfx/wr/webrender/src/glyph_rasterizer/mod.rs b/gfx/wr/webrender/src/glyph_rasterizer/mod.rs
new file mode 100644
index 0000000000..98aa9e23d9
--- /dev/null
+++ b/gfx/wr/webrender/src/glyph_rasterizer/mod.rs
@@ -0,0 +1,1338 @@
+/* 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 api::{FontInstanceFlags, FontSize, BaseFontInstance};
+use api::{FontKey, FontRenderMode, FontTemplate};
+use api::{ColorU, GlyphIndex, GlyphDimensions, SyntheticItalics};
+use api::channel::{unbounded_channel, Receiver, Sender};
+use api::units::*;
+use api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, DirtyRect};
+use crate::internal_types::ResourceCacheError;
+use crate::platform::font::FontContext;
+use crate::device::TextureFilter;
+use crate::gpu_types::UvRectKind;
+use crate::glyph_cache::{GlyphCache, CachedGlyphInfo, GlyphCacheEntry};
+use crate::internal_types::FastHashMap;
+use crate::resource_cache::CachedImageData;
+use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+use crate::gpu_cache::GpuCache;
+use crate::profiler::{self, TransactionProfile};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use rayon::ThreadPool;
+use rayon::prelude::*;
+use euclid::approxeq::ApproxEq;
+use euclid::size2;
+use smallvec::SmallVec;
+use std::cmp;
+use std::cell::Cell;
+use std::hash::{Hash, Hasher};
+use std::mem;
+use std::ops::Deref;
+use std::sync::{Arc, Condvar, Mutex, MutexGuard};
+use std::sync::atomic::{AtomicBool, Ordering};
+
+pub static GLYPH_FLASHING: AtomicBool = AtomicBool::new(false);
+
+impl FontContexts {
+ /// Get access to the font context associated to the current thread.
+ pub fn lock_current_context(&self) -> MutexGuard<FontContext> {
+ let id = self.current_worker_id();
+ self.lock_context(id)
+ }
+
+ pub(in super) fn current_worker_id(&self) -> Option<usize> {
+ self.workers.current_thread_index()
+ }
+}
+
+thread_local! {
+ pub static SEED: Cell<u32> = Cell::new(0);
+}
+
+// super simple random to avoid dependency on rand
+fn random() -> u32 {
+ SEED.with(|seed| {
+ seed.set(seed.get().wrapping_mul(22695477).wrapping_add(1));
+ seed.get()
+ })
+}
+
+impl GlyphRasterizer {
+ pub fn request_glyphs(
+ &mut self,
+ glyph_cache: &mut GlyphCache,
+ font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ texture_cache: &mut TextureCache,
+ gpu_cache: &mut GpuCache,
+ ) {
+ assert!(
+ self.font_contexts
+ .lock_shared_context()
+ .has_font(&font.font_key)
+ );
+
+ let glyph_key_cache = glyph_cache.get_glyph_key_cache_for_font_mut(font.clone());
+
+ // select glyphs that have not been requested yet.
+ for key in glyph_keys {
+ if let Some(entry) = glyph_key_cache.try_get(key) {
+ match entry {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ // Skip the glyph if it is already has a valid texture cache handle.
+ if !texture_cache.request(&glyph.texture_cache_handle, gpu_cache) {
+ continue;
+ }
+ // This case gets hit when we already rasterized the glyph, but the
+ // glyph has been evicted from the texture cache. Just force it to
+ // pending so it gets rematerialized.
+ }
+ // Otherwise, skip the entry if it is blank or pending.
+ GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue,
+ }
+ }
+
+ // Increment the total number of glyphs that are pending. This is used to determine
+ // later whether to use worker threads for the remaining glyphs during resolve time.
+ self.pending_glyph_count += 1;
+ self.glyph_request_count += 1;
+
+ // Find a batch container for the font instance for this glyph. Use get_mut to avoid
+ // cloning the font instance, since this is the common path.
+ match self.pending_glyph_requests.get_mut(&font) {
+ Some(container) => {
+ container.push(*key);
+
+ // If the batch for this font instance is big enough, kick off an async
+ // job to start rasterizing these glyphs on other threads now.
+ if container.len() == 8 {
+ let glyphs = mem::replace(container, SmallVec::new());
+ self.flush_glyph_requests(
+ font.clone(),
+ glyphs,
+ true,
+ );
+ }
+ }
+ None => {
+ // If no batch exists for this font instance, add the glyph to a new one.
+ self.pending_glyph_requests.insert(
+ font.clone(),
+ smallvec![*key],
+ );
+ }
+ }
+
+ glyph_key_cache.add_glyph(*key, GlyphCacheEntry::Pending);
+ }
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ self.enable_multithreading = enable;
+ }
+
+ /// Internal method to flush a list of glyph requests to a set of worker threads,
+ /// or process on this thread if there isn't much work to do (in which case the
+ /// overhead of processing these on a thread is unlikely to be a performance win).
+ fn flush_glyph_requests(
+ &mut self,
+ font: FontInstance,
+ glyphs: SmallVec<[GlyphKey; 16]>,
+ use_workers: bool,
+ ) {
+ let font_contexts = Arc::clone(&self.font_contexts);
+ let glyph_tx = self.glyph_tx.clone();
+ self.pending_glyph_jobs += 1;
+ self.pending_glyph_count -= glyphs.len();
+
+ fn process_glyph(key: &GlyphKey, font_contexts: &FontContexts, font: &FontInstance) -> GlyphRasterJob {
+ profile_scope!("glyph-raster");
+ let mut context = font_contexts.lock_current_context();
+ let mut job = GlyphRasterJob {
+ key: key.clone(),
+ result: context.rasterize_glyph(&font, key),
+ };
+
+ if let Ok(ref mut glyph) = job.result {
+ // Sanity check.
+ let bpp = 4; // We always render glyphs in 32 bits RGBA format.
+ assert_eq!(
+ glyph.bytes.len(),
+ bpp * (glyph.width * glyph.height) as usize
+ );
+
+ // a quick-and-dirty monochrome over
+ fn over(dst: u8, src: u8) -> u8 {
+ let a = src as u32;
+ let a = 256 - a;
+ let dst = ((dst as u32 * a) >> 8) as u8;
+ src + dst
+ }
+
+ if GLYPH_FLASHING.load(Ordering::Relaxed) {
+ let color = (random() & 0xff) as u8;
+ for i in &mut glyph.bytes {
+ *i = over(*i, color);
+ }
+ }
+
+ assert_eq!((glyph.left.fract(), glyph.top.fract()), (0.0, 0.0));
+
+ // Check if the glyph has a bitmap that needs to be downscaled.
+ glyph.downscale_bitmap_if_required(&font);
+
+ // Convert from BGRA8 to R8 if required. In the future we can make it the
+ // backends' responsibility to output glyphs in the desired format,
+ // potentially reducing the number of copies.
+ if glyph.format.image_format().bytes_per_pixel() == 1 {
+ glyph.bytes = glyph.bytes
+ .chunks_mut(4)
+ .map(|pixel| pixel[3])
+ .collect::<Vec<_>>();
+ }
+ }
+
+ job
+ }
+
+ // if the number of glyphs is small, do it inline to avoid the threading overhead;
+ // send the result into glyph_tx so downstream code can't tell the difference.
+ if self.enable_multithreading && use_workers {
+ // spawn an async task to get off of the render backend thread as early as
+ // possible and in that task use rayon's fork join dispatch to rasterize the
+ // glyphs in the thread pool.
+ profile_scope!("spawning process_glyph jobs");
+ self.workers.spawn(move || {
+ let jobs = glyphs
+ .par_iter()
+ .map(|key: &GlyphKey| process_glyph(key, &font_contexts, &font))
+ .collect();
+
+ glyph_tx.send(GlyphRasterJobs { font, jobs }).unwrap();
+ });
+ } else {
+ let jobs = glyphs.iter()
+ .map(|key: &GlyphKey| process_glyph(key, &font_contexts, &font))
+ .collect();
+ glyph_tx.send(GlyphRasterJobs { font, jobs }).unwrap();
+ }
+ }
+
+ pub fn resolve_glyphs(
+ &mut self,
+ glyph_cache: &mut GlyphCache,
+ texture_cache: &mut TextureCache,
+ gpu_cache: &mut GpuCache,
+ profile: &mut TransactionProfile,
+ ) {
+ profile.start_time(profiler::GLYPH_RESOLVE_TIME);
+
+ // Work around the borrow checker, since we call flush_glyph_requests below
+ let mut pending_glyph_requests = mem::replace(
+ &mut self.pending_glyph_requests,
+ FastHashMap::default(),
+ );
+ // If we have a large amount of remaining work to do, spawn to worker threads,
+ // even if that work is shared among a number of different font instances.
+ let use_workers = self.pending_glyph_count >= 8;
+ for (font, pending_glyphs) in pending_glyph_requests.drain() {
+ self.flush_glyph_requests(
+ font,
+ pending_glyphs,
+ use_workers,
+ );
+ }
+ // Restore this so that we don't heap allocate next frame
+ self.pending_glyph_requests = pending_glyph_requests;
+ debug_assert_eq!(self.pending_glyph_count, 0);
+ debug_assert!(self.pending_glyph_requests.is_empty());
+
+ if self.glyph_request_count > 0 {
+ profile.set(profiler::RASTERIZED_GLYPHS, self.glyph_request_count);
+ self.glyph_request_count = 0;
+ }
+
+ profile_scope!("resolve_glyphs");
+ // Pull rasterized glyphs from the queue and update the caches.
+ while self.pending_glyph_jobs > 0 {
+ self.pending_glyph_jobs -= 1;
+
+ // TODO: rather than blocking until all pending glyphs are available
+ // we could try_recv and steal work from the thread pool to take advantage
+ // of the fact that this thread is alive and we avoid the added latency
+ // of blocking it.
+
+ let GlyphRasterJobs { font, mut jobs } = {
+ profile_scope!("blocking wait on glyph_rx");
+ self.glyph_rx
+ .recv()
+ .expect("BUG: Should be glyphs pending!")
+ };
+
+ // Ensure that the glyphs are always processed in the same
+ // order for a given text run (since iterating a hash set doesn't
+ // guarantee order). This can show up as very small float inaccuracy
+ // differences in rasterizers due to the different coordinates
+ // that text runs get associated with by the texture cache allocator.
+ jobs.sort_by(|a, b| a.key.cmp(&b.key));
+
+ let glyph_key_cache = glyph_cache.get_glyph_key_cache_for_font_mut(font);
+
+ for GlyphRasterJob { key, result } in jobs {
+ let glyph_info = match result {
+ Err(_) => GlyphCacheEntry::Blank,
+ Ok(ref glyph) if glyph.width == 0 || glyph.height == 0 => {
+ GlyphCacheEntry::Blank
+ }
+ Ok(glyph) => {
+ let mut texture_cache_handle = TextureCacheHandle::invalid();
+ texture_cache.request(&texture_cache_handle, gpu_cache);
+ texture_cache.update(
+ &mut texture_cache_handle,
+ ImageDescriptor {
+ size: size2(glyph.width, glyph.height),
+ stride: None,
+ format: glyph.format.image_format(),
+ flags: ImageDescriptorFlags::empty(),
+ offset: 0,
+ },
+ TextureFilter::Linear,
+ Some(CachedImageData::Raw(Arc::new(glyph.bytes))),
+ [glyph.left, -glyph.top, glyph.scale],
+ DirtyRect::All,
+ gpu_cache,
+ Some(glyph_key_cache.eviction_notice()),
+ UvRectKind::Rect,
+ Eviction::Auto,
+ TargetShader::Text,
+ );
+ GlyphCacheEntry::Cached(CachedGlyphInfo {
+ texture_cache_handle,
+ format: glyph.format,
+ })
+ }
+ };
+ glyph_key_cache.insert(key, glyph_info);
+ }
+ }
+
+ // Now that we are done with the critical path (rendering the glyphs),
+ // we can schedule removing the fonts if needed.
+ self.remove_dead_fonts();
+
+ profile.end_time(profiler::GLYPH_RESOLVE_TIME);
+ }
+}
+
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FontTransform {
+ pub scale_x: f32,
+ pub skew_x: f32,
+ pub skew_y: f32,
+ pub scale_y: f32,
+}
+
+// Floats don't impl Hash/Eq/Ord...
+impl Eq for FontTransform {}
+impl Ord for FontTransform {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.partial_cmp(other).unwrap_or(cmp::Ordering::Equal)
+ }
+}
+impl Hash for FontTransform {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Note: this is inconsistent with the Eq impl for -0.0 (don't care).
+ self.scale_x.to_bits().hash(state);
+ self.skew_x.to_bits().hash(state);
+ self.skew_y.to_bits().hash(state);
+ self.scale_y.to_bits().hash(state);
+ }
+}
+
+impl FontTransform {
+ const QUANTIZE_SCALE: f32 = 1024.0;
+
+ pub fn new(scale_x: f32, skew_x: f32, skew_y: f32, scale_y: f32) -> Self {
+ FontTransform { scale_x, skew_x, skew_y, scale_y }
+ }
+
+ pub fn identity() -> Self {
+ FontTransform::new(1.0, 0.0, 0.0, 1.0)
+ }
+
+ #[allow(dead_code)]
+ pub fn is_identity(&self) -> bool {
+ *self == FontTransform::identity()
+ }
+
+ pub fn quantize(&self) -> Self {
+ FontTransform::new(
+ (self.scale_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.skew_x * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.skew_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ (self.scale_y * Self::QUANTIZE_SCALE).round() / Self::QUANTIZE_SCALE,
+ )
+ }
+
+ #[allow(dead_code)]
+ pub fn determinant(&self) -> f64 {
+ self.scale_x as f64 * self.scale_y as f64 - self.skew_y as f64 * self.skew_x as f64
+ }
+
+ #[allow(dead_code)]
+ pub fn compute_scale(&self) -> Option<(f64, f64)> {
+ let det = self.determinant();
+ if det != 0.0 {
+ let x_scale = (self.scale_x as f64).hypot(self.skew_y as f64);
+ let y_scale = det.abs() / x_scale;
+ Some((x_scale, y_scale))
+ } else {
+ None
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn pre_scale(&self, scale_x: f32, scale_y: f32) -> Self {
+ FontTransform::new(
+ self.scale_x * scale_x,
+ self.skew_x * scale_y,
+ self.skew_y * scale_x,
+ self.scale_y * scale_y,
+ )
+ }
+
+ #[allow(dead_code)]
+ pub fn scale(&self, scale: f32) -> Self { self.pre_scale(scale, scale) }
+
+ #[allow(dead_code)]
+ pub fn invert_scale(&self, x_scale: f64, y_scale: f64) -> Self {
+ self.pre_scale(x_scale.recip() as f32, y_scale.recip() as f32)
+ }
+
+ pub fn synthesize_italics(&self, angle: SyntheticItalics, size: f64, vertical: bool) -> (Self, (f64, f64)) {
+ let skew_factor = angle.to_skew();
+ if vertical {
+ // origin delta to be applied so that we effectively skew around
+ // the middle rather than edge of the glyph
+ let (tx, ty) = (0.0, -size * 0.5 * skew_factor as f64);
+ (FontTransform::new(
+ self.scale_x + self.skew_x * skew_factor,
+ self.skew_x,
+ self.skew_y + self.scale_y * skew_factor,
+ self.scale_y,
+ ), (self.scale_x as f64 * tx + self.skew_x as f64 * ty,
+ self.skew_y as f64 * tx + self.scale_y as f64 * ty))
+ } else {
+ (FontTransform::new(
+ self.scale_x,
+ self.skew_x - self.scale_x * skew_factor,
+ self.skew_y,
+ self.scale_y - self.skew_y * skew_factor,
+ ), (0.0, 0.0))
+ }
+ }
+
+ pub fn swap_xy(&self) -> Self {
+ FontTransform::new(self.skew_x, self.scale_x, self.scale_y, self.skew_y)
+ }
+
+ pub fn flip_x(&self) -> Self {
+ FontTransform::new(-self.scale_x, self.skew_x, -self.skew_y, self.scale_y)
+ }
+
+ pub fn flip_y(&self) -> Self {
+ FontTransform::new(self.scale_x, -self.skew_x, self.skew_y, -self.scale_y)
+ }
+
+ pub fn transform(&self, point: &LayoutPoint) -> DevicePoint {
+ DevicePoint::new(
+ self.scale_x * point.x + self.skew_x * point.y,
+ self.skew_y * point.x + self.scale_y * point.y,
+ )
+ }
+
+ pub fn get_subpx_dir(&self) -> SubpixelDirection {
+ if self.skew_y.approx_eq(&0.0) {
+ // The X axis is not projected onto the Y axis
+ SubpixelDirection::Horizontal
+ } else if self.scale_x.approx_eq(&0.0) {
+ // The X axis has been swapped with the Y axis
+ SubpixelDirection::Vertical
+ } else {
+ // Use subpixel precision on all axes
+ SubpixelDirection::Mixed
+ }
+ }
+}
+
+impl<'a> From<&'a LayoutToWorldTransform> for FontTransform {
+ fn from(xform: &'a LayoutToWorldTransform) -> Self {
+ FontTransform::new(xform.m11, xform.m21, xform.m12, xform.m22)
+ }
+}
+
+// Some platforms (i.e. Windows) may have trouble rasterizing glyphs above this size.
+// Ensure glyph sizes are reasonably limited to avoid that scenario.
+pub const FONT_SIZE_LIMIT: f32 = 320.0;
+
+/// A mutable font instance description.
+///
+/// Performance is sensitive to the size of this structure, so it should only contain
+/// the fields that we need to modify from the original base font instance.
+#[derive(Clone, PartialEq, Eq, Debug, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FontInstance {
+ pub base: Arc<BaseFontInstance>,
+ pub transform: FontTransform,
+ pub render_mode: FontRenderMode,
+ pub flags: FontInstanceFlags,
+ pub color: ColorU,
+ // The font size is in *device/raster* pixels, not logical pixels.
+ // It is stored as an f32 since we need sub-pixel sizes.
+ pub size: FontSize,
+}
+
+impl Hash for FontInstance {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ // Hash only the base instance's key to avoid the cost of hashing
+ // the rest.
+ self.base.instance_key.hash(state);
+ self.transform.hash(state);
+ self.render_mode.hash(state);
+ self.flags.hash(state);
+ self.color.hash(state);
+ self.size.hash(state);
+ }
+}
+
+impl Deref for FontInstance {
+ type Target = BaseFontInstance;
+ fn deref(&self) -> &BaseFontInstance {
+ self.base.as_ref()
+ }
+}
+
+impl MallocSizeOf for FontInstance {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { 0 }
+}
+
+impl FontInstance {
+ pub fn new(
+ base: Arc<BaseFontInstance>,
+ color: ColorU,
+ render_mode: FontRenderMode,
+ flags: FontInstanceFlags,
+ ) -> Self {
+ FontInstance {
+ transform: FontTransform::identity(),
+ color,
+ size: base.size,
+ base,
+ render_mode,
+ flags,
+ }
+ }
+
+ pub fn from_base(
+ base: Arc<BaseFontInstance>,
+ ) -> Self {
+ FontInstance {
+ transform: FontTransform::identity(),
+ color: ColorU::new(0, 0, 0, 255),
+ size: base.size,
+ render_mode: base.render_mode,
+ flags: base.flags,
+ base,
+ }
+ }
+
+ pub fn use_texture_padding(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::TEXTURE_PADDING)
+ }
+
+ pub fn use_transform_glyphs(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::TRANSFORM_GLYPHS)
+ }
+
+ pub fn get_alpha_glyph_format(&self) -> GlyphFormat {
+ if self.use_transform_glyphs() { GlyphFormat::TransformedAlpha } else { GlyphFormat::Alpha }
+ }
+
+ pub fn get_subpixel_glyph_format(&self) -> GlyphFormat {
+ if self.use_transform_glyphs() { GlyphFormat::TransformedSubpixel } else { GlyphFormat::Subpixel }
+ }
+
+ pub fn disable_subpixel_aa(&mut self) {
+ self.render_mode = self.render_mode.limit_by(FontRenderMode::Alpha);
+ }
+
+ pub fn disable_subpixel_position(&mut self) {
+ self.flags.remove(FontInstanceFlags::SUBPIXEL_POSITION);
+ }
+
+ pub fn use_subpixel_position(&self) -> bool {
+ self.flags.contains(FontInstanceFlags::SUBPIXEL_POSITION) &&
+ self.render_mode != FontRenderMode::Mono
+ }
+
+ pub fn get_subpx_dir(&self) -> SubpixelDirection {
+ if self.use_subpixel_position() {
+ let mut subpx_dir = self.transform.get_subpx_dir();
+ if self.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ subpx_dir = subpx_dir.swap_xy();
+ }
+ subpx_dir
+ } else {
+ SubpixelDirection::None
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_subpx_offset(&self, glyph: &GlyphKey) -> (f64, f64) {
+ if self.use_subpixel_position() {
+ let (dx, dy) = glyph.subpixel_offset();
+ (dx.into(), dy.into())
+ } else {
+ (0.0, 0.0)
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_glyph_format(&self) -> GlyphFormat {
+ match self.render_mode {
+ FontRenderMode::Mono | FontRenderMode::Alpha => self.get_alpha_glyph_format(),
+ FontRenderMode::Subpixel => self.get_subpixel_glyph_format(),
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn get_extra_strikes(&self, x_scale: f64) -> usize {
+ if self.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ let mut bold_offset = self.size.to_f64_px() / 48.0;
+ if bold_offset < 1.0 {
+ bold_offset = 0.25 + 0.75 * bold_offset;
+ }
+ (bold_offset * x_scale).max(1.0).round() as usize
+ } else {
+ 0
+ }
+ }
+
+ pub fn synthesize_italics(&self, transform: FontTransform, size: f64) -> (FontTransform, (f64, f64)) {
+ transform.synthesize_italics(self.synthetic_italics, size, self.flags.contains(FontInstanceFlags::VERTICAL))
+ }
+
+ #[allow(dead_code)]
+ pub fn get_transformed_size(&self) -> f64 {
+ let (_, y_scale) = self.transform.compute_scale().unwrap_or((1.0, 1.0));
+ self.size.to_f64_px() * y_scale
+ }
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
+pub enum SubpixelDirection {
+ None = 0,
+ Horizontal,
+ Vertical,
+ Mixed,
+}
+
+impl SubpixelDirection {
+ // Limit the subpixel direction to what is supported by the glyph format.
+ pub fn limit_by(self, glyph_format: GlyphFormat) -> Self {
+ match glyph_format {
+ GlyphFormat::Bitmap |
+ GlyphFormat::ColorBitmap => SubpixelDirection::None,
+ _ => self,
+ }
+ }
+
+ pub fn swap_xy(self) -> Self {
+ match self {
+ SubpixelDirection::None | SubpixelDirection::Mixed => self,
+ SubpixelDirection::Horizontal => SubpixelDirection::Vertical,
+ SubpixelDirection::Vertical => SubpixelDirection::Horizontal,
+ }
+ }
+}
+
+#[repr(u8)]
+#[derive(Hash, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SubpixelOffset {
+ Zero = 0,
+ Quarter = 1,
+ Half = 2,
+ ThreeQuarters = 3,
+}
+
+impl SubpixelOffset {
+ // Skia quantizes subpixel offsets into 1/4 increments.
+ // Given the absolute position, return the quantized increment
+ fn quantize(pos: f32) -> Self {
+ // Following the conventions of Gecko and Skia, we want
+ // to quantize the subpixel position, such that abs(pos) gives:
+ // [0.0, 0.125) -> Zero
+ // [0.125, 0.375) -> Quarter
+ // [0.375, 0.625) -> Half
+ // [0.625, 0.875) -> ThreeQuarters,
+ // [0.875, 1.0) -> Zero
+ // The unit tests below check for this.
+ let apos = ((pos - pos.floor()) * 8.0) as i32;
+
+ match apos {
+ 1..=2 => SubpixelOffset::Quarter,
+ 3..=4 => SubpixelOffset::Half,
+ 5..=6 => SubpixelOffset::ThreeQuarters,
+ _ => SubpixelOffset::Zero,
+ }
+ }
+}
+
+impl Into<f64> for SubpixelOffset {
+ fn into(self) -> f64 {
+ match self {
+ SubpixelOffset::Zero => 0.0,
+ SubpixelOffset::Quarter => 0.25,
+ SubpixelOffset::Half => 0.5,
+ SubpixelOffset::ThreeQuarters => 0.75,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphKey(u32);
+
+impl GlyphKey {
+ pub fn new(
+ index: u32,
+ point: DevicePoint,
+ subpx_dir: SubpixelDirection,
+ ) -> Self {
+ let (dx, dy) = match subpx_dir {
+ SubpixelDirection::None => (0.0, 0.0),
+ SubpixelDirection::Horizontal => (point.x, 0.0),
+ SubpixelDirection::Vertical => (0.0, point.y),
+ SubpixelDirection::Mixed => (point.x, point.y),
+ };
+ let sox = SubpixelOffset::quantize(dx);
+ let soy = SubpixelOffset::quantize(dy);
+ assert_eq!(0, index & 0xF0000000);
+
+ GlyphKey(index | (sox as u32) << 28 | (soy as u32) << 30)
+ }
+
+ pub fn index(&self) -> GlyphIndex {
+ self.0 & 0x0FFFFFFF
+ }
+
+ fn subpixel_offset(&self) -> (SubpixelOffset, SubpixelOffset) {
+ let x = (self.0 >> 28) as u8 & 3;
+ let y = (self.0 >> 30) as u8 & 3;
+ unsafe {
+ (mem::transmute(x), mem::transmute(y))
+ }
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[allow(dead_code)]
+pub enum GlyphFormat {
+ Alpha,
+ TransformedAlpha,
+ Subpixel,
+ TransformedSubpixel,
+ Bitmap,
+ ColorBitmap,
+}
+
+impl GlyphFormat {
+ pub fn ignore_color(self) -> Self {
+ match self {
+ GlyphFormat::ColorBitmap => GlyphFormat::Bitmap,
+ _ => self,
+ }
+ }
+
+ /// Returns the ImageFormat that a glyph should be stored as in the texture cache.
+ pub fn image_format(&self) -> ImageFormat {
+ // GlyphFormat::Alpha and GlyphFormat::TransformedAlpha should be stored in an R8
+ // texture, but we ran in to some rendering issues on various platforms after doing so.
+ // So keep using BGRA8 for all glyph formats until those are solved.
+ // See bug 1687554.
+ ImageFormat::BGRA8
+ }
+}
+
+pub struct RasterizedGlyph {
+ pub top: f32,
+ pub left: f32,
+ pub width: i32,
+ pub height: i32,
+ pub scale: f32,
+ pub format: GlyphFormat,
+ pub bytes: Vec<u8>,
+}
+
+impl RasterizedGlyph {
+ #[allow(dead_code)]
+ pub fn downscale_bitmap_if_required(&mut self, font: &FontInstance) {
+ // Check if the glyph is going to be downscaled in the shader. If the scaling is
+ // less than 0.5, that means bilinear filtering can't effectively filter the glyph
+ // without aliasing artifacts.
+ //
+ // Instead of fixing this by mipmapping the glyph cache texture, rather manually
+ // produce the appropriate mip level for individual glyphs where bilinear filtering
+ // will still produce acceptable results.
+ match self.format {
+ GlyphFormat::Bitmap | GlyphFormat::ColorBitmap => {},
+ _ => return,
+ }
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let upscaled = x_scale.max(y_scale) as f32;
+ let mut new_scale = self.scale;
+ if new_scale * upscaled <= 0.0 {
+ return;
+ }
+ let mut steps = 0;
+ while new_scale * upscaled <= 0.5 {
+ new_scale *= 2.0;
+ steps += 1;
+ }
+ // If no mipping is necessary, just bail.
+ if steps == 0 {
+ return;
+ }
+
+ // Calculate the actual size of the mip level.
+ let new_width = (self.width as usize + (1 << steps) - 1) >> steps;
+ let new_height = (self.height as usize + (1 << steps) - 1) >> steps;
+ let mut new_bytes: Vec<u8> = Vec::with_capacity(new_width * new_height * 4);
+
+ // Produce destination pixels by applying a box filter to the source pixels.
+ // The box filter corresponds to how graphics drivers may generate mipmaps.
+ for y in 0 .. new_height {
+ for x in 0 .. new_width {
+ // Calculate the number of source samples that contribute to the destination pixel.
+ let src_y = y << steps;
+ let src_x = x << steps;
+ let y_samples = (1 << steps).min(self.height as usize - src_y);
+ let x_samples = (1 << steps).min(self.width as usize - src_x);
+ let num_samples = (x_samples * y_samples) as u32;
+
+ let mut src_idx = (src_y * self.width as usize + src_x) * 4;
+ // Initialize the accumulator with half an increment so that when later divided
+ // by the sample count, it will effectively round the accumulator to the nearest
+ // increment.
+ let mut accum = [num_samples / 2; 4];
+ // Accumulate all the contributing source sampless.
+ for _ in 0 .. y_samples {
+ for _ in 0 .. x_samples {
+ accum[0] += self.bytes[src_idx + 0] as u32;
+ accum[1] += self.bytes[src_idx + 1] as u32;
+ accum[2] += self.bytes[src_idx + 2] as u32;
+ accum[3] += self.bytes[src_idx + 3] as u32;
+ src_idx += 4;
+ }
+ src_idx += (self.width as usize - x_samples) * 4;
+ }
+
+ // Finally, divide by the sample count to get the mean value for the new pixel.
+ new_bytes.extend_from_slice(&[
+ (accum[0] / num_samples) as u8,
+ (accum[1] / num_samples) as u8,
+ (accum[2] / num_samples) as u8,
+ (accum[3] / num_samples) as u8,
+ ]);
+ }
+ }
+
+ // Fix the bounds for the new glyph data.
+ self.top /= (1 << steps) as f32;
+ self.left /= (1 << steps) as f32;
+ self.width = new_width as i32;
+ self.height = new_height as i32;
+ self.scale = new_scale;
+ self.bytes = new_bytes;
+ }
+}
+
+pub struct FontContexts {
+ // These worker are mostly accessed from their corresponding worker threads.
+ // The goal is that there should be no noticeable contention on the mutexes.
+ worker_contexts: Vec<Mutex<FontContext>>,
+ // This worker should be accessed by threads that don't belong to the thread pool
+ // (in theory that's only the render backend thread so no contention expected either).
+ shared_context: Mutex<FontContext>,
+ // Stored here as a convenience to get the current thread index.
+ #[allow(dead_code)]
+ workers: Arc<ThreadPool>,
+ locked_mutex: Mutex<bool>,
+ locked_cond: Condvar,
+}
+
+impl FontContexts {
+
+ /// Get access to any particular font context.
+ ///
+ /// The id is ```Some(i)``` where i is an index between 0 and num_worker_contexts
+ /// for font contexts associated to the thread pool, and None for the shared
+ /// global font context for use outside of the thread pool.
+ pub fn lock_context(&self, id: Option<usize>) -> MutexGuard<FontContext> {
+ match id {
+ Some(index) => self.worker_contexts[index].lock().unwrap(),
+ None => self.shared_context.lock().unwrap(),
+ }
+ }
+
+ /// Get access to the font context usable outside of the thread pool.
+ pub fn lock_shared_context(&self) -> MutexGuard<FontContext> {
+ self.shared_context.lock().unwrap()
+ }
+
+ // number of contexts associated to workers
+ pub fn num_worker_contexts(&self) -> usize {
+ self.worker_contexts.len()
+ }
+}
+
+pub trait AsyncForEach<T> {
+ fn async_for_each<F: Fn(MutexGuard<T>) + Send + 'static>(&self, f: F);
+}
+
+impl AsyncForEach<FontContext> for Arc<FontContexts> {
+ fn async_for_each<F: Fn(MutexGuard<FontContext>) + Send + 'static>(&self, f: F) {
+ // Reset the locked condition.
+ let mut locked = self.locked_mutex.lock().unwrap();
+ *locked = false;
+
+ // Arc that can be safely moved into a spawn closure.
+ let font_contexts = self.clone();
+ // Spawn a new thread on which to run the for-each off the main thread.
+ self.workers.spawn(move || {
+ // Lock the shared and worker contexts up front.
+ let mut locks = Vec::with_capacity(font_contexts.num_worker_contexts() + 1);
+ locks.push(font_contexts.lock_shared_context());
+ for i in 0 .. font_contexts.num_worker_contexts() {
+ locks.push(font_contexts.lock_context(Some(i)));
+ }
+
+ // Signal the locked condition now that all contexts are locked.
+ *font_contexts.locked_mutex.lock().unwrap() = true;
+ font_contexts.locked_cond.notify_all();
+
+ // Now that everything is locked, proceed to processing each locked context.
+ for context in locks {
+ f(context);
+ }
+ });
+
+ // Wait for locked condition before resuming. Safe to proceed thereafter
+ // since any other thread that needs to use a FontContext will try to lock
+ // it first.
+ while !*locked {
+ locked = self.locked_cond.wait(locked).unwrap();
+ }
+ }
+}
+
+pub struct GlyphRasterizer {
+ #[allow(dead_code)]
+ workers: Arc<ThreadPool>,
+ font_contexts: Arc<FontContexts>,
+
+ /// The current number of individual glyphs waiting in pending batches.
+ pending_glyph_count: usize,
+
+ /// The current number of glyph request jobs that have been kicked to worker threads.
+ pending_glyph_jobs: usize,
+
+ /// The number of glyphs requested this frame.
+ glyph_request_count: usize,
+
+ /// A map of current glyph request batches.
+ pending_glyph_requests: FastHashMap<FontInstance, SmallVec<[GlyphKey; 16]>>,
+
+ // Receives the rendered glyphs.
+ glyph_rx: Receiver<GlyphRasterJobs>,
+ glyph_tx: Sender<GlyphRasterJobs>,
+
+ // We defer removing fonts to the end of the frame so that:
+ // - this work is done outside of the critical path,
+ // - we don't have to worry about the ordering of events if a font is used on
+ // a frame where it is used (although it seems unlikely).
+ fonts_to_remove: Vec<FontKey>,
+ // Defer removal of font instances, as for fonts.
+ font_instances_to_remove: Vec<FontInstance>,
+
+ // Whether to parallelize glyph rasterization with rayon.
+ enable_multithreading: bool,
+}
+
+impl GlyphRasterizer {
+ pub fn new(workers: Arc<ThreadPool>) -> Result<Self, ResourceCacheError> {
+ let (glyph_tx, glyph_rx) = unbounded_channel();
+
+ let num_workers = workers.current_num_threads();
+ let mut contexts = Vec::with_capacity(num_workers);
+
+ let shared_context = FontContext::new()?;
+
+ for _ in 0 .. num_workers {
+ contexts.push(Mutex::new(FontContext::new()?));
+ }
+
+ let font_context = FontContexts {
+ worker_contexts: contexts,
+ shared_context: Mutex::new(shared_context),
+ workers: Arc::clone(&workers),
+ locked_mutex: Mutex::new(false),
+ locked_cond: Condvar::new(),
+ };
+
+ Ok(GlyphRasterizer {
+ font_contexts: Arc::new(font_context),
+ pending_glyph_jobs: 0,
+ pending_glyph_count: 0,
+ glyph_request_count: 0,
+ glyph_rx,
+ glyph_tx,
+ workers,
+ fonts_to_remove: Vec::new(),
+ font_instances_to_remove: Vec::new(),
+ enable_multithreading: true,
+ pending_glyph_requests: FastHashMap::default(),
+ })
+ }
+
+ pub fn add_font(&mut self, font_key: FontKey, template: FontTemplate) {
+ self.font_contexts.async_for_each(move |mut context| {
+ context.add_font(&font_key, &template);
+ });
+ }
+
+ pub fn delete_font(&mut self, font_key: FontKey) {
+ self.fonts_to_remove.push(font_key);
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ self.font_instances_to_remove.push(instance.clone());
+ }
+
+ pub fn prepare_font(&self, font: &mut FontInstance) {
+ FontContext::prepare_font(font);
+
+ // Quantize the transform to minimize thrashing of the glyph cache, but
+ // only quantize the transform when preparing to access the glyph cache.
+ // This way, the glyph subpixel positions, which are calculated before
+ // this, can still use the precise transform which is required to match
+ // the subpixel positions computed for glyphs in the text run shader.
+ font.transform = font.transform.quantize();
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ glyph_index: GlyphIndex,
+ ) -> Option<GlyphDimensions> {
+ let glyph_key = GlyphKey::new(
+ glyph_index,
+ DevicePoint::zero(),
+ SubpixelDirection::None,
+ );
+
+ self.font_contexts
+ .lock_shared_context()
+ .get_glyph_dimensions(font, &glyph_key)
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ self.font_contexts
+ .lock_shared_context()
+ .get_glyph_index(font_key, ch)
+ }
+
+ fn remove_dead_fonts(&mut self) {
+ if self.fonts_to_remove.is_empty() && self.font_instances_to_remove.is_empty() {
+ return
+ }
+
+ profile_scope!("remove_dead_fonts");
+ let fonts_to_remove = mem::replace(&mut self.fonts_to_remove, Vec::new());
+ let font_instances_to_remove = mem::replace(& mut self.font_instances_to_remove, Vec::new());
+ self.font_contexts.async_for_each(move |mut context| {
+ for font_key in &fonts_to_remove {
+ context.delete_font(font_key);
+ }
+ for instance in &font_instances_to_remove {
+ context.delete_font_instance(instance);
+ }
+ });
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn reset(&mut self) {
+ //TODO: any signals need to be sent to the workers?
+ self.pending_glyph_jobs = 0;
+ self.pending_glyph_count = 0;
+ self.glyph_request_count = 0;
+ self.fonts_to_remove.clear();
+ self.font_instances_to_remove.clear();
+ }
+}
+
+trait AddFont {
+ fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate);
+}
+
+impl AddFont for FontContext {
+ fn add_font(&mut self, font_key: &FontKey, template: &FontTemplate) {
+ match *template {
+ FontTemplate::Raw(ref bytes, index) => {
+ self.add_raw_font(font_key, bytes.clone(), index);
+ }
+ FontTemplate::Native(ref native_font_handle) => {
+ self.add_native_font(font_key, (*native_font_handle).clone());
+ }
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub(in crate::glyph_rasterizer) struct GlyphRasterJob {
+ key: GlyphKey,
+ result: GlyphRasterResult,
+}
+
+#[allow(dead_code)]
+pub enum GlyphRasterError {
+ LoadFailed,
+}
+
+#[allow(dead_code)]
+pub type GlyphRasterResult = Result<RasterizedGlyph, GlyphRasterError>;
+
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuGlyphCacheKey(pub u32);
+
+#[allow(dead_code)]
+struct GlyphRasterJobs {
+ font: FontInstance,
+ jobs: Vec<GlyphRasterJob>,
+}
+
+#[cfg(test)]
+mod test_glyph_rasterizer {
+ pub const FORMAT: api::ImageFormat = api::ImageFormat::BGRA8;
+
+ #[test]
+ fn rasterize_200_glyphs() {
+ // This test loads a font from disc, the renders 4 requests containing
+ // 50 glyphs each, deletes the font and waits for the result.
+
+ use rayon::ThreadPoolBuilder;
+ use std::fs::File;
+ use std::io::Read;
+ use crate::texture_cache::TextureCache;
+ use crate::glyph_cache::GlyphCache;
+ use crate::gpu_cache::GpuCache;
+ use crate::profiler::TransactionProfile;
+ use api::{FontKey, FontInstanceKey, FontSize, FontTemplate, FontRenderMode,
+ IdNamespace, ColorU};
+ use api::units::DevicePoint;
+ use std::sync::Arc;
+ use crate::glyph_rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer};
+
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .build();
+ let workers = Arc::new(worker.unwrap());
+ let mut glyph_rasterizer = GlyphRasterizer::new(workers).unwrap();
+ let mut glyph_cache = GlyphCache::new();
+ let mut gpu_cache = GpuCache::new_for_testing();
+ let mut texture_cache = TextureCache::new_for_testing(2048, FORMAT);
+ let mut font_file =
+ File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
+ let mut font_data = vec![];
+ font_file
+ .read_to_end(&mut font_data)
+ .expect("failed to read font file");
+
+ let font_key = FontKey::new(IdNamespace(0), 0);
+ glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0));
+
+ let font = FontInstance::from_base(Arc::new(BaseFontInstance {
+ instance_key: FontInstanceKey(IdNamespace(0), 0),
+ font_key,
+ size: FontSize::from_f32_px(32.0),
+ bg_color: ColorU::new(0, 0, 0, 0),
+ render_mode: FontRenderMode::Subpixel,
+ flags: Default::default(),
+ synthetic_italics: Default::default(),
+ platform_options: None,
+ variations: Vec::new(),
+ }));
+
+ let subpx_dir = font.get_subpx_dir();
+
+ let mut glyph_keys = Vec::with_capacity(200);
+ for i in 0 .. 200 {
+ glyph_keys.push(GlyphKey::new(
+ i,
+ DevicePoint::zero(),
+ subpx_dir,
+ ));
+ }
+
+ for i in 0 .. 4 {
+ glyph_rasterizer.request_glyphs(
+ &mut glyph_cache,
+ font.clone(),
+ &glyph_keys[(50 * i) .. (50 * (i + 1))],
+ &mut texture_cache,
+ &mut gpu_cache,
+ );
+ }
+
+ glyph_rasterizer.delete_font(font_key);
+
+ glyph_rasterizer.resolve_glyphs(
+ &mut glyph_cache,
+ &mut TextureCache::new_for_testing(4096, FORMAT),
+ &mut gpu_cache,
+ &mut TransactionProfile::new(),
+ );
+ }
+
+ #[test]
+ fn rasterize_large_glyphs() {
+ // This test loads a font from disc and rasterize a few glyphs with a size of 200px to check
+ // that the texture cache handles them properly.
+ use rayon::ThreadPoolBuilder;
+ use std::fs::File;
+ use std::io::Read;
+ use crate::texture_cache::TextureCache;
+ use crate::glyph_cache::GlyphCache;
+ use crate::gpu_cache::GpuCache;
+ use crate::profiler::TransactionProfile;
+ use api::{FontKey, FontInstanceKey, FontSize, FontTemplate, FontRenderMode,
+ IdNamespace, ColorU};
+ use api::units::DevicePoint;
+ use std::sync::Arc;
+ use crate::glyph_rasterizer::{FontInstance, BaseFontInstance, GlyphKey, GlyphRasterizer};
+
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .build();
+ let workers = Arc::new(worker.unwrap());
+ let mut glyph_rasterizer = GlyphRasterizer::new(workers).unwrap();
+ let mut glyph_cache = GlyphCache::new();
+ let mut gpu_cache = GpuCache::new_for_testing();
+ let mut texture_cache = TextureCache::new_for_testing(2048, FORMAT);
+ let mut font_file =
+ File::open("../wrench/reftests/text/VeraBd.ttf").expect("Couldn't open font file");
+ let mut font_data = vec![];
+ font_file
+ .read_to_end(&mut font_data)
+ .expect("failed to read font file");
+
+ let font_key = FontKey::new(IdNamespace(0), 0);
+ glyph_rasterizer.add_font(font_key, FontTemplate::Raw(Arc::new(font_data), 0));
+
+ let font = FontInstance::from_base(Arc::new(BaseFontInstance {
+ instance_key: FontInstanceKey(IdNamespace(0), 0),
+ font_key,
+ size: FontSize::from_f32_px(200.0),
+ bg_color: ColorU::new(0, 0, 0, 0),
+ render_mode: FontRenderMode::Subpixel,
+ flags: Default::default(),
+ synthetic_italics: Default::default(),
+ platform_options: None,
+ variations: Vec::new(),
+ }));
+
+ let subpx_dir = font.get_subpx_dir();
+
+ let mut glyph_keys = Vec::with_capacity(10);
+ for i in 0 .. 10 {
+ glyph_keys.push(GlyphKey::new(
+ i,
+ DevicePoint::zero(),
+ subpx_dir,
+ ));
+ }
+
+ glyph_rasterizer.request_glyphs(
+ &mut glyph_cache,
+ font.clone(),
+ &glyph_keys,
+ &mut texture_cache,
+ &mut gpu_cache,
+ );
+
+ glyph_rasterizer.delete_font(font_key);
+
+ glyph_rasterizer.resolve_glyphs(
+ &mut glyph_cache,
+ &mut TextureCache::new_for_testing(4096, FORMAT),
+ &mut gpu_cache,
+ &mut TransactionProfile::new(),
+ );
+ }
+
+ #[test]
+ fn test_subpx_quantize() {
+ use crate::glyph_rasterizer::SubpixelOffset;
+
+ assert_eq!(SubpixelOffset::quantize(0.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(-0.0), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(0.1), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.01), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.05), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.12), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.124), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(0.125), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.2), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.25), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.33), SubpixelOffset::Quarter);
+ assert_eq!(SubpixelOffset::quantize(0.374), SubpixelOffset::Quarter);
+
+ assert_eq!(SubpixelOffset::quantize(0.375), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.4), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.5), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.58), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(0.624), SubpixelOffset::Half);
+
+ assert_eq!(SubpixelOffset::quantize(0.625), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.67), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.7), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.78), SubpixelOffset::ThreeQuarters);
+ assert_eq!(SubpixelOffset::quantize(0.874), SubpixelOffset::ThreeQuarters);
+
+ assert_eq!(SubpixelOffset::quantize(0.875), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.89), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.91), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.967), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(0.999), SubpixelOffset::Zero);
+
+ assert_eq!(SubpixelOffset::quantize(-1.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(1.0), SubpixelOffset::Zero);
+ assert_eq!(SubpixelOffset::quantize(1.5), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(-1.625), SubpixelOffset::Half);
+ assert_eq!(SubpixelOffset::quantize(-4.33), SubpixelOffset::ThreeQuarters);
+ }
+}
diff --git a/gfx/wr/webrender/src/gpu_cache.rs b/gfx/wr/webrender/src/gpu_cache.rs
new file mode 100644
index 0000000000..da67f9df64
--- /dev/null
+++ b/gfx/wr/webrender/src/gpu_cache.rs
@@ -0,0 +1,935 @@
+/* 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/. */
+
+//! Overview of the GPU cache.
+//!
+//! The main goal of the GPU cache is to allow on-demand
+//! allocation and construction of GPU resources for the
+//! vertex shaders to consume.
+//!
+//! Every item that wants to be stored in the GPU cache
+//! should create a GpuCacheHandle that is used to refer
+//! to a cached GPU resource. Creating a handle is a
+//! cheap operation, that does *not* allocate room in the
+//! cache.
+//!
+//! On any frame when that data is required, the caller
+//! must request that handle, via ```request```. If the
+//! data is not in the cache, the user provided closure
+//! will be invoked to build the data.
+//!
+//! After ```end_frame``` has occurred, callers can
+//! use the ```get_address``` API to get the allocated
+//! address in the GPU cache of a given resource slot
+//! for this frame.
+
+use api::{DebugFlags, DocumentId, PremultipliedColorF};
+#[cfg(test)]
+use api::IdNamespace;
+use api::units::*;
+use euclid::{HomogeneousVector, Rect};
+use crate::internal_types::{FastHashMap, FastHashSet};
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::{FrameStamp, FrameId};
+use crate::prim_store::VECS_PER_SEGMENT;
+use crate::renderer::MAX_VERTEX_TEXTURE_WIDTH;
+use crate::util::VecHelper;
+use std::{u16, u32};
+use std::num::NonZeroU32;
+use std::ops::Add;
+use std::time::{Duration, Instant};
+
+
+/// At the time of this writing, Firefox uses about 15 GPU cache rows on
+/// startup, and then gradually works its way up to the mid-30s with normal
+/// browsing.
+pub const GPU_CACHE_INITIAL_HEIGHT: i32 = 20;
+const NEW_ROWS_PER_RESIZE: i32 = 10;
+
+/// The number of frames an entry can go unused before being evicted.
+const FRAMES_BEFORE_EVICTION: usize = 10;
+
+/// The ratio of utilized blocks to total blocks for which we start the clock
+/// on reclaiming memory.
+const RECLAIM_THRESHOLD: f32 = 0.2;
+
+/// The amount of time utilization must be below the above threshold before we
+/// blow away the cache and rebuild it.
+const RECLAIM_DELAY_S: u64 = 5;
+
+#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Epoch(u32);
+
+impl Epoch {
+ fn next(&mut self) {
+ *self = Epoch(self.0.wrapping_add(1));
+ }
+}
+
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CacheLocation {
+ block_index: BlockIndex,
+ epoch: Epoch,
+}
+
+/// A single texel in RGBAF32 texture - 16 bytes.
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuBlockData {
+ data: [f32; 4],
+}
+
+impl GpuBlockData {
+ pub const EMPTY: Self = GpuBlockData { data: [0.0; 4] };
+}
+
+/// Conversion helpers for GpuBlockData
+impl From<PremultipliedColorF> for GpuBlockData {
+ fn from(c: PremultipliedColorF) -> Self {
+ GpuBlockData {
+ data: [c.r, c.g, c.b, c.a],
+ }
+ }
+}
+
+impl From<[f32; 4]> for GpuBlockData {
+ fn from(data: [f32; 4]) -> Self {
+ GpuBlockData { data }
+ }
+}
+
+impl<P> From<Rect<f32, P>> for GpuBlockData {
+ fn from(r: Rect<f32, P>) -> Self {
+ GpuBlockData {
+ data: [
+ r.origin.x,
+ r.origin.y,
+ r.size.width,
+ r.size.height,
+ ],
+ }
+ }
+}
+
+impl<P> From<HomogeneousVector<f32, P>> for GpuBlockData {
+ fn from(v: HomogeneousVector<f32, P>) -> Self {
+ GpuBlockData {
+ data: [
+ v.x,
+ v.y,
+ v.z,
+ v.w,
+ ],
+ }
+ }
+}
+
+impl From<TexelRect> for GpuBlockData {
+ fn from(tr: TexelRect) -> Self {
+ GpuBlockData {
+ data: [tr.uv0.x, tr.uv0.y, tr.uv1.x, tr.uv1.y],
+ }
+ }
+}
+
+
+// A handle to a GPU resource.
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuCacheHandle {
+ location: Option<CacheLocation>,
+}
+
+impl GpuCacheHandle {
+ pub fn new() -> Self {
+ GpuCacheHandle { location: None }
+ }
+
+ pub fn as_int(self, gpu_cache: &GpuCache) -> i32 {
+ gpu_cache.get_address(&self).as_int()
+ }
+}
+
+// A unique address in the GPU cache. These are uploaded
+// as part of the primitive instances, to allow the vertex
+// shader to fetch the specific data.
+#[derive(Copy, Debug, Clone, MallocSizeOf, Eq, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GpuCacheAddress {
+ pub u: u16,
+ pub v: u16,
+}
+
+impl GpuCacheAddress {
+ fn new(u: usize, v: usize) -> Self {
+ GpuCacheAddress {
+ u: u as u16,
+ v: v as u16,
+ }
+ }
+
+ pub const INVALID: GpuCacheAddress = GpuCacheAddress {
+ u: u16::MAX,
+ v: u16::MAX,
+ };
+
+ pub fn as_int(self) -> i32 {
+ // TODO(gw): Temporarily encode GPU Cache addresses as a single int.
+ // In the future, we can change the PrimitiveInstanceData struct
+ // to use 2x u16 for the vertex attribute instead of an i32.
+ self.v as i32 * MAX_VERTEX_TEXTURE_WIDTH as i32 + self.u as i32
+ }
+}
+
+impl Add<usize> for GpuCacheAddress {
+ type Output = GpuCacheAddress;
+
+ fn add(self, other: usize) -> GpuCacheAddress {
+ GpuCacheAddress {
+ u: self.u + other as u16,
+ v: self.v,
+ }
+ }
+}
+
+// An entry in a free-list of blocks in the GPU cache.
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Block {
+ // The location in the cache of this block.
+ address: GpuCacheAddress,
+ // The current epoch (generation) of this block.
+ epoch: Epoch,
+ // Index of the next free block in the list it
+ // belongs to (either a free-list or the
+ // occupied list).
+ next: Option<BlockIndex>,
+ // The last frame this block was referenced.
+ last_access_time: FrameId,
+}
+
+impl Block {
+ fn new(
+ address: GpuCacheAddress,
+ next: Option<BlockIndex>,
+ frame_id: FrameId,
+ epoch: Epoch,
+ ) -> Self {
+ Block {
+ address,
+ next,
+ last_access_time: frame_id,
+ epoch,
+ }
+ }
+
+ fn advance_epoch(&mut self, max_epoch: &mut Epoch) {
+ self.epoch.next();
+ if max_epoch.0 < self.epoch.0 {
+ max_epoch.0 = self.epoch.0;
+ }
+ }
+
+ /// Creates an invalid dummy block ID.
+ pub const INVALID: Block = Block {
+ address: GpuCacheAddress { u: 0, v: 0 },
+ epoch: Epoch(0),
+ next: None,
+ last_access_time: FrameId::INVALID,
+ };
+}
+
+/// Represents the index of a Block in the block array. We only create such
+/// structs for blocks that represent the start of a chunk.
+///
+/// Because we use Option<BlockIndex> in a lot of places, we use a NonZeroU32
+/// here and avoid ever using the index zero.
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct BlockIndex(NonZeroU32);
+
+impl BlockIndex {
+ fn new(idx: usize) -> Self {
+ debug_assert!(idx <= u32::MAX as usize);
+ BlockIndex(NonZeroU32::new(idx as u32).expect("Index zero forbidden"))
+ }
+
+ fn get(&self) -> usize {
+ self.0.get() as usize
+ }
+}
+
+// A row in the cache texture.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct Row {
+ // The fixed size of blocks that this row supports.
+ // Each row becomes a slab allocator for a fixed block size.
+ // This means no dealing with fragmentation within a cache
+ // row as items are allocated and freed.
+ block_count_per_item: usize,
+}
+
+impl Row {
+ fn new(block_count_per_item: usize) -> Self {
+ Row {
+ block_count_per_item,
+ }
+ }
+}
+
+// A list of update operations that can be applied on the cache
+// this frame. The list of updates is created by the render backend
+// during frame construction. It's passed to the render thread
+// where GL commands can be applied.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub enum GpuCacheUpdate {
+ Copy {
+ block_index: usize,
+ block_count: usize,
+ address: GpuCacheAddress,
+ },
+}
+
+/// Command to inform the debug display in the renderer when chunks are allocated
+/// or freed.
+#[derive(MallocSizeOf)]
+pub enum GpuCacheDebugCmd {
+ /// Describes an allocated chunk.
+ Alloc(GpuCacheDebugChunk),
+ /// Describes a freed chunk.
+ Free(GpuCacheAddress),
+}
+
+#[derive(Clone, MallocSizeOf)]
+pub struct GpuCacheDebugChunk {
+ pub address: GpuCacheAddress,
+ pub size: usize,
+}
+
+#[must_use]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct GpuCacheUpdateList {
+ /// The frame current update list was generated from.
+ pub frame_id: FrameId,
+ /// Whether the texture should be cleared before updates
+ /// are applied.
+ pub clear: bool,
+ /// The current height of the texture. The render thread
+ /// should resize the texture if required.
+ pub height: i32,
+ /// List of updates to apply.
+ pub updates: Vec<GpuCacheUpdate>,
+ /// A flat list of GPU blocks that are pending upload
+ /// to GPU memory.
+ pub blocks: Vec<GpuBlockData>,
+ /// Whole state GPU block metadata for debugging.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ pub debug_commands: Vec<GpuCacheDebugCmd>,
+}
+
+// Holds the free lists of fixed size blocks. Mostly
+// just serves to work around the borrow checker.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct FreeBlockLists {
+ free_list_1: Option<BlockIndex>,
+ free_list_2: Option<BlockIndex>,
+ free_list_4: Option<BlockIndex>,
+ free_list_8: Option<BlockIndex>,
+ free_list_16: Option<BlockIndex>,
+ free_list_32: Option<BlockIndex>,
+ free_list_64: Option<BlockIndex>,
+ free_list_128: Option<BlockIndex>,
+ free_list_256: Option<BlockIndex>,
+ free_list_341: Option<BlockIndex>,
+ free_list_512: Option<BlockIndex>,
+ free_list_1024: Option<BlockIndex>,
+}
+
+impl FreeBlockLists {
+ fn new() -> Self {
+ FreeBlockLists {
+ free_list_1: None,
+ free_list_2: None,
+ free_list_4: None,
+ free_list_8: None,
+ free_list_16: None,
+ free_list_32: None,
+ free_list_64: None,
+ free_list_128: None,
+ free_list_256: None,
+ free_list_341: None,
+ free_list_512: None,
+ free_list_1024: None,
+ }
+ }
+
+ fn get_actual_block_count_and_free_list(
+ &mut self,
+ block_count: usize,
+ ) -> (usize, &mut Option<BlockIndex>) {
+ // Find the appropriate free list to use based on the block size.
+ //
+ // Note that we cheat a bit with the 341 bucket, since it's not quite
+ // a divisor of 1024, because purecss-francine allocates many 260-block
+ // chunks, and there's no reason we shouldn't pack these three to a row.
+ // This means the allocation statistics will under-report by one block
+ // for each row using 341-block buckets, which is fine.
+ debug_assert_eq!(MAX_VERTEX_TEXTURE_WIDTH, 1024, "Need to update bucketing");
+ match block_count {
+ 0 => panic!("Can't allocate zero sized blocks!"),
+ 1 => (1, &mut self.free_list_1),
+ 2 => (2, &mut self.free_list_2),
+ 3..=4 => (4, &mut self.free_list_4),
+ 5..=8 => (8, &mut self.free_list_8),
+ 9..=16 => (16, &mut self.free_list_16),
+ 17..=32 => (32, &mut self.free_list_32),
+ 33..=64 => (64, &mut self.free_list_64),
+ 65..=128 => (128, &mut self.free_list_128),
+ 129..=256 => (256, &mut self.free_list_256),
+ 257..=341 => (341, &mut self.free_list_341),
+ 342..=512 => (512, &mut self.free_list_512),
+ 513..=1024 => (1024, &mut self.free_list_1024),
+ _ => panic!("Can't allocate > MAX_VERTEX_TEXTURE_WIDTH per resource!"),
+ }
+ }
+}
+
+// CPU-side representation of the GPU resource cache texture.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct Texture {
+ // Current texture height
+ height: i32,
+ // All blocks that have been created for this texture
+ blocks: Vec<Block>,
+ // Metadata about each allocated row.
+ rows: Vec<Row>,
+ // The base Epoch for this texture.
+ base_epoch: Epoch,
+ // The maximum epoch reached. We track this along with the above so
+ // that we can rebuild the Texture and avoid collisions with handles
+ // allocated for the old texture.
+ max_epoch: Epoch,
+ // Free lists of available blocks for each supported
+ // block size in the texture. These are intrusive
+ // linked lists.
+ free_lists: FreeBlockLists,
+ // Linked list of currently occupied blocks. This
+ // makes it faster to iterate blocks looking for
+ // candidates to be evicted from the cache.
+ occupied_list_heads: FastHashMap<DocumentId, BlockIndex>,
+ // Pending blocks that have been written this frame
+ // and will need to be sent to the GPU.
+ pending_blocks: Vec<GpuBlockData>,
+ // Pending update commands.
+ updates: Vec<GpuCacheUpdate>,
+ // Profile stats
+ allocated_block_count: usize,
+ // The stamp at which we first reached our threshold for reclaiming `GpuCache`
+ // memory, or `None` if the threshold hasn't been reached.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ reached_reclaim_threshold: Option<Instant>,
+ // List of debug commands to be sent to the renderer when the GPU cache
+ // debug display is enabled.
+ #[cfg_attr(feature = "serde", serde(skip))]
+ debug_commands: Vec<GpuCacheDebugCmd>,
+ // The current debug flags for the system.
+ debug_flags: DebugFlags,
+}
+
+impl Texture {
+ fn new(base_epoch: Epoch, debug_flags: DebugFlags) -> Self {
+ // Pre-fill the block array with one invalid block so that we never use
+ // 0 for a BlockIndex. This lets us use NonZeroU32 for BlockIndex, which
+ // saves memory.
+ let blocks = vec![Block::INVALID];
+
+ Texture {
+ height: GPU_CACHE_INITIAL_HEIGHT,
+ blocks,
+ rows: Vec::new(),
+ base_epoch,
+ max_epoch: base_epoch,
+ free_lists: FreeBlockLists::new(),
+ pending_blocks: Vec::new(),
+ updates: Vec::new(),
+ occupied_list_heads: FastHashMap::default(),
+ allocated_block_count: 0,
+ reached_reclaim_threshold: None,
+ debug_commands: Vec::new(),
+ debug_flags,
+ }
+ }
+
+ // Push new data into the cache. The ```pending_block_index``` field represents
+ // where the data was pushed into the texture ```pending_blocks``` array.
+ // Return the allocated address for this data.
+ fn push_data(
+ &mut self,
+ pending_block_index: Option<usize>,
+ block_count: usize,
+ frame_stamp: FrameStamp
+ ) -> CacheLocation {
+ debug_assert!(frame_stamp.is_valid());
+ // Find the appropriate free list to use based on the block size.
+ let (alloc_size, free_list) = self.free_lists
+ .get_actual_block_count_and_free_list(block_count);
+
+ // See if we need a new row (if free-list has nothing available)
+ if free_list.is_none() {
+ if self.rows.len() as i32 == self.height {
+ self.height += NEW_ROWS_PER_RESIZE;
+ }
+
+ // Create a new row.
+ let items_per_row = MAX_VERTEX_TEXTURE_WIDTH / alloc_size;
+ let row_index = self.rows.len();
+ self.rows.push(Row::new(alloc_size));
+
+ // Create a ```Block``` for each possible allocation address
+ // in this row, and link it in to the free-list for this
+ // block size.
+ let mut prev_block_index = None;
+ for i in 0 .. items_per_row {
+ let address = GpuCacheAddress::new(i * alloc_size, row_index);
+ let block_index = BlockIndex::new(self.blocks.len());
+ let block = Block::new(address, prev_block_index, frame_stamp.frame_id(), self.base_epoch);
+ self.blocks.push(block);
+ prev_block_index = Some(block_index);
+ }
+
+ *free_list = prev_block_index;
+ }
+
+ // Given the code above, it's now guaranteed that there is a block
+ // available in the appropriate free-list. Pull a block from the
+ // head of the list.
+ let free_block_index = free_list.take().unwrap();
+ let block = &mut self.blocks[free_block_index.get()];
+ *free_list = block.next;
+
+ // Add the block to the occupied linked list.
+ block.next = self.occupied_list_heads.get(&frame_stamp.document_id()).cloned();
+ block.last_access_time = frame_stamp.frame_id();
+ self.occupied_list_heads.insert(frame_stamp.document_id(), free_block_index);
+ self.allocated_block_count += alloc_size;
+
+ if let Some(pending_block_index) = pending_block_index {
+ // Add this update to the pending list of blocks that need
+ // to be updated on the GPU.
+ self.updates.push(GpuCacheUpdate::Copy {
+ block_index: pending_block_index,
+ block_count,
+ address: block.address,
+ });
+ }
+
+ // If we're using the debug display, communicate the allocation to the
+ // renderer thread. Note that we do this regardless of whether or not
+ // pending_block_index is None (if it is, the renderer thread will fill
+ // in the data via a deferred resolve, but the block is still considered
+ // allocated).
+ if self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ self.debug_commands.push(GpuCacheDebugCmd::Alloc(GpuCacheDebugChunk {
+ address: block.address,
+ size: block_count,
+ }));
+ }
+
+ CacheLocation {
+ block_index: free_block_index,
+ epoch: block.epoch,
+ }
+ }
+
+ // Run through the list of occupied cache blocks and evict
+ // any old blocks that haven't been referenced for a while.
+ fn evict_old_blocks(&mut self, frame_stamp: FrameStamp) {
+ debug_assert!(frame_stamp.is_valid());
+ // Prune any old items from the list to make room.
+ // Traverse the occupied linked list and see
+ // which items have not been used for a long time.
+ let mut current_block = self.occupied_list_heads.get(&frame_stamp.document_id()).map(|x| *x);
+ let mut prev_block: Option<BlockIndex> = None;
+
+ while let Some(index) = current_block {
+ let (next_block, should_unlink) = {
+ let block = &mut self.blocks[index.get()];
+
+ let next_block = block.next;
+ let mut should_unlink = false;
+
+ // If this resource has not been used in the last
+ // few frames, free it from the texture and mark
+ // as empty.
+ if block.last_access_time + FRAMES_BEFORE_EVICTION < frame_stamp.frame_id() {
+ should_unlink = true;
+
+ // Get the row metadata from the address.
+ let row = &mut self.rows[block.address.v as usize];
+
+ // Use the row metadata to determine which free-list
+ // this block belongs to.
+ let (_, free_list) = self.free_lists
+ .get_actual_block_count_and_free_list(row.block_count_per_item);
+
+ block.advance_epoch(&mut self.max_epoch);
+ block.next = *free_list;
+ *free_list = Some(index);
+
+ self.allocated_block_count -= row.block_count_per_item;
+
+ if self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ let cmd = GpuCacheDebugCmd::Free(block.address);
+ self.debug_commands.push(cmd);
+ }
+ };
+
+ (next_block, should_unlink)
+ };
+
+ // If the block was released, we will need to remove it
+ // from the occupied linked list.
+ if should_unlink {
+ match prev_block {
+ Some(prev_block) => {
+ self.blocks[prev_block.get()].next = next_block;
+ }
+ None => {
+ match next_block {
+ Some(next_block) => {
+ self.occupied_list_heads.insert(frame_stamp.document_id(), next_block);
+ }
+ None => {
+ self.occupied_list_heads.remove(&frame_stamp.document_id());
+ }
+ }
+ }
+ }
+ } else {
+ prev_block = current_block;
+ }
+
+ current_block = next_block;
+ }
+ }
+
+ /// Returns the ratio of utilized blocks.
+ fn utilization(&self) -> f32 {
+ let total_blocks = self.rows.len() * MAX_VERTEX_TEXTURE_WIDTH;
+ debug_assert!(total_blocks > 0);
+ let ratio = self.allocated_block_count as f32 / total_blocks as f32;
+ debug_assert!(0.0 <= ratio && ratio <= 1.0, "Bad ratio: {}", ratio);
+ ratio
+ }
+}
+
+
+/// A wrapper object for GPU data requests,
+/// works as a container that can only grow.
+#[must_use]
+pub struct GpuDataRequest<'a> {
+ handle: &'a mut GpuCacheHandle,
+ frame_stamp: FrameStamp,
+ start_index: usize,
+ max_block_count: usize,
+ texture: &'a mut Texture,
+}
+
+impl<'a> GpuDataRequest<'a> {
+ pub fn push<B>(&mut self, block: B)
+ where
+ B: Into<GpuBlockData>,
+ {
+ self.texture.pending_blocks.push(block.into());
+ }
+
+ // Write the GPU cache data for an individual segment.
+ pub fn write_segment(
+ &mut self,
+ local_rect: LayoutRect,
+ extra_data: [f32; 4],
+ ) {
+ let _ = VECS_PER_SEGMENT;
+ self.push(local_rect);
+ self.push(extra_data);
+ }
+
+ pub fn current_used_block_num(&self) -> usize {
+ self.texture.pending_blocks.len() - self.start_index
+ }
+}
+
+impl<'a> Drop for GpuDataRequest<'a> {
+ fn drop(&mut self) {
+ // Push the data to the texture pending updates list.
+ let block_count = self.current_used_block_num();
+ debug_assert!(block_count <= self.max_block_count);
+
+ let location = self.texture
+ .push_data(Some(self.start_index), block_count, self.frame_stamp);
+ self.handle.location = Some(location);
+ }
+}
+
+
+/// The main LRU cache interface.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct GpuCache {
+ /// Current FrameId.
+ now: FrameStamp,
+ /// CPU-side texture allocator.
+ texture: Texture,
+ /// Number of blocks requested this frame that don't
+ /// need to be re-uploaded.
+ saved_block_count: usize,
+ /// The current debug flags for the system.
+ debug_flags: DebugFlags,
+ /// Whether there is a pending clear to send with the
+ /// next update.
+ pending_clear: bool,
+ /// Indicates that prepare_for_frames has been called for this group of frames.
+ /// Used for sanity checks.
+ prepared_for_frames: bool,
+ /// This indicates that we performed a cleanup operation which requires all
+ /// documents to build a frame.
+ requires_frame_build: bool,
+ /// The set of documents which have had frames built in this update. Used for
+ /// sanity checks.
+ document_frames_to_build: FastHashSet<DocumentId>,
+}
+
+impl GpuCache {
+ pub fn new() -> Self {
+ let debug_flags = DebugFlags::empty();
+ GpuCache {
+ now: FrameStamp::INVALID,
+ texture: Texture::new(Epoch(0), debug_flags),
+ saved_block_count: 0,
+ debug_flags,
+ pending_clear: false,
+ prepared_for_frames: false,
+ requires_frame_build: false,
+ document_frames_to_build: FastHashSet::default(),
+ }
+ }
+
+ /// Creates a GpuCache and sets it up with a valid `FrameStamp`, which
+ /// is useful for avoiding panics when instantiating the `GpuCache`
+ /// directly from unit test code.
+ #[cfg(test)]
+ pub fn new_for_testing() -> Self {
+ let mut cache = Self::new();
+ let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1));
+ now.advance();
+ cache.prepared_for_frames = true;
+ cache.begin_frame(now);
+ cache
+ }
+
+ /// Drops everything in the GPU cache. Must not be called once gpu cache entries
+ /// for the next frame have already been requested.
+ pub fn clear(&mut self) {
+ assert!(self.texture.updates.is_empty(), "Clearing with pending updates");
+ let mut next_base_epoch = self.texture.max_epoch;
+ next_base_epoch.next();
+ self.texture = Texture::new(next_base_epoch, self.debug_flags);
+ self.saved_block_count = 0;
+ self.pending_clear = true;
+ self.requires_frame_build = true;
+ }
+
+ pub fn requires_frame_build(&self) -> bool {
+ self.requires_frame_build
+ }
+
+ pub fn prepare_for_frames(&mut self) {
+ self.prepared_for_frames = true;
+ if self.should_reclaim_memory() {
+ self.clear();
+ debug_assert!(self.document_frames_to_build.is_empty());
+ for &document_id in self.texture.occupied_list_heads.keys() {
+ self.document_frames_to_build.insert(document_id);
+ }
+ }
+ }
+
+ pub fn bookkeep_after_frames(&mut self) {
+ assert!(self.document_frames_to_build.is_empty());
+ assert!(self.prepared_for_frames);
+ self.requires_frame_build = false;
+ self.prepared_for_frames = false;
+ }
+
+ /// Begin a new frame.
+ pub fn begin_frame(&mut self, stamp: FrameStamp) {
+ debug_assert!(self.texture.pending_blocks.is_empty());
+ assert!(self.prepared_for_frames);
+ profile_scope!("begin_frame");
+ self.now = stamp;
+ self.texture.evict_old_blocks(self.now);
+ self.saved_block_count = 0;
+ }
+
+ // Invalidate a (possibly) existing block in the cache.
+ // This means the next call to request() for this location
+ // will rebuild the data and upload it to the GPU.
+ pub fn invalidate(&mut self, handle: &GpuCacheHandle) {
+ if let Some(ref location) = handle.location {
+ // don't invalidate blocks that are already re-assigned
+ if let Some(block) = self.texture.blocks.get_mut(location.block_index.get()) {
+ if block.epoch == location.epoch {
+ block.advance_epoch(&mut self.texture.max_epoch);
+ }
+ }
+ }
+ }
+
+ /// Request a resource be added to the cache. If the resource
+ /// is already in the cache, `None` will be returned.
+ pub fn request<'a>(&'a mut self, handle: &'a mut GpuCacheHandle) -> Option<GpuDataRequest<'a>> {
+ let mut max_block_count = MAX_VERTEX_TEXTURE_WIDTH;
+ // Check if the allocation for this handle is still valid.
+ if let Some(ref location) = handle.location {
+ if let Some(block) = self.texture.blocks.get_mut(location.block_index.get()) {
+ if block.epoch == location.epoch {
+ max_block_count = self.texture.rows[block.address.v as usize].block_count_per_item;
+ if block.last_access_time != self.now.frame_id() {
+ // Mark last access time to avoid evicting this block.
+ block.last_access_time = self.now.frame_id();
+ self.saved_block_count += max_block_count;
+ }
+ return None;
+ }
+ }
+ }
+
+ debug_assert!(self.now.is_valid());
+ Some(GpuDataRequest {
+ handle,
+ frame_stamp: self.now,
+ start_index: self.texture.pending_blocks.len(),
+ texture: &mut self.texture,
+ max_block_count,
+ })
+ }
+
+ // Push an array of data blocks to be uploaded to the GPU
+ // unconditionally for this frame. The cache handle will
+ // assert if the caller tries to retrieve the address
+ // of this handle on a subsequent frame. This is typically
+ // used for uploading data that changes every frame, and
+ // therefore makes no sense to try and cache.
+ pub fn push_per_frame_blocks(&mut self, blocks: &[GpuBlockData]) -> GpuCacheHandle {
+ let start_index = self.texture.pending_blocks.len();
+ self.texture.pending_blocks.extend_from_slice(blocks);
+ let location = self.texture
+ .push_data(Some(start_index), blocks.len(), self.now);
+ GpuCacheHandle {
+ location: Some(location),
+ }
+ }
+
+ // Reserve space in the cache for per-frame blocks that
+ // will be resolved by the render thread via the
+ // external image callback.
+ pub fn push_deferred_per_frame_blocks(&mut self, block_count: usize) -> GpuCacheHandle {
+ let location = self.texture.push_data(None, block_count, self.now);
+ GpuCacheHandle {
+ location: Some(location),
+ }
+ }
+
+ /// End the frame. Return the list of updates to apply to the
+ /// device specific cache texture.
+ pub fn end_frame(
+ &mut self,
+ profile: &mut TransactionProfile,
+ ) -> FrameStamp {
+ profile_scope!("end_frame");
+ profile.set(profiler::GPU_CACHE_ROWS_TOTAL, self.texture.rows.len());
+ profile.set(profiler::GPU_CACHE_BLOCKS_TOTAL, self.texture.allocated_block_count);
+ profile.set(profiler::GPU_CACHE_BLOCKS_SAVED, self.saved_block_count);
+
+ let reached_threshold =
+ self.texture.rows.len() > (GPU_CACHE_INITIAL_HEIGHT as usize) &&
+ self.texture.utilization() < RECLAIM_THRESHOLD;
+ if reached_threshold {
+ self.texture.reached_reclaim_threshold.get_or_insert_with(Instant::now);
+ } else {
+ self.texture.reached_reclaim_threshold = None;
+ }
+
+ self.document_frames_to_build.remove(&self.now.document_id());
+ self.now
+ }
+
+ /// Returns true if utilization has been low enough for long enough that we
+ /// should blow the cache away and rebuild it.
+ pub fn should_reclaim_memory(&self) -> bool {
+ self.texture.reached_reclaim_threshold
+ .map_or(false, |t| t.elapsed() > Duration::from_secs(RECLAIM_DELAY_S))
+ }
+
+ /// Extract the pending updates from the cache.
+ pub fn extract_updates(&mut self) -> GpuCacheUpdateList {
+ let clear = self.pending_clear;
+ self.pending_clear = false;
+ GpuCacheUpdateList {
+ frame_id: self.now.frame_id(),
+ clear,
+ height: self.texture.height,
+ debug_commands: self.texture.debug_commands.take_and_preallocate(),
+ updates: self.texture.updates.take_and_preallocate(),
+ blocks: self.texture.pending_blocks.take_and_preallocate(),
+ }
+ }
+
+ /// Sets the current debug flags for the system.
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_flags = flags;
+ self.texture.debug_flags = flags;
+ }
+
+ /// Get the actual GPU address in the texture for a given slot ID.
+ /// It's assumed at this point that the given slot has been requested
+ /// and built for this frame. Attempting to get the address for a
+ /// freed or pending slot will panic!
+ pub fn get_address(&self, id: &GpuCacheHandle) -> GpuCacheAddress {
+ let location = id.location.expect("handle not requested or allocated!");
+ let block = &self.texture.blocks[location.block_index.get()];
+ debug_assert_eq!(block.epoch, location.epoch);
+ debug_assert_eq!(block.last_access_time, self.now.frame_id());
+ block.address
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // We can end up with a lot of blocks stored in the global vec, and keeping
+ // them small helps reduce memory overhead.
+ assert_eq!(mem::size_of::<Block>(), 24, "Block size changed");
+}
diff --git a/gfx/wr/webrender/src/gpu_types.rs b/gfx/wr/webrender/src/gpu_types.rs
new file mode 100644
index 0000000000..8124814ed7
--- /dev/null
+++ b/gfx/wr/webrender/src/gpu_types.rs
@@ -0,0 +1,825 @@
+/* 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 api::{AlphaType, PremultipliedColorF, YuvFormat, YuvColorSpace};
+use api::units::*;
+use crate::segment::EdgeAaSegmentMask;
+use crate::spatial_tree::{SpatialTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
+use crate::gpu_cache::{GpuCacheAddress, GpuDataRequest};
+use crate::internal_types::FastHashMap;
+use crate::prim_store::ClipData;
+use crate::render_task::RenderTaskAddress;
+use crate::renderer::ShaderColorMode;
+use std::i32;
+use crate::util::{TransformedRectKind, MatrixHelpers};
+use crate::glyph_rasterizer::SubpixelDirection;
+use crate::util::pack_as_float;
+
+// Contains type that must exactly match the same structures declared in GLSL.
+
+pub const VECS_PER_TRANSFORM: usize = 8;
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ZBufferId(pub i32);
+
+impl ZBufferId {
+ pub fn invalid() -> Self {
+ ZBufferId(i32::MAX)
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ZBufferIdGenerator {
+ next: i32,
+ max_depth_ids: i32,
+}
+
+impl ZBufferIdGenerator {
+ pub fn new(max_depth_ids: i32) -> Self {
+ ZBufferIdGenerator {
+ next: 0,
+ max_depth_ids,
+ }
+ }
+
+ pub fn next(&mut self) -> ZBufferId {
+ debug_assert!(self.next < self.max_depth_ids);
+ let id = ZBufferId(self.next);
+ self.next += 1;
+ id
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub enum RasterizationSpace {
+ Local = 0,
+ Screen = 1,
+}
+
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub enum BoxShadowStretchMode {
+ Stretch = 0,
+ Simple = 1,
+}
+
+#[repr(i32)]
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlurDirection {
+ Horizontal = 0,
+ Vertical,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlurInstance {
+ pub task_address: RenderTaskAddress,
+ pub src_task_address: RenderTaskAddress,
+ pub blur_direction: BlurDirection,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScalingInstance {
+ pub target_rect: DeviceRect,
+ pub source_rect: DeviceIntRect,
+ pub source_layer: i32,
+}
+
+#[derive(Clone, Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SvgFilterInstance {
+ pub task_address: RenderTaskAddress,
+ pub input_1_task_address: RenderTaskAddress,
+ pub input_2_task_address: RenderTaskAddress,
+ pub kind: u16,
+ pub input_count: u16,
+ pub generic_int: u16,
+ pub extra_data_address: GpuCacheAddress,
+}
+
+#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BorderSegment {
+ TopLeft,
+ TopRight,
+ BottomRight,
+ BottomLeft,
+ Left,
+ Top,
+ Right,
+ Bottom,
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderInstance {
+ pub task_origin: DevicePoint,
+ pub local_rect: DeviceRect,
+ pub color0: PremultipliedColorF,
+ pub color1: PremultipliedColorF,
+ pub flags: i32,
+ pub widths: DeviceSize,
+ pub radius: DeviceSize,
+ pub clip_params: [f32; 8],
+}
+
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceCommon {
+ pub sub_rect: DeviceRect,
+ pub task_origin: DevicePoint,
+ pub screen_origin: DevicePoint,
+ pub device_pixel_scale: f32,
+ pub clip_transform_id: TransformPaletteId,
+ pub prim_transform_id: TransformPaletteId,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceImage {
+ pub common: ClipMaskInstanceCommon,
+ pub tile_rect: LayoutRect,
+ pub resource_address: GpuCacheAddress,
+ pub local_rect: LayoutRect,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceRect {
+ pub common: ClipMaskInstanceCommon,
+ pub local_pos: LayoutPoint,
+ pub clip_data: ClipData,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct BoxShadowData {
+ pub src_rect_size: LayoutSize,
+ pub clip_mode: i32,
+ pub stretch_mode_x: i32,
+ pub stretch_mode_y: i32,
+ pub dest_rect: LayoutRect,
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstanceBoxShadow {
+ pub common: ClipMaskInstanceCommon,
+ pub resource_address: GpuCacheAddress,
+ pub shadow_data: BoxShadowData,
+}
+
+/// A clipping primitive drawn into the clipping mask.
+/// Could be an image or a rectangle, which defines the
+/// way `address` is treated.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct ClipMaskInstance {
+ pub clip_transform_id: TransformPaletteId,
+ pub prim_transform_id: TransformPaletteId,
+ pub clip_data_address: GpuCacheAddress,
+ pub resource_address: GpuCacheAddress,
+ pub local_pos: LayoutPoint,
+ pub tile_rect: LayoutRect,
+ pub sub_rect: DeviceRect,
+ pub task_origin: DevicePoint,
+ pub screen_origin: DevicePoint,
+ pub device_pixel_scale: f32,
+}
+
+// 16 bytes per instance should be enough for anyone!
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveInstanceData {
+ data: [i32; 4],
+}
+
+/// Vertex format for picture cache composite shader.
+/// When editing the members, update desc::COMPOSITE
+/// so its list of instance_attributes matches:
+#[derive(Clone, Debug)]
+#[repr(C)]
+pub struct CompositeInstance {
+ // Device space rectangle of surface
+ rect: DeviceRect,
+ // Device space clip rect for this surface
+ clip_rect: DeviceRect,
+ // Color for solid color tiles, white otherwise
+ color: PremultipliedColorF,
+
+ // Packed into a single vec4 (aParams)
+ z_id: f32,
+ color_space_or_uv_type: f32, // YuvColorSpace for YUV;
+ // UV coordinate space for RGB
+ yuv_format: f32, // YuvFormat
+ yuv_rescale: f32,
+
+ // UV rectangles (pixel space) for color / yuv texture planes
+ uv_rects: [TexelRect; 3],
+
+ // Texture array layers for color / yuv texture planes
+ texture_layers: [f32; 3],
+}
+
+impl CompositeInstance {
+ pub fn new(
+ rect: DeviceRect,
+ clip_rect: DeviceRect,
+ color: PremultipliedColorF,
+ layer: f32,
+ z_id: ZBufferId,
+ ) -> Self {
+ let uv = TexelRect::new(0.0, 0.0, 1.0, 1.0);
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color,
+ z_id: z_id.0 as f32,
+ color_space_or_uv_type: pack_as_float(0u32),
+ yuv_format: 0.0,
+ yuv_rescale: 0.0,
+ texture_layers: [layer, 0.0, 0.0],
+ uv_rects: [uv, uv, uv],
+ }
+ }
+
+ pub fn new_rgb(
+ rect: DeviceRect,
+ clip_rect: DeviceRect,
+ color: PremultipliedColorF,
+ layer: f32,
+ z_id: ZBufferId,
+ uv_rect: TexelRect,
+ ) -> Self {
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color,
+ z_id: z_id.0 as f32,
+ color_space_or_uv_type: pack_as_float(1u32),
+ yuv_format: 0.0,
+ yuv_rescale: 0.0,
+ texture_layers: [layer, 0.0, 0.0],
+ uv_rects: [uv_rect, uv_rect, uv_rect],
+ }
+ }
+
+ pub fn new_yuv(
+ rect: DeviceRect,
+ clip_rect: DeviceRect,
+ z_id: ZBufferId,
+ yuv_color_space: YuvColorSpace,
+ yuv_format: YuvFormat,
+ yuv_rescale: f32,
+ texture_layers: [f32; 3],
+ uv_rects: [TexelRect; 3],
+ ) -> Self {
+ CompositeInstance {
+ rect,
+ clip_rect,
+ color: PremultipliedColorF::WHITE,
+ z_id: z_id.0 as f32,
+ color_space_or_uv_type: pack_as_float(yuv_color_space as u32),
+ yuv_format: pack_as_float(yuv_format as u32),
+ yuv_rescale,
+ texture_layers,
+ uv_rects,
+ }
+ }
+}
+
+/// Vertex format for issuing colored quads.
+#[derive(Debug, Clone)]
+#[repr(C)]
+pub struct ClearInstance {
+ pub rect: [f32; 4],
+ pub color: [f32; 4],
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderIndex(pub i32);
+
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaders {
+ // The integer-type headers for a primitive.
+ pub headers_int: Vec<PrimitiveHeaderI>,
+ // The float-type headers for a primitive.
+ pub headers_float: Vec<PrimitiveHeaderF>,
+}
+
+impl PrimitiveHeaders {
+ pub fn new() -> PrimitiveHeaders {
+ PrimitiveHeaders {
+ headers_int: Vec::new(),
+ headers_float: Vec::new(),
+ }
+ }
+
+ // Add a new primitive header.
+ pub fn push(
+ &mut self,
+ prim_header: &PrimitiveHeader,
+ z: ZBufferId,
+ user_data: [i32; 4],
+ ) -> PrimitiveHeaderIndex {
+ debug_assert_eq!(self.headers_int.len(), self.headers_float.len());
+ let id = self.headers_float.len();
+
+ self.headers_float.push(PrimitiveHeaderF {
+ local_rect: prim_header.local_rect,
+ local_clip_rect: prim_header.local_clip_rect,
+ });
+
+ self.headers_int.push(PrimitiveHeaderI {
+ z,
+ unused: 0,
+ specific_prim_address: prim_header.specific_prim_address.as_int(),
+ transform_id: prim_header.transform_id,
+ user_data,
+ });
+
+ PrimitiveHeaderIndex(id as i32)
+ }
+}
+
+// This is a convenience type used to make it easier to pass
+// the common parts around during batching.
+#[derive(Debug)]
+pub struct PrimitiveHeader {
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+ pub specific_prim_address: GpuCacheAddress,
+ pub transform_id: TransformPaletteId,
+}
+
+// f32 parts of a primitive header
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderF {
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+// i32 parts of a primitive header
+// TODO(gw): Compress parts of these down to u16
+#[derive(Debug)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveHeaderI {
+ pub z: ZBufferId,
+ pub specific_prim_address: i32,
+ pub transform_id: TransformPaletteId,
+ pub unused: i32, // To ensure required 16 byte alignment of vertex textures
+ pub user_data: [i32; 4],
+}
+
+pub struct GlyphInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+}
+
+impl GlyphInstance {
+ pub fn new(
+ prim_header_index: PrimitiveHeaderIndex,
+ ) -> Self {
+ GlyphInstance {
+ prim_header_index,
+ }
+ }
+
+ // TODO(gw): Some of these fields can be moved to the primitive
+ // header since they are constant, and some can be
+ // compressed to a smaller size.
+ pub fn build(&self,
+ render_task: RenderTaskAddress,
+ clip_task: RenderTaskAddress,
+ subpx_dir: SubpixelDirection,
+ glyph_index_in_text_run: i32,
+ glyph_uv_rect: GpuCacheAddress,
+ color_mode: ShaderColorMode,
+ ) -> PrimitiveInstanceData {
+ PrimitiveInstanceData {
+ data: [
+ self.prim_header_index.0 as i32,
+ ((render_task.0 as i32) << 16)
+ | clip_task.0 as i32,
+ (subpx_dir as u32 as i32) << 24
+ | (color_mode as u32 as i32) << 16
+ | glyph_index_in_text_run,
+ glyph_uv_rect.as_int(),
+ ],
+ }
+ }
+}
+
+pub struct SplitCompositeInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+ pub polygons_address: GpuCacheAddress,
+ pub z: ZBufferId,
+ pub render_task_address: RenderTaskAddress,
+}
+
+impl From<SplitCompositeInstance> for PrimitiveInstanceData {
+ fn from(instance: SplitCompositeInstance) -> Self {
+ PrimitiveInstanceData {
+ data: [
+ instance.prim_header_index.0,
+ instance.polygons_address.as_int(),
+ instance.z.0,
+ instance.render_task_address.0 as i32,
+ ],
+ }
+ }
+}
+
+bitflags! {
+ /// Flags that define how the common brush shader
+ /// code should process this instance.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct BrushFlags: u8 {
+ /// Apply perspective interpolation to UVs
+ const PERSPECTIVE_INTERPOLATION = 1;
+ /// Do interpolation relative to segment rect,
+ /// rather than primitive rect.
+ const SEGMENT_RELATIVE = 2;
+ /// Repeat UVs horizontally.
+ const SEGMENT_REPEAT_X = 4;
+ /// Repeat UVs vertically.
+ const SEGMENT_REPEAT_Y = 8;
+ /// Horizontally follow border-image-repeat: round.
+ const SEGMENT_REPEAT_X_ROUND = 16;
+ /// Vertically follow border-image-repeat: round.
+ const SEGMENT_REPEAT_Y_ROUND = 32;
+ /// Middle (fill) area of a border-image-repeat.
+ const SEGMENT_NINEPATCH_MIDDLE = 64;
+ /// The extra segment data is a texel rect.
+ const SEGMENT_TEXEL_RECT = 128;
+ }
+}
+
+/// Convenience structure to encode into PrimitiveInstanceData.
+pub struct BrushInstance {
+ pub prim_header_index: PrimitiveHeaderIndex,
+ pub render_task_address: RenderTaskAddress,
+ pub clip_task_address: RenderTaskAddress,
+ pub segment_index: i32,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub brush_flags: BrushFlags,
+ pub resource_address: i32,
+}
+
+impl From<BrushInstance> for PrimitiveInstanceData {
+ fn from(instance: BrushInstance) -> Self {
+ PrimitiveInstanceData {
+ data: [
+ instance.prim_header_index.0,
+ ((instance.render_task_address.0 as i32) << 16)
+ | instance.clip_task_address.0 as i32,
+ instance.segment_index
+ | ((instance.edge_flags.bits() as i32) << 16)
+ | ((instance.brush_flags.bits() as i32) << 24),
+ instance.resource_address,
+ ]
+ }
+ }
+}
+
+/// Convenience structure to encode into the image brush's user data.
+#[derive(Copy, Clone, Debug)]
+pub struct ImageBrushData {
+ pub color_mode: ShaderColorMode,
+ pub alpha_type: AlphaType,
+ pub raster_space: RasterizationSpace,
+ pub opacity: f32,
+}
+
+impl ImageBrushData {
+ #[inline]
+ pub fn encode(&self) -> [i32; 4] {
+ [
+ self.color_mode as i32 | ((self.alpha_type as i32) << 16),
+ self.raster_space as i32,
+ get_shader_opacity(self.opacity),
+ 0,
+ ]
+ }
+}
+
+// Represents the information about a transform palette
+// entry that is passed to shaders. It includes an index
+// into the transform palette, and a set of flags. The
+// only flag currently used determines whether the
+// transform is axis-aligned (and this should have
+// pixel snapping applied).
+#[derive(Copy, Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct TransformPaletteId(pub u32);
+
+impl TransformPaletteId {
+ /// Identity transform ID.
+ pub const IDENTITY: Self = TransformPaletteId(0);
+
+ /// Extract the transform kind from the id.
+ pub fn transform_kind(&self) -> TransformedRectKind {
+ if (self.0 >> 24) == 0 {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ }
+ }
+}
+
+/// The GPU data payload for a transform palette entry.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+pub struct TransformData {
+ transform: LayoutToPictureTransform,
+ inv_transform: PictureToLayoutTransform,
+}
+
+impl TransformData {
+ fn invalid() -> Self {
+ TransformData {
+ transform: LayoutToPictureTransform::identity(),
+ inv_transform: PictureToLayoutTransform::identity(),
+ }
+ }
+}
+
+// Extra data stored about each transform palette entry.
+#[derive(Clone)]
+pub struct TransformMetadata {
+ transform_kind: TransformedRectKind,
+}
+
+impl TransformMetadata {
+ pub fn invalid() -> Self {
+ TransformMetadata {
+ transform_kind: TransformedRectKind::AxisAligned,
+ }
+ }
+}
+
+#[derive(Debug, Hash, Eq, PartialEq)]
+struct RelativeTransformKey {
+ from_index: SpatialNodeIndex,
+ to_index: SpatialNodeIndex,
+}
+
+// Stores a contiguous list of TransformData structs, that
+// are ready for upload to the GPU.
+// TODO(gw): For now, this only stores the complete local
+// to world transform for each spatial node. In
+// the future, the transform palette will support
+// specifying a coordinate system that the transform
+// should be relative to.
+pub struct TransformPalette {
+ transforms: Vec<TransformData>,
+ metadata: Vec<TransformMetadata>,
+ map: FastHashMap<RelativeTransformKey, usize>,
+}
+
+impl TransformPalette {
+ pub fn new(count: usize) -> Self {
+ let _ = VECS_PER_TRANSFORM;
+ TransformPalette {
+ transforms: vec![TransformData::invalid(); count],
+ metadata: vec![TransformMetadata::invalid(); count],
+ map: FastHashMap::default(),
+ }
+ }
+
+ pub fn finish(self) -> Vec<TransformData> {
+ self.transforms
+ }
+
+ pub fn set_world_transform(
+ &mut self,
+ index: SpatialNodeIndex,
+ transform: LayoutToWorldTransform,
+ ) {
+ register_transform(
+ &mut self.metadata,
+ &mut self.transforms,
+ index,
+ ROOT_SPATIAL_NODE_INDEX,
+ // We know the root picture space == world space
+ transform.with_destination::<PicturePixel>(),
+ );
+ }
+
+ fn get_index(
+ &mut self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> usize {
+ if parent_index == ROOT_SPATIAL_NODE_INDEX {
+ child_index.0 as usize
+ } else if child_index == parent_index {
+ 0
+ } else {
+ let key = RelativeTransformKey {
+ from_index: child_index,
+ to_index: parent_index,
+ };
+
+ let metadata = &mut self.metadata;
+ let transforms = &mut self.transforms;
+
+ *self.map
+ .entry(key)
+ .or_insert_with(|| {
+ let transform = spatial_tree.get_relative_transform(
+ child_index,
+ parent_index,
+ )
+ .into_transform()
+ .with_destination::<PicturePixel>();
+
+ register_transform(
+ metadata,
+ transforms,
+ child_index,
+ parent_index,
+ transform,
+ )
+ })
+ }
+ }
+
+ // Get a transform palette id for the given spatial node.
+ // TODO(gw): In the future, it will be possible to specify
+ // a coordinate system id here, to allow retrieving
+ // transforms in the local space of a given spatial node.
+ pub fn get_id(
+ &mut self,
+ from_index: SpatialNodeIndex,
+ to_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> TransformPaletteId {
+ let index = self.get_index(
+ from_index,
+ to_index,
+ spatial_tree,
+ );
+ let transform_kind = self.metadata[index].transform_kind as u32;
+ TransformPaletteId(
+ (index as u32) |
+ (transform_kind << 24)
+ )
+ }
+}
+
+// Texture cache resources can be either a simple rect, or define
+// a polygon within a rect by specifying a UV coordinate for each
+// corner. This is useful for rendering screen-space rasterized
+// off-screen surfaces.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum UvRectKind {
+ // The 2d bounds of the texture cache entry define the
+ // valid UV space for this texture cache entry.
+ Rect,
+ // The four vertices below define a quad within
+ // the texture cache entry rect. The shader can
+ // use a bilerp() to correctly interpolate a
+ // UV coord in the vertex shader.
+ Quad {
+ top_left: DeviceHomogeneousVector,
+ top_right: DeviceHomogeneousVector,
+ bottom_left: DeviceHomogeneousVector,
+ bottom_right: DeviceHomogeneousVector,
+ },
+}
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageSource {
+ pub p0: DevicePoint,
+ pub p1: DevicePoint,
+ pub texture_layer: f32,
+ pub user_data: [f32; 3],
+ pub uv_rect_kind: UvRectKind,
+}
+
+impl ImageSource {
+ pub fn write_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ // see fetch_image_resource in GLSL
+ // has to be VECS_PER_IMAGE_RESOURCE vectors
+ request.push([
+ self.p0.x,
+ self.p0.y,
+ self.p1.x,
+ self.p1.y,
+ ]);
+ request.push([
+ self.texture_layer,
+ self.user_data[0],
+ self.user_data[1],
+ self.user_data[2],
+ ]);
+
+ // If this is a polygon uv kind, then upload the four vertices.
+ if let UvRectKind::Quad { top_left, top_right, bottom_left, bottom_right } = self.uv_rect_kind {
+ // see fetch_image_resource_extra in GLSL
+ //Note: we really need only 3 components per point here: X, Y, and W
+ request.push(top_left);
+ request.push(top_right);
+ request.push(bottom_left);
+ request.push(bottom_right);
+ }
+ }
+}
+
+// Set the local -> world transform for a given spatial
+// node in the transform palette.
+fn register_transform(
+ metadatas: &mut Vec<TransformMetadata>,
+ transforms: &mut Vec<TransformData>,
+ from_index: SpatialNodeIndex,
+ to_index: SpatialNodeIndex,
+ transform: LayoutToPictureTransform,
+) -> usize {
+ // TODO: refactor the calling code to not even try
+ // registering a non-invertible transform.
+ let inv_transform = transform
+ .inverse()
+ .unwrap_or_else(PictureToLayoutTransform::identity);
+
+ let metadata = TransformMetadata {
+ transform_kind: transform.transform_kind()
+ };
+ let data = TransformData {
+ transform,
+ inv_transform,
+ };
+
+ if to_index == ROOT_SPATIAL_NODE_INDEX {
+ let index = from_index.0 as usize;
+ metadatas[index] = metadata;
+ transforms[index] = data;
+ index
+ } else {
+ let index = transforms.len();
+ metadatas.push(metadata);
+ transforms.push(data);
+ index
+ }
+}
+
+pub fn get_shader_opacity(opacity: f32) -> i32 {
+ (opacity * 65535.0).round() as i32
+}
diff --git a/gfx/wr/webrender/src/hit_test.rs b/gfx/wr/webrender/src/hit_test.rs
new file mode 100644
index 0000000000..c68801ba0c
--- /dev/null
+++ b/gfx/wr/webrender/src/hit_test.rs
@@ -0,0 +1,499 @@
+/* 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 api::{BorderRadius, ClipMode, HitTestItem, HitTestResult, ItemTag, PrimitiveFlags};
+use api::{PipelineId, ApiHitTester, ClipId};
+use api::units::*;
+use crate::clip::{ClipItemKind, ClipStore, ClipNode, rounded_rectangle_contains_point};
+use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
+use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
+use std::ops;
+use std::sync::{Arc, Mutex};
+use crate::util::LayoutToWorldFastTransform;
+
+pub struct SharedHitTester {
+ // We don't really need a mutex here. We could do with some sort of
+ // atomic-atomic-ref-counted pointer (an Arc which would let the pointer
+ // be swapped atomically like an AtomicPtr).
+ // In practive this shouldn't cause performance issues, though.
+ hit_tester: Mutex<Arc<HitTester>>,
+}
+
+impl SharedHitTester {
+ pub fn new() -> Self {
+ SharedHitTester {
+ hit_tester: Mutex::new(Arc::new(HitTester::empty())),
+ }
+ }
+
+ pub fn get_ref(&self) -> Arc<HitTester> {
+ let guard = self.hit_tester.lock().unwrap();
+ Arc::clone(&*guard)
+ }
+
+ pub(crate) fn update(&self, new_hit_tester: Arc<HitTester>) {
+ let mut guard = self.hit_tester.lock().unwrap();
+ *guard = new_hit_tester;
+ }
+}
+
+impl ApiHitTester for SharedHitTester {
+ fn hit_test(&self,
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+ ) -> HitTestResult {
+ self.get_ref().hit_test(HitTest::new(pipeline_id, point))
+ }
+}
+
+/// A copy of important spatial node data to use during hit testing. This a copy of
+/// data from the SpatialTree that will persist as a new frame is under construction,
+/// allowing hit tests consistent with the currently rendered frame.
+#[derive(MallocSizeOf)]
+struct HitTestSpatialNode {
+ /// The pipeline id of this node.
+ pipeline_id: PipelineId,
+
+ /// World transform for content transformed by this node.
+ world_content_transform: LayoutToWorldFastTransform,
+
+ /// World viewport transform for content transformed by this node.
+ world_viewport_transform: LayoutToWorldFastTransform,
+
+ /// The accumulated external scroll offset for this spatial node.
+ external_scroll_offset: LayoutVector2D,
+}
+
+#[derive(MallocSizeOf)]
+struct HitTestClipNode {
+ /// A particular point must be inside all of these regions to be considered clipped in
+ /// for the purposes of a hit test.
+ region: HitTestRegion,
+ /// The positioning node for this clip
+ spatial_node_index: SpatialNodeIndex,
+}
+
+impl HitTestClipNode {
+ fn new(
+ node: ClipNode,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ let region = match node.item.kind {
+ ClipItemKind::Rectangle { rect, mode } => {
+ HitTestRegion::Rectangle(rect, mode)
+ }
+ ClipItemKind::RoundedRectangle { rect, radius, mode } => {
+ HitTestRegion::RoundedRectangle(rect, radius, mode)
+ }
+ ClipItemKind::Image { rect, .. } => {
+ HitTestRegion::Rectangle(rect, ClipMode::Clip)
+ }
+ ClipItemKind::BoxShadow { .. } => HitTestRegion::Invalid,
+ };
+
+ HitTestClipNode {
+ region,
+ spatial_node_index,
+ }
+ }
+}
+
+#[derive(Clone, MallocSizeOf)]
+struct HitTestingItem {
+ rect: LayoutRect,
+ clip_rect: LayoutRect,
+ tag: ItemTag,
+ is_backface_visible: bool,
+ spatial_node_index: SpatialNodeIndex,
+ #[ignore_malloc_size_of = "Range"]
+ clip_nodes_range: ops::Range<ClipNodeIndex>,
+}
+
+impl HitTestingItem {
+ fn new(
+ tag: ItemTag,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_nodes_range: ops::Range<ClipNodeIndex>,
+ ) -> HitTestingItem {
+ HitTestingItem {
+ rect: info.rect,
+ clip_rect: info.clip_rect,
+ tag,
+ is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ spatial_node_index,
+ clip_nodes_range,
+ }
+ }
+}
+
+/// Statistics about allocation sizes of current hit tester,
+/// used to pre-allocate size of the next hit tester.
+pub struct HitTestingSceneStats {
+ pub clip_nodes_count: usize,
+ pub items_count: usize,
+}
+
+impl HitTestingSceneStats {
+ pub fn empty() -> Self {
+ HitTestingSceneStats {
+ clip_nodes_count: 0,
+ items_count: 0,
+ }
+ }
+}
+
+#[derive(MallocSizeOf, Debug, Copy, Clone)]
+pub struct ClipNodeIndex(u32);
+
+/// Defines the immutable part of a hit tester for a given scene.
+/// The hit tester is recreated each time a frame is built, since
+/// it relies on the current values of the spatial tree.
+/// However, the clip chain and item definitions don't change,
+/// so they are created once per scene, and shared between
+/// hit tester instances via Arc.
+#[derive(MallocSizeOf)]
+pub struct HitTestingScene {
+ /// Packed array of all hit test clip nodes
+ clip_nodes: Vec<HitTestClipNode>,
+
+ /// List of hit testing primitives.
+ items: Vec<HitTestingItem>,
+
+ /// Current stack of clip ids from stacking context
+ #[ignore_malloc_size_of = "ClipId"]
+ clip_id_stack: Vec<ClipId>,
+
+ /// Last cached clip id, useful for scenes with a lot
+ /// of hit-test items that reference the same clip
+ #[ignore_malloc_size_of = "simple"]
+ cached_clip_id: Option<(ClipId, ops::Range<ClipNodeIndex>)>,
+}
+
+impl HitTestingScene {
+ /// Construct a new hit testing scene, pre-allocating to size
+ /// provided by previous scene stats.
+ pub fn new(stats: &HitTestingSceneStats) -> Self {
+ HitTestingScene {
+ clip_nodes: Vec::with_capacity(stats.clip_nodes_count),
+ items: Vec::with_capacity(stats.items_count),
+ clip_id_stack: Vec::with_capacity(8),
+ cached_clip_id: None,
+ }
+ }
+
+ /// Get stats about the current scene allocation sizes.
+ pub fn get_stats(&self) -> HitTestingSceneStats {
+ HitTestingSceneStats {
+ clip_nodes_count: self.clip_nodes.len(),
+ items_count: self.items.len(),
+ }
+ }
+
+ /// Add a hit testing primitive.
+ pub fn add_item(
+ &mut self,
+ tag: ItemTag,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_id: ClipId,
+ clip_store: &ClipStore,
+ ) {
+ let clip_range = match self.cached_clip_id {
+ Some((cached_clip_id, ref range)) if cached_clip_id == clip_id => {
+ range.clone()
+ }
+ Some(_) | None => {
+ let start = ClipNodeIndex(self.clip_nodes.len() as u32);
+
+ // Flatten all clips from the stacking context hierarchy
+ for clip_id in &self.clip_id_stack {
+ add_clips(
+ *clip_id,
+ clip_store,
+ &mut self.clip_nodes,
+ );
+ }
+
+ // Add the primitive clip
+ add_clips(
+ clip_id,
+ clip_store,
+ &mut self.clip_nodes,
+ );
+
+ let end = ClipNodeIndex(self.clip_nodes.len() as u32);
+
+ let range = ops::Range {
+ start,
+ end,
+ };
+
+ self.cached_clip_id = Some((clip_id, range.clone()));
+
+ range
+ }
+ };
+
+ let item = HitTestingItem::new(
+ tag,
+ info,
+ spatial_node_index,
+ clip_range,
+ );
+
+ self.items.push(item);
+ }
+
+ /// Push a clip onto the current stack
+ pub fn push_clip(
+ &mut self,
+ clip_id: ClipId,
+ ) {
+ // Invalidate the cache since the stack may affect the produced hit test clip struct
+ self.cached_clip_id = None;
+
+ self.clip_id_stack.push(clip_id);
+ }
+
+ /// Pop a clip from the current stack
+ pub fn pop_clip(
+ &mut self,
+ ) {
+ // Invalidate the cache since the stack may affect the produced hit test clip struct
+ self.cached_clip_id = None;
+
+ self.clip_id_stack.pop().unwrap();
+ }
+}
+
+#[derive(MallocSizeOf)]
+enum HitTestRegion {
+ Invalid,
+ Rectangle(LayoutRect, ClipMode),
+ RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
+}
+
+impl HitTestRegion {
+ fn contains(&self, point: &LayoutPoint) -> bool {
+ match *self {
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::Clip) =>
+ rectangle.contains(*point),
+ HitTestRegion::Rectangle(ref rectangle, ClipMode::ClipOut) =>
+ !rectangle.contains(*point),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
+ rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
+ !rounded_rectangle_contains_point(point, &rect, &radii),
+ HitTestRegion::Invalid => true,
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTester {
+ #[ignore_malloc_size_of = "Arc"]
+ scene: Arc<HitTestingScene>,
+ spatial_nodes: Vec<HitTestSpatialNode>,
+ pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
+}
+
+impl HitTester {
+ pub fn empty() -> Self {
+ HitTester {
+ scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
+ spatial_nodes: Vec::new(),
+ pipeline_root_nodes: FastHashMap::default(),
+ }
+ }
+
+ pub fn new(
+ scene: Arc<HitTestingScene>,
+ spatial_tree: &SpatialTree,
+ ) -> HitTester {
+ let mut hit_tester = HitTester {
+ scene,
+ spatial_nodes: Vec::new(),
+ pipeline_root_nodes: FastHashMap::default(),
+ };
+ hit_tester.read_spatial_tree(spatial_tree);
+ hit_tester
+ }
+
+ fn read_spatial_tree(
+ &mut self,
+ spatial_tree: &SpatialTree,
+ ) {
+ self.spatial_nodes.clear();
+
+ self.spatial_nodes.reserve(spatial_tree.spatial_nodes.len());
+ for (index, node) in spatial_tree.spatial_nodes.iter().enumerate() {
+ let index = SpatialNodeIndex::new(index);
+
+ // If we haven't already seen a node for this pipeline, record this one as the root
+ // node.
+ self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
+
+ //TODO: avoid inverting more than necessary:
+ // - if the coordinate system is non-invertible, no need to try any of these concrete transforms
+ // - if there are other places where inversion is needed, let's not repeat the step
+
+ self.spatial_nodes.push(HitTestSpatialNode {
+ pipeline_id: node.pipeline_id,
+ world_content_transform: spatial_tree
+ .get_world_transform(index)
+ .into_fast_transform(),
+ world_viewport_transform: spatial_tree
+ .get_world_viewport_transform(index)
+ .into_fast_transform(),
+ external_scroll_offset: spatial_tree.external_scroll_offset(index),
+ });
+ }
+ }
+
+ pub fn hit_test(&self, test: HitTest) -> HitTestResult {
+ let mut result = HitTestResult::default();
+
+ let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut point_in_layer = None;
+ let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut point_in_viewport = None;
+
+ // For each hit test primitive
+ for item in self.scene.items.iter().rev() {
+ let scroll_node = &self.spatial_nodes[item.spatial_node_index.0 as usize];
+ let pipeline_id = scroll_node.pipeline_id;
+ match (test.pipeline_id, pipeline_id) {
+ (Some(id), node_id) if node_id != id => continue,
+ _ => {},
+ }
+
+ // Update the cached point in layer space, if the spatial node
+ // changed since last primitive.
+ if item.spatial_node_index != current_spatial_node_index {
+ point_in_layer = scroll_node
+ .world_content_transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point));
+ current_spatial_node_index = item.spatial_node_index;
+ }
+
+ // Only consider hit tests on transformable layers.
+ if let Some(point_in_layer) = point_in_layer {
+ // If the item's rect or clip rect don't contain this point,
+ // it's not a valid hit.
+ if !item.rect.contains(point_in_layer) {
+ continue;
+ }
+ if !item.clip_rect.contains(point_in_layer) {
+ continue;
+ }
+
+ // See if any of the clips for this primitive cull out the item.
+ let mut is_valid = true;
+ let clip_nodes = &self.scene.clip_nodes[item.clip_nodes_range.start.0 as usize .. item.clip_nodes_range.end.0 as usize];
+ for clip_node in clip_nodes {
+ let transform = self
+ .spatial_nodes[clip_node.spatial_node_index.0 as usize]
+ .world_content_transform;
+ let transformed_point = match transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point))
+ {
+ Some(point) => point,
+ None => {
+ continue;
+ }
+ };
+ if !clip_node.region.contains(&transformed_point) {
+ is_valid = false;
+ break;
+ }
+ }
+ if !is_valid {
+ continue;
+ }
+
+ // Don't hit items with backface-visibility:hidden if they are facing the back.
+ if !item.is_backface_visible && scroll_node.world_content_transform.is_backface_visible() {
+ continue;
+ }
+
+ // We need to calculate the position of the test point relative to the origin of
+ // the pipeline of the hit item. If we cannot get a transformed point, we are
+ // in a situation with an uninvertible transformation so we should just skip this
+ // result.
+ let root_spatial_node_index = self.pipeline_root_nodes[&pipeline_id];
+ if root_spatial_node_index != current_root_spatial_node_index {
+ let root_node = &self.spatial_nodes[root_spatial_node_index.0 as usize];
+ point_in_viewport = root_node
+ .world_viewport_transform
+ .inverse()
+ .and_then(|inverted| inverted.transform_point2d(test.point))
+ .map(|pt| pt - scroll_node.external_scroll_offset);
+
+ current_root_spatial_node_index = root_spatial_node_index;
+ }
+
+ if let Some(point_in_viewport) = point_in_viewport {
+ result.items.push(HitTestItem {
+ pipeline: pipeline_id,
+ tag: item.tag,
+ point_in_viewport,
+ point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
+ });
+ }
+ }
+ }
+
+ result.items.dedup();
+ result
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub struct HitTest {
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+}
+
+impl HitTest {
+ pub fn new(
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+ ) -> HitTest {
+ HitTest {
+ pipeline_id,
+ point,
+ }
+ }
+}
+
+/// Collect clips for a given ClipId, convert and add them to the hit testing
+/// scene, if not already present.
+fn add_clips(
+ clip_id: ClipId,
+ clip_store: &ClipStore,
+ clip_nodes: &mut Vec<HitTestClipNode>,
+) {
+ let template = &clip_store.templates[&clip_id];
+
+ for clip in &template.clips {
+ let hit_test_clip_node = HitTestClipNode::new(
+ clip.key.into(),
+ clip.clip.spatial_node_index,
+ );
+
+ clip_nodes.push(hit_test_clip_node);
+ }
+
+ // The ClipId parenting is terminated when we reach the root ClipId
+ if clip_id != template.parent {
+ add_clips(
+ template.parent,
+ clip_store,
+ clip_nodes,
+ );
+ }
+}
diff --git a/gfx/wr/webrender/src/image_tiling.rs b/gfx/wr/webrender/src/image_tiling.rs
new file mode 100644
index 0000000000..c83892911b
--- /dev/null
+++ b/gfx/wr/webrender/src/image_tiling.rs
@@ -0,0 +1,816 @@
+/* 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 crate::api::TileSize;
+use crate::api::units::*;
+use crate::segment::EdgeAaSegmentMask;
+use euclid::{point2, size2};
+use std::i32;
+use std::ops::Range;
+
+/// If repetitions are far enough apart that only one is within
+/// the primitive rect, then we can simplify the parameters and
+/// treat the primitive as not repeated.
+/// This can let us avoid unnecessary work later to handle some
+/// of the parameters.
+pub fn simplify_repeated_primitive(
+ stretch_size: &LayoutSize,
+ tile_spacing: &mut LayoutSize,
+ prim_rect: &mut LayoutRect,
+) {
+ let stride = *stretch_size + *tile_spacing;
+
+ if stride.width >= prim_rect.size.width {
+ tile_spacing.width = 0.0;
+ prim_rect.size.width = f32::min(prim_rect.size.width, stretch_size.width);
+ }
+ if stride.height >= prim_rect.size.height {
+ tile_spacing.height = 0.0;
+ prim_rect.size.height = f32::min(prim_rect.size.height, stretch_size.height);
+ }
+}
+
+pub struct Repetition {
+ pub origin: LayoutPoint,
+ pub edge_flags: EdgeAaSegmentMask,
+}
+
+pub struct RepetitionIterator {
+ current_x: i32,
+ x_count: i32,
+ current_y: i32,
+ y_count: i32,
+ row_flags: EdgeAaSegmentMask,
+ current_origin: LayoutPoint,
+ initial_origin: LayoutPoint,
+ stride: LayoutSize,
+}
+
+impl Iterator for RepetitionIterator {
+ type Item = Repetition;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.current_x == self.x_count {
+ self.current_y += 1;
+ if self.current_y >= self.y_count {
+ return None;
+ }
+ self.current_x = 0;
+
+ self.row_flags = EdgeAaSegmentMask::empty();
+ if self.current_y == self.y_count - 1 {
+ self.row_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ self.current_origin.x = self.initial_origin.x;
+ self.current_origin.y += self.stride.height;
+ }
+
+ let mut edge_flags = self.row_flags;
+ if self.current_x == 0 {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ }
+
+ if self.current_x == self.x_count - 1 {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ }
+
+ let repetition = Repetition {
+ origin: self.current_origin,
+ edge_flags,
+ };
+
+ self.current_origin.x += self.stride.width;
+ self.current_x += 1;
+
+ Some(repetition)
+ }
+}
+
+pub fn repetitions(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ stride: LayoutSize,
+) -> RepetitionIterator {
+ assert!(stride.width > 0.0);
+ assert!(stride.height > 0.0);
+
+ let visible_rect = match prim_rect.intersection(&visible_rect) {
+ Some(rect) => rect,
+ None => {
+ return RepetitionIterator {
+ current_origin: LayoutPoint::zero(),
+ initial_origin: LayoutPoint::zero(),
+ current_x: 0,
+ current_y: 0,
+ x_count: 0,
+ y_count: 0,
+ stride,
+ row_flags: EdgeAaSegmentMask::empty(),
+ }
+ }
+ };
+
+ let nx = if visible_rect.origin.x > prim_rect.origin.x {
+ f32::floor((visible_rect.origin.x - prim_rect.origin.x) / stride.width)
+ } else {
+ 0.0
+ };
+
+ let ny = if visible_rect.origin.y > prim_rect.origin.y {
+ f32::floor((visible_rect.origin.y - prim_rect.origin.y) / stride.height)
+ } else {
+ 0.0
+ };
+
+ let x0 = prim_rect.origin.x + nx * stride.width;
+ let y0 = prim_rect.origin.y + ny * stride.height;
+
+ let x_most = visible_rect.max_x();
+ let y_most = visible_rect.max_y();
+
+ let x_count = f32::ceil((x_most - x0) / stride.width) as i32;
+ let y_count = f32::ceil((y_most - y0) / stride.height) as i32;
+
+ let mut row_flags = EdgeAaSegmentMask::TOP;
+ if y_count == 1 {
+ row_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ RepetitionIterator {
+ current_origin: LayoutPoint::new(x0, y0),
+ initial_origin: LayoutPoint::new(x0, y0),
+ current_x: 0,
+ current_y: 0,
+ x_count,
+ y_count,
+ row_flags,
+ stride,
+ }
+}
+
+#[derive(Debug)]
+pub struct Tile {
+ pub rect: LayoutRect,
+ pub offset: TileOffset,
+ pub edge_flags: EdgeAaSegmentMask,
+}
+
+#[derive(Debug)]
+pub struct TileIteratorExtent {
+ /// Range of visible tiles to iterate over in number of tiles.
+ tile_range: Range<i32>,
+ /// Range of tiles of the full image including tiles that are culled out.
+ image_tiles: Range<i32>,
+ /// Size of the first tile in layout space.
+ first_tile_layout_size: f32,
+ /// Size of the last tile in layout space.
+ last_tile_layout_size: f32,
+ /// Position of blob point (0, 0) in layout space.
+ layout_tiling_origin: f32,
+ /// Position of the top-left corner of the primitive rect in layout space.
+ layout_prim_start: f32,
+}
+
+#[derive(Debug)]
+pub struct TileIterator {
+ current_tile: TileOffset,
+ x: TileIteratorExtent,
+ y: TileIteratorExtent,
+ regular_tile_size: LayoutSize,
+}
+
+impl Iterator for TileIterator {
+ type Item = Tile;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // If we reach the end of a row, reset to the beginning of the next row.
+ if self.current_tile.x >= self.x.tile_range.end {
+ self.current_tile.y += 1;
+ self.current_tile.x = self.x.tile_range.start;
+ }
+
+ // Stop iterating if we reach the last tile. We may start here if there
+ // were no tiles to iterate over.
+ if self.current_tile.x >= self.x.tile_range.end || self.current_tile.y >= self.y.tile_range.end {
+ return None;
+ }
+
+ let tile_offset = self.current_tile;
+
+ let mut segment_rect = LayoutRect {
+ origin: LayoutPoint::new(
+ self.x.layout_tiling_origin + tile_offset.x as f32 * self.regular_tile_size.width,
+ self.y.layout_tiling_origin + tile_offset.y as f32 * self.regular_tile_size.height,
+ ),
+ size: self.regular_tile_size,
+ };
+
+ let mut edge_flags = EdgeAaSegmentMask::empty();
+
+ if tile_offset.x == self.x.image_tiles.start {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ segment_rect.size.width = self.x.first_tile_layout_size;
+ segment_rect.origin.x = self.x.layout_prim_start;
+ }
+ if tile_offset.x == self.x.image_tiles.end - 1 {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ segment_rect.size.width = self.x.last_tile_layout_size;
+ }
+
+ if tile_offset.y == self.y.image_tiles.start {
+ segment_rect.size.height = self.y.first_tile_layout_size;
+ segment_rect.origin.y = self.y.layout_prim_start;
+ edge_flags |= EdgeAaSegmentMask::TOP;
+ }
+ if tile_offset.y == self.y.image_tiles.end - 1 {
+ segment_rect.size.height = self.y.last_tile_layout_size;
+ edge_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ assert!(tile_offset.y < self.y.tile_range.end);
+ let tile = Tile {
+ rect: segment_rect,
+ offset: tile_offset,
+ edge_flags,
+ };
+
+ self.current_tile.x += 1;
+
+ Some(tile)
+ }
+}
+
+pub fn tiles(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ image_rect: &DeviceIntRect,
+ device_tile_size: i32,
+) -> TileIterator {
+ // The image resource is tiled. We have to generate an image primitive
+ // for each tile.
+ // We need to do this because the image is broken up into smaller tiles in the texture
+ // cache and the image shader is not able to work with this type of sparse representation.
+
+ // The tiling logic works as follows:
+ //
+ // +-#################-+ -+
+ // | #//| | |//# | | image size
+ // | #//| | |//# | |
+ // +-#--+----+----+--#-+ | -+
+ // | #//| | |//# | | | regular tile size
+ // | #//| | |//# | | |
+ // +-#--+----+----+--#-+ | -+-+
+ // | #//|////|////|//# | | | "leftover" height
+ // | ################# | -+ ---+
+ // +----+----+----+----+
+ //
+ // In the ascii diagram above, a large image is split into tiles of almost regular size.
+ // The tiles on the edges (hatched in the diagram) can be smaller than the regular tiles
+ // and are handled separately in the code (we'll call them boundary tiles).
+ //
+ // Each generated segment corresponds to a tile in the texture cache, with the
+ // assumption that the boundary tiles are sized to fit their own irregular size in the
+ // texture cache.
+ //
+ // Because we can have very large virtual images we iterate over the visible portion of
+ // the image in layer space instead of iterating over all device tiles.
+
+ let visible_rect = match prim_rect.intersection(&visible_rect) {
+ Some(rect) => rect,
+ None => {
+ return TileIterator {
+ current_tile: TileOffset::zero(),
+ x: TileIteratorExtent {
+ tile_range: 0..0,
+ image_tiles: 0..0,
+ first_tile_layout_size: 0.0,
+ last_tile_layout_size: 0.0,
+ layout_tiling_origin: 0.0,
+ layout_prim_start: prim_rect.origin.x,
+ },
+ y: TileIteratorExtent {
+ tile_range: 0..0,
+ image_tiles: 0..0,
+ first_tile_layout_size: 0.0,
+ last_tile_layout_size: 0.0,
+ layout_tiling_origin: 0.0,
+ layout_prim_start: prim_rect.origin.y,
+ },
+ regular_tile_size: LayoutSize::zero(),
+ }
+ }
+ };
+
+ // Size of regular tiles in layout space.
+ let layout_tile_size = LayoutSize::new(
+ device_tile_size as f32 / image_rect.size.width as f32 * prim_rect.size.width,
+ device_tile_size as f32 / image_rect.size.height as f32 * prim_rect.size.height,
+ );
+
+ // The decomposition logic is exactly the same on each axis so we reduce
+ // this to a 1-dimensional problem in an attempt to make the code simpler.
+
+ let x_extent = tiles_1d(
+ layout_tile_size.width,
+ visible_rect.x_range(),
+ prim_rect.min_x(),
+ image_rect.x_range(),
+ device_tile_size,
+ );
+
+ let y_extent = tiles_1d(
+ layout_tile_size.height,
+ visible_rect.y_range(),
+ prim_rect.min_y(),
+ image_rect.y_range(),
+ device_tile_size,
+ );
+
+ TileIterator {
+ current_tile: point2(
+ x_extent.tile_range.start,
+ y_extent.tile_range.start,
+ ),
+ x: x_extent,
+ y: y_extent,
+ regular_tile_size: layout_tile_size,
+ }
+}
+
+/// Decompose tiles along an arbitrary axis.
+///
+/// This does most of the heavy lifting needed for `tiles` but in a single dimension for
+/// the sake of simplicity since the problem is independent on the x and y axes.
+fn tiles_1d(
+ layout_tile_size: f32,
+ layout_visible_range: Range<f32>,
+ layout_prim_start: f32,
+ device_image_range: Range<i32>,
+ device_tile_size: i32,
+) -> TileIteratorExtent {
+ // A few sanity checks.
+ debug_assert!(layout_tile_size > 0.0);
+ debug_assert!(layout_visible_range.end >= layout_visible_range.start);
+ debug_assert!(device_image_range.end > device_image_range.start);
+ debug_assert!(device_tile_size > 0);
+
+ // Sizes of the boundary tiles in pixels.
+ let first_tile_device_size = first_tile_size_1d(&device_image_range, device_tile_size);
+ let last_tile_device_size = last_tile_size_1d(&device_image_range, device_tile_size);
+
+ // [start..end[ Range of tiles of this row/column (in number of tiles) without
+ // taking culling into account.
+ let image_tiles = tile_range_1d(&device_image_range, device_tile_size);
+
+ // Layout offset of tile (0, 0) with respect to the top-left corner of the display item.
+ let layout_offset = device_image_range.start as f32 * layout_tile_size / device_tile_size as f32;
+ // Position in layout space of tile (0, 0).
+ let layout_tiling_origin = layout_prim_start - layout_offset;
+
+ // [start..end[ Range of the visible tiles (because of culling).
+ let visible_tiles_start = f32::floor((layout_visible_range.start - layout_tiling_origin) / layout_tile_size) as i32;
+ let visible_tiles_end = f32::ceil((layout_visible_range.end - layout_tiling_origin) / layout_tile_size) as i32;
+
+ // Combine the above two to get the tiles in the image that are visible this frame.
+ let mut tiles_start = i32::max(image_tiles.start, visible_tiles_start);
+ let tiles_end = i32::min(image_tiles.end, visible_tiles_end);
+ if tiles_start > tiles_end {
+ tiles_start = tiles_end;
+ }
+
+ // The size in layout space of the boundary tiles.
+ let first_tile_layout_size = if tiles_start == image_tiles.start {
+ first_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
+ } else {
+ // boundary tile was culled out, so the new first tile is a regularly sized tile.
+ layout_tile_size
+ };
+
+ // Same here.
+ let last_tile_layout_size = if tiles_end == image_tiles.end {
+ last_tile_device_size as f32 * layout_tile_size / device_tile_size as f32
+ } else {
+ layout_tile_size
+ };
+
+ TileIteratorExtent {
+ tile_range: tiles_start..tiles_end,
+ image_tiles,
+ first_tile_layout_size,
+ last_tile_layout_size,
+ layout_tiling_origin,
+ layout_prim_start,
+ }
+}
+
+/// Compute the range of tiles (in number of tiles) that intersect the provided
+/// image range (in pixels) in an arbitrary dimension.
+///
+/// ```ignore
+///
+/// 0
+/// :
+/// #-+---+---+---+---+---+--#
+/// # | | | | | | #
+/// #-+---+---+---+---+---+--#
+/// ^ : ^
+///
+/// +------------------------+ image_range
+/// +---+ regular_tile_size
+///
+/// ```
+fn tile_range_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> Range<i32> {
+ // Integer division truncates towards zero so with negative values if the first/last
+ // tile isn't a full tile we can get offset by one which we account for here.
+
+ let mut start = image_range.start / regular_tile_size;
+ if image_range.start % regular_tile_size < 0 {
+ start -= 1;
+ }
+
+ let mut end = image_range.end / regular_tile_size;
+ if image_range.end % regular_tile_size > 0 {
+ end += 1;
+ }
+
+ start..end
+}
+
+// Sizes of the first boundary tile in pixels.
+//
+// It can be smaller than the regular tile size if the image is not a multiple
+// of the regular tile size.
+fn first_tile_size_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> i32 {
+ // We have to account for how the % operation behaves for negative values.
+ let image_size = image_range.end - image_range.start;
+ i32::min(
+ match image_range.start % regular_tile_size {
+ // . #------+------+ .
+ // . #//////| | .
+ 0 => regular_tile_size,
+ // (zero) -> 0 . #--+------+ .
+ // . . #//| | .
+ // %(m): ~~>
+ m if m > 0 => regular_tile_size - m,
+ // . . #--+------+ 0 <- (zero)
+ // . . #//| | .
+ // %(m): <~~
+ m => -m,
+ },
+ image_size
+ )
+}
+
+// Sizes of the last boundary tile in pixels.
+//
+// It can be smaller than the regular tile size if the image is not a multiple
+// of the regular tile size.
+fn last_tile_size_1d(
+ image_range: &Range<i32>,
+ regular_tile_size: i32,
+) -> i32 {
+ // We have to account for how the modulo operation behaves for negative values.
+ let image_size = image_range.end - image_range.start;
+ i32::min(
+ match image_range.end % regular_tile_size {
+ // +------+------# .
+ // tiles: . | |//////# .
+ 0 => regular_tile_size,
+ // . +------+--# . 0 <- (zero)
+ // . | |//# . .
+ // modulo (m): <~~
+ m if m < 0 => regular_tile_size + m,
+ // (zero) -> 0 +------+--# . .
+ // . | |//# . .
+ // modulo (m): ~~>
+ m => m,
+ },
+ image_size,
+ )
+}
+
+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<i32>,
+ 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,
+ regular_tile_size: TileSize,
+ tile: TileOffset,
+) -> DeviceIntSize {
+ let regular_tile_size = regular_tile_size as i32;
+ 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_size_1d(
+ img_range: Range<i32>,
+ regular_tile_size: i32,
+ tile_offset: i32,
+) -> i32 {
+ let tile_range = tile_range_1d(&img_range, regular_tile_size);
+
+ // Most tiles are going to have base_size as width and height,
+ // except for tiles around the edges that are shrunk to fit the image data.
+ let actual_size = if tile_offset == tile_range.start {
+ first_tile_size_1d(&img_range, regular_tile_size)
+ } else if tile_offset == tile_range.end - 1 {
+ last_tile_size_1d(&img_range, regular_tile_size)
+ } else {
+ regular_tile_size
+ };
+
+ assert!(actual_size > 0);
+
+ actual_size
+}
+
+pub fn compute_tile_range(
+ visible_area: &DeviceIntRect,
+ tile_size: u16,
+) -> TileRange {
+ let tile_size = tile_size as i32;
+ let x_range = tile_range_1d(&visible_area.x_range(), tile_size);
+ let y_range = tile_range_1d(&visible_area.y_range(), tile_size);
+
+ TileRange {
+ origin: point2(x_range.start, y_range.start),
+ size: size2(x_range.end - x_range.start, y_range.end - y_range.start),
+ }
+}
+
+pub fn for_each_tile_in_range(
+ range: &TileRange,
+ mut callback: impl FnMut(TileOffset),
+) {
+ for y in range.y_range() {
+ for x in range.x_range() {
+ callback(point2(x, y));
+ }
+ }
+}
+
+pub fn compute_valid_tiles_if_bounds_change(
+ prev_rect: &DeviceIntRect,
+ new_rect: &DeviceIntRect,
+ tile_size: u16,
+) -> Option<TileRange> {
+ let intersection = match prev_rect.intersection(new_rect) {
+ Some(rect) => rect,
+ None => {
+ return Some(TileRange::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::<f32>()
+ .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::*;
+ use std::collections::HashSet;
+ use euclid::rect;
+
+ // this checks some additional invariants
+ fn checked_for_each_tile(
+ prim_rect: &LayoutRect,
+ visible_rect: &LayoutRect,
+ device_image_rect: &DeviceIntRect,
+ device_tile_size: i32,
+ callback: &mut dyn FnMut(&LayoutRect, TileOffset, EdgeAaSegmentMask),
+ ) {
+ let mut coverage = LayoutRect::zero();
+ let mut seen_tiles = HashSet::new();
+ for tile in tiles(
+ prim_rect,
+ visible_rect,
+ device_image_rect,
+ device_tile_size,
+ ) {
+ // make sure we don't get sent duplicate tiles
+ assert!(!seen_tiles.contains(&tile.offset));
+ seen_tiles.insert(tile.offset);
+ coverage = coverage.union(&tile.rect);
+ assert!(prim_rect.contains_rect(&tile.rect));
+ callback(&tile.rect, tile.offset, tile.edge_flags);
+ }
+ assert!(prim_rect.contains_rect(&coverage));
+ assert!(coverage.contains_rect(&visible_rect.intersection(&prim_rect).unwrap_or(LayoutRect::zero())));
+ }
+
+ #[test]
+ fn basic() {
+ let mut count = 0;
+ checked_for_each_tile(&rect(0., 0., 1000., 1000.),
+ &rect(75., 75., 400., 400.),
+ &rect(0, 0, 400, 400),
+ 36,
+ &mut |_tile_rect, _tile_offset, _tile_flags| {
+ count += 1;
+ },
+ );
+ assert_eq!(count, 36);
+ }
+
+ #[test]
+ fn empty() {
+ let mut count = 0;
+ checked_for_each_tile(&rect(0., 0., 74., 74.),
+ &rect(75., 75., 400., 400.),
+ &rect(0, 0, 400, 400),
+ 36,
+ &mut |_tile_rect, _tile_offset, _tile_flags| {
+ count += 1;
+ },
+ );
+ assert_eq!(count, 0);
+ }
+
+ #[test]
+ fn test_tiles_1d() {
+ // Exactly one full tile at positive offset.
+ let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Exactly one full tile at negative offset.
+ let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..0, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 0);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Two full tiles at negative and positive offsets.
+ let result = tiles_1d(64.0, -10000.0..10000.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // One partial tile at positive offset, non-zero origin, culled out.
+ let result = tiles_1d(64.0, -100.0..10.0, 64.0, 64..310, 64);
+ assert_eq!(result.tile_range.start, result.tile_range.end);
+
+ // Two tiles at negative and positive offsets, one of which is culled out.
+ // The remaining tile is partially culled but it should still generate a full tile.
+ let result = tiles_1d(64.0, 10.0..10000.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+ let result = tiles_1d(64.0, -10000.0..-10.0, -64.0, -64..64, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 0);
+ assert_eq!(result.first_tile_layout_size, 64.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Stretched tile in layout space device tile size is 64 and layout tile size is 128.
+ // So the resulting tile sizes in layout space should be multiplied by two.
+ let result = tiles_1d(128.0, -10000.0..10000.0, -64.0, -64..32, 64);
+ assert_eq!(result.tile_range.start, -1);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 128.0);
+ assert_eq!(result.last_tile_layout_size, 64.0);
+
+ // Two visible tiles (the rest is culled out).
+ let result = tiles_1d(10.0, 0.0..20.0, 0.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 10.0);
+ assert_eq!(result.last_tile_layout_size, 10.0);
+
+ // Two visible tiles at negative layout offsets (the rest is culled out).
+ let result = tiles_1d(10.0, -20.0..0.0, -20.0, 0..64, 64);
+ assert_eq!(result.tile_range.start, 0);
+ assert_eq!(result.tile_range.end, 1);
+ assert_eq!(result.first_tile_layout_size, 10.0);
+ assert_eq!(result.last_tile_layout_size, 10.0);
+ }
+
+ #[test]
+ fn test_tile_range_1d() {
+ assert_eq!(tile_range_1d(&(0..256), 256), 0..1);
+ assert_eq!(tile_range_1d(&(0..257), 256), 0..2);
+ assert_eq!(tile_range_1d(&(-1..257), 256), -1..2);
+ assert_eq!(tile_range_1d(&(-256..256), 256), -1..1);
+ assert_eq!(tile_range_1d(&(-20..-10), 6), -4..-1);
+ assert_eq!(tile_range_1d(&(20..100), 256), 0..1);
+ }
+
+ #[test]
+ fn test_first_last_tile_size_1d() {
+ assert_eq!(first_tile_size_1d(&(0..10), 64), 10);
+ assert_eq!(first_tile_size_1d(&(-20..0), 64), 20);
+
+ assert_eq!(last_tile_size_1d(&(0..10), 64), 10);
+ assert_eq!(last_tile_size_1d(&(-20..0), 64), 20);
+ }
+
+ #[test]
+ fn doubly_partial_tiles() {
+ // In the following tests the image is a single tile and none of the sides of the tile
+ // align with the tile grid.
+ // This can only happen when we have a single non-aligned partial tile and no regular
+ // tiles.
+ assert_eq!(first_tile_size_1d(&(300..310), 64), 10);
+ assert_eq!(first_tile_size_1d(&(-20..-10), 64), 10);
+
+ assert_eq!(last_tile_size_1d(&(300..310), 64), 10);
+ assert_eq!(last_tile_size_1d(&(-20..-10), 64), 10);
+
+
+ // One partial tile at positve offset, non-zero origin.
+ let result = tiles_1d(64.0, -10000.0..10000.0, 0.0, 300..310, 64);
+ assert_eq!(result.tile_range.start, 4);
+ assert_eq!(result.tile_range.end, 5);
+ 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/intern.rs b/gfx/wr/webrender/src/intern.rs
new file mode 100644
index 0000000000..5dd9c90600
--- /dev/null
+++ b/gfx/wr/webrender/src/intern.rs
@@ -0,0 +1,465 @@
+/* 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/. */
+
+//! The interning module provides a generic data structure
+//! interning container. It is similar in concept to a
+//! traditional string interning container, but it is
+//! specialized to the WR thread model.
+//!
+//! There is an Interner structure, that lives in the
+//! scene builder thread, and a DataStore structure
+//! that lives in the frame builder thread.
+//!
+//! Hashing, interning and handle creation is done by
+//! the interner structure during scene building.
+//!
+//! Delta changes for the interner are pushed during
+//! a transaction to the frame builder. The frame builder
+//! is then able to access the content of the interned
+//! handles quickly, via array indexing.
+//!
+//! Epoch tracking ensures that the garbage collection
+//! step which the interner uses to remove items is
+//! only invoked on items that the frame builder thread
+//! is no longer referencing.
+//!
+//! Items in the data store are stored in a traditional
+//! free-list structure, for content access and memory
+//! usage efficiency.
+//!
+//! The epoch is incremented each time a scene is
+//! built. The most recently used scene epoch is
+//! stored inside each handle. This is then used for
+//! cache invalidation.
+
+use crate::internal_types::FastHashMap;
+use malloc_size_of::MallocSizeOf;
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::marker::PhantomData;
+use std::{ops, u64};
+use crate::util::VecHelper;
+use crate::profiler::TransactionProfile;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+struct Epoch(u32);
+
+/// A list of updates to be applied to the data store,
+/// provided by the interning structure.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct UpdateList<S> {
+ /// Items to insert.
+ pub insertions: Vec<Insertion<S>>,
+
+ /// Items to remove.
+ pub removals: Vec<Removal>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Insertion<S> {
+ pub index: usize,
+ pub uid: ItemUid,
+ pub value: S,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Removal {
+ pub index: usize,
+ pub uid: ItemUid,
+}
+
+impl<S> UpdateList<S> {
+ fn new() -> UpdateList<S> {
+ UpdateList {
+ insertions: Vec::new(),
+ removals: Vec::new(),
+ }
+ }
+
+ fn take_and_preallocate(&mut self) -> UpdateList<S> {
+ UpdateList {
+ insertions: self.insertions.take_and_preallocate(),
+ removals: self.removals.take_and_preallocate(),
+ }
+ }
+}
+
+/// A globally, unique identifier
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq)]
+pub struct ItemUid {
+ uid: u64,
+}
+
+impl ItemUid {
+ // Intended for debug usage only
+ pub fn get_uid(&self) -> u64 {
+ self.uid
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf, PartialEq)]
+pub struct Handle<I> {
+ index: u32,
+ epoch: Epoch,
+ _marker: PhantomData<I>,
+}
+
+impl<I> Clone for Handle<I> {
+ fn clone(&self) -> Self {
+ Handle {
+ index: self.index,
+ epoch: self.epoch,
+ _marker: self._marker,
+ }
+ }
+}
+
+impl<I> Copy for Handle<I> {}
+
+impl<I> Handle<I> {
+ pub fn uid(&self) -> ItemUid {
+ ItemUid {
+ // The index in the freelist + the epoch it was interned generates a stable
+ // unique id for an interned element.
+ uid: ((self.index as u64) << 32) | self.epoch.0 as u64
+ }
+ }
+}
+
+pub trait InternDebug {
+ fn on_interned(&self, _uid: ItemUid) {}
+}
+
+/// The data store lives in the frame builder thread. It
+/// contains a free-list of items for fast access.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct DataStore<I: Internable> {
+ items: Vec<Option<I::StoreData>>,
+}
+
+impl<I: Internable> Default for DataStore<I> {
+ fn default() -> Self {
+ DataStore {
+ items: Vec::new(),
+ }
+ }
+}
+
+impl<I: Internable> DataStore<I> {
+ /// Apply any updates from the scene builder thread to
+ /// this data store.
+ pub fn apply_updates(
+ &mut self,
+ update_list: UpdateList<I::Key>,
+ profile: &mut TransactionProfile,
+ ) {
+ for insertion in update_list.insertions {
+ self.items
+ .entry(insertion.index)
+ .set(Some(insertion.value.into()));
+ }
+
+ for removal in update_list.removals {
+ self.items[removal.index] = None;
+ }
+
+ profile.set(I::PROFILE_COUNTER, self.items.len());
+ }
+}
+
+/// Retrieve an item from the store via handle
+impl<I: Internable> ops::Index<Handle<I>> for DataStore<I> {
+ type Output = I::StoreData;
+ fn index(&self, handle: Handle<I>) -> &I::StoreData {
+ self.items[handle.index as usize].as_ref().expect("Bad datastore lookup")
+ }
+}
+
+/// Retrieve a mutable item from the store via handle
+/// Retrieve an item from the store via handle
+impl<I: Internable> ops::IndexMut<Handle<I>> for DataStore<I> {
+ fn index_mut(&mut self, handle: Handle<I>) -> &mut I::StoreData {
+ self.items[handle.index as usize].as_mut().expect("Bad datastore lookup")
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct ItemDetails<I> {
+ /// Frame that this element was first interned
+ interned_epoch: Epoch,
+ /// Last frame this element was referenced (used to GC intern items)
+ last_used_epoch: Epoch,
+ /// Index into the freelist this item is located
+ index: usize,
+ /// Type marker for create_handle method
+ _marker: PhantomData<I>,
+}
+
+impl<I> ItemDetails<I> {
+ /// Construct a stable handle value from the item details
+ fn create_handle(&self) -> Handle<I> {
+ Handle {
+ index: self.index as u32,
+ epoch: self.interned_epoch,
+ _marker: PhantomData,
+ }
+ }
+}
+
+/// The main interning data structure. This lives in the
+/// scene builder thread, and handles hashing and interning
+/// unique data structures. It also manages a free-list for
+/// the items in the data store, which is synchronized via
+/// an update list of additions / removals.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct Interner<I: Internable> {
+ /// Uniquely map an interning key to a handle
+ map: FastHashMap<I::Key, ItemDetails<I>>,
+ /// List of free slots in the data store for re-use.
+ free_list: Vec<usize>,
+ /// Pending list of updates that need to be applied.
+ update_list: UpdateList<I::Key>,
+ /// The current epoch for the interner.
+ current_epoch: Epoch,
+ /// The information associated with each interned
+ /// item that can be accessed by the interner.
+ local_data: Vec<I::InternData>,
+}
+
+impl<I: Internable> Default for Interner<I> {
+ fn default() -> Self {
+ Interner {
+ map: FastHashMap::default(),
+ free_list: Vec::new(),
+ update_list: UpdateList::new(),
+ current_epoch: Epoch(1),
+ local_data: Vec::new(),
+ }
+ }
+}
+
+impl<I: Internable> Interner<I> {
+ /// Intern a data structure, and return a handle to
+ /// that data. The handle can then be stored in the
+ /// frame builder, and safely accessed via the data
+ /// store that lives in the frame builder thread.
+ /// The provided closure is invoked to build the
+ /// local data about an interned structure if the
+ /// key isn't already interned.
+ pub fn intern<F>(
+ &mut self,
+ data: &I::Key,
+ fun: F,
+ ) -> Handle<I> where F: FnOnce() -> I::InternData {
+ // Use get_mut rather than entry here to avoid
+ // cloning the (sometimes large) key in the common
+ // case, where the data already exists in the interner.
+ if let Some(details) = self.map.get_mut(data) {
+ // Update the last referenced frame for this element
+ details.last_used_epoch = self.current_epoch;
+ // Return a stable handle value for dependency checking
+ return details.create_handle();
+ }
+
+ // We need to intern a new data item. First, find out
+ // if there is a spare slot in the free-list that we
+ // can use. Otherwise, append to the end of the list.
+ let index = match self.free_list.pop() {
+ Some(index) => index,
+ None => self.local_data.len(),
+ };
+
+ // Generate a handle for access via the data store.
+ let handle = Handle {
+ index: index as u32,
+ epoch: self.current_epoch,
+ _marker: PhantomData,
+ };
+
+ let uid = handle.uid();
+
+ // Add a pending update to insert the new data.
+ self.update_list.insertions.push(Insertion {
+ index,
+ uid,
+ value: data.clone(),
+ });
+
+ #[cfg(debug_assertions)]
+ data.on_interned(uid);
+
+ // Store this handle so the next time it is
+ // interned, it gets re-used.
+ self.map.insert(data.clone(), ItemDetails {
+ interned_epoch: self.current_epoch,
+ last_used_epoch: self.current_epoch,
+ index,
+ _marker: PhantomData,
+ });
+
+ // Create the local data for this item that is
+ // being interned.
+ self.local_data.entry(index).set(fun());
+
+ handle
+ }
+
+ /// Retrieve the pending list of updates for an interner
+ /// that need to be applied to the data store. Also run
+ /// a GC step that removes old entries.
+ pub fn end_frame_and_get_pending_updates(&mut self) -> UpdateList<I::Key> {
+ let mut update_list = self.update_list.take_and_preallocate();
+
+ let free_list = &mut self.free_list;
+ let current_epoch = self.current_epoch.0;
+
+ // First, run a GC step. Walk through the handles, and
+ // if we find any that haven't been used for some time,
+ // remove them. If this ever shows up in profiles, we
+ // can make the GC step partial (scan only part of the
+ // map each frame). It also might make sense in the
+ // future to adjust how long items remain in the cache
+ // based on the current size of the list.
+ self.map.retain(|_, details| {
+ if details.last_used_epoch.0 + 10 < current_epoch {
+ // To expire an item:
+ // - Add index to the free-list for re-use.
+ // - Add an update to the data store to invalidate this slot.
+ // - Remove from the hash map.
+ free_list.push(details.index);
+ update_list.removals.push(Removal {
+ index: details.index,
+ uid: details.create_handle().uid(),
+ });
+ return false;
+ }
+
+ true
+ });
+
+ // Begin the next epoch
+ self.current_epoch = Epoch(self.current_epoch.0 + 1);
+
+ update_list
+ }
+}
+
+/// Retrieve the local data for an item from the interner via handle
+impl<I: Internable> ops::Index<Handle<I>> for Interner<I> {
+ type Output = I::InternData;
+ fn index(&self, handle: Handle<I>) -> &I::InternData {
+ &self.local_data[handle.index as usize]
+ }
+}
+
+/// Meta-macro to enumerate the various interner identifiers and types.
+///
+/// IMPORTANT: Keep this synchronized with the list in mozilla-central located at
+/// gfx/webrender_bindings/webrender_ffi.h
+///
+/// Note that this could be a lot less verbose if concat_idents! were stable. :-(
+#[macro_export]
+macro_rules! enumerate_interners {
+ ($macro_name: ident) => {
+ $macro_name! {
+ clip: ClipIntern,
+ prim: PrimitiveKeyKind,
+ normal_border: NormalBorderPrim,
+ image_border: ImageBorder,
+ image: Image,
+ yuv_image: YuvImage,
+ line_decoration: LineDecoration,
+ linear_grad: LinearGradient,
+ radial_grad: RadialGradient,
+ conic_grad: ConicGradient,
+ picture: Picture,
+ text_run: TextRun,
+ filter_data: FilterDataIntern,
+ backdrop: Backdrop,
+ }
+ }
+}
+
+macro_rules! declare_interning_memory_report {
+ ( $( $name:ident: $ty:ident, )+ ) => {
+ ///
+ #[repr(C)]
+ #[derive(AddAssign, Clone, Debug, Default)]
+ pub struct InternerSubReport {
+ $(
+ ///
+ pub $name: usize,
+ )+
+ }
+ }
+}
+
+enumerate_interners!(declare_interning_memory_report);
+
+/// Memory report for interning-related data structures.
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-ostream=false
+#[repr(C)]
+#[derive(Clone, Debug, Default)]
+pub struct InterningMemoryReport {
+ ///
+ pub interners: InternerSubReport,
+ ///
+ pub data_stores: InternerSubReport,
+}
+
+impl ::std::ops::AddAssign for InterningMemoryReport {
+ fn add_assign(&mut self, other: InterningMemoryReport) {
+ self.interners += other.interners;
+ self.data_stores += other.data_stores;
+ }
+}
+
+// The trick to make trait bounds configurable by features.
+mod dummy {
+ #[cfg(not(feature = "capture"))]
+ pub trait Serialize {}
+ #[cfg(not(feature = "capture"))]
+ impl<T> Serialize for T {}
+ #[cfg(not(feature = "replay"))]
+ pub trait Deserialize<'a> {}
+ #[cfg(not(feature = "replay"))]
+ impl<'a, T> Deserialize<'a> for T {}
+}
+#[cfg(feature = "capture")]
+use serde::Serialize as InternSerialize;
+#[cfg(not(feature = "capture"))]
+use self::dummy::Serialize as InternSerialize;
+#[cfg(feature = "replay")]
+use serde::Deserialize as InternDeserialize;
+#[cfg(not(feature = "replay"))]
+use self::dummy::Deserialize as InternDeserialize;
+
+/// Implement `Internable` for a type that wants to participate in interning.
+pub trait Internable: MallocSizeOf {
+ type Key: Eq + Hash + Clone + Debug + MallocSizeOf + InternDebug + InternSerialize + for<'a> InternDeserialize<'a>;
+ type StoreData: From<Self::Key> + MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>;
+ type InternData: MallocSizeOf + InternSerialize + for<'a> InternDeserialize<'a>;
+
+ // Profile counter indices, see the list in profiler.rs
+ const PROFILE_COUNTER: usize;
+}
diff --git a/gfx/wr/webrender/src/internal_types.rs b/gfx/wr/webrender/src/internal_types.rs
new file mode 100644
index 0000000000..e79f11c509
--- /dev/null
+++ b/gfx/wr/webrender/src/internal_types.rs
@@ -0,0 +1,586 @@
+/* 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 api::{ColorF, DocumentId, ExternalImageId, PrimitiveFlags};
+use api::{ImageFormat, NotificationRequest, Shadow, FilterOp, ImageBufferKind};
+use api::units::*;
+use api;
+use crate::render_api::DebugCommand;
+use crate::composite::NativeSurfaceOperation;
+use crate::device::TextureFilter;
+use crate::renderer::PipelineInfo;
+use crate::gpu_cache::GpuCacheUpdateList;
+use crate::frame_builder::Frame;
+use crate::profiler::TransactionProfile;
+use fxhash::FxHasher;
+use plane_split::BspSplitter;
+use smallvec::SmallVec;
+use std::{usize, i32};
+use std::collections::{HashMap, HashSet};
+use std::f32;
+use std::hash::BuildHasherDefault;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::CaptureConfig;
+#[cfg(feature = "capture")]
+use crate::capture::ExternalCaptureImage;
+#[cfg(feature = "replay")]
+use crate::capture::PlainExternalImage;
+
+pub type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
+pub type FastHashSet<K> = HashSet<K, BuildHasherDefault<FxHasher>>;
+
+/// Custom field embedded inside the Polygon struct of the plane-split crate.
+#[derive(Copy, Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PlaneSplitAnchor {
+ pub cluster_index: usize,
+ pub instance_index: usize,
+}
+
+impl PlaneSplitAnchor {
+ pub fn new(cluster_index: usize, instance_index: usize) -> Self {
+ PlaneSplitAnchor {
+ cluster_index,
+ instance_index,
+ }
+ }
+}
+
+impl Default for PlaneSplitAnchor {
+ fn default() -> Self {
+ PlaneSplitAnchor {
+ cluster_index: 0,
+ instance_index: 0,
+ }
+ }
+}
+
+/// A concrete plane splitter type used in WebRender.
+pub type PlaneSplitter = BspSplitter<f64, WorldPixel, PlaneSplitAnchor>;
+
+/// An arbitrary number which we assume opacity is invisible below.
+const OPACITY_EPSILON: f32 = 0.001;
+
+/// Equivalent to api::FilterOp with added internal information
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Filter {
+ Identity,
+ Blur(f32, f32),
+ Brightness(f32),
+ Contrast(f32),
+ Grayscale(f32),
+ HueRotate(f32),
+ Invert(f32),
+ Opacity(api::PropertyBinding<f32>, f32),
+ Saturate(f32),
+ Sepia(f32),
+ DropShadows(SmallVec<[Shadow; 1]>),
+ ColorMatrix(Box<[f32; 20]>),
+ SrgbToLinear,
+ LinearToSrgb,
+ ComponentTransfer,
+ Flood(ColorF),
+}
+
+impl Filter {
+ pub fn is_visible(&self) -> bool {
+ match *self {
+ Filter::Identity |
+ Filter::Blur(..) |
+ Filter::Brightness(..) |
+ Filter::Contrast(..) |
+ Filter::Grayscale(..) |
+ Filter::HueRotate(..) |
+ Filter::Invert(..) |
+ Filter::Saturate(..) |
+ Filter::Sepia(..) |
+ Filter::DropShadows(..) |
+ Filter::ColorMatrix(..) |
+ Filter::SrgbToLinear |
+ Filter::LinearToSrgb |
+ Filter::ComponentTransfer => true,
+ Filter::Opacity(_, amount) => {
+ amount > OPACITY_EPSILON
+ },
+ Filter::Flood(color) => {
+ color.a > OPACITY_EPSILON
+ }
+ }
+ }
+
+ pub fn is_noop(&self) -> bool {
+ match *self {
+ Filter::Identity => false, // this is intentional
+ Filter::Blur(width, height) => width == 0.0 && height == 0.0,
+ Filter::Brightness(amount) => amount == 1.0,
+ Filter::Contrast(amount) => amount == 1.0,
+ Filter::Grayscale(amount) => amount == 0.0,
+ Filter::HueRotate(amount) => amount == 0.0,
+ Filter::Invert(amount) => amount == 0.0,
+ Filter::Opacity(api::PropertyBinding::Value(amount), _) => amount >= 1.0,
+ Filter::Saturate(amount) => amount == 1.0,
+ Filter::Sepia(amount) => amount == 0.0,
+ Filter::DropShadows(ref shadows) => {
+ for shadow in shadows {
+ if shadow.offset.x != 0.0 || shadow.offset.y != 0.0 || shadow.blur_radius != 0.0 {
+ return false;
+ }
+ }
+
+ true
+ }
+ Filter::ColorMatrix(ref matrix) => {
+ **matrix == [
+ 1.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ 0.0, 0.0, 0.0, 0.0
+ ]
+ }
+ Filter::Opacity(api::PropertyBinding::Binding(..), _) |
+ Filter::SrgbToLinear |
+ Filter::LinearToSrgb |
+ Filter::ComponentTransfer |
+ Filter::Flood(..) => false,
+ }
+ }
+
+
+ pub fn as_int(&self) -> i32 {
+ // Must be kept in sync with brush_blend.glsl
+ match *self {
+ Filter::Identity => 0, // matches `Contrast(1)`
+ Filter::Contrast(..) => 0,
+ Filter::Grayscale(..) => 1,
+ Filter::HueRotate(..) => 2,
+ Filter::Invert(..) => 3,
+ Filter::Saturate(..) => 4,
+ Filter::Sepia(..) => 5,
+ Filter::Brightness(..) => 6,
+ Filter::ColorMatrix(..) => 7,
+ Filter::SrgbToLinear => 8,
+ Filter::LinearToSrgb => 9,
+ Filter::Flood(..) => 10,
+ Filter::ComponentTransfer => 11,
+ Filter::Blur(..) => 12,
+ Filter::DropShadows(..) => 13,
+ Filter::Opacity(..) => 14,
+ }
+ }
+}
+
+impl From<FilterOp> for Filter {
+ fn from(op: FilterOp) -> Self {
+ match op {
+ FilterOp::Identity => Filter::Identity,
+ FilterOp::Blur(w, h) => Filter::Blur(w, h),
+ FilterOp::Brightness(b) => Filter::Brightness(b),
+ FilterOp::Contrast(c) => Filter::Contrast(c),
+ FilterOp::Grayscale(g) => Filter::Grayscale(g),
+ FilterOp::HueRotate(h) => Filter::HueRotate(h),
+ FilterOp::Invert(i) => Filter::Invert(i),
+ FilterOp::Opacity(binding, opacity) => Filter::Opacity(binding, opacity),
+ FilterOp::Saturate(s) => Filter::Saturate(s),
+ FilterOp::Sepia(s) => Filter::Sepia(s),
+ FilterOp::ColorMatrix(mat) => Filter::ColorMatrix(Box::new(mat)),
+ FilterOp::SrgbToLinear => Filter::SrgbToLinear,
+ FilterOp::LinearToSrgb => Filter::LinearToSrgb,
+ FilterOp::ComponentTransfer => Filter::ComponentTransfer,
+ FilterOp::DropShadow(shadow) => Filter::DropShadows(smallvec![shadow]),
+ FilterOp::Flood(color) => Filter::Flood(color),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub enum Swizzle {
+ Rgba,
+ Bgra,
+}
+
+impl Default for Swizzle {
+ fn default() -> Self {
+ Swizzle::Rgba
+ }
+}
+
+/// Swizzle settings of the texture cache.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub struct SwizzleSettings {
+ /// Swizzle required on sampling a texture with BGRA8 format.
+ pub bgra8_sampling_swizzle: Swizzle,
+}
+
+/// An ID for a texture that is owned by the `texture_cache` module.
+///
+/// This can include atlases or standalone textures allocated via the texture
+/// cache (e.g. if an image is too large to be added to an atlas). The texture
+/// cache manages the allocation and freeing of these IDs, and the rendering
+/// thread maintains a map from cache texture ID to native texture.
+///
+/// We never reuse IDs, so we use a u64 here to be safe.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheTextureId(pub u32);
+
+impl CacheTextureId {
+ pub const INVALID: CacheTextureId = CacheTextureId(!0);
+}
+
+/// Canonical type for texture layer indices.
+///
+/// WebRender is currently not very consistent about layer index types. Some
+/// places use i32 (since that's the type used in various OpenGL APIs), some
+/// places use u32 (since having it be signed is non-sensical, but the
+/// underlying graphics APIs generally operate on 32-bit integers) and some
+/// places use usize (since that's most natural in Rust).
+///
+/// Going forward, we aim to us usize throughout the codebase, since that allows
+/// operations like indexing without a cast, and convert to the required type in
+/// the device module when making calls into the platform layer.
+pub type LayerIndex = usize;
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DeferredResolveIndex(pub u32);
+
+/// Identifies the source of an input texture to a shader.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureSource {
+ /// Equivalent to `None`, allowing us to avoid using `Option`s everywhere.
+ Invalid,
+ /// An entry in the texture cache.
+ TextureCache(CacheTextureId, Swizzle),
+ /// An external image texture, mananged by the embedding.
+ External(DeferredResolveIndex, ImageBufferKind),
+ /// Select a dummy 1x1 white texture. This can be used by image
+ /// shaders that want to draw a solid color.
+ Dummy,
+}
+
+impl TextureSource {
+ pub fn image_buffer_kind(&self) -> ImageBufferKind {
+ match *self {
+ TextureSource::TextureCache(..) => ImageBufferKind::Texture2D,
+
+ TextureSource::External(_, image_buffer_kind) => image_buffer_kind,
+
+ // Render tasks use texture arrays for now.
+ TextureSource::Dummy => ImageBufferKind::Texture2D,
+
+ TextureSource::Invalid => ImageBufferKind::Texture2D,
+ }
+ }
+
+ #[inline]
+ pub fn is_compatible(
+ &self,
+ other: &TextureSource,
+ ) -> bool {
+ *self == TextureSource::Invalid ||
+ *other == TextureSource::Invalid ||
+ self == other
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetInfo {
+ pub has_depth: bool,
+}
+
+#[derive(Debug)]
+pub enum TextureUpdateSource {
+ External {
+ id: ExternalImageId,
+ channel_index: u8,
+ },
+ Bytes { data: Arc<Vec<u8>> },
+ /// Clears the target area, rather than uploading any pixels. Used when the
+ /// texture cache debug display is active.
+ DebugClear,
+}
+
+/// Command to allocate, reallocate, or free a texture for the texture cache.
+#[derive(Debug)]
+pub struct TextureCacheAllocation {
+ /// The virtual ID (i.e. distinct from device ID) of the texture.
+ pub id: CacheTextureId,
+ /// Details corresponding to the operation in question.
+ pub kind: TextureCacheAllocationKind,
+}
+
+/// Information used when allocating / reallocating.
+#[derive(Debug)]
+pub struct TextureCacheAllocInfo {
+ pub width: i32,
+ pub height: i32,
+ pub layer_count: i32,
+ pub format: ImageFormat,
+ pub filter: TextureFilter,
+ pub target: ImageBufferKind,
+ /// Indicates whether this corresponds to one of the shared texture caches.
+ pub is_shared_cache: bool,
+ /// If true, this texture requires a depth target.
+ pub has_depth: bool,
+}
+
+/// Sub-operation-specific information for allocation operations.
+#[derive(Debug)]
+pub enum TextureCacheAllocationKind {
+ /// Performs an initial texture allocation.
+ Alloc(TextureCacheAllocInfo),
+ /// Reallocates the texture without preserving its contents.
+ Reset(TextureCacheAllocInfo),
+ /// Frees the texture and the corresponding cache ID.
+ Free,
+}
+
+/// Command to update the contents of the texture cache.
+#[derive(Debug)]
+pub struct TextureCacheUpdate {
+ pub rect: DeviceIntRect,
+ pub stride: Option<i32>,
+ pub offset: i32,
+ pub layer_index: i32,
+ pub format_override: Option<ImageFormat>,
+ pub source: TextureUpdateSource,
+}
+
+/// Atomic set of commands to manipulate the texture cache, generated on the
+/// RenderBackend thread and executed on the Renderer thread.
+///
+/// The list of allocation operations is processed before the updates. This is
+/// important to allow coalescing of certain allocation operations.
+#[derive(Default)]
+pub struct TextureUpdateList {
+ /// Indicates that there was some kind of cleanup clear operation. Used for
+ /// sanity checks.
+ pub clears_shared_cache: bool,
+ /// Commands to alloc/realloc/free the textures. Processed first.
+ pub allocations: Vec<TextureCacheAllocation>,
+ /// Commands to update the contents of the textures. Processed second.
+ pub updates: FastHashMap<CacheTextureId, Vec<TextureCacheUpdate>>,
+}
+
+impl TextureUpdateList {
+ /// Mints a new `TextureUpdateList`.
+ pub fn new() -> Self {
+ TextureUpdateList {
+ clears_shared_cache: false,
+ allocations: Vec::new(),
+ updates: FastHashMap::default(),
+ }
+ }
+
+ /// Returns true if this is a no-op (no updates to be applied).
+ pub fn is_nop(&self) -> bool {
+ self.allocations.is_empty() && self.updates.is_empty()
+ }
+
+ /// Sets the clears_shared_cache flag for renderer-side sanity checks.
+ #[inline]
+ pub fn note_clear(&mut self) {
+ self.clears_shared_cache = true;
+ }
+
+ /// Pushes an update operation onto the list.
+ #[inline]
+ pub fn push_update(&mut self, id: CacheTextureId, update: TextureCacheUpdate) {
+ self.updates
+ .entry(id)
+ .or_default()
+ .push(update);
+ }
+
+ /// Sends a command to the Renderer to clear the portion of the shared region
+ /// we just freed. Used when the texture cache debugger is enabled.
+ #[cold]
+ pub fn push_debug_clear(
+ &mut self,
+ id: CacheTextureId,
+ origin: DeviceIntPoint,
+ width: i32,
+ height: i32,
+ layer_index: usize
+ ) {
+ let size = DeviceIntSize::new(width, height);
+ let rect = DeviceIntRect::new(origin, size);
+ self.push_update(id, TextureCacheUpdate {
+ rect,
+ stride: None,
+ offset: 0,
+ layer_index: layer_index as i32,
+ format_override: None,
+ source: TextureUpdateSource::DebugClear,
+ });
+ }
+
+
+ /// Pushes an allocation operation onto the list.
+ pub fn push_alloc(&mut self, id: CacheTextureId, info: TextureCacheAllocInfo) {
+ debug_assert!(!self.allocations.iter().any(|x| x.id == id));
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Alloc(info),
+ });
+ }
+
+ /// Pushes a reallocation operation onto the list, potentially coalescing
+ /// with previous operations.
+ pub fn push_reset(&mut self, id: CacheTextureId, info: TextureCacheAllocInfo) {
+ self.debug_assert_coalesced(id);
+
+ // Drop any unapplied updates to the to-be-freed texture.
+ self.updates.remove(&id);
+
+ // Coallesce this realloc into a previous alloc or realloc, if available.
+ if let Some(cur) = self.allocations.iter_mut().find(|x| x.id == id) {
+ match cur.kind {
+ TextureCacheAllocationKind::Alloc(ref mut i) => *i = info,
+ TextureCacheAllocationKind::Reset(ref mut i) => *i = info,
+ TextureCacheAllocationKind::Free => panic!("Resetting freed texture"),
+ }
+ return
+ }
+
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Reset(info),
+ });
+ }
+
+ /// Pushes a free operation onto the list, potentially coalescing with
+ /// previous operations.
+ pub fn push_free(&mut self, id: CacheTextureId) {
+ self.debug_assert_coalesced(id);
+
+ // Drop any unapplied updates to the to-be-freed texture.
+ self.updates.remove(&id);
+
+ // Drop any allocations for it as well. If we happen to be allocating and
+ // freeing in the same batch, we can collapse them to a no-op.
+ let idx = self.allocations.iter().position(|x| x.id == id);
+ let removed_kind = idx.map(|i| self.allocations.remove(i).kind);
+ match removed_kind {
+ Some(TextureCacheAllocationKind::Alloc(..)) => { /* no-op! */ },
+ Some(TextureCacheAllocationKind::Free) => panic!("Double free"),
+ Some(TextureCacheAllocationKind::Reset(..)) |
+ None => {
+ self.allocations.push(TextureCacheAllocation {
+ id,
+ kind: TextureCacheAllocationKind::Free,
+ });
+ }
+ };
+ }
+
+ fn debug_assert_coalesced(&self, id: CacheTextureId) {
+ debug_assert!(
+ self.allocations.iter().filter(|x| x.id == id).count() <= 1,
+ "Allocations should have been coalesced",
+ );
+ }
+}
+
+/// A list of updates built by the render backend that should be applied
+/// by the renderer thread.
+pub struct ResourceUpdateList {
+ /// List of OS native surface create / destroy operations to apply.
+ pub native_surface_updates: Vec<NativeSurfaceOperation>,
+
+ /// Atomic set of texture cache updates to apply.
+ pub texture_updates: TextureUpdateList,
+}
+
+impl ResourceUpdateList {
+ /// Returns true if this update list has no effect.
+ pub fn is_nop(&self) -> bool {
+ self.texture_updates.is_nop() && self.native_surface_updates.is_empty()
+ }
+}
+
+/// Wraps a frame_builder::Frame, but conceptually could hold more information
+pub struct RenderedDocument {
+ pub frame: Frame,
+ pub is_new_scene: bool,
+ pub profile: TransactionProfile,
+}
+
+pub enum DebugOutput {
+ FetchDocuments(String),
+ FetchClipScrollTree(String),
+ #[cfg(feature = "capture")]
+ SaveCapture(CaptureConfig, Vec<ExternalCaptureImage>),
+ #[cfg(feature = "replay")]
+ LoadCapture(CaptureConfig, Vec<PlainExternalImage>),
+}
+
+#[allow(dead_code)]
+pub enum ResultMsg {
+ DebugCommand(DebugCommand),
+ DebugOutput(DebugOutput),
+ RefreshShader(PathBuf),
+ UpdateGpuCache(GpuCacheUpdateList),
+ UpdateResources {
+ resource_updates: ResourceUpdateList,
+ memory_pressure: bool,
+ },
+ PublishPipelineInfo(PipelineInfo),
+ PublishDocument(
+ DocumentId,
+ RenderedDocument,
+ ResourceUpdateList,
+ ),
+ AppendNotificationRequests(Vec<NotificationRequest>),
+ ForceRedraw,
+}
+
+#[derive(Clone, Debug)]
+pub struct ResourceCacheError {
+ description: String,
+}
+
+impl ResourceCacheError {
+ pub fn new(description: String) -> ResourceCacheError {
+ ResourceCacheError {
+ description,
+ }
+ }
+}
+
+/// Primitive metadata we pass around in a bunch of places
+#[derive(Copy, Clone, Debug)]
+pub struct LayoutPrimitiveInfo {
+ /// NOTE: this is *ideally* redundant with the clip_rect
+ /// but that's an ongoing project, so for now it exists and is used :(
+ pub rect: LayoutRect,
+ pub clip_rect: LayoutRect,
+ pub flags: PrimitiveFlags,
+}
+
+impl LayoutPrimitiveInfo {
+ pub fn with_clip_rect(rect: LayoutRect, clip_rect: LayoutRect) -> Self {
+ Self {
+ rect,
+ clip_rect,
+ flags: PrimitiveFlags::default(),
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/lib.rs b/gfx/wr/webrender/src/lib.rs
new file mode 100644
index 0000000000..17dc0f5707
--- /dev/null
+++ b/gfx/wr/webrender/src/lib.rs
@@ -0,0 +1,236 @@
+/* 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/. */
+
+/*!
+A GPU based renderer for the web.
+
+It serves as an experimental render backend for [Servo](https://servo.org/),
+but it can also be used as such in a standalone application.
+
+# External dependencies
+WebRender currently depends on [FreeType](https://www.freetype.org/)
+
+# Api Structure
+The main entry point to WebRender is the [`crate::Renderer`].
+
+By calling [`Renderer::new(...)`](crate::Renderer::new) you get a [`Renderer`], as well as
+a [`RenderApiSender`](api::RenderApiSender). Your [`Renderer`] is responsible to render the
+previously processed frames onto the screen.
+
+By calling [`yourRenderApiSender.create_api()`](api::RenderApiSender::create_api), you'll
+get a [`RenderApi`](api::RenderApi) instance, which is responsible for managing resources
+and documents. A worker thread is used internally to untie the workload from the application
+thread and therefore be able to make better use of multicore systems.
+
+## Frame
+
+What is referred to as a `frame`, is the current geometry on the screen.
+A new Frame is created by calling [`set_display_list()`](api::Transaction::set_display_list)
+on the [`RenderApi`](api::RenderApi). When the geometry is processed, the application will be
+informed via a [`RenderNotifier`](api::RenderNotifier), a callback which you pass to
+[`Renderer::new`].
+More information about [stacking contexts][stacking_contexts].
+
+[`set_display_list()`](api::Transaction::set_display_list) also needs to be supplied with
+[`BuiltDisplayList`](api::BuiltDisplayList)s. These are obtained by finalizing a
+[`DisplayListBuilder`](api::DisplayListBuilder). These are used to draw your geometry. But it
+doesn't only contain trivial geometry, it can also store another
+[`StackingContext`](api::StackingContext), as they're nestable.
+
+[stacking_contexts]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
+*/
+
+#![cfg_attr(feature = "cargo-clippy", allow(clippy::unreadable_literal, clippy::new_without_default, clippy::too_many_arguments))]
+
+
+// Cribbed from the |matches| crate, for simplicity.
+macro_rules! matches {
+ ($expression:expr, $($pattern:tt)+) => {
+ match $expression {
+ $($pattern)+ => true,
+ _ => false
+ }
+ }
+}
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate cfg_if;
+#[macro_use]
+extern crate cstr;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate malloc_size_of_derive;
+#[cfg(any(feature = "serde"))]
+#[macro_use]
+extern crate serde;
+#[macro_use]
+extern crate tracy_rs;
+#[macro_use]
+extern crate derive_more;
+extern crate malloc_size_of;
+extern crate svg_fmt;
+
+#[macro_use]
+mod profiler;
+
+mod batch;
+mod border;
+mod box_shadow;
+#[cfg(any(feature = "capture", feature = "replay"))]
+mod capture;
+mod clip;
+mod space;
+mod spatial_tree;
+mod composite;
+mod debug_colors;
+mod debug_font_data;
+mod debug_item;
+#[cfg(feature = "debugger")]
+mod debug_server;
+mod device;
+mod ellipse;
+mod filterdata;
+mod frame_builder;
+mod frame_graph;
+mod freelist;
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+mod gamma_lut;
+mod glyph_cache;
+mod glyph_rasterizer;
+mod gpu_cache;
+mod gpu_types;
+mod hit_test;
+mod internal_types;
+mod lru_cache;
+mod picture;
+mod prepare;
+mod prim_store;
+mod print_tree;
+mod render_backend;
+mod render_target;
+mod render_task_graph;
+mod render_task_cache;
+mod render_task;
+mod renderer;
+mod resource_cache;
+mod scene;
+mod scene_builder_thread;
+mod scene_building;
+mod screen_capture;
+mod segment;
+mod spatial_node;
+mod texture_pack;
+mod texture_cache;
+mod tile_cache;
+mod util;
+mod visibility;
+mod api_resources;
+mod image_tiling;
+///
+pub mod intern;
+///
+pub mod render_api;
+
+mod shader_source {
+ include!(concat!(env!("OUT_DIR"), "/shaders.rs"));
+}
+
+mod platform {
+ #[cfg(target_os = "macos")]
+ pub use crate::platform::macos::font;
+ #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))]
+ pub use crate::platform::unix::font;
+ #[cfg(target_os = "windows")]
+ pub use crate::platform::windows::font;
+
+ #[cfg(target_os = "macos")]
+ pub mod macos {
+ pub mod font;
+ }
+ #[cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))]
+ pub mod unix {
+ pub mod font;
+ }
+ #[cfg(target_os = "windows")]
+ pub mod windows {
+ pub mod font;
+ }
+}
+
+#[cfg(target_os = "macos")]
+extern crate core_foundation;
+#[cfg(target_os = "macos")]
+extern crate core_graphics;
+#[cfg(target_os = "macos")]
+extern crate core_text;
+
+#[cfg(all(unix, not(target_os = "macos")))]
+extern crate freetype;
+#[cfg(all(unix, not(target_os = "macos")))]
+extern crate libc;
+
+#[cfg(target_os = "windows")]
+extern crate dwrote;
+
+extern crate bincode;
+extern crate byteorder;
+pub extern crate euclid;
+extern crate fxhash;
+extern crate gleam;
+extern crate num_traits;
+extern crate plane_split;
+extern crate rayon;
+#[cfg(feature = "ron")]
+extern crate ron;
+#[cfg(feature = "debugger")]
+extern crate serde_json;
+#[macro_use]
+extern crate smallvec;
+extern crate time;
+#[cfg(feature = "debugger")]
+extern crate ws;
+#[cfg(feature = "debugger")]
+extern crate image_loader;
+#[cfg(feature = "debugger")]
+extern crate base64;
+#[cfg(all(feature = "capture", feature = "png"))]
+extern crate png;
+#[cfg(test)]
+extern crate rand;
+
+pub extern crate api;
+extern crate webrender_build;
+
+#[doc(hidden)]
+pub use crate::composite::{CompositorConfig, Compositor, CompositorCapabilities, CompositorSurfaceTransform};
+pub use crate::composite::{NativeSurfaceId, NativeTileId, NativeSurfaceInfo, PartialPresentCompositor};
+pub use crate::device::{UploadMethod, VertexUsageHint, get_gl_target, get_unoptimized_shader_source};
+pub use crate::device::{ProgramBinary, ProgramCache, ProgramCacheObserver, FormatDesc};
+pub use crate::device::Device;
+pub use crate::frame_builder::ChasePrimitive;
+pub use crate::prim_store::PrimitiveDebugId;
+pub use crate::profiler::{ProfilerHooks, set_profiler_hooks};
+pub use crate::renderer::{
+ AsyncPropertySampler, CpuProfile, DebugFlags, GpuProfile, GraphicsApi,
+ GraphicsApiInfo, PipelineInfo, Renderer, RendererError, RendererOptions, RenderResults,
+ RendererStats, SceneBuilderHooks, Shaders, SharedShaders, ThreadListener, ShaderPrecacheFlags,
+ MAX_VERTEX_TEXTURE_WIDTH, ONE_TIME_USAGE_HINT,
+};
+pub use crate::hit_test::SharedHitTester;
+pub use crate::internal_types::FastHashMap;
+pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle};
+pub use crate::texture_cache::TextureCacheConfig;
+pub use api as webrender_api;
+pub use webrender_build::shader::ProgramSourceDigest;
+pub use crate::picture::{TileDescriptor, TileId, InvalidationReason};
+pub use crate::picture::{PrimitiveCompareResult, PrimitiveCompareResultDetail, CompareHelperResult};
+pub use crate::picture::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset, TileCacheLoggerUpdateLists};
+pub use crate::intern::ItemUid;
+pub use crate::render_api::*;
+pub use crate::tile_cache::{PictureCacheDebugInfo, DirtyTileDebugInfo, TileDebugInfo, SliceDebugInfo};
diff --git a/gfx/wr/webrender/src/lru_cache.rs b/gfx/wr/webrender/src/lru_cache.rs
new file mode 100644
index 0000000000..a71302ff04
--- /dev/null
+++ b/gfx/wr/webrender/src/lru_cache.rs
@@ -0,0 +1,665 @@
+/* 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 crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use std::{mem, num};
+
+/*
+ This module implements a least recently used cache structure, which is
+ used by the texture cache to manage the lifetime of items inside the
+ texture cache. It has a few special pieces of functionality that the
+ texture cache requires, but should be usable as a general LRU cache
+ type if useful in other areas.
+
+ The cache is implemented with two types of backing freelists. These allow
+ random access to the underlying data, while being efficient in both
+ memory access and allocation patterns.
+
+ The "entries" freelist stores the elements being cached (for example, the
+ CacheEntry structure for the texture cache). These elements are stored
+ in arbitrary order, reusing empty slots in the freelist where possible.
+
+ The "lru_index" freelists store the LRU tracking information. Although the
+ tracking elements are stored in arbitrary order inside a freelist for
+ efficiency, they use next/prev links to represent a doubly-linked list,
+ kept sorted in order of recent use. The next link is also used to store
+ the current freelist within the array when the element is not occupied.
+
+ The LRU cache allows having multiple LRU "partitions". Every entry is tracked
+ by exactly one partition at any time; all partitions refer to entries in the
+ shared freelist. Entries can move between partitions, if replace_or_insert is
+ called with a new partition index for an existing handle.
+ The partitioning is used by the texture cache so that, for example, allocating
+ more glyph entries does not cause eviction of image entries (which go into
+ a different shared texture). If an existing handle's entry is reallocated with
+ a new size, it might need to move from a shared texture to a standalone
+ texture; in this case the handle will move to a different LRU partition.
+ */
+
+/// Stores the data supplied by the user to be cached, and an index
+/// into the LRU tracking freelist for this element.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct LRUCacheEntry<T> {
+ /// The LRU partition that tracks this entry.
+ partition_index: u8,
+
+ /// The location of the LRU tracking element for this cache entry in the
+ /// right LRU partition.
+ lru_index: ItemIndex,
+
+ /// The cached data provided by the caller for this element.
+ value: T,
+}
+
+/// The main public interface to the LRU cache
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct LRUCache<T, M> {
+ /// A free list of cache entries, and indices into the LRU tracking list
+ entries: FreeList<LRUCacheEntry<T>, M>,
+ /// The LRU tracking list, allowing O(1) access to the oldest element
+ lru: Vec<LRUTracker<FreeListHandle<M>>>,
+}
+
+impl<T, M> LRUCache<T, M> {
+ /// Construct a new LRU cache
+ pub fn new(lru_partition_count: usize) -> Self {
+ assert!(lru_partition_count <= u8::MAX as usize + 1);
+ LRUCache {
+ entries: FreeList::new(),
+ lru: (0..lru_partition_count).map(|_| LRUTracker::new()).collect(),
+ }
+ }
+
+ /// Insert a new element into the cache. Returns a weak handle for callers to
+ /// access the data, since the lifetime is managed by the LRU algorithm and it
+ /// may be evicted at any time.
+ pub fn push_new(
+ &mut self,
+ partition_index: u8,
+ value: T,
+ ) -> WeakFreeListHandle<M> {
+ // It's a slightly awkward process to insert an element, since we don't know
+ // the index of the LRU tracking element until we've got a handle for the
+ // underlying cached data.
+
+ // Insert the data provided by the caller
+ let handle = self.entries.insert(LRUCacheEntry {
+ partition_index: 0,
+ lru_index: ItemIndex(num::NonZeroU32::new(1).unwrap()),
+ value
+ });
+
+ // Get a weak handle to return to the caller
+ let weak_handle = handle.weak();
+
+ // Add an LRU tracking node that owns the strong handle, and store the location
+ // of this inside the cache entry.
+ let entry = self.entries.get_mut(&handle);
+ let lru_index = self.lru[partition_index as usize].push_new(handle);
+ entry.partition_index = partition_index;
+ entry.lru_index = lru_index;
+
+ weak_handle
+ }
+
+ /// Get immutable access to the data at a given slot. Since this takes a weak
+ /// handle, it may have been evicted, so returns an Option.
+ pub fn get_opt(
+ &self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&T> {
+ self.entries
+ .get_opt(handle)
+ .map(|entry| {
+ &entry.value
+ })
+ }
+
+ /// Get mutable access to the data at a given slot. Since this takes a weak
+ /// handle, it may have been evicted, so returns an Option.
+ pub fn get_opt_mut(
+ &mut self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&mut T> {
+ self.entries
+ .get_opt_mut(handle)
+ .map(|entry| {
+ &mut entry.value
+ })
+ }
+
+ /// Return a reference to the oldest item in the cache, keeping it in the cache.
+ /// If the cache is empty, this will return None.
+ pub fn peek_oldest(&self, partition_index: u8) -> Option<&T> {
+ self.lru[partition_index as usize]
+ .peek_front()
+ .map(|handle| {
+ let entry = self.entries.get(handle);
+ &entry.value
+ })
+ }
+
+ /// Remove the oldest item from the cache. This is used to select elements to
+ /// be evicted. If the cache is empty, this will return None.
+ pub fn pop_oldest(
+ &mut self,
+ partition_index: u8,
+ ) -> Option<T> {
+ self.lru[partition_index as usize]
+ .pop_front()
+ .map(|handle| {
+ let entry = self.entries.free(handle);
+ entry.value
+ })
+ }
+
+ /// This is a special case of `push_new`, which is a requirement for the texture
+ /// cache. Sometimes, we want to replace the content of an existing handle if it
+ /// exists, or insert a new element if the handle is invalid (for example, if an
+ /// image is resized and it moves to a new location in the texture atlas). This
+ /// method returns the old cache entry if it existed, so it can be freed by the caller.
+ #[must_use]
+ pub fn replace_or_insert(
+ &mut self,
+ handle: &mut WeakFreeListHandle<M>,
+ partition_index: u8,
+ data: T,
+ ) -> Option<T> {
+ match self.entries.get_opt_mut(handle) {
+ Some(entry) => {
+ if entry.partition_index != partition_index {
+ // Move to a different partition.
+ let strong_handle = self.lru[entry.partition_index as usize].remove(entry.lru_index);
+ let lru_index = self.lru[partition_index as usize].push_new(strong_handle);
+ entry.partition_index = partition_index;
+ entry.lru_index = lru_index;
+ }
+ Some(mem::replace(&mut entry.value, data))
+ }
+ None => {
+ *handle = self.push_new(partition_index, data);
+ None
+ }
+ }
+ }
+
+ /// This is used by the calling code to signal that the element that this handle
+ /// references has been used on this frame. Internally, it updates the links in
+ /// the LRU tracking element to move this item to the end of the LRU list. Returns
+ /// the underlying data in case the client wants to mutate it.
+ pub fn touch(
+ &mut self,
+ handle: &WeakFreeListHandle<M>,
+ ) -> Option<&mut T> {
+ let lru = &mut self.lru;
+
+ self.entries
+ .get_opt_mut(handle)
+ .map(|entry| {
+ lru[entry.partition_index as usize].mark_used(entry.lru_index);
+ &mut entry.value
+ })
+ }
+
+ /// Try to validate that the state of the cache is consistent
+ #[cfg(test)]
+ fn validate(&self) {
+ for lru in &self.lru {
+ lru.validate();
+ }
+ }
+}
+
+/// Index of an LRU tracking element
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, MallocSizeOf)]
+struct ItemIndex(num::NonZeroU32);
+
+impl ItemIndex {
+ fn as_usize(&self) -> usize {
+ self.0.get() as usize
+ }
+}
+
+/// Stores a strong handle controlling the lifetime of the data in the LRU
+/// cache, and a doubly-linked list node specifying where in the current LRU
+/// order this element exists. These items are themselves backed by a freelist
+/// to minimize heap allocations and improve cache access patterns.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+struct Item<H> {
+ prev: Option<ItemIndex>,
+ next: Option<ItemIndex>,
+ handle: Option<H>,
+}
+
+/// Internal implementation of the LRU tracking list
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+struct LRUTracker<H> {
+ /// Current head of the list - this is the oldest item that will be evicted next.
+ head: Option<ItemIndex>,
+ /// Current tail of the list - this is the most recently used element.
+ tail: Option<ItemIndex>,
+ /// As tracking items are removed, they are stored in a freelist, to minimize heap allocations
+ free_list_head: Option<ItemIndex>,
+ /// The freelist that stores all the LRU tracking items
+ items: Vec<Item<H>>,
+}
+
+impl<H> LRUTracker<H> where H: std::fmt::Debug {
+ /// Construct a new LRU tracker
+ fn new() -> Self {
+ // Push a dummy entry in the vec that is never used. This ensures the NonZeroU32
+ // property is respected, and we never create an ItemIndex(0).
+ let items = vec![
+ Item {
+ prev: None,
+ next: None,
+ handle: None,
+ },
+ ];
+
+ LRUTracker {
+ head: None,
+ tail: None,
+ free_list_head: None,
+ items,
+ }
+ }
+
+ /// Internal function that takes an item index, and links it to the
+ /// end of the tracker list (makes it the newest item).
+ fn link_as_new_tail(
+ &mut self,
+ item_index: ItemIndex,
+ ) {
+ match (self.head, self.tail) {
+ (Some(..), Some(tail)) => {
+ // Both a head and a tail
+ self.items[item_index.as_usize()].prev = Some(tail);
+ self.items[item_index.as_usize()].next = None;
+
+ self.items[tail.as_usize()].next = Some(item_index);
+ self.tail = Some(item_index);
+ }
+ (None, None) => {
+ // No head/tail, currently empty list
+ self.items[item_index.as_usize()].prev = None;
+ self.items[item_index.as_usize()].next = None;
+
+ self.head = Some(item_index);
+ self.tail = Some(item_index);
+ }
+ (Some(..), None) | (None, Some(..)) => {
+ // Invalid state
+ unreachable!();
+ }
+ }
+ }
+
+ /// Internal function that takes an LRU item index, and removes it from
+ /// the current doubly linked list. Used during removal of items, and also
+ /// when items are moved to the back of the list as they're touched.
+ fn unlink(
+ &mut self,
+ item_index: ItemIndex,
+ ) {
+ let (next, prev) = {
+ let item = &self.items[item_index.as_usize()];
+ (item.next, item.prev)
+ };
+
+ match next {
+ Some(next) => {
+ self.items[next.as_usize()].prev = prev;
+ }
+ None => {
+ debug_assert_eq!(self.tail, Some(item_index));
+ self.tail = prev;
+ }
+ }
+
+ match prev {
+ Some(prev) => {
+ self.items[prev.as_usize()].next = next;
+ }
+ None => {
+ debug_assert_eq!(self.head, Some(item_index));
+ self.head = next;
+ }
+ }
+ }
+
+ /// Push a new LRU tracking item on to the back of the list, marking
+ /// it as the most recent item.
+ fn push_new(
+ &mut self,
+ handle: H,
+ ) -> ItemIndex {
+ // See if there is a slot available in the current free list
+ let item_index = match self.free_list_head {
+ Some(index) => {
+ // Reuse an existing slot
+ let item = &mut self.items[index.as_usize()];
+
+ assert!(item.handle.is_none());
+ item.handle = Some(handle);
+
+ self.free_list_head = item.next;
+
+ index
+ }
+ None => {
+ // No free slots available, push to the end of the array
+ let index = ItemIndex(num::NonZeroU32::new(self.items.len() as u32).unwrap());
+
+ self.items.push(Item {
+ prev: None,
+ next: None,
+ handle: Some(handle),
+ });
+
+ index
+ }
+ };
+
+ // Now link this element into the LRU list
+ self.link_as_new_tail(item_index);
+
+ item_index
+ }
+
+ /// Returns a reference to the oldest element, or None if the list is empty.
+ fn peek_front(&self) -> Option<&H> {
+ self.head.map(|head| self.items[head.as_usize()].handle.as_ref().unwrap())
+ }
+
+ /// Remove the oldest element from the front of the LRU list. Returns None
+ /// if the list is empty.
+ fn pop_front(
+ &mut self,
+ ) -> Option<H> {
+ let handle = match (self.head, self.tail) {
+ (Some(head), Some(tail)) => {
+ let item_index = head;
+
+ // Head and tail are the same - removing the only element
+ if head == tail {
+ self.head = None;
+ self.tail = None;
+ } else {
+ // Update the head of the list, popping the first element off
+ let new_head = self.items[head.as_usize()].next.unwrap();
+ self.head = Some(new_head);
+ self.items[new_head.as_usize()].prev = None;
+ }
+
+ // Add this item to the freelist for later use
+ self.items[item_index.as_usize()].next = self.free_list_head;
+ self.free_list_head = Some(item_index);
+
+ // Return the handle to the user
+ Some(self.items[item_index.as_usize()].handle.take().unwrap())
+ }
+ (None, None) => {
+ // List is empty
+ None
+ }
+ (Some(..), None) | (None, Some(..)) => {
+ // Invalid state
+ unreachable!();
+ }
+ };
+
+ handle
+ }
+
+ /// Manually remove an item from the LRU tracking list. This is used
+ /// when an element switches from one LRU partition to a different one.
+ fn remove(
+ &mut self,
+ index: ItemIndex,
+ ) -> H {
+ // Remove from the LRU list
+ self.unlink(index);
+
+ let handle = self.items[index.as_usize()].handle.take().unwrap();
+
+ // Add LRU item to the freelist for future use.
+ self.items[index.as_usize()].next = self.free_list_head;
+ self.free_list_head = Some(index);
+
+ handle
+ }
+
+ /// Called to mark that an item was used on this frame. It unlinks the
+ /// tracking item, and then re-links it to the back of the list.
+ fn mark_used(
+ &mut self,
+ index: ItemIndex,
+ ) {
+ self.unlink(index);
+ self.link_as_new_tail(index);
+ }
+
+ /// Try to validate that the state of the linked lists are consistent
+ #[cfg(test)]
+ fn validate(&self) {
+ use std::collections::HashSet;
+
+ // Must have a valid head/tail or be empty
+ assert!((self.head.is_none() && self.tail.is_none()) || (self.head.is_some() && self.tail.is_some()));
+
+ // If there is a head, the prev of the head must be none
+ if let Some(head) = self.head {
+ assert!(self.items[head.as_usize()].prev.is_none());
+ }
+
+ // If there is a tail, the next of the tail must be none
+ if let Some(tail) = self.tail {
+ assert!(self.items[tail.as_usize()].next.is_none());
+ }
+
+ // Collect all free and valid items, both in forwards and reverse order
+ let mut free_items = Vec::new();
+ let mut free_items_set = HashSet::new();
+ let mut valid_items_front = Vec::new();
+ let mut valid_items_front_set = HashSet::new();
+ let mut valid_items_reverse = Vec::new();
+ let mut valid_items_reverse_set = HashSet::new();
+
+ let mut current = self.free_list_head;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ free_items.push(index);
+ assert!(free_items_set.insert(index));
+ current = item.next;
+ }
+
+ current = self.head;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ valid_items_front.push(index);
+ assert!(valid_items_front_set.insert(index));
+ current = item.next;
+ }
+
+ current = self.tail;
+ while let Some(index) = current {
+ let item = &self.items[index.as_usize()];
+ valid_items_reverse.push(index);
+ assert!(!valid_items_reverse_set.contains(&index));
+ valid_items_reverse_set.insert(index);
+ current = item.prev;
+ }
+
+ // Ensure set lengths match the vec lengths (should be enforced by the assert check during insert anyway)
+ assert_eq!(valid_items_front.len(), valid_items_front_set.len());
+ assert_eq!(valid_items_reverse.len(), valid_items_reverse_set.len());
+
+ // Length of the array should equal free + valid items count + 1 (dummy entry)
+ assert_eq!(free_items.len() + valid_items_front.len() + 1, self.items.len());
+
+ // Should be same number of items whether iterating forwards or reverse
+ assert_eq!(valid_items_front.len(), valid_items_reverse.len());
+
+ // Ensure there are no items considered in the free list that are also in the valid list
+ assert!(free_items_set.intersection(&valid_items_reverse_set).collect::<HashSet<_>>().is_empty());
+ assert!(free_items_set.intersection(&valid_items_front_set).collect::<HashSet<_>>().is_empty());
+
+ // Should be the same number of items regardless of iteration direction
+ assert_eq!(valid_items_front_set.len(), valid_items_reverse_set.len());
+
+ // Ensure that the ordering is exactly the same, regardless of iteration direction
+ for (i0, i1) in valid_items_front.iter().zip(valid_items_reverse.iter().rev()) {
+ assert_eq!(i0, i1);
+ }
+ }
+}
+
+#[test]
+fn test_lru_tracker_push_peek() {
+ // Push elements, peek and ensure:
+ // - peek_oldest returns None before first element pushed
+ // - peek_oldest returns oldest element
+ // - subsequent calls to peek_oldest return same element (nothing was removed)
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ cache.validate();
+
+ assert_eq!(cache.peek_oldest(0), None);
+
+ for i in 0 .. NUM_ELEMENTS {
+ cache.push_new(0, i);
+ }
+ cache.validate();
+
+ assert_eq!(cache.peek_oldest(0), Some(&0));
+ assert_eq!(cache.peek_oldest(0), Some(&0));
+
+ cache.pop_oldest(0);
+ assert_eq!(cache.peek_oldest(0), Some(&1));
+}
+
+#[test]
+fn test_lru_tracker_push_pop() {
+ // Push elements, pop them all off and ensure:
+ // - Returned in oldest order
+ // - pop_oldest returns None after last element popped
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ cache.push_new(0, i);
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ assert_eq!(cache.pop_oldest(0), Some(i));
+ }
+ cache.validate();
+
+ assert_eq!(cache.pop_oldest(0), None);
+}
+
+#[test]
+fn test_lru_tracker_push_touch_pop() {
+ // Push elements, touch even handles, pop them all off and ensure:
+ // - Returned in correct order
+ // - pop_oldest returns None after last element popped
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ cache.touch(&handles[i*2]);
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert_eq!(cache.pop_oldest(0), Some(i*2+1));
+ }
+ cache.validate();
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert_eq!(cache.pop_oldest(0), Some(i*2));
+ }
+ cache.validate();
+
+ assert_eq!(cache.pop_oldest(0), None);
+}
+
+#[test]
+fn test_lru_tracker_push_get() {
+ // Push elements, ensure:
+ // - get access via weak handles works
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert!(cache.get_opt(&handles[i]) == Some(&i));
+ }
+ cache.validate();
+}
+
+#[test]
+fn test_lru_tracker_push_replace_get() {
+ // Push elements, replace contents, ensure:
+ // - each element was replaced with new data correctly
+ // - replace_or_insert works for invalid handles
+ struct CacheMarker;
+ const NUM_ELEMENTS: usize = 50;
+
+ let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
+ let mut handles = Vec::new();
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ handles.push(cache.push_new(0, i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS {
+ assert_eq!(cache.replace_or_insert(&mut handles[i], 0, i * 2), Some(i));
+ }
+ cache.validate();
+
+ for i in 0 .. NUM_ELEMENTS/2 {
+ assert!(cache.get_opt(&handles[i]) == Some(&(i * 2)));
+ }
+ cache.validate();
+
+ let mut empty_handle = WeakFreeListHandle::invalid();
+ assert_eq!(cache.replace_or_insert(&mut empty_handle, 0, 100), None);
+ assert_eq!(cache.get_opt(&empty_handle), Some(&100));
+}
diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs
new file mode 100644
index 0000000000..f71cc57a43
--- /dev/null
+++ b/gfx/wr/webrender/src/picture.rs
@@ -0,0 +1,7192 @@
+/* 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/. */
+
+//! A picture represents a dynamically rendered image.
+//!
+//! # Overview
+//!
+//! Pictures consists of:
+//!
+//! - A number of primitives that are drawn onto the picture.
+//! - A composite operation describing how to composite this
+//! picture into its parent.
+//! - A configuration describing how to draw the primitives on
+//! this picture (e.g. in screen space or local space).
+//!
+//! The tree of pictures are generated during scene building.
+//!
+//! Depending on their composite operations pictures can be rendered into
+//! intermediate targets or folded into their parent picture.
+//!
+//! ## Picture caching
+//!
+//! Pictures can be cached to reduce the amount of rasterization happening per
+//! frame.
+//!
+//! When picture caching is enabled, the scene is cut into a small number of slices,
+//! typically:
+//!
+//! - content slice
+//! - UI slice
+//! - background UI slice which is hidden by the other two slices most of the time.
+//!
+//! Each of these slice is made up of fixed-size large tiles of 2048x512 pixels
+//! (or 128x128 for the UI slice).
+//!
+//! Tiles can be either cached rasterized content into a texture or "clear tiles"
+//! that contain only a solid color rectangle rendered directly during the composite
+//! pass.
+//!
+//! ## Invalidation
+//!
+//! Each tile keeps track of the elements that affect it, which can be:
+//!
+//! - primitives
+//! - clips
+//! - image keys
+//! - opacity bindings
+//! - transforms
+//!
+//! These dependency lists are built each frame and compared to the previous frame to
+//! see if the tile changed.
+//!
+//! The tile's primitive dependency information is organized in a quadtree, each node
+//! storing an index buffer of tile primitive dependencies.
+//!
+//! The union of the invalidated leaves of each quadtree produces a per-tile dirty rect
+//! which defines the scissor rect used when replaying the tile's drawing commands and
+//! can be used for partial present.
+//!
+//! ## Display List shape
+//!
+//! WR will first look for an iframe item in the root stacking context to apply
+//! picture caching to. If that's not found, it will apply to the entire root
+//! stacking context of the display list. Apart from that, the format of the
+//! display list is not important to picture caching. Each time a new scroll root
+//! is encountered, a new picture cache slice will be created. If the display
+//! list contains more than some arbitrary number of slices (currently 8), the
+//! content will all be squashed into a single slice, in order to save GPU memory
+//! and compositing performance.
+//!
+//! ## Compositor Surfaces
+//!
+//! Sometimes, a primitive would prefer to exist as a native compositor surface.
+//! This allows a large and/or regularly changing primitive (such as a video, or
+//! webgl canvas) to be updated each frame without invalidating the content of
+//! tiles, and can provide a significant performance win and battery saving.
+//!
+//! Since drawing a primitive as a compositor surface alters the ordering of
+//! primitives in a tile, we use 'overlay tiles' to ensure correctness. If a
+//! tile has a compositor surface, _and_ that tile has primitives that overlap
+//! the compositor surface rect, the tile switches to be drawn in alpha mode.
+//!
+//! We rely on only promoting compositor surfaces that are opaque primitives.
+//! With this assumption, the tile(s) that intersect the compositor surface get
+//! a 'cutout' in the rectangle where the compositor surface exists (not the
+//! entire tile), allowing that tile to be drawn as an alpha tile after the
+//! compositor surface.
+//!
+//! Tiles are only drawn in overlay mode if there is content that exists on top
+//! of the compositor surface. Otherwise, we can draw the tiles in the normal fast
+//! path before the compositor surface is drawn. Use of the per-tile valid and
+//! dirty rects ensure that we do a minimal amount of per-pixel work here to
+//! blend the overlay tile (this is not always optimal right now, but will be
+//! improved as a follow up).
+
+use api::{MixBlendMode, PremultipliedColorF, FilterPrimitiveKind};
+use api::{PropertyBinding, PropertyBindingId, FilterPrimitive};
+use api::{DebugFlags, ImageKey, ColorF, ColorU, PrimitiveFlags};
+use api::{ImageRendering, ColorDepth, YuvColorSpace, YuvFormat};
+use api::units::*;
+use crate::box_shadow::BLUR_SAMPLE_SCALE;
+use crate::clip::{ClipStore, ClipChainInstance, ClipChainId, ClipInstance};
+use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX,
+ SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
+};
+use crate::composite::{CompositorKind, CompositeState, NativeSurfaceId, NativeTileId};
+use crate::composite::{ExternalSurfaceDescriptor, ExternalSurfaceDependency};
+use crate::debug_colors;
+use euclid::{vec2, vec3, Point2D, Scale, Size2D, Vector2D, Vector3D, Rect, Transform3D, SideOffsets2D};
+use euclid::approxeq::ApproxEq;
+use crate::filterdata::SFilterData;
+use crate::intern::ItemUid;
+use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter, PlaneSplitAnchor, TextureSource};
+use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::{UvRectKind, ZBufferId};
+use plane_split::{Clipper, Polygon, Splitter};
+use crate::prim_store::{PrimitiveTemplateKind, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
+use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveScratchBuffer};
+use crate::print_tree::{PrintTree, PrintTreePrinter};
+use crate::render_backend::{DataStores, FrameId};
+use crate::render_task_graph::RenderTaskId;
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{BlurTask, RenderTask, RenderTaskLocation, BlurTaskCache};
+use crate::render_task::{StaticRenderTaskSurface, RenderTaskKind};
+use crate::resource_cache::{ResourceCache, ImageGeneration};
+use crate::space::{SpaceMapper, SpaceSnapper};
+use crate::scene::SceneProperties;
+use smallvec::SmallVec;
+use std::{mem, u8, marker, u32};
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::collections::hash_map::Entry;
+use std::ops::Range;
+use crate::texture_cache::TextureCacheHandle;
+use crate::util::{MaxRect, VecHelper, MatrixHelpers, Recycler, raster_rect_to_device_pixels, ScaleOffset};
+use crate::filterdata::{FilterDataHandle};
+use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo};
+use crate::visibility::{PrimitiveVisibilityMask, PrimitiveVisibilityFlags, FrameVisibilityContext};
+use crate::visibility::{VisibilityState, FrameVisibilityState};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use ron;
+#[cfg(feature = "capture")]
+use crate::scene_builder_thread::InternerUpdates;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::intern::{Internable, UpdateList};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::clip::ClipIntern;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::filterdata::FilterDataIntern;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use api::PrimitiveKeyKind;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::backdrop::Backdrop;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::image::{Image, YuvImage};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::line_dec::LineDecoration;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::picture::Picture;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::prim_store::text_run::TextRun;
+
+#[cfg(feature = "capture")]
+use std::fs::File;
+#[cfg(feature = "capture")]
+use std::io::prelude::*;
+#[cfg(feature = "capture")]
+use std::path::PathBuf;
+use crate::scene_building::{SliceFlags};
+
+#[cfg(feature = "replay")]
+// used by tileview so don't use an internal_types FastHashMap
+use std::collections::HashMap;
+
+// Maximum blur radius for blur filter (different than box-shadow blur).
+// Taken from FilterNodeSoftware.cpp in Gecko.
+pub const MAX_BLUR_RADIUS: f32 = 100.;
+
+/// Specify whether a surface allows subpixel AA text rendering.
+#[derive(Debug, Clone, PartialEq)]
+pub enum SubpixelMode {
+ /// This surface allows subpixel AA text
+ Allow,
+ /// Subpixel AA text cannot be drawn on this surface
+ Deny,
+ /// Subpixel AA can be drawn on this surface, if not intersecting
+ /// with the excluded regions, and inside the allowed rect.
+ Conditional {
+ allowed_rect: PictureRect,
+ excluded_rects: Vec<PictureRect>,
+ },
+}
+
+/// A comparable transform matrix, that compares with epsilon checks.
+#[derive(Debug, Clone)]
+struct MatrixKey {
+ m: [f32; 16],
+}
+
+impl PartialEq for MatrixKey {
+ fn eq(&self, other: &Self) -> bool {
+ const EPSILON: f32 = 0.001;
+
+ // TODO(gw): It's possible that we may need to adjust the epsilon
+ // to be tighter on most of the matrix, except the
+ // translation parts?
+ for (i, j) in self.m.iter().zip(other.m.iter()) {
+ if !i.approx_eq_eps(j, &EPSILON) {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+/// A comparable / hashable version of a coordinate space mapping. Used to determine
+/// if a transform dependency for a tile has changed.
+#[derive(Debug, PartialEq, Clone)]
+enum TransformKey {
+ Local,
+ ScaleOffset {
+ scale_x: f32,
+ scale_y: f32,
+ offset_x: f32,
+ offset_y: f32,
+ },
+ Transform {
+ m: MatrixKey,
+ }
+}
+
+impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey {
+ fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey {
+ match transform {
+ CoordinateSpaceMapping::Local => {
+ TransformKey::Local
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ TransformKey::ScaleOffset {
+ scale_x: scale_offset.scale.x,
+ scale_y: scale_offset.scale.y,
+ offset_x: scale_offset.offset.x,
+ offset_y: scale_offset.offset.y,
+ }
+ }
+ CoordinateSpaceMapping::Transform(ref m) => {
+ TransformKey::Transform {
+ m: MatrixKey {
+ m: m.to_array(),
+ },
+ }
+ }
+ }
+ }
+}
+
+/// Information about a picture that is pushed / popped on the
+/// PictureUpdateState during picture traversal pass.
+struct PictureInfo {
+ /// The spatial node for this picture.
+ _spatial_node_index: SpatialNodeIndex,
+}
+
+/// Unit for tile coordinates.
+#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub struct TileCoordinate;
+
+// Geometry types for tile coordinates.
+pub type TileOffset = Point2D<i32, TileCoordinate>;
+pub type TileSize = Size2D<i32, TileCoordinate>;
+pub type TileRect = Rect<i32, TileCoordinate>;
+
+/// The maximum number of compositor surfaces that are allowed per picture cache. This
+/// is an arbitrary number that should be enough for common cases, but low enough to
+/// prevent performance and memory usage drastically degrading in pathological cases.
+const MAX_COMPOSITOR_SURFACES: usize = 4;
+
+/// The size in device pixels of a normal cached tile.
+pub const TILE_SIZE_DEFAULT: DeviceIntSize = DeviceIntSize {
+ width: 1024,
+ height: 512,
+ _unit: marker::PhantomData,
+};
+
+/// The size in device pixels of a tile for horizontal scroll bars
+pub const TILE_SIZE_SCROLLBAR_HORIZONTAL: DeviceIntSize = DeviceIntSize {
+ width: 1024,
+ height: 32,
+ _unit: marker::PhantomData,
+};
+
+/// The size in device pixels of a tile for vertical scroll bars
+pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize {
+ width: 32,
+ height: 1024,
+ _unit: marker::PhantomData,
+};
+
+/// The maximum size per axis of a surface,
+/// in WorldPixel coordinates.
+const MAX_SURFACE_SIZE: f32 = 4096.0;
+/// Maximum size of a compositor surface.
+const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0;
+
+/// The maximum number of sub-dependencies (e.g. clips, transforms) we can handle
+/// per-primitive. If a primitive has more than this, it will invalidate every frame.
+const MAX_PRIM_SUB_DEPS: usize = u8::MAX as usize;
+
+/// Used to get unique tile IDs, even when the tile cache is
+/// destroyed between display lists / scenes.
+static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0);
+
+fn clamp(value: i32, low: i32, high: i32) -> i32 {
+ value.max(low).min(high)
+}
+
+fn clampf(value: f32, low: f32, high: f32) -> f32 {
+ value.max(low).min(high)
+}
+
+/// Clamps the blur radius depending on scale factors.
+fn clamp_blur_radius(blur_radius: f32, scale_factors: (f32, f32)) -> f32 {
+ // Clamping must occur after scale factors are applied, but scale factors are not applied
+ // until later on. To clamp the blur radius, we first apply the scale factors and then clamp
+ // and finally revert the scale factors.
+
+ // TODO: the clamping should be done on a per-axis basis, but WR currently only supports
+ // having a single value for both x and y blur.
+ let largest_scale_factor = f32::max(scale_factors.0, scale_factors.1);
+ let scaled_blur_radius = blur_radius * largest_scale_factor;
+
+ if scaled_blur_radius > MAX_BLUR_RADIUS {
+ MAX_BLUR_RADIUS / largest_scale_factor
+ } else {
+ // Return the original blur radius to avoid any rounding errors
+ blur_radius
+ }
+}
+
+/// An index into the prims array in a TileDescriptor.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDependencyIndex(pub u32);
+
+/// Information about the state of a binding.
+#[derive(Debug)]
+pub struct BindingInfo<T> {
+ /// The current value retrieved from dynamic scene properties.
+ value: T,
+ /// True if it was changed (or is new) since the last frame build.
+ changed: bool,
+}
+
+/// Information stored in a tile descriptor for a binding.
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Binding<T> {
+ Value(T),
+ Binding(PropertyBindingId),
+}
+
+impl<T> From<PropertyBinding<T>> for Binding<T> {
+ fn from(binding: PropertyBinding<T>) -> Binding<T> {
+ match binding {
+ PropertyBinding::Binding(key, _) => Binding::Binding(key.id),
+ PropertyBinding::Value(value) => Binding::Value(value),
+ }
+ }
+}
+
+pub type OpacityBinding = Binding<f32>;
+pub type OpacityBindingInfo = BindingInfo<f32>;
+
+pub type ColorBinding = Binding<ColorU>;
+pub type ColorBindingInfo = BindingInfo<ColorU>;
+
+/// A dependency for a transform is defined by the spatial node index + frame it was used
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeKey {
+ spatial_node_index: SpatialNodeIndex,
+ frame_id: FrameId,
+}
+
+/// A helper for comparing spatial nodes between frames. The comparisons
+/// are done by-value, so that if the shape of the spatial node tree
+/// changes, invalidations aren't done simply due to the spatial node
+/// index changing between display lists.
+struct SpatialNodeComparer {
+ /// The root spatial node index of the tile cache
+ ref_spatial_node_index: SpatialNodeIndex,
+ /// Maintains a map of currently active transform keys
+ spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>,
+ /// A cache of recent comparisons between prev and current spatial nodes
+ compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>,
+ /// A set of frames that we need to retain spatial node entries for
+ referenced_frames: FastHashSet<FrameId>,
+}
+
+impl SpatialNodeComparer {
+ /// Construct a new comparer
+ fn new() -> Self {
+ SpatialNodeComparer {
+ ref_spatial_node_index: ROOT_SPATIAL_NODE_INDEX,
+ spatial_nodes: FastHashMap::default(),
+ compare_cache: FastHashMap::default(),
+ referenced_frames: FastHashSet::default(),
+ }
+ }
+
+ /// Advance to the next frame
+ fn next_frame(
+ &mut self,
+ ref_spatial_node_index: SpatialNodeIndex,
+ ) {
+ // Drop any node information for unreferenced frames, to ensure that the
+ // hashmap doesn't grow indefinitely!
+ let referenced_frames = &self.referenced_frames;
+ self.spatial_nodes.retain(|key, _| {
+ referenced_frames.contains(&key.frame_id)
+ });
+
+ // Update the root spatial node for this comparer
+ self.ref_spatial_node_index = ref_spatial_node_index;
+ self.compare_cache.clear();
+ self.referenced_frames.clear();
+ }
+
+ /// Register a transform that is used, and build the transform key for it if new.
+ fn register_used_transform(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ frame_id: FrameId,
+ spatial_tree: &SpatialTree,
+ ) {
+ let key = SpatialNodeKey {
+ spatial_node_index,
+ frame_id,
+ };
+
+ if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) {
+ entry.insert(
+ get_transform_key(
+ spatial_node_index,
+ self.ref_spatial_node_index,
+ spatial_tree,
+ )
+ );
+ }
+ }
+
+ /// Return true if the transforms for two given spatial nodes are considered equivalent
+ fn are_transforms_equivalent(
+ &mut self,
+ prev_spatial_node_key: &SpatialNodeKey,
+ curr_spatial_node_key: &SpatialNodeKey,
+ ) -> bool {
+ let key = (*prev_spatial_node_key, *curr_spatial_node_key);
+ let spatial_nodes = &self.spatial_nodes;
+
+ *self.compare_cache
+ .entry(key)
+ .or_insert_with(|| {
+ let prev = &spatial_nodes[&prev_spatial_node_key];
+ let curr = &spatial_nodes[&curr_spatial_node_key];
+ curr == prev
+ })
+ }
+
+ /// Ensure that the comparer won't GC any nodes for a given frame id
+ fn retain_for_frame(&mut self, frame_id: FrameId) {
+ self.referenced_frames.insert(frame_id);
+ }
+}
+
+// Immutable context passed to picture cache tiles during pre_update
+struct TilePreUpdateContext {
+ /// Maps from picture cache coords -> world space coords.
+ pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
+
+ /// The fractional position of the picture cache, which may
+ /// require invalidation of all tiles.
+ fract_offset: PictureVector2D,
+ device_fract_offset: DeviceVector2D,
+
+ /// The optional background color of the picture cache instance
+ background_color: Option<ColorF>,
+
+ /// The visible part of the screen in world coords.
+ global_screen_world_rect: WorldRect,
+
+ /// Current size of tiles in picture units.
+ tile_size: PictureSize,
+
+ /// The current frame id for this picture cache
+ frame_id: FrameId,
+}
+
+// Immutable context passed to picture cache tiles during post_update
+struct TilePostUpdateContext<'a> {
+ /// Maps from picture cache coords -> world space coords.
+ pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
+
+ /// Global scale factor from world -> device pixels.
+ global_device_pixel_scale: DevicePixelScale,
+
+ /// The local clip rect (in picture space) of the entire picture cache
+ local_clip_rect: PictureRect,
+
+ /// The calculated backdrop information for this cache instance.
+ backdrop: BackdropInfo,
+
+ /// Information about opacity bindings from the picture cache.
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+
+ /// Information about color bindings from the picture cache.
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+
+ /// Current size in device pixels of tiles for this cache
+ current_tile_size: DeviceIntSize,
+
+ /// The local rect of the overall picture cache
+ local_rect: PictureRect,
+
+ /// A list of the external surfaces that are present on this slice
+ external_surfaces: &'a [ExternalSurfaceDescriptor],
+
+ /// Pre-allocated z-id to assign to opaque tiles during post_update. We
+ /// use a different z-id for opaque/alpha tiles, so that compositor
+ /// surfaces (such as videos) can have a z-id between these values,
+ /// which allows compositor surfaces to occlude opaque tiles, but not
+ /// alpha tiles.
+ z_id_opaque: ZBufferId,
+
+ /// Pre-allocated z-id to assign to alpha tiles during post_update
+ z_id_alpha: ZBufferId,
+
+ /// If true, the scale factor of the root transform for this picture
+ /// cache changed, so we need to invalidate the tile and re-render.
+ invalidate_all: bool,
+}
+
+// Mutable state passed to picture cache tiles during post_update
+struct TilePostUpdateState<'a> {
+ /// Allow access to the texture cache for requesting tiles
+ resource_cache: &'a mut ResourceCache,
+
+ /// Current configuration and setup for compositing all the picture cache tiles in renderer.
+ composite_state: &'a mut CompositeState,
+
+ /// A cache of comparison results to avoid re-computation during invalidation.
+ compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+
+ /// Information about transform node differences from last frame.
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+}
+
+/// Information about the dependencies of a single primitive instance.
+struct PrimitiveDependencyInfo {
+ /// Unique content identifier of the primitive.
+ prim_uid: ItemUid,
+
+ /// The (conservative) clipped area in picture space this primitive occupies.
+ prim_clip_box: PictureBox2D,
+
+ /// Image keys this primitive depends on.
+ images: SmallVec<[ImageDependency; 8]>,
+
+ /// Opacity bindings this primitive depends on.
+ opacity_bindings: SmallVec<[OpacityBinding; 4]>,
+
+ /// Color binding this primitive depends on.
+ color_binding: Option<ColorBinding>,
+
+ /// Clips that this primitive depends on.
+ clips: SmallVec<[ItemUid; 8]>,
+
+ /// Spatial nodes references by the clip dependencies of this primitive.
+ spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>,
+
+ /// If true, this primitive has been promoted to be a compositor surface.
+ is_compositor_surface: bool,
+}
+
+impl PrimitiveDependencyInfo {
+ /// Construct dependency info for a new primitive.
+ fn new(
+ prim_uid: ItemUid,
+ prim_clip_box: PictureBox2D,
+ ) -> Self {
+ PrimitiveDependencyInfo {
+ prim_uid,
+ images: SmallVec::new(),
+ opacity_bindings: SmallVec::new(),
+ color_binding: None,
+ prim_clip_box,
+ clips: SmallVec::new(),
+ spatial_nodes: SmallVec::new(),
+ is_compositor_surface: false,
+ }
+ }
+}
+
+/// A stable ID for a given tile, to help debugging. These are also used
+/// as unique identifiers for tile surfaces when using a native compositor.
+#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileId(pub usize);
+
+/// A descriptor for the kind of texture that a picture cache tile will
+/// be drawn into.
+#[derive(Debug)]
+pub enum SurfaceTextureDescriptor {
+ /// When using the WR compositor, the tile is drawn into an entry
+ /// in the WR texture cache.
+ TextureCache {
+ handle: TextureCacheHandle
+ },
+ /// When using an OS compositor, the tile is drawn into a native
+ /// surface identified by arbitrary id.
+ Native {
+ /// The arbitrary id of this tile.
+ id: Option<NativeTileId>,
+ },
+}
+
+/// This is the same as a `SurfaceTextureDescriptor` but has been resolved
+/// into a texture cache handle (if appropriate) that can be used by the
+/// batching and compositing code in the renderer.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ResolvedSurfaceTexture {
+ TextureCache {
+ /// The texture ID to draw to.
+ texture: TextureSource,
+ /// Slice index in the texture array to draw to.
+ layer: i32,
+ },
+ Native {
+ /// The arbitrary id of this tile.
+ id: NativeTileId,
+ /// The size of the tile in device pixels.
+ size: DeviceIntSize,
+ }
+}
+
+impl SurfaceTextureDescriptor {
+ /// Create a resolved surface texture for this descriptor
+ pub fn resolve(
+ &self,
+ resource_cache: &ResourceCache,
+ size: DeviceIntSize,
+ ) -> ResolvedSurfaceTexture {
+ match self {
+ SurfaceTextureDescriptor::TextureCache { handle } => {
+ let cache_item = resource_cache.texture_cache.get(handle);
+
+ ResolvedSurfaceTexture::TextureCache {
+ texture: cache_item.texture_id,
+ layer: cache_item.texture_layer,
+ }
+ }
+ SurfaceTextureDescriptor::Native { id } => {
+ ResolvedSurfaceTexture::Native {
+ id: id.expect("bug: native surface not allocated"),
+ size,
+ }
+ }
+ }
+ }
+}
+
+/// The backing surface for this tile.
+#[derive(Debug)]
+pub enum TileSurface {
+ Texture {
+ /// Descriptor for the surface that this tile draws into.
+ descriptor: SurfaceTextureDescriptor,
+ /// Bitfield specifying the dirty region(s) that are relevant to this tile.
+ visibility_mask: PrimitiveVisibilityMask,
+ },
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+impl TileSurface {
+ fn kind(&self) -> &'static str {
+ match *self {
+ TileSurface::Color { .. } => "Color",
+ TileSurface::Texture { .. } => "Texture",
+ TileSurface::Clear => "Clear",
+ }
+ }
+}
+
+/// Optional extra information returned by is_same when
+/// logging is enabled.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum CompareHelperResult<T> {
+ /// Primitives match
+ Equal,
+ /// Counts differ
+ Count {
+ prev_count: u8,
+ curr_count: u8,
+ },
+ /// Sentinel
+ Sentinel,
+ /// Two items are not equal
+ NotEqual {
+ prev: T,
+ curr: T,
+ },
+ /// User callback returned true on item
+ PredicateTrue {
+ curr: T
+ },
+}
+
+/// The result of a primitive dependency comparison. Size is a u8
+/// since this is a hot path in the code, and keeping the data small
+/// is a performance win.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(u8)]
+pub enum PrimitiveCompareResult {
+ /// Primitives match
+ Equal,
+ /// Something in the PrimitiveDescriptor was different
+ Descriptor,
+ /// The clip node content or spatial node changed
+ Clip,
+ /// The value of the transform changed
+ Transform,
+ /// An image dependency was dirty
+ Image,
+ /// The value of an opacity binding changed
+ OpacityBinding,
+ /// The value of a color binding changed
+ ColorBinding,
+}
+
+/// A more detailed version of PrimitiveCompareResult used when
+/// debug logging is enabled.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum PrimitiveCompareResultDetail {
+ /// Primitives match
+ Equal,
+ /// Something in the PrimitiveDescriptor was different
+ Descriptor {
+ old: PrimitiveDescriptor,
+ new: PrimitiveDescriptor,
+ },
+ /// The clip node content or spatial node changed
+ Clip {
+ detail: CompareHelperResult<ItemUid>,
+ },
+ /// The value of the transform changed
+ Transform {
+ detail: CompareHelperResult<SpatialNodeKey>,
+ },
+ /// An image dependency was dirty
+ Image {
+ detail: CompareHelperResult<ImageDependency>,
+ },
+ /// The value of an opacity binding changed
+ OpacityBinding {
+ detail: CompareHelperResult<OpacityBinding>,
+ },
+ /// The value of a color binding changed
+ ColorBinding {
+ detail: CompareHelperResult<ColorBinding>,
+ },
+}
+
+/// Debugging information about why a tile was invalidated
+#[derive(Debug,Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum InvalidationReason {
+ /// The fractional offset changed
+ FractionalOffset {
+ old: DeviceVector2D,
+ new: DeviceVector2D,
+ },
+ /// The background color changed
+ BackgroundColor {
+ old: Option<ColorF>,
+ new: Option<ColorF>,
+ },
+ /// The opaque state of the backing native surface changed
+ SurfaceOpacityChanged{
+ became_opaque: bool
+ },
+ /// There was no backing texture (evicted or never rendered)
+ NoTexture,
+ /// There was no backing native surface (never rendered, or recreated)
+ NoSurface,
+ /// The primitive count in the dependency list was different
+ PrimCount {
+ old: Option<Vec<ItemUid>>,
+ new: Option<Vec<ItemUid>>,
+ },
+ /// The content of one of the primitives was different
+ Content {
+ /// What changed in the primitive that was different
+ prim_compare_result: PrimitiveCompareResult,
+ prim_compare_result_detail: Option<PrimitiveCompareResultDetail>,
+ },
+ // The compositor type changed
+ CompositorKindChanged,
+ // The valid region of the tile changed
+ ValidRectChanged,
+ // The overall scale of the picture cache changed
+ ScaleChanged,
+}
+
+/// A minimal subset of Tile for debug capturing
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileSerializer {
+ pub rect: PictureRect,
+ pub current_descriptor: TileDescriptor,
+ pub device_fract_offset: DeviceVector2D,
+ pub id: TileId,
+ pub root: TileNode,
+ pub background_color: Option<ColorF>,
+ pub invalidation_reason: Option<InvalidationReason>
+}
+
+/// A minimal subset of TileCacheInstance for debug capturing
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileCacheInstanceSerializer {
+ pub slice: usize,
+ pub tiles: FastHashMap<TileOffset, TileSerializer>,
+ pub background_color: Option<ColorF>,
+ pub fract_offset: PictureVector2D,
+}
+
+/// Information about a cached tile.
+pub struct Tile {
+ /// The grid position of this tile within the picture cache
+ pub tile_offset: TileOffset,
+ /// The current world rect of this tile.
+ pub world_tile_rect: WorldRect,
+ /// The current local rect of this tile.
+ pub local_tile_rect: PictureRect,
+ /// Same as local_tile_rect, but in min/max form as an optimization
+ pub local_tile_box: PictureBox2D,
+ /// The picture space dirty rect for this tile.
+ local_dirty_rect: PictureRect,
+ /// The device space dirty rect for this tile.
+ /// TODO(gw): We have multiple dirty rects available due to the quadtree above. In future,
+ /// expose these as multiple dirty rects, which will help in some cases.
+ pub device_dirty_rect: DeviceRect,
+ /// Device space rect that contains valid pixels region of this tile.
+ pub device_valid_rect: DeviceRect,
+ /// Uniquely describes the content of this tile, in a way that can be
+ /// (reasonably) efficiently hashed and compared.
+ pub current_descriptor: TileDescriptor,
+ /// The content descriptor for this tile from the previous frame.
+ pub prev_descriptor: TileDescriptor,
+ /// Handle to the backing surface for this tile.
+ pub surface: Option<TileSurface>,
+ /// If true, this tile is marked valid, and the existing texture
+ /// cache handle can be used. Tiles are invalidated during the
+ /// build_dirty_regions method.
+ pub is_valid: bool,
+ /// If true, this tile intersects with the currently visible screen
+ /// rect, and will be drawn.
+ pub is_visible: bool,
+ /// The current fractional offset of the cache transform root. If this changes,
+ /// all tiles need to be invalidated and redrawn, since snapping differences are
+ /// likely to occur.
+ device_fract_offset: DeviceVector2D,
+ /// The tile id is stable between display lists and / or frames,
+ /// if the tile is retained. Useful for debugging tile evictions.
+ pub id: TileId,
+ /// If true, the tile was determined to be opaque, which means blending
+ /// can be disabled when drawing it.
+ pub is_opaque: bool,
+ /// Root node of the quadtree dirty rect tracker.
+ root: TileNode,
+ /// The last rendered background color on this tile.
+ background_color: Option<ColorF>,
+ /// The first reason the tile was invalidated this frame.
+ invalidation_reason: Option<InvalidationReason>,
+ /// If true, this tile has one or more compositor surfaces affecting it.
+ pub has_compositor_surface: bool,
+ /// The local space valid rect for any primitives found prior to the first compositor
+ /// surface that affects this tile.
+ bg_local_valid_rect: PictureBox2D,
+ /// The local space valid rect for any primitives found after the first compositor
+ /// surface that affects this tile.
+ fg_local_valid_rect: PictureBox2D,
+ /// z-buffer id for this tile, which is one of z_id_opaque or z_id_alpha, depending on tile opacity
+ pub z_id: ZBufferId,
+ /// The last frame this tile had its dependencies updated (dependency updating is
+ /// skipped if a tile is off-screen).
+ pub last_updated_frame_id: FrameId,
+}
+
+impl Tile {
+ /// Construct a new, invalid tile.
+ fn new(tile_offset: TileOffset) -> Self {
+ let id = TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed));
+
+ Tile {
+ tile_offset,
+ local_tile_rect: PictureRect::zero(),
+ local_tile_box: PictureBox2D::zero(),
+ world_tile_rect: WorldRect::zero(),
+ device_valid_rect: DeviceRect::zero(),
+ local_dirty_rect: PictureRect::zero(),
+ device_dirty_rect: DeviceRect::zero(),
+ surface: None,
+ current_descriptor: TileDescriptor::new(),
+ prev_descriptor: TileDescriptor::new(),
+ is_valid: false,
+ is_visible: false,
+ device_fract_offset: DeviceVector2D::zero(),
+ id,
+ is_opaque: false,
+ root: TileNode::new_leaf(Vec::new()),
+ background_color: None,
+ invalidation_reason: None,
+ has_compositor_surface: false,
+ bg_local_valid_rect: PictureBox2D::zero(),
+ fg_local_valid_rect: PictureBox2D::zero(),
+ z_id: ZBufferId::invalid(),
+ last_updated_frame_id: FrameId::INVALID,
+ }
+ }
+
+ /// Print debug information about this tile to a tree printer.
+ fn print(&self, pt: &mut dyn PrintTreePrinter) {
+ pt.new_level(format!("Tile {:?}", self.id));
+ pt.add_item(format!("local_tile_rect: {:?}", self.local_tile_rect));
+ pt.add_item(format!("device_fract_offset: {:?}", self.device_fract_offset));
+ pt.add_item(format!("background_color: {:?}", self.background_color));
+ pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason));
+ self.current_descriptor.print(pt);
+ pt.end_level();
+ }
+
+ /// Check if the content of the previous and current tile descriptors match
+ fn update_dirty_rects(
+ &mut self,
+ ctx: &TilePostUpdateContext,
+ state: &mut TilePostUpdateState,
+ invalidation_reason: &mut Option<InvalidationReason>,
+ frame_context: &FrameVisibilityContext,
+ ) -> PictureRect {
+ let mut prim_comparer = PrimitiveComparer::new(
+ &self.prev_descriptor,
+ &self.current_descriptor,
+ state.resource_cache,
+ state.spatial_node_comparer,
+ ctx.opacity_bindings,
+ ctx.color_bindings,
+ );
+
+ let mut dirty_rect = PictureBox2D::zero();
+ self.root.update_dirty_rects(
+ &self.prev_descriptor.prims,
+ &self.current_descriptor.prims,
+ &mut prim_comparer,
+ &mut dirty_rect,
+ state.compare_cache,
+ invalidation_reason,
+ frame_context,
+ );
+
+ dirty_rect.to_rect()
+ }
+
+ /// Invalidate a tile based on change in content. This
+ /// must be called even if the tile is not currently
+ /// visible on screen. We might be able to improve this
+ /// later by changing how ComparableVec is used.
+ fn update_content_validity(
+ &mut self,
+ ctx: &TilePostUpdateContext,
+ state: &mut TilePostUpdateState,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ // Check if the contents of the primitives, clips, and
+ // other dependencies are the same.
+ state.compare_cache.clear();
+ let mut invalidation_reason = None;
+ let dirty_rect = self.update_dirty_rects(
+ ctx,
+ state,
+ &mut invalidation_reason,
+ frame_context,
+ );
+ if !dirty_rect.is_empty() {
+ self.invalidate(
+ Some(dirty_rect),
+ invalidation_reason.expect("bug: no invalidation_reason"),
+ );
+ }
+ if ctx.invalidate_all {
+ self.invalidate(None, InvalidationReason::ScaleChanged);
+ }
+ // TODO(gw): We can avoid invalidating the whole tile in some cases here,
+ // but it should be a fairly rare invalidation case.
+ if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect {
+ self.invalidate(None, InvalidationReason::ValidRectChanged);
+ state.composite_state.dirty_rects_are_valid = false;
+ }
+ }
+
+ /// Invalidate this tile. If `invalidation_rect` is None, the entire
+ /// tile is invalidated.
+ fn invalidate(
+ &mut self,
+ invalidation_rect: Option<PictureRect>,
+ reason: InvalidationReason,
+ ) {
+ self.is_valid = false;
+
+ match invalidation_rect {
+ Some(rect) => {
+ self.local_dirty_rect = self.local_dirty_rect.union(&rect);
+ }
+ None => {
+ self.local_dirty_rect = self.local_tile_rect;
+ }
+ }
+
+ if self.invalidation_reason.is_none() {
+ self.invalidation_reason = Some(reason);
+ }
+ }
+
+ /// Called during pre_update of a tile cache instance. Allows the
+ /// tile to setup state before primitive dependency calculations.
+ fn pre_update(
+ &mut self,
+ ctx: &TilePreUpdateContext,
+ ) {
+ // Ensure each tile is offset by the appropriate amount from the
+ // origin, such that the content origin will be a whole number and
+ // the snapping will be consistent.
+ self.local_tile_rect = PictureRect::new(
+ PicturePoint::new(
+ self.tile_offset.x as f32 * ctx.tile_size.width + ctx.fract_offset.x,
+ self.tile_offset.y as f32 * ctx.tile_size.height + ctx.fract_offset.y,
+ ),
+ ctx.tile_size,
+ );
+ self.local_tile_box = PictureBox2D::new(
+ self.local_tile_rect.origin,
+ self.local_tile_rect.bottom_right(),
+ );
+ self.bg_local_valid_rect = PictureBox2D::zero();
+ self.fg_local_valid_rect = PictureBox2D::zero();
+ self.invalidation_reason = None;
+ self.has_compositor_surface = false;
+
+ self.world_tile_rect = ctx.pic_to_world_mapper
+ .map(&self.local_tile_rect)
+ .expect("bug: map local tile rect");
+
+ // Check if this tile is currently on screen.
+ self.is_visible = self.world_tile_rect.intersects(&ctx.global_screen_world_rect);
+
+ // If the tile isn't visible, early exit, skipping the normal set up to
+ // validate dependencies. Instead, we will only compare the current tile
+ // dependencies the next time it comes into view.
+ if !self.is_visible {
+ return;
+ }
+
+ // We may need to rerender if glyph subpixel positions have changed. Note
+ // that we update the tile fract offset itself after we have completed
+ // invalidation. This allows for other whole tile invalidation cases to
+ // update the fract offset appropriately.
+ let fract_delta = self.device_fract_offset - ctx.device_fract_offset;
+ let fract_changed = fract_delta.x.abs() > 0.01 || fract_delta.y.abs() > 0.01;
+ if fract_changed {
+ self.invalidate(None, InvalidationReason::FractionalOffset {
+ old: self.device_fract_offset,
+ new: ctx.device_fract_offset });
+ }
+
+ if ctx.background_color != self.background_color {
+ self.invalidate(None, InvalidationReason::BackgroundColor {
+ old: self.background_color,
+ new: ctx.background_color });
+ self.background_color = ctx.background_color;
+ }
+
+ // Clear any dependencies so that when we rebuild them we
+ // can compare if the tile has the same content.
+ mem::swap(
+ &mut self.current_descriptor,
+ &mut self.prev_descriptor,
+ );
+ self.current_descriptor.clear();
+ self.root.clear(self.local_tile_rect.to_box2d());
+
+ // Since this tile is determined to be visible, it will get updated
+ // dependencies, so update the frame id we are storing dependencies for.
+ self.last_updated_frame_id = ctx.frame_id;
+ }
+
+ /// Add dependencies for a given primitive to this tile.
+ fn add_prim_dependency(
+ &mut self,
+ info: &PrimitiveDependencyInfo,
+ ) {
+ // If this tile isn't currently visible, we don't want to update the dependencies
+ // for this tile, as an optimization, since it won't be drawn anyway.
+ if !self.is_visible {
+ return;
+ }
+
+ // If this primitive is a compositor surface, any tile it affects must be
+ // drawn as an overlay tile.
+ if info.is_compositor_surface {
+ self.has_compositor_surface = true;
+ } else {
+ // Incorporate the bounding rect of the primitive in the local valid rect
+ // for this tile. This is used to minimize the size of the scissor rect
+ // during rasterization and the draw rect during composition of partial tiles.
+
+ // Once we have encountered 1+ compositor surfaces affecting this tile, include
+ // this bounding rect in the foreground. Otherwise, include in the background rect.
+ // This allows us to determine if we found any primitives that are on top of the
+ // compositor surface(s) for this tile. If so, we need to draw the tile with alpha
+ // blending as an overlay.
+ if self.has_compositor_surface {
+ self.fg_local_valid_rect = self.fg_local_valid_rect.union(&info.prim_clip_box);
+ } else {
+ self.bg_local_valid_rect = self.bg_local_valid_rect.union(&info.prim_clip_box);
+ }
+ }
+
+ // Include any image keys this tile depends on.
+ self.current_descriptor.images.extend_from_slice(&info.images);
+
+ // Include any opacity bindings this primitive depends on.
+ self.current_descriptor.opacity_bindings.extend_from_slice(&info.opacity_bindings);
+
+ // Include any clip nodes that this primitive depends on.
+ self.current_descriptor.clips.extend_from_slice(&info.clips);
+
+ // Include any transforms that this primitive depends on.
+ for spatial_node_index in &info.spatial_nodes {
+ self.current_descriptor.transforms.push(
+ SpatialNodeKey {
+ spatial_node_index: *spatial_node_index,
+ frame_id: self.last_updated_frame_id,
+ }
+ );
+ }
+
+ // Include any color bindings this primitive depends on.
+ if info.color_binding.is_some() {
+ self.current_descriptor.color_bindings.insert(
+ self.current_descriptor.color_bindings.len(), info.color_binding.unwrap());
+ }
+
+ // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port,
+ // which can cause invalidations when a new display list with changed
+ // display port is received. To work around this, clamp the prim clip rect
+ // to the tile boundaries - if the clip hasn't affected the tile, then the
+ // changed clip can't affect the content of the primitive on this tile.
+ // In future, we could consider supplying the display port clip from Gecko
+ // in a different way (e.g. as a scroll frame clip) which still provides
+ // the desired clip for checkerboarding, but doesn't require this extra
+ // work below.
+
+ // TODO(gw): This is a hot part of the code - we could probably optimize further by:
+ // - Using min/max instead of clamps below (if we guarantee the rects are well formed)
+
+ let tile_p0 = self.local_tile_box.min;
+ let tile_p1 = self.local_tile_box.max;
+
+ let prim_clip_box = PictureBox2D::new(
+ PicturePoint::new(
+ clampf(info.prim_clip_box.min.x, tile_p0.x, tile_p1.x),
+ clampf(info.prim_clip_box.min.y, tile_p0.y, tile_p1.y),
+ ),
+ PicturePoint::new(
+ clampf(info.prim_clip_box.max.x, tile_p0.x, tile_p1.x),
+ clampf(info.prim_clip_box.max.y, tile_p0.y, tile_p1.y),
+ ),
+ );
+
+ // Update the tile descriptor, used for tile comparison during scene swaps.
+ let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32);
+
+ // We know that the casts below will never overflow because the array lengths are
+ // truncated to MAX_PRIM_SUB_DEPS during update_prim_dependencies.
+ debug_assert!(info.spatial_nodes.len() <= MAX_PRIM_SUB_DEPS);
+ debug_assert!(info.clips.len() <= MAX_PRIM_SUB_DEPS);
+ debug_assert!(info.images.len() <= MAX_PRIM_SUB_DEPS);
+ debug_assert!(info.opacity_bindings.len() <= MAX_PRIM_SUB_DEPS);
+
+ self.current_descriptor.prims.push(PrimitiveDescriptor {
+ prim_uid: info.prim_uid,
+ prim_clip_box,
+ transform_dep_count: info.spatial_nodes.len() as u8,
+ clip_dep_count: info.clips.len() as u8,
+ image_dep_count: info.images.len() as u8,
+ opacity_binding_dep_count: info.opacity_bindings.len() as u8,
+ color_binding_dep_count: if info.color_binding.is_some() { 1 } else { 0 } as u8,
+ });
+
+ // Add this primitive to the dirty rect quadtree.
+ self.root.add_prim(prim_index, &info.prim_clip_box);
+ }
+
+ /// Called during tile cache instance post_update. Allows invalidation and dirty
+ /// rect calculation after primitive dependencies have been updated.
+ fn post_update(
+ &mut self,
+ ctx: &TilePostUpdateContext,
+ state: &mut TilePostUpdateState,
+ frame_context: &FrameVisibilityContext,
+ ) -> bool {
+ // Register the frame id of this tile with the spatial node comparer, to ensure
+ // that it doesn't GC any spatial nodes from the comparer that are referenced
+ // by this tile. Must be done before we early exit below, so that we retain
+ // spatial node info even for tiles that are currently not visible.
+ state.spatial_node_comparer.retain_for_frame(self.last_updated_frame_id);
+
+ // If tile is not visible, just early out from here - we don't update dependencies
+ // so don't want to invalidate, merge, split etc. The tile won't need to be drawn
+ // (and thus updated / invalidated) until it is on screen again.
+ if !self.is_visible {
+ return false;
+ }
+
+ // Calculate the overall valid rect for this tile, including both the foreground
+ // and background local valid rects.
+ self.current_descriptor.local_valid_rect =
+ self.bg_local_valid_rect
+ .union(&self.fg_local_valid_rect)
+ .to_rect();
+
+ // TODO(gw): In theory, the local tile rect should always have an
+ // intersection with the overall picture rect. In practice,
+ // due to some accuracy issues with how fract_offset (and
+ // fp accuracy) are used in the calling method, this isn't
+ // always true. In this case, it's safe to set the local
+ // valid rect to zero, which means it will be clipped out
+ // and not affect the scene. In future, we should fix the
+ // accuracy issue above, so that this assumption holds, but
+ // it shouldn't have any noticeable effect on performance
+ // or memory usage (textures should never get allocated).
+ self.current_descriptor.local_valid_rect = self.local_tile_rect
+ .intersection(&ctx.local_rect)
+ .and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect))
+ .unwrap_or_else(PictureRect::zero);
+
+ // The device_valid_rect is referenced during `update_content_validity` so it
+ // must be updated here first.
+ let world_valid_rect = ctx.pic_to_world_mapper
+ .map(&self.current_descriptor.local_valid_rect)
+ .expect("bug: map local valid rect");
+
+ // The device rect is guaranteed to be aligned on a device pixel - the round
+ // is just to deal with float accuracy. However, the valid rect is not
+ // always aligned to a device pixel. To handle this, round out to get all
+ // required pixels, and intersect with the tile device rect.
+ let device_rect = (self.world_tile_rect * ctx.global_device_pixel_scale).round();
+ self.device_valid_rect = (world_valid_rect * ctx.global_device_pixel_scale)
+ .round_out()
+ .intersection(&device_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ // Invalidate the tile based on the content changing.
+ self.update_content_validity(ctx, state, frame_context);
+
+ // If there are no primitives there is no need to draw or cache it.
+ if self.current_descriptor.prims.is_empty() {
+ // If there is a native compositor surface allocated for this (now empty) tile
+ // it must be freed here, otherwise the stale tile with previous contents will
+ // be composited. If the tile subsequently gets new primitives added to it, the
+ // surface will be re-allocated when it's added to the composite draw list.
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() {
+ if let Some(id) = id.take() {
+ state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ self.is_visible = false;
+ return false;
+ }
+
+ // Check if this tile can be considered opaque. Opacity state must be updated only
+ // after all early out checks have been performed. Otherwise, we might miss updating
+ // the native surface next time this tile becomes visible.
+ let clipped_rect = self.current_descriptor.local_valid_rect
+ .intersection(&ctx.local_clip_rect)
+ .unwrap_or_else(PictureRect::zero);
+ let mut is_opaque = ctx.backdrop.opaque_rect.contains_rect(&clipped_rect);
+
+ if self.has_compositor_surface {
+ // If we found primitive(s) that are ordered _after_ the first compositor
+ // surface, _and_ intersect with any compositor surface, then we will need
+ // to draw this tile with alpha blending, as an overlay to the compositor surface.
+ let fg_world_valid_rect = ctx.pic_to_world_mapper
+ .map(&self.fg_local_valid_rect.to_rect())
+ .expect("bug: map fg local valid rect");
+ let fg_device_valid_rect = fg_world_valid_rect * ctx.global_device_pixel_scale;
+
+ for surface in ctx.external_surfaces {
+ if surface.device_rect.intersects(&fg_device_valid_rect) {
+ is_opaque = false;
+ break;
+ }
+ }
+ }
+
+ // Set the correct z_id for this tile based on opacity
+ if is_opaque {
+ self.z_id = ctx.z_id_opaque;
+ } else {
+ self.z_id = ctx.z_id_alpha;
+ }
+
+ if is_opaque != self.is_opaque {
+ // If opacity changed, the native compositor surface and all tiles get invalidated.
+ // (this does nothing if not using native compositor mode).
+ // TODO(gw): This property probably changes very rarely, so it is OK to invalidate
+ // everything in this case. If it turns out that this isn't true, we could
+ // consider other options, such as per-tile opacity (natively supported
+ // on CoreAnimation, and supported if backed by non-virtual surfaces in
+ // DirectComposition).
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = self.surface {
+ if let Some(id) = id.take() {
+ state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ // Invalidate the entire tile to force a redraw.
+ self.invalidate(None, InvalidationReason::SurfaceOpacityChanged { became_opaque: is_opaque });
+ self.is_opaque = is_opaque;
+ }
+
+ // Check if the selected composite mode supports dirty rect updates. For Draw composite
+ // mode, we can always update the content with smaller dirty rects, unless there is a
+ // driver bug to workaround. For native composite mode, we can only use dirty rects if
+ // the compositor supports partial surface updates.
+ let (supports_dirty_rects, supports_simple_prims) = match state.composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ (frame_context.config.gpu_supports_render_target_partial_update, true)
+ }
+ CompositorKind::Native { max_update_rects, .. } => {
+ (max_update_rects > 0, false)
+ }
+ };
+
+ // TODO(gw): Consider using smaller tiles and/or tile splits for
+ // native compositors that don't support dirty rects.
+ if supports_dirty_rects {
+ // Only allow splitting for normal content sized tiles
+ if ctx.current_tile_size == state.resource_cache.texture_cache.default_picture_tile_size() {
+ let max_split_level = 3;
+
+ // Consider splitting / merging dirty regions
+ self.root.maybe_merge_or_split(
+ 0,
+ &self.current_descriptor.prims,
+ max_split_level,
+ );
+ }
+ }
+
+ // The dirty rect will be set correctly by now. If the underlying platform
+ // doesn't support partial updates, and this tile isn't valid, force the dirty
+ // rect to be the size of the entire tile.
+ if !self.is_valid && !supports_dirty_rects {
+ self.local_dirty_rect = self.local_tile_rect;
+ }
+
+ // See if this tile is a simple color, in which case we can just draw
+ // it as a rect, and avoid allocating a texture surface and drawing it.
+ // TODO(gw): Initial native compositor interface doesn't support simple
+ // color tiles. We can definitely support this in DC, so this
+ // should be added as a follow up.
+ let is_simple_prim =
+ ctx.backdrop.kind.is_some() &&
+ self.current_descriptor.prims.len() == 1 &&
+ self.is_opaque &&
+ supports_simple_prims;
+
+ // Set up the backing surface for this tile.
+ let surface = if is_simple_prim {
+ // If we determine the tile can be represented by a color, set the
+ // surface unconditionally (this will drop any previously used
+ // texture cache backing surface).
+ match ctx.backdrop.kind {
+ Some(BackdropKind::Color { color }) => {
+ TileSurface::Color {
+ color,
+ }
+ }
+ Some(BackdropKind::Clear) => {
+ TileSurface::Clear
+ }
+ None => {
+ // This should be prevented by the is_simple_prim check above.
+ unreachable!();
+ }
+ }
+ } else {
+ // If this tile will be backed by a surface, we want to retain
+ // the texture handle from the previous frame, if possible. If
+ // the tile was previously a color, or not set, then just set
+ // up a new texture cache handle.
+ match self.surface.take() {
+ Some(TileSurface::Texture { descriptor, visibility_mask }) => {
+ // Reuse the existing descriptor and vis mask
+ TileSurface::Texture {
+ descriptor,
+ visibility_mask,
+ }
+ }
+ Some(TileSurface::Color { .. }) | Some(TileSurface::Clear) | None => {
+ // This is the case where we are constructing a tile surface that
+ // involves drawing to a texture. Create the correct surface
+ // descriptor depending on the compositing mode that will read
+ // the output.
+ let descriptor = match state.composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ // For a texture cache entry, create an invalid handle that
+ // will be allocated when update_picture_cache is called.
+ SurfaceTextureDescriptor::TextureCache {
+ handle: TextureCacheHandle::invalid(),
+ }
+ }
+ CompositorKind::Native { .. } => {
+ // Create a native surface surface descriptor, but don't allocate
+ // a surface yet. The surface is allocated *after* occlusion
+ // culling occurs, so that only visible tiles allocate GPU memory.
+ SurfaceTextureDescriptor::Native {
+ id: None,
+ }
+ }
+ };
+
+ TileSurface::Texture {
+ descriptor,
+ visibility_mask: PrimitiveVisibilityMask::empty(),
+ }
+ }
+ }
+ };
+
+ // Store the current surface backing info for use during batching.
+ self.surface = Some(surface);
+
+ true
+ }
+}
+
+/// Defines a key that uniquely identifies a primitive instance.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDescriptor {
+ /// Uniquely identifies the content of the primitive template.
+ pub prim_uid: ItemUid,
+ /// The clip rect for this primitive. Included here in
+ /// dependencies since there is no entry in the clip chain
+ /// dependencies for the local clip rect.
+ pub prim_clip_box: PictureBox2D,
+ /// The number of extra dependencies that this primitive has.
+ transform_dep_count: u8,
+ image_dep_count: u8,
+ opacity_binding_dep_count: u8,
+ clip_dep_count: u8,
+ color_binding_dep_count: u8,
+}
+
+impl PartialEq for PrimitiveDescriptor {
+ fn eq(&self, other: &Self) -> bool {
+ const EPSILON: f32 = 0.001;
+
+ if self.prim_uid != other.prim_uid {
+ return false;
+ }
+
+ if !self.prim_clip_box.min.x.approx_eq_eps(&other.prim_clip_box.min.x, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.min.y.approx_eq_eps(&other.prim_clip_box.min.y, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.max.x.approx_eq_eps(&other.prim_clip_box.max.x, &EPSILON) {
+ return false;
+ }
+ if !self.prim_clip_box.max.y.approx_eq_eps(&other.prim_clip_box.max.y, &EPSILON) {
+ return false;
+ }
+
+ true
+ }
+}
+
+/// A small helper to compare two arrays of primitive dependencies.
+struct CompareHelper<'a, T> where T: Copy {
+ offset_curr: usize,
+ offset_prev: usize,
+ curr_items: &'a [T],
+ prev_items: &'a [T],
+}
+
+impl<'a, T> CompareHelper<'a, T> where T: Copy + PartialEq {
+ /// Construct a new compare helper for a current / previous set of dependency information.
+ fn new(
+ prev_items: &'a [T],
+ curr_items: &'a [T],
+ ) -> Self {
+ CompareHelper {
+ offset_curr: 0,
+ offset_prev: 0,
+ curr_items,
+ prev_items,
+ }
+ }
+
+ /// Reset the current position in the dependency array to the start
+ fn reset(&mut self) {
+ self.offset_prev = 0;
+ self.offset_curr = 0;
+ }
+
+ /// Test if two sections of the dependency arrays are the same, by checking both
+ /// item equality, and a user closure to see if the content of the item changed.
+ fn is_same<F>(
+ &self,
+ prev_count: u8,
+ curr_count: u8,
+ mut f: F,
+ opt_detail: Option<&mut CompareHelperResult<T>>,
+ ) -> bool where F: FnMut(&T, &T) -> bool {
+ // If the number of items is different, trivial reject.
+ if prev_count != curr_count {
+ if let Some(detail) = opt_detail { *detail = CompareHelperResult::Count{ prev_count, curr_count }; }
+ return false;
+ }
+ // If both counts are 0, then no need to check these dependencies.
+ if curr_count == 0 {
+ if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; }
+ return true;
+ }
+ // If both counts are u8::MAX, this is a sentinel that we can't compare these
+ // deps, so just trivial reject.
+ if curr_count as usize == MAX_PRIM_SUB_DEPS {
+ if let Some(detail) = opt_detail { *detail = CompareHelperResult::Sentinel; }
+ return false;
+ }
+
+ let end_prev = self.offset_prev + prev_count as usize;
+ let end_curr = self.offset_curr + curr_count as usize;
+
+ let curr_items = &self.curr_items[self.offset_curr .. end_curr];
+ let prev_items = &self.prev_items[self.offset_prev .. end_prev];
+
+ for (curr, prev) in curr_items.iter().zip(prev_items.iter()) {
+ if !f(prev, curr) {
+ if let Some(detail) = opt_detail { *detail = CompareHelperResult::PredicateTrue{ curr: *curr }; }
+ return false;
+ }
+ }
+
+ if let Some(detail) = opt_detail { *detail = CompareHelperResult::Equal; }
+ true
+ }
+
+ // Advance the prev dependency array by a given amount
+ fn advance_prev(&mut self, count: u8) {
+ self.offset_prev += count as usize;
+ }
+
+ // Advance the current dependency array by a given amount
+ fn advance_curr(&mut self, count: u8) {
+ self.offset_curr += count as usize;
+ }
+}
+
+/// Uniquely describes the content of this tile, in a way that can be
+/// (reasonably) efficiently hashed and compared.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileDescriptor {
+ /// List of primitive instance unique identifiers. The uid is guaranteed
+ /// to uniquely describe the content of the primitive template, while
+ /// the other parameters describe the clip chain and instance params.
+ pub prims: Vec<PrimitiveDescriptor>,
+
+ /// List of clip node descriptors.
+ clips: Vec<ItemUid>,
+
+ /// List of image keys that this tile depends on.
+ images: Vec<ImageDependency>,
+
+ /// The set of opacity bindings that this tile depends on.
+ // TODO(gw): Ugh, get rid of all opacity binding support!
+ opacity_bindings: Vec<OpacityBinding>,
+
+ /// List of the effects of transforms that we care about
+ /// tracking for this tile.
+ transforms: Vec<SpatialNodeKey>,
+
+ /// Picture space rect that contains valid pixels region of this tile.
+ local_valid_rect: PictureRect,
+
+ /// List of the effects of color that we care about
+ /// tracking for this tile.
+ color_bindings: Vec<ColorBinding>,
+}
+
+impl TileDescriptor {
+ fn new() -> Self {
+ TileDescriptor {
+ prims: Vec::new(),
+ clips: Vec::new(),
+ opacity_bindings: Vec::new(),
+ images: Vec::new(),
+ transforms: Vec::new(),
+ local_valid_rect: PictureRect::zero(),
+ color_bindings: Vec::new(),
+ }
+ }
+
+ /// Print debug information about this tile descriptor to a tree printer.
+ fn print(&self, pt: &mut dyn PrintTreePrinter) {
+ pt.new_level("current_descriptor".to_string());
+
+ pt.new_level("prims".to_string());
+ for prim in &self.prims {
+ pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid()));
+ pt.add_item(format!("clip: p0={},{} p1={},{}",
+ prim.prim_clip_box.min.x,
+ prim.prim_clip_box.min.y,
+ prim.prim_clip_box.max.x,
+ prim.prim_clip_box.max.y,
+ ));
+ pt.add_item(format!("deps: t={} i={} o={} c={} color={}",
+ prim.transform_dep_count,
+ prim.image_dep_count,
+ prim.opacity_binding_dep_count,
+ prim.clip_dep_count,
+ prim.color_binding_dep_count,
+ ));
+ pt.end_level();
+ }
+ pt.end_level();
+
+ if !self.clips.is_empty() {
+ pt.new_level("clips".to_string());
+ for clip in &self.clips {
+ pt.new_level(format!("clip uid={}", clip.get_uid()));
+ pt.end_level();
+ }
+ pt.end_level();
+ }
+
+ if !self.images.is_empty() {
+ pt.new_level("images".to_string());
+ for info in &self.images {
+ pt.new_level(format!("key={:?}", info.key));
+ pt.add_item(format!("generation={:?}", info.generation));
+ pt.end_level();
+ }
+ pt.end_level();
+ }
+
+ if !self.opacity_bindings.is_empty() {
+ pt.new_level("opacity_bindings".to_string());
+ for opacity_binding in &self.opacity_bindings {
+ pt.new_level(format!("binding={:?}", opacity_binding));
+ pt.end_level();
+ }
+ pt.end_level();
+ }
+
+ if !self.transforms.is_empty() {
+ pt.new_level("transforms".to_string());
+ for transform in &self.transforms {
+ pt.new_level(format!("spatial_node={:?}", transform));
+ pt.end_level();
+ }
+ pt.end_level();
+ }
+
+ if !self.color_bindings.is_empty() {
+ pt.new_level("color_bindings".to_string());
+ for color_binding in &self.color_bindings {
+ pt.new_level(format!("binding={:?}", color_binding));
+ pt.end_level();
+ }
+ pt.end_level();
+ }
+
+ pt.end_level();
+ }
+
+ /// Clear the dependency information for a tile, when the dependencies
+ /// are being rebuilt.
+ fn clear(&mut self) {
+ self.prims.clear();
+ self.clips.clear();
+ self.opacity_bindings.clear();
+ self.images.clear();
+ self.transforms.clear();
+ self.local_valid_rect = PictureRect::zero();
+ self.color_bindings.clear();
+ }
+}
+
+/// Stores both the world and devices rects for a single dirty rect.
+#[derive(Debug, Clone)]
+pub struct DirtyRegionRect {
+ /// Bitfield for picture render tasks that draw this dirty region.
+ pub visibility_mask: PrimitiveVisibilityMask,
+ /// The dirty region in space of the picture cache
+ pub rect_in_pic_space: PictureRect,
+}
+
+/// Represents the dirty region of a tile cache picture.
+#[derive(Debug, Clone)]
+pub struct DirtyRegion {
+ /// The individual dirty rects of this region.
+ pub dirty_rects: Vec<DirtyRegionRect>,
+
+ /// The overall dirty rect, a combination of dirty_rects
+ pub combined: WorldRect,
+
+ /// Spatial node of the picture cache this region represents
+ spatial_node_index: SpatialNodeIndex,
+}
+
+impl DirtyRegion {
+ /// Construct a new dirty region tracker.
+ pub fn new(
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ DirtyRegion {
+ dirty_rects: Vec::with_capacity(PrimitiveVisibilityMask::MAX_DIRTY_REGIONS),
+ combined: WorldRect::zero(),
+ spatial_node_index,
+ }
+ }
+
+ /// Reset the dirty regions back to empty
+ pub fn reset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ ) {
+ self.dirty_rects.clear();
+ self.combined = WorldRect::zero();
+ self.spatial_node_index = spatial_node_index;
+ }
+
+ /// Add a dirty region to the tracker. Returns the visibility mask that corresponds to
+ /// this region in the tracker.
+ pub fn add_dirty_region(
+ &mut self,
+ rect_in_pic_space: PictureRect,
+ spatial_tree: &SpatialTree,
+ ) -> PrimitiveVisibilityMask {
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ let world_rect = map_pic_to_world
+ .map(&rect_in_pic_space)
+ .expect("bug");
+
+ // Include this in the overall dirty rect
+ self.combined = self.combined.union(&world_rect);
+
+ let dirty_region_index = self.dirty_rects.len();
+ let mut visibility_mask = PrimitiveVisibilityMask::empty();
+
+ if dirty_region_index < PrimitiveVisibilityMask::MAX_DIRTY_REGIONS {
+ visibility_mask.set_visible(dirty_region_index);
+
+ self.dirty_rects.push(DirtyRegionRect {
+ visibility_mask,
+ rect_in_pic_space,
+ });
+ } else {
+ // If we run out of dirty regions, then force the last dirty region to
+ // be a union of any remaining regions. This is an inefficiency, in that
+ // we'll add items to batches later on that are redundant / outside this
+ // tile, but it's really rare except in pathological cases (even on a
+ // 4k screen, the typical dirty region count is < 16).
+ visibility_mask.set_visible(PrimitiveVisibilityMask::MAX_DIRTY_REGIONS - 1);
+
+ let combined_region = self.dirty_rects.last_mut().unwrap();
+ combined_region.rect_in_pic_space = combined_region.rect_in_pic_space.union(&rect_in_pic_space);
+ }
+
+ visibility_mask
+ }
+
+ // TODO(gw): This returns a heap allocated object. Perhaps we can simplify this
+ // logic? Although - it's only used very rarely so it may not be an issue.
+ pub fn inflate(
+ &self,
+ inflate_amount: f32,
+ spatial_tree: &SpatialTree,
+ ) -> DirtyRegion {
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ WorldRect::max_rect(),
+ spatial_tree,
+ );
+
+ let mut dirty_rects = Vec::with_capacity(self.dirty_rects.len());
+ let mut combined = WorldRect::zero();
+
+ for rect in &self.dirty_rects {
+ let rect_in_pic_space = rect.rect_in_pic_space.inflate(inflate_amount, inflate_amount);
+
+ let world_rect = map_pic_to_world
+ .map(&rect_in_pic_space)
+ .expect("bug");
+
+ combined = combined.union(&world_rect);
+ dirty_rects.push(DirtyRegionRect {
+ visibility_mask: rect.visibility_mask,
+ rect_in_pic_space,
+ });
+ }
+
+ DirtyRegion {
+ dirty_rects,
+ combined,
+ spatial_node_index: self.spatial_node_index,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum BackdropKind {
+ Color {
+ color: ColorF,
+ },
+ Clear,
+}
+
+/// Stores information about the calculated opaque backdrop of this slice.
+#[derive(Debug, Copy, Clone)]
+pub struct BackdropInfo {
+ /// The picture space rectangle that is known to be opaque. This is used
+ /// to determine where subpixel AA can be used, and where alpha blending
+ /// can be disabled.
+ pub opaque_rect: PictureRect,
+ /// Kind of the backdrop
+ pub kind: Option<BackdropKind>,
+}
+
+impl BackdropInfo {
+ fn empty() -> Self {
+ BackdropInfo {
+ opaque_rect: PictureRect::zero(),
+ kind: None,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct TileCacheLoggerSlice {
+ pub serialized_slice: String,
+ pub local_to_world_transform: Transform3D<f32, PicturePixel, WorldPixel>,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+macro_rules! declare_tile_cache_logger_updatelists {
+ ( $( $name:ident : $ty:ty, )+ ) => {
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ struct TileCacheLoggerUpdateListsSerializer {
+ pub ron_string: Vec<String>,
+ }
+
+ pub struct TileCacheLoggerUpdateLists {
+ $(
+ /// Generate storage, one per interner.
+ /// the tuple is a workaround to avoid the need for multiple
+ /// fields that start with $name (macro concatenation).
+ /// the string is .ron serialized updatelist at capture time;
+ /// the updates is the list of DataStore updates (avoid UpdateList
+ /// due to Default() requirements on the Keys) reconstructed at
+ /// load time.
+ pub $name: (Vec<String>, Vec<UpdateList<<$ty as Internable>::Key>>),
+ )+
+ }
+
+ impl TileCacheLoggerUpdateLists {
+ pub fn new() -> Self {
+ TileCacheLoggerUpdateLists {
+ $(
+ $name : ( Vec::new(), Vec::new() ),
+ )+
+ }
+ }
+
+ /// serialize all interners in updates to .ron
+ #[cfg(feature = "capture")]
+ fn serialize_updates(
+ &mut self,
+ updates: &InternerUpdates
+ ) {
+ $(
+ self.$name.0.push(ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap());
+ )+
+ }
+
+ fn is_empty(&self) -> bool {
+ $(
+ if !self.$name.0.is_empty() { return false; }
+ )+
+ true
+ }
+
+ #[cfg(feature = "capture")]
+ fn to_ron(&self) -> String {
+ let mut serializer =
+ TileCacheLoggerUpdateListsSerializer { ron_string: Vec::new() };
+ $(
+ serializer.ron_string.push(
+ ron::ser::to_string_pretty(&self.$name.0, Default::default()).unwrap());
+ )+
+ ron::ser::to_string_pretty(&serializer, Default::default()).unwrap()
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn from_ron(&mut self, text: &str) {
+ let serializer : TileCacheLoggerUpdateListsSerializer =
+ match ron::de::from_str(&text) {
+ Ok(data) => { data }
+ Err(e) => {
+ println!("ERROR: failed to deserialize updatelist: {:?}\n{:?}", &text, e);
+ return;
+ }
+ };
+ let mut index = 0;
+ $(
+ let ron_lists : Vec<String> = ron::de::from_str(&serializer.ron_string[index]).unwrap();
+ self.$name.1 = ron_lists.iter()
+ .map( |list| ron::de::from_str(&list).unwrap() )
+ .collect();
+ index = index + 1;
+ )+
+ // error: value assigned to `index` is never read
+ let _ = index;
+ }
+
+ /// helper method to add a stringified version of all interned keys into
+ /// a lookup table based on ItemUid. Use strings as a form of type erasure
+ /// so all UpdateLists can go into a single map.
+ /// Then during analysis, when we see an invalidation reason due to
+ /// "ItemUid such and such was added to the tile primitive list", the lookup
+ /// allows mapping that back into something readable.
+ #[cfg(feature = "replay")]
+ pub fn insert_in_lookup(
+ &mut self,
+ itemuid_to_string: &mut HashMap<ItemUid, String>)
+ {
+ $(
+ {
+ for list in &self.$name.1 {
+ for insertion in &list.insertions {
+ itemuid_to_string.insert(
+ insertion.uid,
+ format!("{:?}", insertion.value));
+ }
+ }
+ }
+ )+
+ }
+ }
+ }
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+crate::enumerate_interners!(declare_tile_cache_logger_updatelists);
+
+#[cfg(not(any(feature = "capture", feature = "replay")))]
+pub struct TileCacheLoggerUpdateLists {
+}
+
+#[cfg(not(any(feature = "capture", feature = "replay")))]
+impl TileCacheLoggerUpdateLists {
+ pub fn new() -> Self { TileCacheLoggerUpdateLists {} }
+ fn is_empty(&self) -> bool { true }
+}
+
+/// Log tile cache activity for one single frame.
+/// Also stores the commands sent to the interning data_stores
+/// so we can see which items were created or destroyed this frame,
+/// and correlate that with tile invalidation activity.
+pub struct TileCacheLoggerFrame {
+ /// slices in the frame, one per take_context call
+ pub slices: Vec<TileCacheLoggerSlice>,
+ /// interning activity
+ pub update_lists: TileCacheLoggerUpdateLists
+}
+
+impl TileCacheLoggerFrame {
+ pub fn new() -> Self {
+ TileCacheLoggerFrame {
+ slices: Vec::new(),
+ update_lists: TileCacheLoggerUpdateLists::new()
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.slices.is_empty() && self.update_lists.is_empty()
+ }
+}
+
+/// Log tile cache activity whenever anything happens in take_context.
+pub struct TileCacheLogger {
+ /// next write pointer
+ pub write_index : usize,
+ /// ron serialization of tile caches;
+ pub frames: Vec<TileCacheLoggerFrame>
+}
+
+impl TileCacheLogger {
+ pub fn new(
+ num_frames: usize
+ ) -> Self {
+ let mut frames = Vec::with_capacity(num_frames);
+ for _i in 0..num_frames { // no Clone so no resize
+ frames.push(TileCacheLoggerFrame::new());
+ }
+ TileCacheLogger {
+ write_index: 0,
+ frames
+ }
+ }
+
+ pub fn is_enabled(&self) -> bool {
+ !self.frames.is_empty()
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn add(
+ &mut self,
+ serialized_slice: String,
+ local_to_world_transform: Transform3D<f32, PicturePixel, WorldPixel>
+ ) {
+ if !self.is_enabled() {
+ return;
+ }
+ self.frames[self.write_index].slices.push(
+ TileCacheLoggerSlice {
+ serialized_slice,
+ local_to_world_transform });
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn serialize_updates(&mut self, updates: &InternerUpdates) {
+ if !self.is_enabled() {
+ return;
+ }
+ self.frames[self.write_index].update_lists.serialize_updates(updates);
+ }
+
+ /// see if anything was written in this frame, and if so,
+ /// advance the write index in a circular way and clear the
+ /// recorded string.
+ pub fn advance(&mut self) {
+ if !self.is_enabled() || self.frames[self.write_index].is_empty() {
+ return;
+ }
+ self.write_index = self.write_index + 1;
+ if self.write_index >= self.frames.len() {
+ self.write_index = 0;
+ }
+ self.frames[self.write_index] = TileCacheLoggerFrame::new();
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn save_capture(
+ &self, root: &PathBuf
+ ) {
+ if !self.is_enabled() {
+ return;
+ }
+ use std::fs;
+
+ info!("saving tile cache log");
+ let path_tile_cache = root.join("tile_cache");
+ if !path_tile_cache.is_dir() {
+ fs::create_dir(&path_tile_cache).unwrap();
+ }
+
+ let mut files_written = 0;
+ for ix in 0..self.frames.len() {
+ // ...and start with write_index, since that's the oldest entry
+ // that we're about to overwrite. However when we get to
+ // save_capture, we've add()ed entries but haven't advance()d yet,
+ // so the actual oldest entry is write_index + 1
+ let index = (self.write_index + 1 + ix) % self.frames.len();
+ if self.frames[index].is_empty() {
+ continue;
+ }
+
+ let filename = path_tile_cache.join(format!("frame{:05}.ron", files_written));
+ let mut output = File::create(filename).unwrap();
+ output.write_all(b"// slice data\n").unwrap();
+ output.write_all(b"[\n").unwrap();
+ for item in &self.frames[index].slices {
+ output.write_all(b"( transform:\n").unwrap();
+ let transform =
+ ron::ser::to_string_pretty(
+ &item.local_to_world_transform, Default::default()).unwrap();
+ output.write_all(transform.as_bytes()).unwrap();
+ output.write_all(b",\n tile_cache:\n").unwrap();
+ output.write_all(item.serialized_slice.as_bytes()).unwrap();
+ output.write_all(b"\n),\n").unwrap();
+ }
+ output.write_all(b"]\n\n").unwrap();
+
+ output.write_all(b"// @@@ chunk @@@\n\n").unwrap();
+
+ output.write_all(b"// interning data\n").unwrap();
+ output.write_all(self.frames[index].update_lists.to_ron().as_bytes()).unwrap();
+
+ files_written = files_written + 1;
+ }
+ }
+}
+
+/// Represents the native surfaces created for a picture cache, if using
+/// a native compositor. An opaque and alpha surface is always created,
+/// but tiles are added to a surface based on current opacity. If the
+/// calculated opacity of a tile changes, the tile is invalidated and
+/// attached to a different native surface. This means that we don't
+/// need to invalidate the entire surface if only some tiles are changing
+/// opacity. It also means we can take advantage of opaque tiles on cache
+/// slices where only some of the tiles are opaque. There is an assumption
+/// that creating a native surface is cheap, and only when a tile is added
+/// to a surface is there a significant cost. This assumption holds true
+/// for the current native compositor implementations on Windows and Mac.
+pub struct NativeSurface {
+ /// Native surface for opaque tiles
+ pub opaque: NativeSurfaceId,
+ /// Native surface for alpha tiles
+ pub alpha: NativeSurfaceId,
+}
+
+/// Hash key for an external native compositor surface
+#[derive(PartialEq, Eq, Hash)]
+pub struct ExternalNativeSurfaceKey {
+ /// The YUV/RGB image keys that are used to draw this surface.
+ pub image_keys: [ImageKey; 3],
+ /// The current device size of the surface.
+ pub size: DeviceIntSize,
+ /// True if this is an 'external' compositor surface created via
+ /// Compositor::create_external_surface.
+ pub is_external_surface: bool,
+}
+
+/// Information about a native compositor surface cached between frames.
+pub struct ExternalNativeSurface {
+ /// If true, the surface was used this frame. Used for a simple form
+ /// of GC to remove old surfaces.
+ pub used_this_frame: bool,
+ /// The native compositor surface handle
+ pub native_surface_id: NativeSurfaceId,
+ /// List of image keys, and current image generations, that are drawn in this surface.
+ /// The image generations are used to check if the compositor surface is dirty and
+ /// needs to be updated.
+ pub image_dependencies: [ImageDependency; 3],
+}
+
+/// The key that identifies a tile cache instance. For now, it's simple the index of
+/// the slice as it was created during scene building.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SliceId(usize);
+
+impl SliceId {
+ pub fn new(index: usize) -> Self {
+ SliceId(index)
+ }
+}
+
+/// Information that is required to reuse or create a new tile cache. Created
+/// during scene building and passed to the render backend / frame builder.
+pub struct TileCacheParams {
+ // Index of the slice (also effectively the key of the tile cache, though we use SliceId where that matters)
+ pub slice: usize,
+ // Flags describing content of this cache (e.g. scrollbars)
+ pub slice_flags: SliceFlags,
+ // The anchoring spatial node / scroll root
+ pub spatial_node_index: SpatialNodeIndex,
+ // Optional background color of this tilecache. If present, can be used as an optimization
+ // to enable opaque blending and/or subpixel AA in more places.
+ pub background_color: Option<ColorF>,
+ // List of clips shared by all prims that are promoted to this tile cache
+ pub shared_clips: Vec<ClipInstance>,
+ // The clip chain handle representing `shared_clips`
+ pub shared_clip_chain: ClipChainId,
+ // Virtual surface sizes are always square, so this represents both the width and height
+ pub virtual_surface_size: i32,
+}
+
+/// Represents a cache of tiles that make up a picture primitives.
+pub struct TileCacheInstance {
+ /// Index of the tile cache / slice for this frame builder. It's determined
+ /// by the setup_picture_caching method during flattening, which splits the
+ /// picture tree into multiple slices. It's used as a simple input to the tile
+ /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed
+ /// between display lists - this seems very unlikely to occur on most pages, but
+ /// can be revisited if we ever notice that.
+ pub slice: usize,
+ /// Propagated information about the slice
+ pub slice_flags: SliceFlags,
+ /// The currently selected tile size to use for this cache
+ pub current_tile_size: DeviceIntSize,
+ /// The positioning node for this tile cache.
+ pub spatial_node_index: SpatialNodeIndex,
+ /// Hash of tiles present in this picture.
+ pub tiles: FastHashMap<TileOffset, Box<Tile>>,
+ /// A helper struct to map local rects into surface coords.
+ map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
+ /// A helper struct to map child picture rects into picture cache surface coords.
+ map_child_pic_to_surface: SpaceMapper<PicturePixel, PicturePixel>,
+ /// List of opacity bindings, with some extra information
+ /// about whether they changed since last frame.
+ opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating.
+ old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ /// A helper to compare transforms between previous and current frame.
+ spatial_node_comparer: SpatialNodeComparer,
+ /// List of color bindings, with some extra information
+ /// about whether they changed since last frame.
+ color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating.
+ old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ /// The current dirty region tracker for this picture.
+ pub dirty_region: DirtyRegion,
+ /// Current size of tiles in picture units.
+ tile_size: PictureSize,
+ /// Tile coords of the currently allocated grid.
+ tile_rect: TileRect,
+ /// Pre-calculated versions of the tile_rect above, used to speed up the
+ /// calculations in get_tile_coords_for_rect.
+ tile_bounds_p0: TileOffset,
+ tile_bounds_p1: TileOffset,
+ /// Local rect (unclipped) of the picture this cache covers.
+ pub local_rect: PictureRect,
+ /// The local clip rect, from the shared clips of this picture.
+ pub local_clip_rect: PictureRect,
+ /// The surface index that this tile cache will be drawn into.
+ surface_index: SurfaceIndex,
+ /// The background color from the renderer. If this is set opaque, we know it's
+ /// fine to clear the tiles to this and allow subpixel text on the first slice.
+ pub background_color: Option<ColorF>,
+ /// Information about the calculated backdrop content of this cache.
+ pub backdrop: BackdropInfo,
+ /// The allowed subpixel mode for this surface, which depends on the detected
+ /// opacity of the background.
+ pub subpixel_mode: SubpixelMode,
+ /// A list of clip handles that exist on every (top-level) primitive in this picture.
+ /// It's often the case that these are root / fixed position clips. By handling them
+ /// here, we can avoid applying them to the items, which reduces work, but more importantly
+ /// reduces invalidations.
+ pub shared_clips: Vec<ClipInstance>,
+ /// The clip chain that represents the shared_clips above. Used to build the local
+ /// clip rect for this tile cache.
+ shared_clip_chain: ClipChainId,
+ /// The current transform of the picture cache root spatial node
+ root_transform: ScaleOffset,
+ /// The number of frames until this cache next evaluates what tile size to use.
+ /// If a picture rect size is regularly changing just around a size threshold,
+ /// we don't want to constantly invalidate and reallocate different tile size
+ /// configuration each frame.
+ frames_until_size_eval: usize,
+ /// The current fractional offset of the cached picture
+ fract_offset: PictureVector2D,
+ /// The current device fractional offset of the cached picture
+ device_fract_offset: DeviceVector2D,
+ /// For DirectComposition, virtual surfaces don't support negative coordinates. However,
+ /// picture cache tile coordinates can be negative. To handle this, we apply an offset
+ /// to each tile in DirectComposition. We want to change this as little as possible,
+ /// to avoid invalidating tiles. However, if we have a picture cache tile coordinate
+ /// which is outside the virtual surface bounds, we must change this to allow
+ /// correct remapping of the coordinates passed to BeginDraw in DC.
+ virtual_offset: DeviceIntPoint,
+ /// keep around the hash map used as compare_cache to avoid reallocating it each
+ /// frame.
+ compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+ /// The allocated compositor surfaces for this picture cache. May be None if
+ /// not using native compositor, or if the surface was destroyed and needs
+ /// to be reallocated next time this surface contains valid tiles.
+ pub native_surface: Option<NativeSurface>,
+ /// The current device position of this cache. Used to set the compositor
+ /// offset of the surface when building the visual tree.
+ pub device_position: DevicePoint,
+ /// The currently considered tile size override. Used to check if we should
+ /// re-evaluate tile size, even if the frame timer hasn't expired.
+ tile_size_override: Option<DeviceIntSize>,
+ /// List of external surfaces that have been promoted from primitives
+ /// in this tile cache.
+ pub external_surfaces: Vec<ExternalSurfaceDescriptor>,
+ /// z-buffer ID assigned to opaque tiles in this slice
+ pub z_id_opaque: ZBufferId,
+ /// A cache of compositor surfaces that are retained between frames
+ pub external_native_surface_cache: FastHashMap<ExternalNativeSurfaceKey, ExternalNativeSurface>,
+ /// Current frame ID of this tile cache instance. Used for book-keeping / garbage collecting
+ frame_id: FrameId,
+}
+
+enum SurfacePromotionResult {
+ Failed,
+ Success {
+ flip_y: bool,
+ }
+}
+
+impl TileCacheInstance {
+ pub fn new(params: TileCacheParams) -> Self {
+ TileCacheInstance {
+ slice: params.slice,
+ slice_flags: params.slice_flags,
+ spatial_node_index: params.spatial_node_index,
+ tiles: FastHashMap::default(),
+ map_local_to_surface: SpaceMapper::new(
+ ROOT_SPATIAL_NODE_INDEX,
+ PictureRect::zero(),
+ ),
+ map_child_pic_to_surface: SpaceMapper::new(
+ ROOT_SPATIAL_NODE_INDEX,
+ PictureRect::zero(),
+ ),
+ opacity_bindings: FastHashMap::default(),
+ old_opacity_bindings: FastHashMap::default(),
+ spatial_node_comparer: SpatialNodeComparer::new(),
+ color_bindings: FastHashMap::default(),
+ old_color_bindings: FastHashMap::default(),
+ dirty_region: DirtyRegion::new(params.spatial_node_index),
+ tile_size: PictureSize::zero(),
+ tile_rect: TileRect::zero(),
+ tile_bounds_p0: TileOffset::zero(),
+ tile_bounds_p1: TileOffset::zero(),
+ local_rect: PictureRect::zero(),
+ local_clip_rect: PictureRect::zero(),
+ surface_index: SurfaceIndex(0),
+ background_color: params.background_color,
+ backdrop: BackdropInfo::empty(),
+ subpixel_mode: SubpixelMode::Allow,
+ root_transform: ScaleOffset::identity(),
+ shared_clips: params.shared_clips,
+ shared_clip_chain: params.shared_clip_chain,
+ current_tile_size: DeviceIntSize::zero(),
+ frames_until_size_eval: 0,
+ fract_offset: PictureVector2D::zero(),
+ device_fract_offset: DeviceVector2D::zero(),
+ // Default to centering the virtual offset in the middle of the DC virtual surface
+ virtual_offset: DeviceIntPoint::new(
+ params.virtual_surface_size / 2,
+ params.virtual_surface_size / 2,
+ ),
+ compare_cache: FastHashMap::default(),
+ native_surface: None,
+ device_position: DevicePoint::zero(),
+ tile_size_override: None,
+ external_surfaces: Vec::new(),
+ z_id_opaque: ZBufferId::invalid(),
+ external_native_surface_cache: FastHashMap::default(),
+ frame_id: FrameId::INVALID,
+ }
+ }
+
+ /// Reset this tile cache with the updated parameters from a new scene
+ /// that has arrived. This allows the tile cache to be retained across
+ /// new scenes.
+ pub fn prepare_for_new_scene(
+ &mut self,
+ params: TileCacheParams,
+ ) {
+ // We should only receive updated state for matching slice key
+ assert_eq!(self.slice, params.slice);
+
+ // Store the parameters from the scene builder for this slice. Other
+ // params in the tile cache are retained and reused, or are always
+ // updated during pre/post_update.
+ self.slice_flags = params.slice_flags;
+ self.spatial_node_index = params.spatial_node_index;
+ self.background_color = params.background_color;
+ self.shared_clips = params.shared_clips;
+ self.shared_clip_chain = params.shared_clip_chain;
+
+ // Since the slice flags may have changed, ensure we re-evaluate the
+ // appropriate tile size for this cache next update.
+ self.frames_until_size_eval = 0;
+ }
+
+ /// Destroy any manually managed resources before this picture cache is
+ /// destroyed, such as native compositor surfaces.
+ pub fn destroy(
+ self,
+ resource_cache: &mut ResourceCache,
+ ) {
+ if let Some(native_surface) = self.native_surface {
+ resource_cache.destroy_compositor_surface(native_surface.opaque);
+ resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+
+ for (_, external_surface) in self.external_native_surface_cache {
+ resource_cache.destroy_compositor_surface(external_surface.native_surface_id)
+ }
+ }
+
+ /// Returns true if this tile cache is considered opaque.
+ pub fn is_opaque(&self) -> bool {
+ // If known opaque due to background clear color and being the first slice.
+ // The background_color will only be Some(..) if this is the first slice.
+ match self.background_color {
+ Some(color) => color.a >= 1.0,
+ None => false
+ }
+ }
+
+ /// Get the tile coordinates for a given rectangle.
+ fn get_tile_coords_for_rect(
+ &self,
+ rect: &PictureRect,
+ ) -> (TileOffset, TileOffset) {
+ // Get the tile coordinates in the picture space.
+ let mut p0 = TileOffset::new(
+ (rect.origin.x / self.tile_size.width).floor() as i32,
+ (rect.origin.y / self.tile_size.height).floor() as i32,
+ );
+
+ let mut p1 = TileOffset::new(
+ ((rect.origin.x + rect.size.width) / self.tile_size.width).ceil() as i32,
+ ((rect.origin.y + rect.size.height) / self.tile_size.height).ceil() as i32,
+ );
+
+ // Clamp the tile coordinates here to avoid looping over irrelevant tiles later on.
+ p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+ p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
+ p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x);
+ p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y);
+
+ (p0, p1)
+ }
+
+ /// Update transforms, opacity, color bindings and tile rects.
+ pub fn pre_update(
+ &mut self,
+ pic_rect: PictureRect,
+ surface_index: SurfaceIndex,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ ) -> WorldRect {
+ self.external_surfaces.clear();
+ self.surface_index = surface_index;
+ self.local_rect = pic_rect;
+ self.local_clip_rect = PictureRect::max_rect();
+
+ // Opaque surfaces get the first z_id. Compositor surfaces then get
+ // allocated a z_id each. After all compositor surfaces are added,
+ // then we allocate a z_id for alpha tiles.
+ self.z_id_opaque = frame_state.composite_state.z_generator.next();
+
+ // Reset the opaque rect + subpixel mode, as they are calculated
+ // during the prim dependency checks.
+ self.backdrop = BackdropInfo::empty();
+
+ self.map_local_to_surface = SpaceMapper::new(
+ self.spatial_node_index,
+ pic_rect,
+ );
+ self.map_child_pic_to_surface = SpaceMapper::new(
+ self.spatial_node_index,
+ pic_rect,
+ );
+
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ // If there is a valid set of shared clips, build a clip chain instance for this,
+ // which will provide a local clip rect. This is useful for establishing things
+ // like whether the backdrop rect supplied by Gecko can be considered opaque.
+ if self.shared_clip_chain != ClipChainId::NONE {
+ let shared_clips = &mut frame_state.scratch.picture.clip_chain_ids;
+ shared_clips.clear();
+
+ let mut current_clip_chain_id = self.shared_clip_chain;
+ while current_clip_chain_id != ClipChainId::NONE {
+ shared_clips.push(current_clip_chain_id);
+ let clip_chain_node = &frame_state.clip_store.clip_chain_nodes[current_clip_chain_id.0 as usize];
+ current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+ }
+
+ frame_state.clip_store.set_active_clips(
+ LayoutRect::max_rect(),
+ self.spatial_node_index,
+ self.map_local_to_surface.ref_spatial_node_index,
+ &shared_clips,
+ frame_context.spatial_tree,
+ &mut frame_state.data_stores.clip,
+ );
+
+ let clip_chain_instance = frame_state.clip_store.build_clip_chain_instance(
+ pic_rect.cast_unit(),
+ &self.map_local_to_surface,
+ &pic_to_world_mapper,
+ frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ frame_context.global_device_pixel_scale,
+ &frame_context.global_screen_world_rect,
+ &mut frame_state.data_stores.clip,
+ true,
+ false,
+ );
+
+ // Ensure that if the entire picture cache is clipped out, the local
+ // clip rect is zero. This makes sure we don't register any occluders
+ // that are actually off-screen.
+ self.local_clip_rect = clip_chain_instance.map_or(PictureRect::zero(), |clip_chain_instance| {
+ clip_chain_instance.pic_clip_rect
+ });
+ }
+
+ // Advance the current frame ID counter for this picture cache (must be done
+ // after any retained prev state is taken above).
+ self.frame_id.advance();
+
+ // Notify the spatial node comparer that a new frame has started, and the
+ // current reference spatial node for this tile cache.
+ self.spatial_node_comparer.next_frame(self.spatial_node_index);
+
+ // At the start of the frame, step through each current compositor surface
+ // and mark it as unused. Later, this is used to free old compositor surfaces.
+ // TODO(gw): In future, we might make this more sophisticated - for example,
+ // retaining them for >1 frame if unused, or retaining them in some
+ // kind of pool to reduce future allocations.
+ for external_native_surface in self.external_native_surface_cache.values_mut() {
+ external_native_surface.used_this_frame = false;
+ }
+
+ // Only evaluate what tile size to use fairly infrequently, so that we don't end
+ // up constantly invalidating and reallocating tiles if the picture rect size is
+ // changing near a threshold value.
+ if self.frames_until_size_eval == 0 ||
+ self.tile_size_override != frame_context.config.tile_size_override {
+
+ // Work out what size tile is appropriate for this picture cache.
+ let desired_tile_size = match frame_context.config.tile_size_override {
+ Some(tile_size_override) => {
+ tile_size_override
+ }
+ None => {
+ if self.slice_flags.contains(SliceFlags::IS_SCROLLBAR) {
+ if pic_rect.size.width <= pic_rect.size.height {
+ TILE_SIZE_SCROLLBAR_VERTICAL
+ } else {
+ TILE_SIZE_SCROLLBAR_HORIZONTAL
+ }
+ } else {
+ frame_state.resource_cache.texture_cache.default_picture_tile_size()
+ }
+ }
+ };
+
+ // If the desired tile size has changed, then invalidate and drop any
+ // existing tiles.
+ if desired_tile_size != self.current_tile_size {
+ // Destroy any native surfaces on the tiles that will be dropped due
+ // to resizing.
+ if let Some(native_surface) = self.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ self.tiles.clear();
+ self.tile_rect = TileRect::zero();
+ self.current_tile_size = desired_tile_size;
+ }
+
+ // Reset counter until next evaluating the desired tile size. This is an
+ // arbitrary value.
+ self.frames_until_size_eval = 120;
+ self.tile_size_override = frame_context.config.tile_size_override;
+ }
+
+ // Map an arbitrary point in picture space to world space, to work out
+ // what the fractional translation is that's applied by this scroll root.
+ // TODO(gw): I'm not 100% sure this is right. At least, in future, we should
+ // make a specific API for this, and/or enforce that the picture
+ // cache transform only includes scale and/or translation (we
+ // already ensure it doesn't have perspective).
+ let world_origin = pic_to_world_mapper
+ .map(&PictureRect::new(PicturePoint::zero(), PictureSize::new(1.0, 1.0)))
+ .expect("bug: unable to map origin to world space")
+ .origin;
+
+ // Get the desired integer device coordinate
+ let device_origin = world_origin * frame_context.global_device_pixel_scale;
+ let desired_device_origin = device_origin.round();
+ self.device_position = desired_device_origin;
+ self.device_fract_offset = desired_device_origin - device_origin;
+
+ // Unmap from device space to world space rect
+ let ref_world_rect = WorldRect::new(
+ desired_device_origin / frame_context.global_device_pixel_scale,
+ WorldSize::new(1.0, 1.0),
+ );
+
+ // Unmap from world space to picture space; this should be the fractional offset
+ // required in picture space to align in device space
+ self.fract_offset = pic_to_world_mapper
+ .unmap(&ref_world_rect)
+ .expect("bug: unable to unmap ref world rect")
+ .origin
+ .to_vector();
+
+ // Do a hacky diff of opacity binding values from the last frame. This is
+ // used later on during tile invalidation tests.
+ let current_properties = frame_context.scene_properties.float_properties();
+ mem::swap(&mut self.opacity_bindings, &mut self.old_opacity_bindings);
+
+ self.opacity_bindings.clear();
+ for (id, value) in current_properties {
+ let changed = match self.old_opacity_bindings.get(id) {
+ Some(old_property) => !old_property.value.approx_eq(value),
+ None => true,
+ };
+ self.opacity_bindings.insert(*id, OpacityBindingInfo {
+ value: *value,
+ changed,
+ });
+ }
+
+ // Do a hacky diff of color binding values from the last frame. This is
+ // used later on during tile invalidation tests.
+ let current_properties = frame_context.scene_properties.color_properties();
+ mem::swap(&mut self.color_bindings, &mut self.old_color_bindings);
+
+ self.color_bindings.clear();
+ for (id, value) in current_properties {
+ let changed = match self.old_color_bindings.get(id) {
+ Some(old_property) => old_property.value != (*value).into(),
+ None => true,
+ };
+ self.color_bindings.insert(*id, ColorBindingInfo {
+ value: (*value).into(),
+ changed,
+ });
+ }
+
+ let world_tile_size = WorldSize::new(
+ self.current_tile_size.width as f32 / frame_context.global_device_pixel_scale.0,
+ self.current_tile_size.height as f32 / frame_context.global_device_pixel_scale.0,
+ );
+
+ // We know that this is an exact rectangle, since we (for now) only support tile
+ // caches where the scroll root is in the root coordinate system.
+ let local_tile_rect = pic_to_world_mapper
+ .unmap(&WorldRect::new(WorldPoint::zero(), world_tile_size))
+ .expect("bug: unable to get local tile rect");
+
+ self.tile_size = local_tile_rect.size;
+
+ let screen_rect_in_pic_space = pic_to_world_mapper
+ .unmap(&frame_context.global_screen_world_rect)
+ .expect("unable to unmap screen rect");
+
+ // Inflate the needed rect a bit, so that we retain tiles that we have drawn
+ // but have just recently gone off-screen. This means that we avoid re-drawing
+ // tiles if the user is scrolling up and down small amounts, at the cost of
+ // a bit of extra texture memory.
+ let desired_rect_in_pic_space = screen_rect_in_pic_space
+ .inflate(0.0, 1.0 * self.tile_size.height);
+
+ let needed_rect_in_pic_space = desired_rect_in_pic_space
+ .intersection(&pic_rect)
+ .unwrap_or_else(PictureRect::zero);
+
+ let p0 = needed_rect_in_pic_space.origin;
+ let p1 = needed_rect_in_pic_space.bottom_right();
+
+ let x0 = (p0.x / local_tile_rect.size.width).floor() as i32;
+ let x1 = (p1.x / local_tile_rect.size.width).ceil() as i32;
+
+ let y0 = (p0.y / local_tile_rect.size.height).floor() as i32;
+ let y1 = (p1.y / local_tile_rect.size.height).ceil() as i32;
+
+ let x_tiles = x1 - x0;
+ let y_tiles = y1 - y0;
+ let new_tile_rect = TileRect::new(
+ TileOffset::new(x0, y0),
+ TileSize::new(x_tiles, y_tiles),
+ );
+
+ // Determine whether the current bounds of the tile grid will exceed the
+ // bounds of the DC virtual surface, taking into account the current
+ // virtual offset. If so, we need to invalidate all tiles, and set up
+ // a new virtual offset, centered around the current tile grid.
+
+ if let CompositorKind::Native { virtual_surface_size, .. } = frame_context.config.compositor_kind {
+ // We only need to invalidate in this case if the underlying platform
+ // uses virtual surfaces.
+ if virtual_surface_size > 0 {
+ // Get the extremities of the tile grid after virtual offset is applied
+ let tx0 = self.virtual_offset.x + x0 * self.current_tile_size.width;
+ let ty0 = self.virtual_offset.y + y0 * self.current_tile_size.height;
+ let tx1 = self.virtual_offset.x + (x1+1) * self.current_tile_size.width;
+ let ty1 = self.virtual_offset.y + (y1+1) * self.current_tile_size.height;
+
+ let need_new_virtual_offset = tx0 < 0 ||
+ ty0 < 0 ||
+ tx1 >= virtual_surface_size ||
+ ty1 >= virtual_surface_size;
+
+ if need_new_virtual_offset {
+ // Calculate a new virtual offset, centered around the middle of the
+ // current tile grid. This means we won't need to invalidate and get
+ // a new offset for a long time!
+ self.virtual_offset = DeviceIntPoint::new(
+ (virtual_surface_size/2) - ((x0 + x1) / 2) * self.current_tile_size.width,
+ (virtual_surface_size/2) - ((y0 + y1) / 2) * self.current_tile_size.height,
+ );
+
+ // Invalidate all native tile surfaces. They will be re-allocated next time
+ // they are scheduled to be rasterized.
+ for tile in self.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ // TODO(gw): Add a new invalidation reason for virtual offset changing
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+ }
+
+ // Destroy the native virtual surfaces. They will be re-allocated next time a tile
+ // that references them is scheduled to draw.
+ if let Some(native_surface) = self.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+ }
+ }
+ }
+
+ // Rebuild the tile grid if the picture cache rect has changed.
+ if new_tile_rect != self.tile_rect {
+ let mut old_tiles = mem::replace(&mut self.tiles, FastHashMap::default());
+ self.tiles.reserve(new_tile_rect.size.area() as usize);
+
+ for y in y0 .. y1 {
+ for x in x0 .. x1 {
+ let key = TileOffset::new(x, y);
+ let tile = old_tiles
+ .remove(&key)
+ .unwrap_or_else(|| {
+ Box::new(Tile::new(key))
+ });
+ self.tiles.insert(key, tile);
+ }
+ }
+
+ // When old tiles that remain after the loop, dirty rects are not valid.
+ if !old_tiles.is_empty() {
+ frame_state.composite_state.dirty_rects_are_valid = false;
+ }
+
+ // Any old tiles that remain after the loop above are going to be dropped. For
+ // simple composite mode, the texture cache handle will expire and be collected
+ // by the texture cache. For native compositor mode, we need to explicitly
+ // invoke a callback to the client to destroy that surface.
+ frame_state.composite_state.destroy_native_tiles(
+ old_tiles.values_mut(),
+ frame_state.resource_cache,
+ );
+ }
+
+ // This is duplicated information from tile_rect, but cached here to avoid
+ // redundant calculations during get_tile_coords_for_rect
+ self.tile_bounds_p0 = TileOffset::new(x0, y0);
+ self.tile_bounds_p1 = TileOffset::new(x1, y1);
+ self.tile_rect = new_tile_rect;
+
+ let mut world_culling_rect = WorldRect::zero();
+
+ let ctx = TilePreUpdateContext {
+ pic_to_world_mapper,
+ fract_offset: self.fract_offset,
+ device_fract_offset: self.device_fract_offset,
+ background_color: self.background_color,
+ global_screen_world_rect: frame_context.global_screen_world_rect,
+ tile_size: self.tile_size,
+ frame_id: self.frame_id,
+ };
+
+ // Pre-update each tile
+ for tile in self.tiles.values_mut() {
+ tile.pre_update(&ctx);
+
+ // Only include the tiles that are currently in view into the world culling
+ // rect. This is a very important optimization for a couple of reasons:
+ // (1) Primitives that intersect with tiles in the grid that are not currently
+ // visible can be skipped from primitive preparation, clip chain building
+ // and tile dependency updates.
+ // (2) When we need to allocate an off-screen surface for a child picture (for
+ // example a CSS filter) we clip the size of the GPU surface to the world
+ // culling rect below (to ensure we draw enough of it to be sampled by any
+ // tiles that reference it). Making the world culling rect only affected
+ // by visible tiles (rather than the entire virtual tile display port) can
+ // result in allocating _much_ smaller GPU surfaces for cases where the
+ // true off-screen surface size is very large.
+ if tile.is_visible {
+ world_culling_rect = world_culling_rect.union(&tile.world_tile_rect);
+ }
+ }
+
+ // If compositor mode is changed, need to drop all incompatible tiles.
+ match frame_context.config.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ for tile in self.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ }
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+
+ if let Some(native_surface) = self.native_surface.take() {
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque);
+ frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha);
+ }
+
+ for (_, external_surface) in self.external_native_surface_cache.drain() {
+ frame_state.resource_cache.destroy_compositor_surface(external_surface.native_surface_id)
+ }
+ }
+ CompositorKind::Native { .. } => {
+ // This could hit even when compositor mode is not changed,
+ // then we need to check if there are incompatible tiles.
+ for tile in self.tiles.values_mut() {
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { .. }, .. }) = tile.surface {
+ tile.surface = None;
+ // Invalidate the entire tile to force a redraw.
+ tile.invalidate(None, InvalidationReason::CompositorKindChanged);
+ }
+ }
+ }
+ }
+
+ world_culling_rect
+ }
+
+ fn can_promote_to_surface(
+ &mut self,
+ flags: PrimitiveFlags,
+ prim_clip_chain: &ClipChainInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ on_picture_surface: bool,
+ frame_context: &FrameVisibilityContext,
+ ) -> SurfacePromotionResult {
+ // Check if this primitive _wants_ to be promoted to a compositor surface.
+ if !flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // For now, only support a small (arbitrary) number of compositor surfaces.
+ if self.external_surfaces.len() == MAX_COMPOSITOR_SURFACES {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // If a complex clip is being applied to this primitive, it can't be
+ // promoted directly to a compositor surface (we might be able to
+ // do this in limited cases in future, some native compositors do
+ // support rounded rect clips, for example)
+ if prim_clip_chain.needs_mask {
+ return SurfacePromotionResult::Failed;
+ }
+
+ // If not on the same surface as the picture cache, it has some kind of
+ // complex effect (such as a filter, mix-blend-mode or 3d transform).
+ if !on_picture_surface {
+ return SurfacePromotionResult::Failed;
+ }
+
+ let mapper : SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ prim_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ &frame_context.spatial_tree);
+ let transform = mapper.get_transform();
+ if !transform.is_2d_scale_translation() {
+ return SurfacePromotionResult::Failed;
+ }
+ if transform.m11 < 0.0 {
+ return SurfacePromotionResult::Failed;
+ }
+
+ SurfacePromotionResult::Success {
+ flip_y: transform.m22 < 0.0,
+ }
+ }
+
+ fn setup_compositor_surfaces_yuv(
+ &mut self,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ prim_rect: PictureRect,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ frame_context: &FrameVisibilityContext,
+ image_dependencies: &[ImageDependency;3],
+ api_keys: &[ImageKey; 3],
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ image_rendering: ImageRendering,
+ color_depth: ColorDepth,
+ color_space: YuvColorSpace,
+ format: YuvFormat,
+ ) -> bool {
+ self.setup_compositor_surfaces_impl(
+ prim_info,
+ flags,
+ prim_rect,
+ local_prim_rect,
+ prim_spatial_node_index,
+ frame_context,
+ ExternalSurfaceDependency::Yuv {
+ image_dependencies: *image_dependencies,
+ color_space,
+ format,
+ rescale: color_depth.rescaling_factor(),
+ },
+ api_keys,
+ resource_cache,
+ composite_state,
+ image_rendering,
+ )
+ }
+
+ fn setup_compositor_surfaces_rgb(
+ &mut self,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ prim_rect: PictureRect,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ frame_context: &FrameVisibilityContext,
+ image_dependency: ImageDependency,
+ api_key: ImageKey,
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ image_rendering: ImageRendering,
+ flip_y: bool,
+ ) -> bool {
+ let mut api_keys = [ImageKey::DUMMY; 3];
+ api_keys[0] = api_key;
+ self.setup_compositor_surfaces_impl(
+ prim_info,
+ flags,
+ prim_rect,
+ local_prim_rect,
+ prim_spatial_node_index,
+ frame_context,
+ ExternalSurfaceDependency::Rgb {
+ image_dependency,
+ flip_y,
+ },
+ &api_keys,
+ resource_cache,
+ composite_state,
+ image_rendering,
+ )
+ }
+
+ // returns false if composition is not available for this surface,
+ // and the non-compositor path should be used to draw it instead.
+ fn setup_compositor_surfaces_impl(
+ &mut self,
+ prim_info: &mut PrimitiveDependencyInfo,
+ flags: PrimitiveFlags,
+ prim_rect: PictureRect,
+ local_prim_rect: LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ frame_context: &FrameVisibilityContext,
+ dependency: ExternalSurfaceDependency,
+ api_keys: &[ImageKey; 3],
+ resource_cache: &mut ResourceCache,
+ composite_state: &mut CompositeState,
+ image_rendering: ImageRendering,
+ ) -> bool {
+ prim_info.is_compositor_surface = true;
+
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let world_clip_rect = pic_to_world_mapper
+ .map(&prim_info.prim_clip_box.to_rect())
+ .expect("bug: unable to map clip to world space");
+
+ let is_visible = world_clip_rect.intersects(&frame_context.global_screen_world_rect);
+ if !is_visible {
+ return true;
+ }
+
+ let world_rect = pic_to_world_mapper
+ .map(&prim_rect)
+ .expect("bug: unable to map the primitive to world space");
+ let device_rect = (world_rect * frame_context.global_device_pixel_scale).round();
+
+ // TODO(gw): Is there any case where if the primitive ends up on a fractional
+ // boundary we want to _skip_ promoting to a compositor surface and
+ // draw it as part of the content?
+ let (surface_rect, transform) = match composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ (device_rect, Transform3D::identity())
+ }
+ CompositorKind::Native { .. } => {
+ // If we have a Native Compositor, then we can support doing the transformation
+ // as part of compositing. Use the local prim rect for the external surface, and
+ // compute the full local to device transform to provide to the compositor.
+ let surface_to_world_mapper : SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ prim_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+ let prim_origin = Vector3D::new(local_prim_rect.origin.x, local_prim_rect.origin.y, 0.0);
+ let world_to_device_scale = Transform3D::from_scale(frame_context.global_device_pixel_scale);
+ let transform = surface_to_world_mapper.get_transform().pre_translate(prim_origin).then(&world_to_device_scale);
+
+ (local_prim_rect.cast_unit(), transform)
+ }
+ };
+
+ let clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round();
+
+ if surface_rect.size.width >= MAX_COMPOSITOR_SURFACES_SIZE ||
+ surface_rect.size.height >= MAX_COMPOSITOR_SURFACES_SIZE {
+ return false;
+ }
+
+ // If this primitive is an external image, and supports being used
+ // directly by a native compositor, then lookup the external image id
+ // so we can pass that through.
+ let external_image_id = if flags.contains(PrimitiveFlags::SUPPORTS_EXTERNAL_COMPOSITOR_SURFACE) {
+ resource_cache.get_image_properties(api_keys[0])
+ .and_then(|properties| properties.external_image)
+ .and_then(|image| Some(image.id))
+ } else {
+ None
+ };
+
+ // When using native compositing, we need to find an existing native surface
+ // handle to use, or allocate a new one. For existing native surfaces, we can
+ // also determine whether this needs to be updated, depending on whether the
+ // image generation(s) of the planes have changed since last composite.
+ let (native_surface_id, update_params) = match composite_state.compositor_kind {
+ CompositorKind::Draw { .. } => {
+ (None, None)
+ }
+ CompositorKind::Native { .. } => {
+ let native_surface_size = surface_rect.size.round().to_i32();
+
+ let key = ExternalNativeSurfaceKey {
+ image_keys: *api_keys,
+ size: native_surface_size,
+ is_external_surface: external_image_id.is_some(),
+ };
+
+ let native_surface = self.external_native_surface_cache
+ .entry(key)
+ .or_insert_with(|| {
+ // No existing surface, so allocate a new compositor surface.
+ let native_surface_id = match external_image_id {
+ Some(_external_image) => {
+ // If we have a suitable external image, then create an external
+ // surface to attach to.
+ resource_cache.create_compositor_external_surface(true)
+ }
+ None => {
+ // Otherwise create a normal compositor surface and a single
+ // compositor tile that covers the entire surface.
+ let native_surface_id =
+ resource_cache.create_compositor_surface(
+ DeviceIntPoint::zero(),
+ native_surface_size,
+ true,
+ );
+
+ let tile_id = NativeTileId {
+ surface_id: native_surface_id,
+ x: 0,
+ y: 0,
+ };
+ resource_cache.create_compositor_tile(tile_id);
+
+ native_surface_id
+ }
+ };
+
+ ExternalNativeSurface {
+ used_this_frame: true,
+ native_surface_id,
+ image_dependencies: [ImageDependency::INVALID; 3],
+ }
+ });
+
+ // Mark that the surface is referenced this frame so that the
+ // backing native surface handle isn't freed.
+ native_surface.used_this_frame = true;
+
+ let update_params = match external_image_id {
+ Some(external_image) => {
+ // If this is an external image surface, then there's no update
+ // to be done. Just attach the current external image to the surface
+ // and we're done.
+ resource_cache.attach_compositor_external_image(
+ native_surface.native_surface_id,
+ external_image,
+ );
+ None
+ }
+ None => {
+ // If the image dependencies match, there is no need to update
+ // the backing native surface.
+ match dependency {
+ ExternalSurfaceDependency::Yuv{ image_dependencies, .. } => {
+ if image_dependencies == native_surface.image_dependencies {
+ None
+ } else {
+ Some(native_surface_size)
+ }
+ },
+ ExternalSurfaceDependency::Rgb{ image_dependency, .. } => {
+ if image_dependency == native_surface.image_dependencies[0] {
+ None
+ } else {
+ Some(native_surface_size)
+ }
+ },
+ }
+ }
+ };
+
+ (Some(native_surface.native_surface_id), update_params)
+ }
+ };
+
+ // Each compositor surface allocates a unique z-id
+ self.external_surfaces.push(ExternalSurfaceDescriptor {
+ local_rect: prim_info.prim_clip_box.to_rect(),
+ local_clip_rect: prim_info.prim_clip_box.to_rect(),
+ dependency,
+ image_rendering,
+ device_rect,
+ surface_rect,
+ clip_rect,
+ transform: transform.cast_unit(),
+ z_id: composite_state.z_generator.next(),
+ native_surface_id,
+ update_params,
+ });
+
+ true
+ }
+
+ /// Update the dependencies for each tile for a given primitive instance.
+ pub fn update_prim_dependencies(
+ &mut self,
+ prim_instance: &mut PrimitiveInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ local_prim_rect: LayoutRect,
+ frame_context: &FrameVisibilityContext,
+ data_stores: &DataStores,
+ clip_store: &ClipStore,
+ pictures: &[PicturePrimitive],
+ resource_cache: &mut ResourceCache,
+ color_bindings: &ColorBindingStorage,
+ surface_stack: &[SurfaceIndex],
+ composite_state: &mut CompositeState,
+ ) {
+ // This primitive exists on the last element on the current surface stack.
+ profile_scope!("update_prim_dependencies");
+ let prim_surface_index = *surface_stack.last().unwrap();
+ let prim_clip_chain = &prim_instance.vis.clip_chain;
+
+ self.map_local_to_surface.set_target_spatial_node(
+ prim_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // Map the primitive local rect into picture space.
+ let prim_rect = match self.map_local_to_surface.map(&local_prim_rect) {
+ Some(rect) => rect,
+ None => return,
+ };
+
+ // If the rect is invalid, no need to create dependencies.
+ if prim_rect.size.is_empty() {
+ return;
+ }
+
+ // If the primitive is directly drawn onto this picture cache surface, then
+ // the pic_clip_rect is in the same space. If not, we need to map it from
+ // the surface space into the picture cache space.
+ let on_picture_surface = prim_surface_index == self.surface_index;
+ let pic_clip_rect = if on_picture_surface {
+ prim_clip_chain.pic_clip_rect
+ } else {
+ // We want to get the rect in the tile cache surface space that this primitive
+ // occupies, in order to enable correct invalidation regions. Each surface
+ // that exists in the chain between this primitive and the tile cache surface
+ // may have an arbitrary inflation factor (for example, in the case of a series
+ // of nested blur elements). To account for this, step through the current
+ // surface stack, mapping the primitive rect into each surface space, including
+ // the inflation factor from each intermediate surface.
+ let mut current_pic_clip_rect = prim_clip_chain.pic_clip_rect;
+ let mut current_spatial_node_index = frame_context
+ .surfaces[prim_surface_index.0]
+ .surface_spatial_node_index;
+
+ for surface_index in surface_stack.iter().rev() {
+ let surface = &frame_context.surfaces[surface_index.0];
+
+ let map_local_to_surface = SpaceMapper::new_with_target(
+ surface.surface_spatial_node_index,
+ current_spatial_node_index,
+ surface.rect,
+ frame_context.spatial_tree,
+ );
+
+ // Map the rect into the parent surface, and inflate if this surface requires
+ // it. If the rect can't be mapping (e.g. due to an invalid transform) then
+ // just bail out from the dependencies and cull this primitive.
+ current_pic_clip_rect = match map_local_to_surface.map(&current_pic_clip_rect) {
+ Some(rect) => {
+ rect.inflate(surface.inflation_factor, surface.inflation_factor)
+ }
+ None => {
+ return;
+ }
+ };
+
+ current_spatial_node_index = surface.surface_spatial_node_index;
+ }
+
+ current_pic_clip_rect
+ };
+
+ // Get the tile coordinates in the picture space.
+ let (p0, p1) = self.get_tile_coords_for_rect(&pic_clip_rect);
+
+ // If the primitive is outside the tiling rects, it's known to not
+ // be visible.
+ if p0.x == p1.x || p0.y == p1.y {
+ return;
+ }
+
+ // Build the list of resources that this primitive has dependencies on.
+ let mut prim_info = PrimitiveDependencyInfo::new(
+ prim_instance.uid(),
+ pic_clip_rect.to_box2d(),
+ );
+
+ // Include the prim spatial node, if differs relative to cache root.
+ if prim_spatial_node_index != self.spatial_node_index {
+ prim_info.spatial_nodes.push(prim_spatial_node_index);
+ }
+
+ // If there was a clip chain, add any clip dependencies to the list for this tile.
+ let clip_instances = &clip_store
+ .clip_node_instances[prim_clip_chain.clips_range.to_range()];
+ for clip_instance in clip_instances {
+ prim_info.clips.push(clip_instance.handle.uid());
+
+ // If the clip has the same spatial node, the relative transform
+ // will always be the same, so there's no need to depend on it.
+ if clip_instance.spatial_node_index != self.spatial_node_index
+ && !prim_info.spatial_nodes.contains(&clip_instance.spatial_node_index) {
+ prim_info.spatial_nodes.push(clip_instance.spatial_node_index);
+ }
+ }
+
+ // Certain primitives may select themselves to be a backdrop candidate, which is
+ // then applied below.
+ let mut backdrop_candidate = None;
+
+
+ // For pictures, we don't (yet) know the valid clip rect, so we can't correctly
+ // use it to calculate the local bounding rect for the tiles. If we include them
+ // then we may calculate a bounding rect that is too large, since it won't include
+ // the clip bounds of the picture. Excluding them from the bounding rect here
+ // fixes any correctness issues (the clips themselves are considered when we
+ // consider the bounds of the primitives that are *children* of the picture),
+ // however it does potentially result in some un-necessary invalidations of a
+ // tile (in cases where the picture local rect affects the tile, but the clip
+ // rect eventually means it doesn't affect that tile).
+ // TODO(gw): Get picture clips earlier (during the initial picture traversal
+ // pass) so that we can calculate these correctly.
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index,.. } => {
+ // Pictures can depend on animated opacity bindings.
+ let pic = &pictures[pic_index.0];
+ if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.requested_composite_mode {
+ prim_info.opacity_bindings.push(binding.into());
+ }
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, color_binding_index, .. } => {
+ // Rectangles can only form a backdrop candidate if they are known opaque.
+ // TODO(gw): We could resolve the opacity binding here, but the common
+ // case for background rects is that they don't have animated opacity.
+ let color = match data_stores.prim[data_handle].kind {
+ PrimitiveTemplateKind::Rectangle { color, .. } => {
+ frame_context.scene_properties.resolve_color(&color)
+ }
+ _ => unreachable!(),
+ };
+ if color.a >= 1.0 {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: Some(BackdropKind::Color { color }),
+ });
+ }
+
+ if color_binding_index != ColorBindingIndex::INVALID {
+ prim_info.color_binding = Some(color_bindings[color_binding_index].into());
+ }
+ }
+ PrimitiveInstanceKind::Image { data_handle, ref mut is_compositor_surface, .. } => {
+ let image_key = &data_stores.image[data_handle];
+ let image_data = &image_key.kind;
+
+ let mut promote_to_surface = false;
+ let mut promote_with_flip_y = false;
+ match self.can_promote_to_surface(image_key.common.flags,
+ prim_clip_chain,
+ prim_spatial_node_index,
+ on_picture_surface,
+ frame_context) {
+ SurfacePromotionResult::Failed => {
+ }
+ SurfacePromotionResult::Success{flip_y} => {
+ promote_to_surface = true;
+ promote_with_flip_y = flip_y;
+ }
+ }
+
+ if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) {
+ // For an image to be a possible opaque backdrop, it must:
+ // - Have a valid, opaque image descriptor
+ // - Not use tiling (since they can fail to draw)
+ // - Not having any spacing / padding
+ if image_properties.descriptor.is_opaque() &&
+ image_properties.tiling.is_none() &&
+ image_data.tile_spacing == LayoutSize::zero() {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: None,
+ });
+ }
+ }
+
+ if promote_to_surface {
+ promote_to_surface = self.setup_compositor_surfaces_rgb(
+ &mut prim_info,
+ image_key.common.flags,
+ prim_rect,
+ local_prim_rect,
+ prim_spatial_node_index,
+ frame_context,
+ ImageDependency {
+ key: image_data.key,
+ generation: resource_cache.get_image_generation(image_data.key),
+ },
+ image_data.key,
+ resource_cache,
+ composite_state,
+ image_data.image_rendering,
+ promote_with_flip_y,
+ );
+ }
+
+ if !promote_to_surface {
+ prim_info.images.push(ImageDependency {
+ key: image_data.key,
+ generation: resource_cache.get_image_generation(image_data.key),
+ });
+ }
+
+ *is_compositor_surface = promote_to_surface;
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, ref mut is_compositor_surface, .. } => {
+ let prim_data = &data_stores.yuv_image[data_handle];
+ let mut promote_to_surface = match self.can_promote_to_surface(
+ prim_data.common.flags,
+ prim_clip_chain,
+ prim_spatial_node_index,
+ on_picture_surface,
+ frame_context) {
+ SurfacePromotionResult::Failed => false,
+ SurfacePromotionResult::Success{flip_y} => !flip_y,
+ };
+
+ // TODO(gw): When we support RGBA images for external surfaces, we also
+ // need to check if opaque (YUV images are implicitly opaque).
+
+ // If this primitive is being promoted to a surface, construct an external
+ // surface descriptor for use later during batching and compositing. We only
+ // add the image keys for this primitive as a dependency if this is _not_
+ // a promoted surface, since we don't want the tiles to invalidate when the
+ // video content changes, if it's a compositor surface!
+ if promote_to_surface {
+ // Build dependency for each YUV plane, with current image generation for
+ // later detection of when the composited surface has changed.
+ let mut image_dependencies = [ImageDependency::INVALID; 3];
+ for (key, dep) in prim_data.kind.yuv_key.iter().cloned().zip(image_dependencies.iter_mut()) {
+ *dep = ImageDependency {
+ key,
+ generation: resource_cache.get_image_generation(key),
+ }
+ }
+
+ promote_to_surface = self.setup_compositor_surfaces_yuv(
+ &mut prim_info,
+ prim_data.common.flags,
+ prim_rect,
+ local_prim_rect,
+ prim_spatial_node_index,
+ frame_context,
+ &image_dependencies,
+ &prim_data.kind.yuv_key,
+ resource_cache,
+ composite_state,
+ prim_data.kind.image_rendering,
+ prim_data.kind.color_depth,
+ prim_data.kind.color_space,
+ prim_data.kind.format,
+ );
+ }
+
+ if !promote_to_surface {
+ prim_info.images.extend(
+ prim_data.kind.yuv_key.iter().map(|key| {
+ ImageDependency {
+ key: *key,
+ generation: resource_cache.get_image_generation(*key),
+ }
+ })
+ );
+ }
+
+ // Store on the YUV primitive instance whether this is a promoted surface.
+ // This is used by the batching code to determine whether to draw the
+ // image to the content tiles, or just a transparent z-write.
+ *is_compositor_surface = promote_to_surface;
+
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let border_data = &data_stores.image_border[data_handle].kind;
+ prim_info.images.push(ImageDependency {
+ key: border_data.request.key,
+ generation: resource_cache.get_image_generation(border_data.request.key),
+ });
+ }
+ PrimitiveInstanceKind::Clear { .. } => {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: Some(BackdropKind::Clear),
+ });
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.linear_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: None,
+ });
+ }
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.conic_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: None,
+ });
+ }
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let gradient_data = &data_stores.radial_grad[data_handle];
+ if gradient_data.stops_opacity.is_opaque
+ && gradient_data.tile_spacing == LayoutSize::zero()
+ {
+ backdrop_candidate = Some(BackdropInfo {
+ opaque_rect: pic_clip_rect,
+ kind: None,
+ });
+ }
+ }
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::Backdrop { .. } => {
+ // These don't contribute dependencies
+ }
+ };
+
+ // If this primitive considers itself a backdrop candidate, apply further
+ // checks to see if it matches all conditions to be a backdrop.
+ let mut vis_flags = PrimitiveVisibilityFlags::empty();
+
+ if let Some(backdrop_candidate) = backdrop_candidate {
+ let is_suitable_backdrop = match backdrop_candidate.kind {
+ Some(BackdropKind::Clear) => {
+ // Clear prims are special - they always end up in their own slice,
+ // and always set the backdrop. In future, we hope to completely
+ // remove clear prims, since they don't integrate with the compositing
+ // system cleanly.
+ true
+ }
+ Some(BackdropKind::Color { .. }) | None => {
+ // Check a number of conditions to see if we can consider this
+ // primitive as an opaque backdrop rect. Several of these are conservative
+ // checks and could be relaxed in future. However, these checks
+ // are quick and capture the common cases of background rects and images.
+ // Specifically, we currently require:
+ // - The primitive is on the main picture cache surface.
+ // - Same coord system as picture cache (ensures rects are axis-aligned).
+ // - No clip masks exist.
+ let same_coord_system = {
+ let prim_spatial_node = &frame_context.spatial_tree
+ .spatial_nodes[prim_spatial_node_index.0 as usize];
+ let surface_spatial_node = &frame_context.spatial_tree
+ .spatial_nodes[self.spatial_node_index.0 as usize];
+
+ prim_spatial_node.coordinate_system_id == surface_spatial_node.coordinate_system_id
+ };
+
+ same_coord_system && on_picture_surface
+ }
+ };
+
+ if is_suitable_backdrop
+ && self.external_surfaces.is_empty()
+ && !prim_clip_chain.needs_mask {
+
+ if backdrop_candidate.opaque_rect.contains_rect(&self.backdrop.opaque_rect) {
+ self.backdrop.opaque_rect = backdrop_candidate.opaque_rect;
+ }
+
+ if let Some(kind) = backdrop_candidate.kind {
+ if backdrop_candidate.opaque_rect.contains_rect(&self.local_rect) {
+ // If we have a color backdrop, mark the visibility flags
+ // of the primitive so it is skipped during batching (and
+ // also clears any previous primitives).
+ if let BackdropKind::Color { .. } = kind {
+ vis_flags |= PrimitiveVisibilityFlags::IS_BACKDROP;
+ }
+
+ self.backdrop.kind = Some(kind);
+ }
+ }
+ }
+ }
+
+ // Record any new spatial nodes in the used list.
+ for spatial_node_index in &prim_info.spatial_nodes {
+ self.spatial_node_comparer.register_used_transform(
+ *spatial_node_index,
+ self.frame_id,
+ frame_context.spatial_tree,
+ );
+ }
+
+ // Truncate the lengths of dependency arrays to the max size we can handle.
+ // Any arrays this size or longer will invalidate every frame.
+ prim_info.clips.truncate(MAX_PRIM_SUB_DEPS);
+ prim_info.opacity_bindings.truncate(MAX_PRIM_SUB_DEPS);
+ prim_info.spatial_nodes.truncate(MAX_PRIM_SUB_DEPS);
+ prim_info.images.truncate(MAX_PRIM_SUB_DEPS);
+
+ // Normalize the tile coordinates before adding to tile dependencies.
+ // For each affected tile, mark any of the primitive dependencies.
+ for y in p0.y .. p1.y {
+ for x in p0.x .. p1.x {
+ // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile?
+ let key = TileOffset::new(x, y);
+ let tile = self.tiles.get_mut(&key).expect("bug: no tile");
+
+ tile.add_prim_dependency(&prim_info);
+ }
+ }
+
+ prim_instance.vis.flags = vis_flags;
+ prim_instance.vis.state = VisibilityState::Coarse {
+ rect_in_pic_space: pic_clip_rect,
+ };
+ }
+
+ /// Print debug information about this picture cache to a tree printer.
+ fn print(&self) {
+ // TODO(gw): This initial implementation is very basic - just printing
+ // the picture cache state to stdout. In future, we can
+ // make this dump each frame to a file, and produce a report
+ // stating which frames had invalidations. This will allow
+ // diff'ing the invalidation states in a visual tool.
+ let mut pt = PrintTree::new("Picture Cache");
+
+ pt.new_level(format!("Slice {:?}", self.slice));
+
+ pt.add_item(format!("fract_offset: {:?}", self.fract_offset));
+ pt.add_item(format!("background_color: {:?}", self.background_color));
+
+ for y in self.tile_bounds_p0.y .. self.tile_bounds_p1.y {
+ for x in self.tile_bounds_p0.x .. self.tile_bounds_p1.x {
+ let key = TileOffset::new(x, y);
+ let tile = &self.tiles[&key];
+ tile.print(&mut pt);
+ }
+ }
+
+ pt.end_level();
+ }
+
+ fn calculate_subpixel_mode(&self) -> SubpixelMode {
+ // If the overall tile cache is known opaque, subpixel AA is allowed everywhere
+ if self.is_opaque() {
+ return SubpixelMode::Allow;
+ }
+
+ // If we didn't find any valid opaque backdrop, no subpixel AA allowed
+ if self.backdrop.opaque_rect.is_empty() {
+ return SubpixelMode::Deny;
+ }
+
+ // If the opaque backdrop rect covers the entire tile cache surface,
+ // we can allow subpixel AA anywhere, skipping the per-text-run tests
+ // later on during primitive preparation.
+ if self.backdrop.opaque_rect.contains_rect(&self.local_rect) {
+ return SubpixelMode::Allow;
+ }
+
+ // If none of the simple cases above match, we need to build a list
+ // of excluded rects (compositor surfaces) and a valid inclusion rect
+ // (known opaque area) where we can support subpixel AA.
+ // TODO(gw): In future, it may make sense to have > 1 inclusion rect,
+ // but this handles the common cases.
+ // TODO(gw): If a text run gets animated such that it's moving in a way that is
+ // sometimes intersecting with the video rect, this can result in subpixel
+ // AA flicking on/off for that text run. It's probably very rare, but
+ // something we should handle in future.
+
+ let excluded_rects = self.external_surfaces
+ .iter()
+ .map(|s| {
+ s.local_rect
+ })
+ .collect();
+
+ SubpixelMode::Conditional {
+ allowed_rect: self.backdrop.opaque_rect,
+ excluded_rects,
+ }
+ }
+
+ /// Apply any updates after prim dependency updates. This applies
+ /// any late tile invalidations, and sets up the dirty rect and
+ /// set of tile blits.
+ pub fn post_update(
+ &mut self,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ ) {
+ self.dirty_region.reset(self.spatial_node_index);
+ self.subpixel_mode = self.calculate_subpixel_mode();
+
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ // Register the opaque region of this tile cache as an occluder, which
+ // is used later in the frame to occlude other tiles.
+ if !self.backdrop.opaque_rect.is_empty() {
+ let backdrop_rect = self.backdrop.opaque_rect
+ .intersection(&self.local_rect)
+ .and_then(|r| {
+ r.intersection(&self.local_clip_rect)
+ });
+
+ if let Some(backdrop_rect) = backdrop_rect {
+ let world_backdrop_rect = map_pic_to_world
+ .map(&backdrop_rect)
+ .expect("bug: unable to map backdrop to world space");
+
+ // Since we register the entire backdrop rect, use the opaque z-id for the
+ // picture cache slice.
+ frame_state.composite_state.register_occluder(
+ self.z_id_opaque,
+ world_backdrop_rect,
+ );
+ }
+ }
+
+ // Register any external compositor surfaces as potential occluders. This
+ // is especially useful when viewing video in full-screen mode, as it is
+ // able to occlude every background tile (avoiding allocation, rasterizion
+ // and compositing).
+ for external_surface in &self.external_surfaces {
+ let local_surface_rect = external_surface.local_rect
+ .intersection(&external_surface.local_clip_rect)
+ .and_then(|r| {
+ r.intersection(&self.local_clip_rect)
+ });
+
+ if let Some(local_surface_rect) = local_surface_rect {
+ let world_surface_rect = map_pic_to_world
+ .map(&local_surface_rect)
+ .expect("bug: unable to map external surface to world space");
+
+ frame_state.composite_state.register_occluder(
+ external_surface.z_id,
+ world_surface_rect,
+ );
+ }
+ }
+
+ // A simple GC of the native external surface cache, to remove and free any
+ // surfaces that were not referenced during the update_prim_dependencies pass.
+ self.external_native_surface_cache.retain(|_, surface| {
+ if !surface.used_this_frame {
+ frame_state.resource_cache.destroy_compositor_surface(surface.native_surface_id);
+ }
+
+ surface.used_this_frame
+ });
+
+ // Detect if the picture cache was scrolled or scaled. In this case,
+ // the device space dirty rects aren't applicable (until we properly
+ // integrate with OS compositors that can handle scrolling slices).
+ let root_transform = frame_context
+ .spatial_tree
+ .get_relative_transform(
+ self.spatial_node_index,
+ ROOT_SPATIAL_NODE_INDEX,
+ );
+ let root_transform = match root_transform {
+ CoordinateSpaceMapping::Local => ScaleOffset::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset,
+ CoordinateSpaceMapping::Transform(..) => panic!("bug: picture caches don't support complex transforms"),
+ };
+ const EPSILON: f32 = 0.001;
+ let root_translation_changed =
+ !root_transform.offset.x.approx_eq_eps(&self.root_transform.offset.x, &EPSILON) ||
+ !root_transform.offset.y.approx_eq_eps(&self.root_transform.offset.y, &EPSILON);
+ let root_scale_changed =
+ !root_transform.scale.x.approx_eq_eps(&self.root_transform.scale.x, &EPSILON) ||
+ !root_transform.scale.y.approx_eq_eps(&self.root_transform.scale.y, &EPSILON);
+
+ if root_translation_changed || root_scale_changed || frame_context.config.force_invalidation {
+ self.root_transform = root_transform;
+ frame_state.composite_state.dirty_rects_are_valid = false;
+ }
+
+ let pic_to_world_mapper = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ self.spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ // All compositor surfaces have allocated a z_id, so reserve a z_id for alpha tiles.
+ let z_id_alpha = frame_state.composite_state.z_generator.next();
+
+ let ctx = TilePostUpdateContext {
+ pic_to_world_mapper,
+ global_device_pixel_scale: frame_context.global_device_pixel_scale,
+ local_clip_rect: self.local_clip_rect,
+ backdrop: self.backdrop,
+ opacity_bindings: &self.opacity_bindings,
+ color_bindings: &self.color_bindings,
+ current_tile_size: self.current_tile_size,
+ local_rect: self.local_rect,
+ external_surfaces: &self.external_surfaces,
+ z_id_opaque: self.z_id_opaque,
+ z_id_alpha,
+ invalidate_all: root_scale_changed || frame_context.config.force_invalidation,
+ };
+
+ let mut state = TilePostUpdateState {
+ resource_cache: frame_state.resource_cache,
+ composite_state: frame_state.composite_state,
+ compare_cache: &mut self.compare_cache,
+ spatial_node_comparer: &mut self.spatial_node_comparer,
+ };
+
+ // Step through each tile and invalidate if the dependencies have changed. Determine
+ // the current opacity setting and whether it's changed.
+ for tile in self.tiles.values_mut() {
+ tile.post_update(&ctx, &mut state, frame_context);
+ }
+ }
+}
+
+pub struct PictureScratchBuffer {
+ surface_stack: Vec<SurfaceIndex>,
+ picture_stack: Vec<PictureInfo>,
+ clip_chain_ids: Vec<ClipChainId>,
+}
+
+impl Default for PictureScratchBuffer {
+ fn default() -> Self {
+ PictureScratchBuffer {
+ surface_stack: Vec::new(),
+ picture_stack: Vec::new(),
+ clip_chain_ids: Vec::new(),
+ }
+ }
+}
+
+impl PictureScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.surface_stack.clear();
+ self.picture_stack.clear();
+ self.clip_chain_ids.clear();
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.surface_stack);
+ recycler.recycle_vec(&mut self.picture_stack);
+ }
+ }
+
+/// Maintains a stack of picture and surface information, that
+/// is used during the initial picture traversal.
+pub struct PictureUpdateState<'a> {
+ surfaces: &'a mut Vec<SurfaceInfo>,
+ surface_stack: Vec<SurfaceIndex>,
+ picture_stack: Vec<PictureInfo>,
+}
+
+impl<'a> PictureUpdateState<'a> {
+ pub fn update_all(
+ buffers: &mut PictureScratchBuffer,
+ surfaces: &'a mut Vec<SurfaceInfo>,
+ pic_index: PictureIndex,
+ picture_primitives: &mut [PicturePrimitive],
+ frame_context: &FrameBuildingContext,
+ gpu_cache: &mut GpuCache,
+ clip_store: &ClipStore,
+ data_stores: &mut DataStores,
+ ) {
+ profile_scope!("UpdatePictures");
+ profile_marker!("UpdatePictures");
+
+ let mut state = PictureUpdateState {
+ surfaces,
+ surface_stack: buffers.surface_stack.take().cleared(),
+ picture_stack: buffers.picture_stack.take().cleared(),
+ };
+
+ state.surface_stack.push(SurfaceIndex(0));
+
+ state.update(
+ pic_index,
+ picture_primitives,
+ frame_context,
+ gpu_cache,
+ clip_store,
+ data_stores,
+ );
+
+ buffers.surface_stack = state.surface_stack.take();
+ buffers.picture_stack = state.picture_stack.take();
+ }
+
+ /// Return the current surface
+ fn current_surface(&self) -> &SurfaceInfo {
+ &self.surfaces[self.surface_stack.last().unwrap().0]
+ }
+
+ /// Return the current surface (mutable)
+ fn current_surface_mut(&mut self) -> &mut SurfaceInfo {
+ &mut self.surfaces[self.surface_stack.last().unwrap().0]
+ }
+
+ /// Push a new surface onto the update stack.
+ fn push_surface(
+ &mut self,
+ surface: SurfaceInfo,
+ ) -> SurfaceIndex {
+ let surface_index = SurfaceIndex(self.surfaces.len());
+ self.surfaces.push(surface);
+ self.surface_stack.push(surface_index);
+ surface_index
+ }
+
+ /// Pop a surface on the way up the picture traversal
+ fn pop_surface(&mut self) -> SurfaceIndex{
+ self.surface_stack.pop().unwrap()
+ }
+
+ /// Push information about a picture on the update stack
+ fn push_picture(
+ &mut self,
+ info: PictureInfo,
+ ) {
+ self.picture_stack.push(info);
+ }
+
+ /// Pop the picture info off, on the way up the picture traversal
+ fn pop_picture(
+ &mut self,
+ ) -> PictureInfo {
+ self.picture_stack.pop().unwrap()
+ }
+
+ /// Update a picture, determining surface configuration,
+ /// rasterization roots, and (in future) whether there
+ /// are cached surfaces that can be used by this picture.
+ fn update(
+ &mut self,
+ pic_index: PictureIndex,
+ picture_primitives: &mut [PicturePrimitive],
+ frame_context: &FrameBuildingContext,
+ gpu_cache: &mut GpuCache,
+ clip_store: &ClipStore,
+ data_stores: &mut DataStores,
+ ) {
+ if let Some(prim_list) = picture_primitives[pic_index.0].pre_update(
+ self,
+ frame_context,
+ ) {
+ for cluster in &prim_list.clusters {
+ if !cluster.flags.contains(ClusterFlags::IS_PICTURE) {
+ continue;
+ }
+ for prim_instance in &prim_list.prim_instances[cluster.prim_range()] {
+ let child_pic_index = match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
+ _ => unreachable!(),
+ };
+
+ self.update(
+ child_pic_index,
+ picture_primitives,
+ frame_context,
+ gpu_cache,
+ clip_store,
+ data_stores,
+ );
+ }
+ }
+
+ picture_primitives[pic_index.0].post_update(
+ prim_list,
+ self,
+ frame_context,
+ data_stores,
+ );
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct SurfaceIndex(pub usize);
+
+pub const ROOT_SURFACE_INDEX: SurfaceIndex = SurfaceIndex(0);
+
+/// Describes the render task configuration for a picture surface.
+#[derive(Debug)]
+pub enum SurfaceRenderTasks {
+ /// The common type of surface is a single render task
+ Simple(RenderTaskId),
+ /// Some surfaces draw their content, and then have further tasks applied
+ /// to that input (such as blur passes for shadows). These tasks have a root
+ /// (the output of the surface), and a port (for attaching child task dependencies
+ /// to the content).
+ Chained { root_task_id: RenderTaskId, port_task_id: RenderTaskId },
+ /// Picture caches are a single surface consisting of multiple render
+ /// tasks, one per tile with dirty content.
+ Tiled(Vec<RenderTaskId>),
+}
+
+/// Information about an offscreen surface. For now,
+/// it contains information about the size and coordinate
+/// system of the surface. In the future, it will contain
+/// information about the contents of the surface, which
+/// will allow surfaces to be cached / retained between
+/// frames and display lists.
+#[derive(Debug)]
+pub struct SurfaceInfo {
+ /// A local rect defining the size of this surface, in the
+ /// coordinate system of the surface itself.
+ pub rect: PictureRect,
+ /// Part of the surface that we know to be opaque.
+ pub opaque_rect: PictureRect,
+ /// Helper structs for mapping local rects in different
+ /// coordinate systems into the surface coordinates.
+ pub map_local_to_surface: SpaceMapper<LayoutPixel, PicturePixel>,
+ /// Defines the positioning node for the surface itself,
+ /// and the rasterization root for this surface.
+ pub raster_spatial_node_index: SpatialNodeIndex,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ /// This is set when the render task is created.
+ pub render_tasks: Option<SurfaceRenderTasks>,
+ /// How much the local surface rect should be inflated (for blur radii).
+ pub inflation_factor: f32,
+ /// The device pixel ratio specific to this surface.
+ pub device_pixel_scale: DevicePixelScale,
+ /// The scale factors of the surface to raster transform.
+ pub scale_factors: (f32, f32),
+}
+
+impl SurfaceInfo {
+ pub fn new(
+ surface_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ inflation_factor: f32,
+ world_rect: WorldRect,
+ spatial_tree: &SpatialTree,
+ device_pixel_scale: DevicePixelScale,
+ scale_factors: (f32, f32),
+ ) -> Self {
+ let map_surface_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ surface_spatial_node_index,
+ world_rect,
+ spatial_tree,
+ );
+
+ let pic_bounds = map_surface_to_world
+ .unmap(&map_surface_to_world.bounds)
+ .unwrap_or_else(PictureRect::max_rect);
+
+ let map_local_to_surface = SpaceMapper::new(
+ surface_spatial_node_index,
+ pic_bounds,
+ );
+
+ SurfaceInfo {
+ rect: PictureRect::zero(),
+ opaque_rect: PictureRect::zero(),
+ map_local_to_surface,
+ render_tasks: None,
+ raster_spatial_node_index,
+ surface_spatial_node_index,
+ inflation_factor,
+ device_pixel_scale,
+ scale_factors,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct RasterConfig {
+ /// How this picture should be composited into
+ /// the parent surface.
+ pub composite_mode: PictureCompositeMode,
+ /// Index to the surface descriptor for this
+ /// picture.
+ pub surface_index: SurfaceIndex,
+ /// Whether this picture establishes a rasterization root.
+ pub establishes_raster_root: bool,
+ /// Scaling factor applied to fit within MAX_SURFACE_SIZE when
+ /// establishing a raster root.
+ /// Most code doesn't need to know about it, since it is folded
+ /// into device_pixel_scale when the rendertask is set up.
+ /// However e.g. text rasterization uses it to ensure consistent
+ /// on-screen font size.
+ pub root_scaling_factor: f32,
+ /// The world rect of this picture clipped to the current culling
+ /// rect. This is used for determining the size of the render
+ /// target rect for this surface, and calculating raster scale
+ /// factors.
+ pub clipped_bounding_rect: WorldRect,
+}
+
+bitflags! {
+ /// A set of flags describing why a picture may need a backing surface.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct BlitReason: u32 {
+ /// Mix-blend-mode on a child that requires isolation.
+ const ISOLATE = 1;
+ /// Clip node that _might_ require a surface.
+ const CLIP = 2;
+ /// Preserve-3D requires a surface for plane-splitting.
+ const PRESERVE3D = 4;
+ /// A backdrop that is reused which requires a surface.
+ const BACKDROP = 8;
+ }
+}
+
+/// Specifies how this Picture should be composited
+/// onto the target it belongs to.
+#[allow(dead_code)]
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum PictureCompositeMode {
+ /// Apply CSS mix-blend-mode effect.
+ MixBlend(MixBlendMode),
+ /// Apply a CSS filter (except component transfer).
+ Filter(Filter),
+ /// Apply a component transfer filter.
+ ComponentTransferFilter(FilterDataHandle),
+ /// Draw to intermediate surface, copy straight across. This
+ /// is used for CSS isolation, and plane splitting.
+ Blit(BlitReason),
+ /// Used to cache a picture as a series of tiles.
+ TileCache {
+ slice_id: SliceId,
+ },
+ /// Apply an SVG filter
+ SvgFilter(Vec<FilterPrimitive>, Vec<SFilterData>),
+}
+
+impl PictureCompositeMode {
+ pub fn inflate_picture_rect(&self, picture_rect: PictureRect, scale_factors: (f32, f32)) -> PictureRect {
+ let mut result_rect = picture_rect;
+ match self {
+ PictureCompositeMode::Filter(filter) => match filter {
+ Filter::Blur(width, height) => {
+ let width_factor = clamp_blur_radius(*width, scale_factors).ceil() * BLUR_SAMPLE_SCALE;
+ let height_factor = clamp_blur_radius(*height, scale_factors).ceil() * BLUR_SAMPLE_SCALE;
+ result_rect = picture_rect.inflate(width_factor, height_factor);
+ },
+ Filter::DropShadows(shadows) => {
+ let mut max_inflation: f32 = 0.0;
+ for shadow in shadows {
+ max_inflation = max_inflation.max(shadow.blur_radius);
+ }
+ max_inflation = clamp_blur_radius(max_inflation, scale_factors).ceil() * BLUR_SAMPLE_SCALE;
+ result_rect = picture_rect.inflate(max_inflation, max_inflation);
+ },
+ _ => {}
+ }
+ PictureCompositeMode::SvgFilter(primitives, _) => {
+ let mut output_rects = Vec::with_capacity(primitives.len());
+ for (cur_index, primitive) in primitives.iter().enumerate() {
+ let output_rect = match primitive.kind {
+ FilterPrimitiveKind::Blur(ref primitive) => {
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect);
+ let width_factor = primitive.width.round() * BLUR_SAMPLE_SCALE;
+ let height_factor = primitive.height.round() * BLUR_SAMPLE_SCALE;
+ input.inflate(width_factor, height_factor)
+ }
+ FilterPrimitiveKind::DropShadow(ref primitive) => {
+ let inflation_factor = primitive.shadow.blur_radius.ceil() * BLUR_SAMPLE_SCALE;
+ let input = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect);
+ let shadow_rect = input.inflate(inflation_factor, inflation_factor);
+ input.union(&shadow_rect.translate(primitive.shadow.offset * Scale::new(1.0)))
+ }
+ FilterPrimitiveKind::Blend(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect))
+ }
+ FilterPrimitiveKind::Composite(ref primitive) => {
+ primitive.input1.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect)
+ .union(&primitive.input2.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect))
+ }
+ FilterPrimitiveKind::Identity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect),
+ FilterPrimitiveKind::Opacity(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect),
+ FilterPrimitiveKind::ColorMatrix(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect),
+ FilterPrimitiveKind::ComponentTransfer(ref primitive) =>
+ primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect),
+ FilterPrimitiveKind::Offset(ref primitive) => {
+ let input_rect = primitive.input.to_index(cur_index).map(|index| output_rects[index]).unwrap_or(picture_rect);
+ input_rect.translate(primitive.offset * Scale::new(1.0))
+ },
+
+ FilterPrimitiveKind::Flood(..) => picture_rect,
+ };
+ output_rects.push(output_rect);
+ result_rect = result_rect.union(&output_rect);
+ }
+ }
+ _ => {},
+ }
+ result_rect
+ }
+}
+
+/// Enum value describing the place of a picture in a 3D context.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum Picture3DContext<C> {
+ /// The picture is not a part of 3D context sub-hierarchy.
+ Out,
+ /// The picture is a part of 3D context.
+ In {
+ /// Additional data per child for the case of this a root of 3D hierarchy.
+ root_data: Option<Vec<C>>,
+ /// The spatial node index of an "ancestor" element, i.e. one
+ /// that establishes the transformed element's containing block.
+ ///
+ /// See CSS spec draft for more details:
+ /// https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
+ ancestor_index: SpatialNodeIndex,
+ },
+}
+
+/// Information about a preserve-3D hierarchy child that has been plane-split
+/// and ordered according to the view direction.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct OrderedPictureChild {
+ pub anchor: PlaneSplitAnchor,
+ pub spatial_node_index: SpatialNodeIndex,
+ pub gpu_address: GpuCacheAddress,
+}
+
+bitflags! {
+ /// A set of flags describing why a picture may need a backing surface.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct ClusterFlags: u32 {
+ /// This cluster is a picture
+ const IS_PICTURE = 1;
+ /// Whether this cluster is visible when the position node is a backface.
+ const IS_BACKFACE_VISIBLE = 2;
+ /// This flag is set during the first pass picture traversal, depending on whether
+ /// the cluster is visible or not. It's read during the second pass when primitives
+ /// consult their owning clusters to see if the primitive itself is visible.
+ const IS_VISIBLE = 4;
+ /// Is a backdrop-filter cluster that requires special handling during post_update.
+ const IS_BACKDROP_FILTER = 8;
+ }
+}
+
+/// Descriptor for a cluster of primitives. For now, this is quite basic but will be
+/// extended to handle more spatial clustering of primitives.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveCluster {
+ /// The positioning node for this cluster.
+ pub spatial_node_index: SpatialNodeIndex,
+ /// The bounding rect of the cluster, in the local space of the spatial node.
+ /// This is used to quickly determine the overall bounding rect for a picture
+ /// during the first picture traversal, which is needed for local scale
+ /// determination, and render task size calculations.
+ bounding_rect: LayoutRect,
+ /// a part of the cluster that we know to be opaque if any. Does not always
+ /// describe the entire opaque region, but all content within that rect must
+ /// be opaque.
+ pub opaque_rect: LayoutRect,
+ /// The range of primitive instance indices associated with this cluster.
+ pub prim_range: Range<usize>,
+ /// Various flags / state for this cluster.
+ pub flags: ClusterFlags,
+}
+
+impl PrimitiveCluster {
+ /// Construct a new primitive cluster for a given positioning node.
+ fn new(
+ spatial_node_index: SpatialNodeIndex,
+ flags: ClusterFlags,
+ first_instance_index: usize,
+ ) -> Self {
+ PrimitiveCluster {
+ bounding_rect: LayoutRect::zero(),
+ opaque_rect: LayoutRect::zero(),
+ spatial_node_index,
+ flags,
+ prim_range: first_instance_index..first_instance_index
+ }
+ }
+
+ /// Return true if this cluster is compatible with the given params
+ pub fn is_compatible(
+ &self,
+ spatial_node_index: SpatialNodeIndex,
+ flags: ClusterFlags,
+ ) -> bool {
+ self.flags == flags && self.spatial_node_index == spatial_node_index
+ }
+
+ pub fn prim_range(&self) -> Range<usize> {
+ self.prim_range.clone()
+ }
+
+ /// Add a primitive instance to this cluster, at the start or end
+ fn add_instance(
+ &mut self,
+ culling_rect: &LayoutRect,
+ instance_index: usize,
+ ) {
+ debug_assert_eq!(instance_index, self.prim_range.end);
+ self.bounding_rect = self.bounding_rect.union(culling_rect);
+ self.prim_range.end += 1;
+ }
+}
+
+/// A list of primitive instances that are added to a picture
+/// This ensures we can keep a list of primitives that
+/// are pictures, for a fast initial traversal of the picture
+/// tree without walking the instance list.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveList {
+ /// List of primitives grouped into clusters.
+ pub clusters: Vec<PrimitiveCluster>,
+ pub prim_instances: Vec<PrimitiveInstance>,
+}
+
+impl PrimitiveList {
+ /// Construct an empty primitive list. This is
+ /// just used during the take_context / restore_context
+ /// borrow check dance, which will be removed as the
+ /// picture traversal pass is completed.
+ pub fn empty() -> Self {
+ PrimitiveList {
+ clusters: Vec::new(),
+ prim_instances: Vec::new(),
+ }
+ }
+
+ /// Add a primitive instance to the end of the list
+ pub fn add_prim(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ prim_flags: PrimitiveFlags,
+ ) {
+ let mut flags = ClusterFlags::empty();
+
+ // Pictures are always put into a new cluster, to make it faster to
+ // iterate all pictures in a given primitive list.
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { .. } => {
+ flags.insert(ClusterFlags::IS_PICTURE);
+ }
+ PrimitiveInstanceKind::Backdrop { .. } => {
+ flags.insert(ClusterFlags::IS_BACKDROP_FILTER);
+ }
+ _ => {}
+ }
+
+ if prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE) {
+ flags.insert(ClusterFlags::IS_BACKFACE_VISIBLE);
+ }
+
+ let culling_rect = prim_instance.clip_set.local_clip_rect
+ .intersection(&prim_rect)
+ .unwrap_or_else(LayoutRect::zero);
+
+ // Primitive lengths aren't evenly distributed among primitive lists:
+ // We often have a large amount of single primitive lists, a
+ // few below 20~30 primitives, and even fewer lists (maybe a couple)
+ // in the multiple hundreds with nothing in between.
+ // We can see in profiles that reallocating vectors while pushing
+ // primitives is taking a large amount of the total scene build time,
+ // so we take advantage of what we know about the length distributions
+ // to go for an adapted vector growth pattern that avoids over-allocating
+ // for the many small allocations while avoiding a lot of reallocation by
+ // quickly converging to the common sizes.
+ // Rust's default vector growth strategy (when pushing elements one by one)
+ // is to double the capacity every time.
+ let prims_len = self.prim_instances.len();
+ if prims_len == self.prim_instances.capacity() {
+ let next_alloc = match prims_len {
+ 1 ..= 31 => 32 - prims_len,
+ 32 ..= 256 => 512 - prims_len,
+ _ => prims_len * 2,
+ };
+
+ self.prim_instances.reserve(next_alloc);
+ }
+
+ let instance_index = prims_len;
+ self.prim_instances.push(prim_instance);
+
+ if let Some(cluster) = self.clusters.last_mut() {
+ if cluster.is_compatible(spatial_node_index, flags) {
+ cluster.add_instance(&culling_rect, instance_index);
+ return;
+ }
+ }
+
+ // Same idea with clusters, using a different distribution.
+ let clusters_len = self.clusters.len();
+ if clusters_len == self.clusters.capacity() {
+ let next_alloc = match clusters_len {
+ 1 ..= 15 => 16 - clusters_len,
+ 16 ..= 127 => 128 - clusters_len,
+ _ => clusters_len * 2,
+ };
+
+ self.clusters.reserve(next_alloc);
+ }
+
+ let mut cluster = PrimitiveCluster::new(
+ spatial_node_index,
+ flags,
+ instance_index,
+ );
+
+ cluster.add_instance(&culling_rect, instance_index);
+ self.clusters.push(cluster);
+ }
+
+ /// Returns true if there are no clusters (and thus primitives)
+ pub fn is_empty(&self) -> bool {
+ self.clusters.is_empty()
+ }
+}
+
+/// Defines configuration options for a given picture primitive.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PictureOptions {
+ /// If true, WR should inflate the bounding rect of primitives when
+ /// using a filter effect that requires inflation.
+ pub inflate_if_required: bool,
+}
+
+impl Default for PictureOptions {
+ fn default() -> Self {
+ PictureOptions {
+ inflate_if_required: true,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PicturePrimitive {
+ /// List of primitives, and associated info for this picture.
+ pub prim_list: PrimitiveList,
+
+ #[cfg_attr(feature = "capture", serde(skip))]
+ pub state: Option<PictureState>,
+
+ /// If true, apply the local clip rect to primitive drawn
+ /// in this picture.
+ pub apply_local_clip_rect: bool,
+ /// If false and transform ends up showing the back of the picture,
+ /// it will be considered invisible.
+ pub is_backface_visible: bool,
+
+ // If a mix-blend-mode, contains the render task for
+ // the readback of the framebuffer that we use to sample
+ // from in the mix-blend-mode shader.
+ // For drop-shadow filter, this will store the original
+ // picture task which would be rendered on screen after
+ // blur pass.
+ pub secondary_render_task_id: Option<RenderTaskId>,
+ /// How this picture should be composited.
+ /// If None, don't composite - just draw directly on parent surface.
+ pub requested_composite_mode: Option<PictureCompositeMode>,
+
+ pub raster_config: Option<RasterConfig>,
+ pub context_3d: Picture3DContext<OrderedPictureChild>,
+
+ // Optional cache handles for storing extra data
+ // in the GPU cache, depending on the type of
+ // picture.
+ pub extra_gpu_data_handles: SmallVec<[GpuCacheHandle; 1]>,
+
+ /// The spatial node index of this picture when it is
+ /// composited into the parent picture.
+ pub spatial_node_index: SpatialNodeIndex,
+
+ /// The conservative local rect of this picture. It is
+ /// built dynamically during the first picture traversal.
+ /// It is composed of already snapped primitives.
+ pub estimated_local_rect: LayoutRect,
+
+ /// The local rect of this picture. It is built
+ /// dynamically during the frame visibility update. It
+ /// differs from the estimated_local_rect because it
+ /// will not contain culled primitives, takes into
+ /// account surface inflation and the whole clip chain.
+ /// It is frequently the same, but may be quite
+ /// different depending on how much was culled.
+ pub precise_local_rect: LayoutRect,
+
+ /// Store the state of the previous precise local rect
+ /// for this picture. We need this in order to know when
+ /// to invalidate segments / drop-shadow gpu cache handles.
+ pub prev_precise_local_rect: LayoutRect,
+
+ /// If false, this picture needs to (re)build segments
+ /// if it supports segment rendering. This can occur
+ /// if the local rect of the picture changes due to
+ /// transform animation and/or scrolling.
+ pub segments_are_valid: bool,
+
+ /// The config options for this picture.
+ pub options: PictureOptions,
+
+ /// Set to true if we know for sure the picture is fully opaque.
+ pub is_opaque: bool,
+}
+
+impl PicturePrimitive {
+ pub fn print<T: PrintTreePrinter>(
+ &self,
+ pictures: &[Self],
+ self_index: PictureIndex,
+ pt: &mut T,
+ ) {
+ pt.new_level(format!("{:?}", self_index));
+ pt.add_item(format!("cluster_count: {:?}", self.prim_list.clusters.len()));
+ pt.add_item(format!("estimated_local_rect: {:?}", self.estimated_local_rect));
+ pt.add_item(format!("precise_local_rect: {:?}", self.precise_local_rect));
+ pt.add_item(format!("spatial_node_index: {:?}", self.spatial_node_index));
+ pt.add_item(format!("raster_config: {:?}", self.raster_config));
+ pt.add_item(format!("requested_composite_mode: {:?}", self.requested_composite_mode));
+
+ for cluster in &self.prim_list.clusters {
+ if !cluster.flags.contains(ClusterFlags::IS_PICTURE) {
+ continue;
+ }
+ for instance in &self.prim_list.prim_instances[cluster.prim_range()] {
+ let index = match instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
+ _ => unreachable!(),
+ };
+ pictures[index.0].print(pictures, index, pt);
+ }
+ }
+
+ pt.end_level();
+ }
+
+ /// Returns true if this picture supports segmented rendering.
+ pub fn can_use_segments(&self) -> bool {
+ match self.raster_config {
+ // TODO(gw): Support brush segment rendering for filter and mix-blend
+ // shaders. It's possible this already works, but I'm just
+ // applying this optimization to Blit mode for now.
+ Some(RasterConfig { composite_mode: PictureCompositeMode::MixBlend(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::Filter(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::ComponentTransferFilter(..), .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { .. }, .. }) |
+ Some(RasterConfig { composite_mode: PictureCompositeMode::SvgFilter(..), .. }) |
+ None => {
+ false
+ }
+ Some(RasterConfig { composite_mode: PictureCompositeMode::Blit(reason), ..}) => {
+ reason == BlitReason::CLIP
+ }
+ }
+ }
+
+ fn resolve_scene_properties(&mut self, properties: &SceneProperties) -> bool {
+ match self.requested_composite_mode {
+ Some(PictureCompositeMode::Filter(ref mut filter)) => {
+ match *filter {
+ Filter::Opacity(ref binding, ref mut value) => {
+ *value = properties.resolve_float(binding);
+ }
+ _ => {}
+ }
+
+ filter.is_visible()
+ }
+ _ => true,
+ }
+ }
+
+ pub fn is_visible(&self) -> bool {
+ match self.requested_composite_mode {
+ Some(PictureCompositeMode::Filter(ref filter)) => {
+ filter.is_visible()
+ }
+ _ => true,
+ }
+ }
+
+ // TODO(gw): We have the PictureOptions struct available. We
+ // should move some of the parameter list in this
+ // method to be part of the PictureOptions, and
+ // avoid adding new parameters here.
+ pub fn new_image(
+ requested_composite_mode: Option<PictureCompositeMode>,
+ context_3d: Picture3DContext<OrderedPictureChild>,
+ apply_local_clip_rect: bool,
+ flags: PrimitiveFlags,
+ prim_list: PrimitiveList,
+ spatial_node_index: SpatialNodeIndex,
+ options: PictureOptions,
+ ) -> Self {
+ PicturePrimitive {
+ prim_list,
+ state: None,
+ secondary_render_task_id: None,
+ requested_composite_mode,
+ raster_config: None,
+ context_3d,
+ extra_gpu_data_handles: SmallVec::new(),
+ apply_local_clip_rect,
+ is_backface_visible: flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ spatial_node_index,
+ estimated_local_rect: LayoutRect::zero(),
+ precise_local_rect: LayoutRect::zero(),
+ prev_precise_local_rect: LayoutRect::zero(),
+ options,
+ segments_are_valid: false,
+ is_opaque: false,
+ }
+ }
+
+ pub fn take_context(
+ &mut self,
+ pic_index: PictureIndex,
+ surface_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ parent_surface_index: SurfaceIndex,
+ parent_subpixel_mode: &SubpixelMode,
+ frame_state: &mut FrameBuildingState,
+ frame_context: &FrameBuildingContext,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_cache_logger: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
+ if !self.is_visible() {
+ return None;
+ }
+
+ profile_scope!("take_context");
+
+ // Extract the raster and surface spatial nodes from the raster
+ // config, if this picture establishes a surface. Otherwise just
+ // pass in the spatial node indices from the parent context.
+ let (raster_spatial_node_index, surface_spatial_node_index, surface_index, inflation_factor) = match self.raster_config {
+ Some(ref raster_config) => {
+ let surface = &frame_state.surfaces[raster_config.surface_index.0];
+
+ (
+ surface.raster_spatial_node_index,
+ self.spatial_node_index,
+ raster_config.surface_index,
+ surface.inflation_factor,
+ )
+ }
+ None => {
+ (
+ raster_spatial_node_index,
+ surface_spatial_node_index,
+ parent_surface_index,
+ 0.0,
+ )
+ }
+ };
+
+ let map_pic_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ surface_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
+ .unwrap_or_else(PictureRect::max_rect);
+
+ let map_local_to_pic = SpaceMapper::new(
+ surface_spatial_node_index,
+ pic_bounds,
+ );
+
+ let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let plane_splitter = match self.context_3d {
+ Picture3DContext::Out => {
+ None
+ }
+ Picture3DContext::In { root_data: Some(_), .. } => {
+ Some(PlaneSplitter::new())
+ }
+ Picture3DContext::In { root_data: None, .. } => {
+ None
+ }
+ };
+
+ match self.raster_config {
+ Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ let tile_cache = tile_caches.get_mut(&slice_id).unwrap();
+ let mut debug_info = SliceDebugInfo::new();
+ let mut surface_tasks = Vec::with_capacity(tile_cache.tiles.len());
+ let device_pixel_scale = frame_state
+ .surfaces[surface_index.0]
+ .device_pixel_scale;
+
+ // Get the overall world space rect of the picture cache. Used to clip
+ // the tile rects below for occlusion testing to the relevant area.
+ let world_clip_rect = map_pic_to_world
+ .map(&tile_cache.local_clip_rect)
+ .expect("bug: unable to map clip rect");
+ let device_clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round();
+
+ for tile in tile_cache.tiles.values_mut() {
+
+ if tile.is_visible {
+ // Get the world space rect that this tile will actually occupy on screem
+ let device_draw_rect = device_clip_rect.intersection(&tile.device_valid_rect);
+
+ // If that draw rect is occluded by some set of tiles in front of it,
+ // then mark it as not visible and skip drawing. When it's not occluded
+ // it will fail this test, and get rasterized by the render task setup
+ // code below.
+ match device_draw_rect {
+ Some(device_draw_rect) => {
+ // Only check for occlusion on visible tiles that are fixed position.
+ if tile_cache.spatial_node_index == ROOT_SPATIAL_NODE_INDEX &&
+ frame_state.composite_state.occluders.is_tile_occluded(tile.z_id, device_draw_rect) {
+ // If this tile has an allocated native surface, free it, since it's completely
+ // occluded. We will need to re-allocate this surface if it becomes visible,
+ // but that's likely to be rare (e.g. when there is no content display list
+ // for a frame or two during a tab switch).
+ let surface = tile.surface.as_mut().expect("no tile surface set!");
+
+ if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { id, .. }, .. } = surface {
+ if let Some(id) = id.take() {
+ frame_state.resource_cache.destroy_compositor_tile(id);
+ }
+ }
+
+ tile.is_visible = false;
+
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Occluded,
+ );
+ }
+
+ continue;
+ }
+ }
+ None => {
+ tile.is_visible = false;
+ }
+ }
+ }
+
+ // If we get here, we want to ensure that the surface remains valid in the texture
+ // cache, _even if_ it's not visible due to clipping or being scrolled off-screen.
+ // This ensures that we retain valid tiles that are off-screen, but still in the
+ // display port of this tile cache instance.
+ if let Some(TileSurface::Texture { descriptor, .. }) = tile.surface.as_ref() {
+ if let SurfaceTextureDescriptor::TextureCache { ref handle, .. } = descriptor {
+ frame_state.resource_cache.texture_cache.request(
+ handle,
+ frame_state.gpu_cache,
+ );
+ }
+ }
+
+ // If the tile has been found to be off-screen / clipped, skip any further processing.
+ if !tile.is_visible {
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Culled,
+ );
+ }
+
+ continue;
+ }
+
+ if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
+ tile.root.draw_debug_rects(
+ &map_pic_to_world,
+ tile.is_opaque,
+ tile.current_descriptor.local_valid_rect,
+ scratch,
+ frame_context.global_device_pixel_scale,
+ );
+
+ let label_offset = DeviceVector2D::new(20.0, 30.0);
+ let tile_device_rect = tile.world_tile_rect * frame_context.global_device_pixel_scale;
+ if tile_device_rect.size.height >= label_offset.y {
+ let surface = tile.surface.as_ref().expect("no tile surface set!");
+
+ scratch.push_debug_string(
+ tile_device_rect.origin + label_offset,
+ debug_colors::RED,
+ format!("{:?}: s={} is_opaque={} surface={}",
+ tile.id,
+ tile_cache.slice,
+ tile.is_opaque,
+ surface.kind(),
+ ),
+ );
+ }
+ }
+
+ if let TileSurface::Texture { descriptor, .. } = tile.surface.as_mut().unwrap() {
+ match descriptor {
+ SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
+ // Invalidate if the backing texture was evicted.
+ if frame_state.resource_cache.texture_cache.is_allocated(handle) {
+ // Request the backing texture so it won't get evicted this frame.
+ // We specifically want to mark the tile texture as used, even
+ // if it's detected not visible below and skipped. This is because
+ // we maintain the set of tiles we care about based on visibility
+ // during pre_update. If a tile still exists after that, we are
+ // assuming that it's either visible or we want to retain it for
+ // a while in case it gets scrolled back onto screen soon.
+ // TODO(gw): Consider switching to manual eviction policy?
+ frame_state.resource_cache.texture_cache.request(handle, frame_state.gpu_cache);
+ } else {
+ // If the texture was evicted on a previous frame, we need to assume
+ // that the entire tile rect is dirty.
+ tile.invalidate(None, InvalidationReason::NoTexture);
+ }
+ }
+ SurfaceTextureDescriptor::Native { id, .. } => {
+ if id.is_none() {
+ // There is no current surface allocation, so ensure the entire tile is invalidated
+ tile.invalidate(None, InvalidationReason::NoSurface);
+ }
+ }
+ }
+ }
+
+ // Ensure that the dirty rect doesn't extend outside the local valid rect.
+ tile.local_dirty_rect = tile.local_dirty_rect
+ .intersection(&tile.current_descriptor.local_valid_rect)
+ .unwrap_or_else(PictureRect::zero);
+
+ // Update the world/device dirty rect
+ let world_dirty_rect = map_pic_to_world.map(&tile.local_dirty_rect).expect("bug");
+
+ let device_rect = (tile.world_tile_rect * frame_context.global_device_pixel_scale).round();
+ tile.device_dirty_rect = (world_dirty_rect * frame_context.global_device_pixel_scale)
+ .round_out()
+ .intersection(&device_rect)
+ .unwrap_or_else(DeviceRect::zero);
+
+ if tile.is_valid {
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Valid,
+ );
+ }
+
+ continue;
+ }
+
+ // Get the visibility mask bit(s) for this tile from the dirty region tracker. This must be done
+ // outside the if statement below, so that we include in the dirty region tiles that are handled
+ // by a background color only (no surface allocation).
+ let tile_vis_mask = tile_cache.dirty_region.add_dirty_region(
+ tile.local_dirty_rect,
+ frame_context.spatial_tree,
+ );
+
+ // Ensure that this texture is allocated.
+ if let TileSurface::Texture { ref mut descriptor, ref mut visibility_mask } = tile.surface.as_mut().unwrap() {
+ *visibility_mask = tile_vis_mask;
+
+ match descriptor {
+ SurfaceTextureDescriptor::TextureCache { ref mut handle } => {
+ if !frame_state.resource_cache.texture_cache.is_allocated(handle) {
+ frame_state.resource_cache.texture_cache.update_picture_cache(
+ tile_cache.current_tile_size,
+ handle,
+ frame_state.gpu_cache,
+ );
+ }
+ }
+ SurfaceTextureDescriptor::Native { id } => {
+ if id.is_none() {
+ // Allocate a native surface id if we're in native compositing mode,
+ // and we don't have a surface yet (due to first frame, or destruction
+ // due to tile size changing etc).
+ if tile_cache.native_surface.is_none() {
+ let opaque = frame_state
+ .resource_cache
+ .create_compositor_surface(
+ tile_cache.virtual_offset,
+ tile_cache.current_tile_size,
+ true,
+ );
+
+ let alpha = frame_state
+ .resource_cache
+ .create_compositor_surface(
+ tile_cache.virtual_offset,
+ tile_cache.current_tile_size,
+ false,
+ );
+
+ tile_cache.native_surface = Some(NativeSurface {
+ opaque,
+ alpha,
+ });
+ }
+
+ // Create the tile identifier and allocate it.
+ let surface_id = if tile.is_opaque {
+ tile_cache.native_surface.as_ref().unwrap().opaque
+ } else {
+ tile_cache.native_surface.as_ref().unwrap().alpha
+ };
+
+ let tile_id = NativeTileId {
+ surface_id,
+ x: tile.tile_offset.x,
+ y: tile.tile_offset.y,
+ };
+
+ frame_state.resource_cache.create_compositor_tile(tile_id);
+
+ *id = Some(tile_id);
+ }
+ }
+ }
+
+ let content_origin_f = tile.world_tile_rect.origin * device_pixel_scale;
+ let content_origin = content_origin_f.round();
+ debug_assert!((content_origin_f.x - content_origin.x).abs() < 0.01);
+ debug_assert!((content_origin_f.y - content_origin.y).abs() < 0.01);
+
+ let surface = descriptor.resolve(
+ frame_state.resource_cache,
+ tile_cache.current_tile_size,
+ );
+
+ let scissor_rect = tile.device_dirty_rect
+ .translate(-device_rect.origin.to_vector())
+ .round()
+ .to_i32();
+
+ let valid_rect = tile.device_valid_rect
+ .translate(-device_rect.origin.to_vector())
+ .round()
+ .to_i32();
+
+ let task_size = tile_cache.current_tile_size;
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new(
+ RenderTaskLocation::Static {
+ surface: StaticRenderTaskSurface::PictureCache {
+ surface,
+ },
+ rect: task_size.into(),
+ },
+ RenderTaskKind::new_picture(
+ task_size,
+ tile_cache.current_tile_size.to_f32(),
+ pic_index,
+ content_origin,
+ UvRectKind::Rect,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ *visibility_mask,
+ Some(scissor_rect),
+ Some(valid_rect),
+ )
+ ),
+ );
+
+ surface_tasks.push(render_task_id);
+ }
+
+ if frame_context.fb_config.testing {
+ debug_info.tiles.insert(
+ tile.tile_offset,
+ TileDebugInfo::Dirty(DirtyTileDebugInfo {
+ local_valid_rect: tile.current_descriptor.local_valid_rect,
+ local_dirty_rect: tile.local_dirty_rect,
+ }),
+ );
+ }
+
+ // If the entire tile valid region is dirty, we can update the fract offset
+ // at which the tile was rendered.
+ if tile.device_dirty_rect.contains_rect(&tile.device_valid_rect) {
+ tile.device_fract_offset = tile_cache.device_fract_offset;
+ }
+
+ // Now that the tile is valid, reset the dirty rect.
+ tile.local_dirty_rect = PictureRect::zero();
+ tile.is_valid = true;
+ }
+
+ // If invalidation debugging is enabled, dump the picture cache state to a tree printer.
+ if frame_context.debug_flags.contains(DebugFlags::INVALIDATION_DBG) {
+ tile_cache.print();
+ }
+
+ // If testing mode is enabled, write some information about the current state
+ // of this picture cache (made available in RenderResults).
+ if frame_context.fb_config.testing {
+ frame_state.composite_state
+ .picture_cache_debug
+ .slices
+ .insert(
+ tile_cache.slice,
+ debug_info,
+ );
+ }
+
+ frame_state.init_surface_tiled(
+ surface_index,
+ surface_tasks,
+ );
+ }
+ Some(ref mut raster_config) => {
+ let pic_rect = self.precise_local_rect.cast_unit();
+
+ let mut device_pixel_scale = frame_state
+ .surfaces[raster_config.surface_index.0]
+ .device_pixel_scale;
+
+ let scale_factors = frame_state
+ .surfaces[raster_config.surface_index.0]
+ .scale_factors;
+
+ // If the primitive has a filter that can sample with an offset, the clip rect has
+ // to take it into account.
+ let clip_inflation = match raster_config.composite_mode {
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let mut max_offset = vec2(0.0, 0.0);
+ let mut min_offset = vec2(0.0, 0.0);
+ for shadow in shadows {
+ let offset = layout_vector_as_picture_vector(shadow.offset);
+ max_offset = max_offset.max(offset);
+ min_offset = min_offset.min(offset);
+ }
+
+ // Get the shadow offsets in world space.
+ let raster_min = map_pic_to_raster.map_vector(min_offset);
+ let raster_max = map_pic_to_raster.map_vector(max_offset);
+ let world_min = map_raster_to_world.map_vector(raster_min);
+ let world_max = map_raster_to_world.map_vector(raster_max);
+
+ // Grow the clip in the opposite direction of the shadow's offset.
+ SideOffsets2D::from_vectors_outer(
+ -world_max.max(vec2(0.0, 0.0)),
+ -world_min.min(vec2(0.0, 0.0)),
+ )
+ }
+ _ => SideOffsets2D::zero(),
+ };
+
+ let (mut clipped, mut unclipped) = match get_raster_rects(
+ pic_rect,
+ &map_pic_to_raster,
+ &map_raster_to_world,
+ raster_config.clipped_bounding_rect.outer_rect(clip_inflation),
+ device_pixel_scale,
+ ) {
+ Some(info) => info,
+ None => {
+ return None
+ }
+ };
+ let transform = map_pic_to_raster.get_transform();
+
+ /// If the picture (raster_config) establishes a raster root,
+ /// its requested resolution won't be clipped by the parent or
+ /// viewport; so we need to make sure the requested resolution is
+ /// "reasonable", ie. <= MAX_SURFACE_SIZE. If not, scale the
+ /// picture down until it fits that limit. This results in a new
+ /// device_rect, a new unclipped rect, and a new device_pixel_scale.
+ ///
+ /// Since the adjusted device_pixel_scale is passed into the
+ /// RenderTask (and then the shader via RenderTaskData) this mostly
+ /// works transparently, reusing existing support for variable DPI
+ /// support. The on-the-fly scaling can be seen as on-the-fly,
+ /// per-task DPI adjustment. Logical pixels are unaffected.
+ ///
+ /// The scaling factor is returned to the caller; blur radius,
+ /// font size, etc. need to be scaled accordingly.
+ fn adjust_scale_for_max_surface_size(
+ raster_config: &RasterConfig,
+ max_target_size: i32,
+ pic_rect: PictureRect,
+ map_pic_to_raster: &SpaceMapper<PicturePixel, RasterPixel>,
+ map_raster_to_world: &SpaceMapper<RasterPixel, WorldPixel>,
+ clipped_prim_bounding_rect: WorldRect,
+ device_pixel_scale : &mut DevicePixelScale,
+ device_rect: &mut DeviceRect,
+ unclipped: &mut DeviceRect) -> Option<f32>
+ {
+ let limit = if raster_config.establishes_raster_root {
+ MAX_SURFACE_SIZE
+ } else {
+ max_target_size as f32
+ };
+ if device_rect.size.width > limit || device_rect.size.height > limit {
+ // round_out will grow by 1 integer pixel if origin is on a
+ // fractional position, so keep that margin for error with -1:
+ let scale = (limit as f32 - 1.0) /
+ (f32::max(device_rect.size.width, device_rect.size.height));
+ *device_pixel_scale = *device_pixel_scale * Scale::new(scale);
+ let new_device_rect = device_rect.to_f32() * Scale::new(scale);
+ *device_rect = new_device_rect.round_out();
+
+ *unclipped = match get_raster_rects(
+ pic_rect,
+ &map_pic_to_raster,
+ &map_raster_to_world,
+ clipped_prim_bounding_rect,
+ *device_pixel_scale
+ ) {
+ Some(info) => info.1,
+ None => {
+ return None
+ }
+ };
+ Some(scale)
+ }
+ else
+ {
+ None
+ }
+ }
+
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {
+ unreachable!("handled above");
+ }
+ PictureCompositeMode::Filter(Filter::Blur(width, height)) => {
+ let width_std_deviation = clamp_blur_radius(width, scale_factors) * device_pixel_scale.0;
+ let height_std_deviation = clamp_blur_radius(height, scale_factors) * device_pixel_scale.0;
+ let mut blur_std_deviation = DeviceSize::new(
+ width_std_deviation * scale_factors.0,
+ height_std_deviation * scale_factors.1
+ );
+ let mut device_rect = if self.options.inflate_if_required {
+ let inflation_factor = frame_state.surfaces[raster_config.surface_index.0].inflation_factor;
+ let inflation_factor = inflation_factor * device_pixel_scale.0;
+
+ // The clipped field is the part of the picture that is visible
+ // on screen. The unclipped field is the screen-space rect of
+ // the complete picture, if no screen / clip-chain was applied
+ // (this includes the extra space for blur region). To ensure
+ // that we draw a large enough part of the picture to get correct
+ // blur results, inflate that clipped area by the blur range, and
+ // then intersect with the total screen rect, to minimize the
+ // allocation size.
+ clipped
+ .inflate(inflation_factor * scale_factors.0, inflation_factor * scale_factors.1)
+ .intersection(&unclipped)
+ .unwrap()
+ } else {
+ clipped
+ };
+
+ let mut original_size = device_rect.size;
+
+ // Adjust the size to avoid introducing sampling errors during the down-scaling passes.
+ // what would be even better is to rasterize the picture at the down-scaled size
+ // directly.
+ device_rect.size = BlurTask::adjusted_blur_source_size(
+ device_rect.size,
+ blur_std_deviation,
+ );
+
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut device_rect, &mut unclipped,
+ ) {
+ blur_std_deviation = blur_std_deviation * scale;
+ original_size = original_size.to_f32() * scale;
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &device_rect,
+ device_pixel_scale,
+ );
+
+ let task_size = device_rect.size.to_i32();
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ device_rect.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ let blur_render_task_id = RenderTask::new_blur(
+ blur_std_deviation,
+ picture_task_id,
+ frame_state.rg_builder,
+ RenderTargetKind::Color,
+ None,
+ original_size.to_i32(),
+ );
+
+ frame_state.init_surface_chain(
+ raster_config.surface_index,
+ blur_render_task_id,
+ picture_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ let mut max_std_deviation = 0.0;
+ for shadow in shadows {
+ max_std_deviation = f32::max(max_std_deviation, shadow.blur_radius);
+ }
+ max_std_deviation = clamp_blur_radius(max_std_deviation, scale_factors) * device_pixel_scale.0;
+ let max_blur_range = max_std_deviation * BLUR_SAMPLE_SCALE;
+
+ // We cast clipped to f32 instead of casting unclipped to i32
+ // because unclipped can overflow an i32.
+ let mut device_rect = clipped
+ .inflate(max_blur_range * scale_factors.0, max_blur_range * scale_factors.1)
+ .intersection(&unclipped)
+ .unwrap();
+
+ device_rect.size = BlurTask::adjusted_blur_source_size(
+ device_rect.size,
+ DeviceSize::new(
+ max_std_deviation * scale_factors.0,
+ max_std_deviation * scale_factors.1
+ ),
+ );
+
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut device_rect, &mut unclipped,
+ ) {
+ // std_dev adjusts automatically from using device_pixel_scale
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &device_rect,
+ device_pixel_scale,
+ );
+
+ let task_size = device_rect.size.to_i32();
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ device_rect.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ ),
+ )
+ );
+
+ // Add this content picture as a dependency of the parent surface, to
+ // ensure it isn't free'd after the shadow uses it as an input.
+ frame_state.add_child_render_task(
+ parent_surface_index,
+ picture_task_id,
+ );
+
+ self.secondary_render_task_id = Some(picture_task_id);
+
+ let mut blur_tasks = BlurTaskCache::default();
+
+ self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+
+ let mut blur_render_task_id = picture_task_id;
+ for shadow in shadows {
+ let blur_radius = clamp_blur_radius(shadow.blur_radius, scale_factors) * device_pixel_scale.0;
+ blur_render_task_id = RenderTask::new_blur(
+ DeviceSize::new(
+ blur_radius * scale_factors.0,
+ blur_radius * scale_factors.1,
+ ),
+ picture_task_id,
+ frame_state.rg_builder,
+ RenderTargetKind::Color,
+ Some(&mut blur_tasks),
+ device_rect.size.to_i32(),
+ );
+ }
+
+ // TODO(nical) the second one should to be the blur's task id but we have several blurs now
+ frame_state.init_surface_chain(
+ raster_config.surface_index,
+ blur_render_task_id,
+ picture_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::MixBlend(..) if !frame_context.fb_config.gpu_supports_advanced_blend => {
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut clipped, &mut unclipped,
+ ) {
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &clipped,
+ device_pixel_scale,
+ );
+
+ let readback_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ clipped.size.to_i32(),
+ RenderTaskKind::new_readback(),
+ )
+ );
+
+ frame_state.add_child_render_task(
+ parent_surface_index,
+ readback_task_id,
+ );
+
+ self.secondary_render_task_id = Some(readback_task_id);
+
+ let task_size = clipped.size.to_i32();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ clipped.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ frame_state.init_surface(
+ raster_config.surface_index,
+ render_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::Filter(..) => {
+
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut clipped, &mut unclipped,
+ ) {
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &clipped,
+ device_pixel_scale,
+ );
+
+ let task_size = clipped.size.to_i32();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ clipped.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ frame_state.init_surface(
+ raster_config.surface_index,
+ render_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::ComponentTransferFilter(..) => {
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut clipped, &mut unclipped,
+ ) {
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &clipped,
+ device_pixel_scale,
+ );
+
+ let task_size = clipped.size.to_i32();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ clipped.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ frame_state.init_surface(
+ raster_config.surface_index,
+ render_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::Blit(_) => {
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut clipped, &mut unclipped,
+ ) {
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &clipped,
+ device_pixel_scale,
+ );
+
+ let task_size = clipped.size.to_i32();
+
+ let render_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ clipped.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ frame_state.init_surface(
+ raster_config.surface_index,
+ render_task_id,
+ parent_surface_index,
+ );
+ }
+ PictureCompositeMode::SvgFilter(ref primitives, ref filter_datas) => {
+
+ if let Some(scale) = adjust_scale_for_max_surface_size(
+ raster_config, frame_context.fb_config.max_target_size,
+ pic_rect, &map_pic_to_raster, &map_raster_to_world,
+ raster_config.clipped_bounding_rect,
+ &mut device_pixel_scale, &mut clipped, &mut unclipped,
+ ) {
+ raster_config.root_scaling_factor = scale;
+ }
+
+ let uv_rect_kind = calculate_uv_rect_kind(
+ &pic_rect,
+ &transform,
+ &clipped,
+ device_pixel_scale,
+ );
+
+ let task_size = clipped.size.to_i32();
+
+ let picture_task_id = frame_state.rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_picture(
+ task_size,
+ unclipped.size,
+ pic_index,
+ clipped.origin,
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ PrimitiveVisibilityMask::all(),
+ None,
+ None,
+ )
+ )
+ );
+
+ let filter_task_id = RenderTask::new_svg_filter(
+ primitives,
+ filter_datas,
+ frame_state.rg_builder,
+ clipped.size.to_i32(),
+ uv_rect_kind,
+ picture_task_id,
+ device_pixel_scale,
+ );
+
+ frame_state.init_surface_chain(
+ raster_config.surface_index,
+ filter_task_id,
+ picture_task_id,
+ parent_surface_index,
+ );
+ }
+ }
+ }
+ None => {}
+ };
+
+ #[cfg(feature = "capture")]
+ {
+ if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+ if let Some(PictureCompositeMode::TileCache { slice_id }) = self.requested_composite_mode {
+ if let Some(ref tile_cache) = tile_caches.get(&slice_id) {
+ // extract just the fields that we're interested in
+ let mut tile_cache_tiny = TileCacheInstanceSerializer {
+ slice: tile_cache.slice,
+ tiles: FastHashMap::default(),
+ background_color: tile_cache.background_color,
+ fract_offset: tile_cache.fract_offset
+ };
+ for (key, tile) in &tile_cache.tiles {
+ tile_cache_tiny.tiles.insert(*key, TileSerializer {
+ rect: tile.local_tile_rect,
+ current_descriptor: tile.current_descriptor.clone(),
+ device_fract_offset: tile.device_fract_offset,
+ id: tile.id,
+ root: tile.root.clone(),
+ background_color: tile.background_color,
+ invalidation_reason: tile.invalidation_reason.clone()
+ });
+ }
+ let text = ron::ser::to_string_pretty(&tile_cache_tiny, Default::default()).unwrap();
+ tile_cache_logger.add(text, map_pic_to_world.get_transform());
+ }
+ }
+ }
+ }
+ #[cfg(not(feature = "capture"))]
+ {
+ let _tile_cache_logger = tile_cache_logger; // unused variable fix
+ }
+
+ let state = PictureState {
+ //TODO: check for MAX_CACHE_SIZE here?
+ map_local_to_pic,
+ map_pic_to_world,
+ map_pic_to_raster,
+ map_raster_to_world,
+ plane_splitter,
+ };
+
+ let mut dirty_region_count = 0;
+
+ // If this is a picture cache, push the dirty region to ensure any
+ // child primitives are culled and clipped to the dirty rect(s).
+ if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) = self.raster_config {
+ let dirty_region = tile_caches[&slice_id].dirty_region.clone();
+ frame_state.push_dirty_region(dirty_region);
+ dirty_region_count += 1;
+ }
+
+ if inflation_factor > 0.0 {
+ let inflated_region = frame_state.current_dirty_region().inflate(
+ inflation_factor,
+ frame_context.spatial_tree,
+ );
+ frame_state.push_dirty_region(inflated_region);
+ dirty_region_count += 1;
+ }
+
+ // Disallow subpixel AA if an intermediate surface is needed.
+ // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
+ let (is_passthrough, subpixel_mode) = match self.raster_config {
+ Some(RasterConfig { ref composite_mode, .. }) => {
+ let subpixel_mode = match composite_mode {
+ PictureCompositeMode::TileCache { slice_id } => {
+ tile_caches[&slice_id].subpixel_mode.clone()
+ }
+ PictureCompositeMode::Blit(..) |
+ PictureCompositeMode::ComponentTransferFilter(..) |
+ PictureCompositeMode::Filter(..) |
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::SvgFilter(..) => {
+ // TODO(gw): We can take advantage of the same logic that
+ // exists in the opaque rect detection for tile
+ // caches, to allow subpixel text on other surfaces
+ // that can be detected as opaque.
+ SubpixelMode::Deny
+ }
+ };
+
+ (false, subpixel_mode)
+ }
+ None => {
+ (true, SubpixelMode::Allow)
+ }
+ };
+
+ // Still disable subpixel AA if parent forbids it
+ let subpixel_mode = match (parent_subpixel_mode, subpixel_mode) {
+ (SubpixelMode::Allow, SubpixelMode::Allow) => {
+ // Both parent and this surface unconditionally allow subpixel AA
+ SubpixelMode::Allow
+ }
+ (SubpixelMode::Allow, SubpixelMode::Conditional { allowed_rect, excluded_rects }) => {
+ // Parent allows, but we are conditional subpixel AA
+ SubpixelMode::Conditional {
+ allowed_rect,
+ excluded_rects,
+ }
+ }
+ (SubpixelMode::Conditional { allowed_rect, excluded_rects }, SubpixelMode::Allow) => {
+ // Propagate conditional subpixel mode to child pictures that allow subpixel AA
+ SubpixelMode::Conditional {
+ allowed_rect: *allowed_rect,
+ excluded_rects: excluded_rects.clone(),
+ }
+ }
+ (SubpixelMode::Conditional { .. }, SubpixelMode::Conditional { ..}) => {
+ unreachable!("bug: only top level picture caches have conditional subpixel");
+ }
+ (SubpixelMode::Deny, _) | (_, SubpixelMode::Deny) => {
+ // Either parent or this surface explicitly deny subpixel, these take precedence
+ SubpixelMode::Deny
+ }
+ };
+
+ let context = PictureContext {
+ pic_index,
+ apply_local_clip_rect: self.apply_local_clip_rect,
+ is_passthrough,
+ raster_spatial_node_index,
+ surface_spatial_node_index,
+ surface_index,
+ dirty_region_count,
+ subpixel_mode,
+ };
+
+ let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
+
+ Some((context, state, prim_list))
+ }
+
+ pub fn restore_context(
+ &mut self,
+ prim_list: PrimitiveList,
+ context: PictureContext,
+ state: PictureState,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ // Pop any dirty regions this picture set
+ for _ in 0 .. context.dirty_region_count {
+ frame_state.pop_dirty_region();
+ }
+
+ self.prim_list = prim_list;
+ self.state = Some(state);
+ }
+
+ pub fn take_state(&mut self) -> PictureState {
+ self.state.take().expect("bug: no state present!")
+ }
+
+ /// Add a primitive instance to the plane splitter. The function would generate
+ /// an appropriate polygon, clip it against the frustum, and register with the
+ /// given plane splitter.
+ pub fn add_split_plane(
+ splitter: &mut PlaneSplitter,
+ spatial_tree: &SpatialTree,
+ prim_spatial_node_index: SpatialNodeIndex,
+ original_local_rect: LayoutRect,
+ combined_local_clip_rect: &LayoutRect,
+ world_rect: WorldRect,
+ plane_split_anchor: PlaneSplitAnchor,
+ ) -> bool {
+ let transform = spatial_tree
+ .get_world_transform(prim_spatial_node_index);
+ let matrix = transform.clone().into_transform().cast();
+
+ // Apply the local clip rect here, before splitting. This is
+ // because the local clip rect can't be applied in the vertex
+ // shader for split composites, since we are drawing polygons
+ // rather that rectangles. The interpolation still works correctly
+ // since we determine the UVs by doing a bilerp with a factor
+ // from the original local rect.
+ let local_rect = match original_local_rect
+ .intersection(combined_local_clip_rect)
+ {
+ Some(rect) => rect.cast(),
+ None => return false,
+ };
+ let world_rect = world_rect.cast();
+
+ match transform {
+ CoordinateSpaceMapping::Local => {
+ let polygon = Polygon::from_rect(
+ local_rect * Scale::new(1.0),
+ plane_split_anchor,
+ );
+ splitter.add(polygon);
+ }
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) if scale_offset.scale == Vector2D::new(1.0, 1.0) => {
+ let inv_matrix = scale_offset.inverse().to_transform().cast();
+ let polygon = Polygon::from_transformed_rect_with_inverse(
+ local_rect,
+ &matrix,
+ &inv_matrix,
+ plane_split_anchor,
+ ).unwrap();
+ splitter.add(polygon);
+ }
+ CoordinateSpaceMapping::ScaleOffset(_) |
+ CoordinateSpaceMapping::Transform(_) => {
+ let mut clipper = Clipper::new();
+ let results = clipper.clip_transformed(
+ Polygon::from_rect(
+ local_rect,
+ plane_split_anchor,
+ ),
+ &matrix,
+ Some(world_rect),
+ );
+ if let Ok(results) = results {
+ for poly in results {
+ splitter.add(poly);
+ }
+ }
+ }
+ }
+
+ true
+ }
+
+ pub fn resolve_split_planes(
+ &mut self,
+ splitter: &mut PlaneSplitter,
+ gpu_cache: &mut GpuCache,
+ spatial_tree: &SpatialTree,
+ ) {
+ let ordered = match self.context_3d {
+ Picture3DContext::In { root_data: Some(ref mut list), .. } => list,
+ _ => panic!("Expected to find 3D context root"),
+ };
+ ordered.clear();
+
+ // Process the accumulated split planes and order them for rendering.
+ // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
+ let sorted = splitter.sort(vec3(0.0, 0.0, 1.0));
+ ordered.reserve(sorted.len());
+ for poly in sorted {
+ let cluster = &self.prim_list.clusters[poly.anchor.cluster_index];
+ let spatial_node_index = cluster.spatial_node_index;
+ let transform = match spatial_tree
+ .get_world_transform(spatial_node_index)
+ .inverse()
+ {
+ Some(transform) => transform.into_transform(),
+ // logging this would be a bit too verbose
+ None => continue,
+ };
+
+ let local_points = [
+ transform.transform_point3d(poly.points[0].cast()),
+ transform.transform_point3d(poly.points[1].cast()),
+ transform.transform_point3d(poly.points[2].cast()),
+ transform.transform_point3d(poly.points[3].cast()),
+ ];
+
+ // If any of the points are un-transformable, just drop this
+ // plane from drawing.
+ if local_points.iter().any(|p| p.is_none()) {
+ continue;
+ }
+
+ let p0 = local_points[0].unwrap();
+ let p1 = local_points[1].unwrap();
+ let p2 = local_points[2].unwrap();
+ let p3 = local_points[3].unwrap();
+ let gpu_blocks = [
+ [p0.x, p0.y, p1.x, p1.y].into(),
+ [p2.x, p2.y, p3.x, p3.y].into(),
+ ];
+ let gpu_handle = gpu_cache.push_per_frame_blocks(&gpu_blocks);
+ let gpu_address = gpu_cache.get_address(&gpu_handle);
+
+ ordered.push(OrderedPictureChild {
+ anchor: poly.anchor,
+ spatial_node_index,
+ gpu_address,
+ });
+ }
+ }
+
+ /// Called during initial picture traversal, before we know the
+ /// bounding rect of children. It is possible to determine the
+ /// surface / raster config now though.
+ fn pre_update(
+ &mut self,
+ state: &mut PictureUpdateState,
+ frame_context: &FrameBuildingContext,
+ ) -> Option<PrimitiveList> {
+ // Reset raster config in case we early out below.
+ self.raster_config = None;
+
+ // Resolve animation properties, and early out if the filter
+ // properties make this picture invisible.
+ if !self.resolve_scene_properties(frame_context.scene_properties) {
+ return None;
+ }
+
+ // For out-of-preserve-3d pictures, the backface visibility is determined by
+ // the local transform only.
+ // Note: we aren't taking the transform relativce to the parent picture,
+ // since picture tree can be more dense than the corresponding spatial tree.
+ if !self.is_backface_visible {
+ if let Picture3DContext::Out = self.context_3d {
+ match frame_context.spatial_tree.get_local_visible_face(self.spatial_node_index) {
+ VisibleFace::Front => {}
+ VisibleFace::Back => return None,
+ }
+ }
+ }
+
+ // Push information about this pic on stack for children to read.
+ state.push_picture(PictureInfo {
+ _spatial_node_index: self.spatial_node_index,
+ });
+
+ // See if this picture actually needs a surface for compositing.
+ // TODO(gw): FPC: Remove the actual / requested composite mode distinction.
+ let actual_composite_mode = self.requested_composite_mode.clone();
+
+ if let Some(composite_mode) = actual_composite_mode {
+ // Retrieve the positioning node information for the parent surface.
+ let parent_raster_node_index = state.current_surface().raster_spatial_node_index;
+ let parent_device_pixel_scale = state.current_surface().device_pixel_scale;
+ let surface_spatial_node_index = self.spatial_node_index;
+
+ // Filters must be applied before transforms, to do this, we can mark this picture as establishing a raster root.
+ let has_svg_filter = if let PictureCompositeMode::SvgFilter(..) = composite_mode {
+ true
+ } else {
+ false
+ };
+
+ let surface_to_parent_transform = frame_context.spatial_tree
+ .get_relative_transform(surface_spatial_node_index, parent_raster_node_index);
+
+ // Check if there is perspective or if an SVG filter is applied, and thus whether a new
+ // rasterization root should be established.
+ let establishes_raster_root = has_svg_filter || surface_to_parent_transform.is_perspective();
+
+ let (raster_spatial_node_index, device_pixel_scale) = if establishes_raster_root {
+ // If a raster root is established, this surface should be scaled based on the scale factors of the surface raster to parent raster transform.
+ // This scaling helps ensure that the content in this surface does not become blurry or pixelated when composited in the parent surface.
+ let scale_factors = surface_to_parent_transform.scale_factors();
+
+ // Pick the largest scale factor of the transform for the scaling factor.
+ // Currently, we ensure that the scaling factor is >= 1.0 as a smaller scale factor can result in blurry output.
+ let scaling_factor = scale_factors.0.max(scale_factors.1).max(1.0);
+
+ let device_pixel_scale = parent_device_pixel_scale * Scale::new(scaling_factor);
+ (surface_spatial_node_index, device_pixel_scale)
+ } else {
+ (parent_raster_node_index, parent_device_pixel_scale)
+ };
+
+ let scale_factors = frame_context
+ .spatial_tree
+ .get_relative_transform(surface_spatial_node_index, raster_spatial_node_index)
+ .scale_factors();
+
+ // This inflation factor is to be applied to all primitives within the surface.
+ // Only inflate if the caller hasn't already inflated the bounding rects for this filter.
+ let mut inflation_factor = 0.0;
+ if self.options.inflate_if_required {
+ match composite_mode {
+ PictureCompositeMode::Filter(Filter::Blur(width, height)) => {
+ let blur_radius = f32::max(clamp_blur_radius(width, scale_factors), clamp_blur_radius(height, scale_factors));
+ // The amount of extra space needed for primitives inside
+ // this picture to ensure the visibility check is correct.
+ inflation_factor = blur_radius * BLUR_SAMPLE_SCALE;
+ }
+ PictureCompositeMode::SvgFilter(ref primitives, _) => {
+ let mut max = 0.0;
+ for primitive in primitives {
+ if let FilterPrimitiveKind::Blur(ref blur) = primitive.kind {
+ max = f32::max(max, blur.width);
+ max = f32::max(max, blur.height);
+ }
+ }
+ inflation_factor = clamp_blur_radius(max, scale_factors) * BLUR_SAMPLE_SCALE;
+ }
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ // TODO(gw): This is incorrect, since we don't consider the drop shadow
+ // offset. However, fixing that is a larger task, so this is
+ // an improvement on the current case (this at least works where
+ // the offset of the drop-shadow is ~0, which is often true).
+
+ // Can't use max_by_key here since f32 isn't Ord
+ let mut max_blur_radius: f32 = 0.0;
+ for shadow in shadows {
+ max_blur_radius = max_blur_radius.max(shadow.blur_radius);
+ }
+
+ inflation_factor = clamp_blur_radius(max_blur_radius, scale_factors) * BLUR_SAMPLE_SCALE;
+ }
+ _ => {}
+ }
+ }
+
+ let surface = SurfaceInfo::new(
+ surface_spatial_node_index,
+ raster_spatial_node_index,
+ inflation_factor,
+ frame_context.global_screen_world_rect,
+ &frame_context.spatial_tree,
+ device_pixel_scale,
+ scale_factors,
+ );
+
+ self.raster_config = Some(RasterConfig {
+ composite_mode,
+ establishes_raster_root,
+ surface_index: state.push_surface(surface),
+ root_scaling_factor: 1.0,
+ clipped_bounding_rect: WorldRect::zero(),
+ });
+ }
+
+ Some(mem::replace(&mut self.prim_list, PrimitiveList::empty()))
+ }
+
+ /// Called after updating child pictures during the initial
+ /// picture traversal.
+ fn post_update(
+ &mut self,
+ prim_list: PrimitiveList,
+ state: &mut PictureUpdateState,
+ frame_context: &FrameBuildingContext,
+ data_stores: &mut DataStores,
+ ) {
+ // Restore the pictures list used during recursion.
+ self.prim_list = prim_list;
+
+ // Pop the state information about this picture.
+ state.pop_picture();
+
+ let surface = state.current_surface_mut();
+
+ for cluster in &mut self.prim_list.clusters {
+ cluster.flags.remove(ClusterFlags::IS_VISIBLE);
+
+ // Skip the cluster if backface culled.
+ if !cluster.flags.contains(ClusterFlags::IS_BACKFACE_VISIBLE) {
+ // For in-preserve-3d primitives and pictures, the backface visibility is
+ // evaluated relative to the containing block.
+ if let Picture3DContext::In { ancestor_index, .. } = self.context_3d {
+ let mut face = VisibleFace::Front;
+ frame_context.spatial_tree.get_relative_transform_with_face(
+ cluster.spatial_node_index,
+ ancestor_index,
+ Some(&mut face),
+ );
+ if face == VisibleFace::Back {
+ continue
+ }
+ }
+ }
+
+ // No point including this cluster if it can't be transformed
+ let spatial_node = &frame_context
+ .spatial_tree
+ .spatial_nodes[cluster.spatial_node_index.0 as usize];
+ if !spatial_node.invertible {
+ continue;
+ }
+
+ // Update any primitives/cluster bounding rects that can only be done
+ // with information available during frame building.
+ if cluster.flags.contains(ClusterFlags::IS_BACKDROP_FILTER) {
+ let backdrop_to_world_mapper = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ cluster.spatial_node_index,
+ LayoutRect::max_rect(),
+ frame_context.spatial_tree,
+ );
+
+ for prim_instance in &mut self.prim_list.prim_instances[cluster.prim_range()] {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Backdrop { data_handle, .. } => {
+ // The actual size and clip rect of this primitive are determined by computing the bounding
+ // box of the projected rect of the backdrop-filter element onto the backdrop.
+ let prim_data = &mut data_stores.backdrop[data_handle];
+ let spatial_node_index = prim_data.kind.spatial_node_index;
+
+ // We cannot use the relative transform between the backdrop and the element because
+ // that doesn't take into account any projection transforms that both spatial nodes are children of.
+ // Instead, we first project from the element to the world space and get a flattened 2D bounding rect
+ // in the screen space, we then map this rect from the world space to the backdrop space to get the
+ // proper bounding box where the backdrop-filter needs to be processed.
+
+ let prim_to_world_mapper = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ spatial_node_index,
+ LayoutRect::max_rect(),
+ frame_context.spatial_tree,
+ );
+
+ // First map to the screen and get a flattened rect
+ let prim_rect = prim_to_world_mapper.map(&prim_data.kind.border_rect).unwrap_or_else(LayoutRect::zero);
+ // Backwards project the flattened rect onto the backdrop
+ let prim_rect = backdrop_to_world_mapper.unmap(&prim_rect).unwrap_or_else(LayoutRect::zero);
+
+ // TODO(aosmond): Is this safe? Updating the primitive size during
+ // frame building is usually problematic since scene building will cache
+ // the primitive information in the GPU already.
+ prim_data.common.prim_rect = prim_rect;
+ prim_instance.clip_set.local_clip_rect = prim_rect;
+
+ // Update the cluster bounding rect now that we have the backdrop rect.
+ cluster.bounding_rect = cluster.bounding_rect.union(&prim_rect);
+ }
+ _ => {
+ panic!("BUG: unexpected deferred primitive kind for cluster updates");
+ }
+ }
+ }
+ }
+
+ // Map the cluster bounding rect into the space of the surface, and
+ // include it in the surface bounding rect.
+ surface.map_local_to_surface.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ // Mark the cluster visible, since it passed the invertible and
+ // backface checks.
+ cluster.flags.insert(ClusterFlags::IS_VISIBLE);
+ if let Some(cluster_rect) = surface.map_local_to_surface.map(&cluster.bounding_rect) {
+ surface.rect = surface.rect.union(&cluster_rect);
+ }
+ }
+
+ // If this picture establishes a surface, then map the surface bounding
+ // rect into the parent surface coordinate space, and propagate that up
+ // to the parent.
+ if let Some(ref mut raster_config) = self.raster_config {
+ let surface = state.current_surface_mut();
+ // Inflate the local bounding rect if required by the filter effect.
+ // This inflaction factor is to be applied to the surface itself.
+ if self.options.inflate_if_required {
+ surface.rect = raster_config.composite_mode.inflate_picture_rect(surface.rect, surface.scale_factors);
+
+ // The picture's local rect is calculated as the union of the
+ // snapped primitive rects, which should result in a snapped
+ // local rect, unless it was inflated. This is also done during
+ // update visibility when calculating the picture's precise
+ // local rect.
+ let snap_surface_to_raster = SpaceSnapper::new_with_target(
+ surface.raster_spatial_node_index,
+ self.spatial_node_index,
+ surface.device_pixel_scale,
+ frame_context.spatial_tree,
+ );
+
+ surface.rect = snap_surface_to_raster.snap_rect(&surface.rect);
+ }
+
+ let mut surface_rect = surface.rect * Scale::new(1.0);
+
+ // Pop this surface from the stack
+ let surface_index = state.pop_surface();
+ debug_assert_eq!(surface_index, raster_config.surface_index);
+
+ // Set the estimated and precise local rects. The precise local rect
+ // may be changed again during frame visibility.
+ self.estimated_local_rect = surface_rect;
+ self.precise_local_rect = surface_rect;
+
+ // Drop shadows draw both a content and shadow rect, so need to expand the local
+ // rect of any surfaces to be composited in parent surfaces correctly.
+ match raster_config.composite_mode {
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ for shadow in shadows {
+ let shadow_rect = self.estimated_local_rect.translate(shadow.offset);
+ surface_rect = surface_rect.union(&shadow_rect);
+ }
+ }
+ _ => {}
+ }
+
+ // Propagate up to parent surface, now that we know this surface's static rect
+ let parent_surface = state.current_surface_mut();
+ parent_surface.map_local_to_surface.set_target_spatial_node(
+ self.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+ if let Some(parent_surface_rect) = parent_surface
+ .map_local_to_surface
+ .map(&surface_rect)
+ {
+ parent_surface.rect = parent_surface.rect.union(&parent_surface_rect);
+ }
+ }
+ }
+
+ pub fn prepare_for_render(
+ &mut self,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ ) -> bool {
+ let mut pic_state_for_children = self.take_state();
+
+ if let Some(ref mut splitter) = pic_state_for_children.plane_splitter {
+ self.resolve_split_planes(
+ splitter,
+ &mut frame_state.gpu_cache,
+ &frame_context.spatial_tree,
+ );
+ }
+
+ let raster_config = match self.raster_config {
+ Some(ref mut raster_config) => raster_config,
+ None => {
+ return true
+ }
+ };
+
+ // TODO(gw): Almost all of the Picture types below use extra_gpu_cache_data
+ // to store the same type of data. The exception is the filter
+ // with a ColorMatrix, which stores the color matrix here. It's
+ // probably worth tidying this code up to be a bit more consistent.
+ // Perhaps store the color matrix after the common data, even though
+ // it's not used by that shader.
+
+ match raster_config.composite_mode {
+ PictureCompositeMode::TileCache { .. } => {}
+ PictureCompositeMode::Filter(Filter::Blur(..)) => {}
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ self.extra_gpu_data_handles.resize(shadows.len(), GpuCacheHandle::new());
+ for (shadow, extra_handle) in shadows.iter().zip(self.extra_gpu_data_handles.iter_mut()) {
+ if let Some(mut request) = frame_state.gpu_cache.request(extra_handle) {
+ // Basic brush primitive header is (see end of prepare_prim_for_render_inner in prim_store.rs)
+ // [brush specific data]
+ // [segment_rect, segment data]
+ let shadow_rect = self.precise_local_rect.translate(shadow.offset);
+
+ // ImageBrush colors
+ request.push(shadow.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.precise_local_rect.size.width,
+ self.precise_local_rect.size.height,
+ 0.0,
+ 0.0,
+ ]);
+
+ // segment rect / extra data
+ request.push(shadow_rect);
+ request.push([0.0, 0.0, 0.0, 0.0]);
+ }
+ }
+ }
+ PictureCompositeMode::MixBlend(..) if !frame_context.fb_config.gpu_supports_advanced_blend => {}
+ PictureCompositeMode::Filter(ref filter) => {
+ match *filter {
+ Filter::ColorMatrix(ref m) => {
+ if self.extra_gpu_data_handles.is_empty() {
+ self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+ }
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
+ for i in 0..5 {
+ request.push([m[i*4], m[i*4+1], m[i*4+2], m[i*4+3]]);
+ }
+ }
+ }
+ Filter::Flood(ref color) => {
+ if self.extra_gpu_data_handles.is_empty() {
+ self.extra_gpu_data_handles.push(GpuCacheHandle::new());
+ }
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.extra_gpu_data_handles[0]) {
+ request.push(color.to_array());
+ }
+ }
+ _ => {}
+ }
+ }
+ PictureCompositeMode::ComponentTransferFilter(handle) => {
+ let filter_data = &mut data_stores.filter_data[handle];
+ filter_data.update(frame_state);
+ }
+ PictureCompositeMode::MixBlend(..) |
+ PictureCompositeMode::Blit(_) |
+ PictureCompositeMode::SvgFilter(..) => {}
+ }
+
+ true
+ }
+}
+
+// Calculate a single homogeneous screen-space UV for a picture.
+fn calculate_screen_uv(
+ local_pos: &PicturePoint,
+ transform: &PictureToRasterTransform,
+ rendered_rect: &DeviceRect,
+ device_pixel_scale: DevicePixelScale,
+) -> DeviceHomogeneousVector {
+ let raster_pos = transform.transform_point2d_homogeneous(*local_pos);
+
+ DeviceHomogeneousVector::new(
+ (raster_pos.x * device_pixel_scale.0 - rendered_rect.origin.x * raster_pos.w) / rendered_rect.size.width,
+ (raster_pos.y * device_pixel_scale.0 - rendered_rect.origin.y * raster_pos.w) / rendered_rect.size.height,
+ 0.0,
+ raster_pos.w,
+ )
+}
+
+// Calculate a UV rect within an image based on the screen space
+// vertex positions of a picture.
+fn calculate_uv_rect_kind(
+ pic_rect: &PictureRect,
+ transform: &PictureToRasterTransform,
+ rendered_rect: &DeviceRect,
+ device_pixel_scale: DevicePixelScale,
+) -> UvRectKind {
+ let top_left = calculate_screen_uv(
+ &pic_rect.origin,
+ transform,
+ &rendered_rect,
+ device_pixel_scale,
+ );
+
+ let top_right = calculate_screen_uv(
+ &pic_rect.top_right(),
+ transform,
+ &rendered_rect,
+ device_pixel_scale,
+ );
+
+ let bottom_left = calculate_screen_uv(
+ &pic_rect.bottom_left(),
+ transform,
+ &rendered_rect,
+ device_pixel_scale,
+ );
+
+ let bottom_right = calculate_screen_uv(
+ &pic_rect.bottom_right(),
+ transform,
+ &rendered_rect,
+ device_pixel_scale,
+ );
+
+ UvRectKind::Quad {
+ top_left,
+ top_right,
+ bottom_left,
+ bottom_right,
+ }
+}
+
+fn create_raster_mappers(
+ surface_spatial_node_index: SpatialNodeIndex,
+ raster_spatial_node_index: SpatialNodeIndex,
+ world_rect: WorldRect,
+ spatial_tree: &SpatialTree,
+) -> (SpaceMapper<RasterPixel, WorldPixel>, SpaceMapper<PicturePixel, RasterPixel>) {
+ let map_raster_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ raster_spatial_node_index,
+ world_rect,
+ spatial_tree,
+ );
+
+ let raster_bounds = map_raster_to_world.unmap(&world_rect)
+ .unwrap_or_else(RasterRect::max_rect);
+
+ let map_pic_to_raster = SpaceMapper::new_with_target(
+ raster_spatial_node_index,
+ surface_spatial_node_index,
+ raster_bounds,
+ spatial_tree,
+ );
+
+ (map_raster_to_world, map_pic_to_raster)
+}
+
+fn get_transform_key(
+ spatial_node_index: SpatialNodeIndex,
+ cache_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+) -> TransformKey {
+ // Note: this is the only place where we don't know beforehand if the tile-affecting
+ // spatial node is below or above the current picture.
+ let transform = if cache_spatial_node_index >= spatial_node_index {
+ spatial_tree
+ .get_relative_transform(
+ cache_spatial_node_index,
+ spatial_node_index,
+ )
+ } else {
+ spatial_tree
+ .get_relative_transform(
+ spatial_node_index,
+ cache_spatial_node_index,
+ )
+ };
+ transform.into()
+}
+
+/// A key for storing primitive comparison results during tile dependency tests.
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+struct PrimitiveComparisonKey {
+ prev_index: PrimitiveDependencyIndex,
+ curr_index: PrimitiveDependencyIndex,
+}
+
+/// Information stored an image dependency
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageDependency {
+ pub key: ImageKey,
+ pub generation: ImageGeneration,
+}
+
+impl ImageDependency {
+ pub const INVALID: ImageDependency = ImageDependency {
+ key: ImageKey::DUMMY,
+ generation: ImageGeneration::INVALID,
+ };
+}
+
+/// A helper struct to compare a primitive and all its sub-dependencies.
+struct PrimitiveComparer<'a> {
+ clip_comparer: CompareHelper<'a, ItemUid>,
+ transform_comparer: CompareHelper<'a, SpatialNodeKey>,
+ image_comparer: CompareHelper<'a, ImageDependency>,
+ opacity_comparer: CompareHelper<'a, OpacityBinding>,
+ color_comparer: CompareHelper<'a, ColorBinding>,
+ resource_cache: &'a ResourceCache,
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+}
+
+impl<'a> PrimitiveComparer<'a> {
+ fn new(
+ prev: &'a TileDescriptor,
+ curr: &'a TileDescriptor,
+ resource_cache: &'a ResourceCache,
+ spatial_node_comparer: &'a mut SpatialNodeComparer,
+ opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+ color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+ ) -> Self {
+ let clip_comparer = CompareHelper::new(
+ &prev.clips,
+ &curr.clips,
+ );
+
+ let transform_comparer = CompareHelper::new(
+ &prev.transforms,
+ &curr.transforms,
+ );
+
+ let image_comparer = CompareHelper::new(
+ &prev.images,
+ &curr.images,
+ );
+
+ let opacity_comparer = CompareHelper::new(
+ &prev.opacity_bindings,
+ &curr.opacity_bindings,
+ );
+
+ let color_comparer = CompareHelper::new(
+ &prev.color_bindings,
+ &curr.color_bindings,
+ );
+
+ PrimitiveComparer {
+ clip_comparer,
+ transform_comparer,
+ image_comparer,
+ opacity_comparer,
+ color_comparer,
+ resource_cache,
+ spatial_node_comparer,
+ opacity_bindings,
+ color_bindings,
+ }
+ }
+
+ fn reset(&mut self) {
+ self.clip_comparer.reset();
+ self.transform_comparer.reset();
+ self.image_comparer.reset();
+ self.opacity_comparer.reset();
+ self.color_comparer.reset();
+ }
+
+ fn advance_prev(&mut self, prim: &PrimitiveDescriptor) {
+ self.clip_comparer.advance_prev(prim.clip_dep_count);
+ self.transform_comparer.advance_prev(prim.transform_dep_count);
+ self.image_comparer.advance_prev(prim.image_dep_count);
+ self.opacity_comparer.advance_prev(prim.opacity_binding_dep_count);
+ self.color_comparer.advance_prev(prim.color_binding_dep_count);
+ }
+
+ fn advance_curr(&mut self, prim: &PrimitiveDescriptor) {
+ self.clip_comparer.advance_curr(prim.clip_dep_count);
+ self.transform_comparer.advance_curr(prim.transform_dep_count);
+ self.image_comparer.advance_curr(prim.image_dep_count);
+ self.opacity_comparer.advance_curr(prim.opacity_binding_dep_count);
+ self.color_comparer.advance_curr(prim.color_binding_dep_count);
+ }
+
+ /// Check if two primitive descriptors are the same.
+ fn compare_prim(
+ &mut self,
+ prev: &PrimitiveDescriptor,
+ curr: &PrimitiveDescriptor,
+ opt_detail: Option<&mut PrimitiveCompareResultDetail>,
+ ) -> PrimitiveCompareResult {
+ let resource_cache = self.resource_cache;
+ let spatial_node_comparer = &mut self.spatial_node_comparer;
+ let opacity_bindings = self.opacity_bindings;
+ let color_bindings = self.color_bindings;
+
+ // Check equality of the PrimitiveDescriptor
+ if prev != curr {
+ if let Some(detail) = opt_detail {
+ *detail = PrimitiveCompareResultDetail::Descriptor{ old: *prev, new: *curr };
+ }
+ return PrimitiveCompareResult::Descriptor;
+ }
+
+ // Check if any of the clips this prim has are different.
+ let mut clip_result = CompareHelperResult::Equal;
+ if !self.clip_comparer.is_same(
+ prev.clip_dep_count,
+ curr.clip_dep_count,
+ |prev, curr| {
+ prev == curr
+ },
+ if opt_detail.is_some() { Some(&mut clip_result) } else { None }
+ ) {
+ if let Some(detail) = opt_detail { *detail = PrimitiveCompareResultDetail::Clip{ detail: clip_result }; }
+ return PrimitiveCompareResult::Clip;
+ }
+
+ // Check if any of the transforms this prim has are different.
+ let mut transform_result = CompareHelperResult::Equal;
+ if !self.transform_comparer.is_same(
+ prev.transform_dep_count,
+ curr.transform_dep_count,
+ |prev, curr| {
+ spatial_node_comparer.are_transforms_equivalent(prev, curr)
+ },
+ if opt_detail.is_some() { Some(&mut transform_result) } else { None },
+ ) {
+ if let Some(detail) = opt_detail {
+ *detail = PrimitiveCompareResultDetail::Transform{ detail: transform_result };
+ }
+ return PrimitiveCompareResult::Transform;
+ }
+
+ // Check if any of the images this prim has are different.
+ let mut image_result = CompareHelperResult::Equal;
+ if !self.image_comparer.is_same(
+ prev.image_dep_count,
+ curr.image_dep_count,
+ |prev, curr| {
+ prev == curr &&
+ resource_cache.get_image_generation(curr.key) == curr.generation
+ },
+ if opt_detail.is_some() { Some(&mut image_result) } else { None },
+ ) {
+ if let Some(detail) = opt_detail {
+ *detail = PrimitiveCompareResultDetail::Image{ detail: image_result };
+ }
+ return PrimitiveCompareResult::Image;
+ }
+
+ // Check if any of the opacity bindings this prim has are different.
+ let mut bind_result = CompareHelperResult::Equal;
+ if !self.opacity_comparer.is_same(
+ prev.opacity_binding_dep_count,
+ curr.opacity_binding_dep_count,
+ |prev, curr| {
+ if prev != curr {
+ return false;
+ }
+
+ if let OpacityBinding::Binding(id) = curr {
+ if opacity_bindings
+ .get(id)
+ .map_or(true, |info| info.changed) {
+ return false;
+ }
+ }
+
+ true
+ },
+ if opt_detail.is_some() { Some(&mut bind_result) } else { None },
+ ) {
+ if let Some(detail) = opt_detail {
+ *detail = PrimitiveCompareResultDetail::OpacityBinding{ detail: bind_result };
+ }
+ return PrimitiveCompareResult::OpacityBinding;
+ }
+
+ // Check if any of the color bindings this prim has are different.
+ let mut bind_result = CompareHelperResult::Equal;
+ if !self.color_comparer.is_same(
+ prev.color_binding_dep_count,
+ curr.color_binding_dep_count,
+ |prev, curr| {
+ if prev != curr {
+ return false;
+ }
+
+ if let ColorBinding::Binding(id) = curr {
+ if color_bindings
+ .get(id)
+ .map_or(true, |info| info.changed) {
+ return false;
+ }
+ }
+
+ true
+ },
+ if opt_detail.is_some() { Some(&mut bind_result) } else { None },
+ ) {
+ if let Some(detail) = opt_detail {
+ *detail = PrimitiveCompareResultDetail::ColorBinding{ detail: bind_result };
+ }
+ return PrimitiveCompareResult::ColorBinding;
+ }
+
+ PrimitiveCompareResult::Equal
+ }
+}
+
+/// Details for a node in a quadtree that tracks dirty rects for a tile.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TileNodeKind {
+ Leaf {
+ /// The index buffer of primitives that affected this tile previous frame
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ prev_indices: Vec<PrimitiveDependencyIndex>,
+ /// The index buffer of primitives that affect this tile on this frame
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ curr_indices: Vec<PrimitiveDependencyIndex>,
+ /// A bitset of which of the last 64 frames have been dirty for this leaf.
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ dirty_tracker: u64,
+ /// The number of frames since this node split or merged.
+ #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))]
+ frames_since_modified: usize,
+ },
+ Node {
+ /// The four children of this node
+ children: Vec<TileNode>,
+ },
+}
+
+/// The kind of modification that a tile wants to do
+#[derive(Copy, Clone, PartialEq, Debug)]
+enum TileModification {
+ Split,
+ Merge,
+}
+
+/// A node in the dirty rect tracking quadtree.
+#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TileNode {
+ /// Leaf or internal node
+ pub kind: TileNodeKind,
+ /// Rect of this node in the same space as the tile cache picture
+ pub rect: PictureBox2D,
+}
+
+impl TileNode {
+ /// Construct a new leaf node, with the given primitive dependency index buffer
+ fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self {
+ TileNode {
+ kind: TileNodeKind::Leaf {
+ prev_indices: Vec::new(),
+ curr_indices,
+ dirty_tracker: 0,
+ frames_since_modified: 0,
+ },
+ rect: PictureBox2D::zero(),
+ }
+ }
+
+ /// Draw debug information about this tile node
+ fn draw_debug_rects(
+ &self,
+ pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
+ is_opaque: bool,
+ local_valid_rect: PictureRect,
+ scratch: &mut PrimitiveScratchBuffer,
+ global_device_pixel_scale: DevicePixelScale,
+ ) {
+ match self.kind {
+ TileNodeKind::Leaf { dirty_tracker, .. } => {
+ let color = if (dirty_tracker & 1) != 0 {
+ debug_colors::RED
+ } else if is_opaque {
+ debug_colors::GREEN
+ } else {
+ debug_colors::YELLOW
+ };
+
+ if let Some(local_rect) = local_valid_rect.intersection(&self.rect.to_rect()) {
+ let world_rect = pic_to_world_mapper
+ .map(&local_rect)
+ .unwrap();
+ let device_rect = world_rect * global_device_pixel_scale;
+
+ let outer_color = color.scale_alpha(0.3);
+ let inner_color = outer_color.scale_alpha(0.5);
+ scratch.push_debug_rect(
+ device_rect.inflate(-3.0, -3.0),
+ outer_color,
+ inner_color
+ );
+ }
+ }
+ TileNodeKind::Node { ref children, .. } => {
+ for child in children.iter() {
+ child.draw_debug_rects(
+ pic_to_world_mapper,
+ is_opaque,
+ local_valid_rect,
+ scratch,
+ global_device_pixel_scale,
+ );
+ }
+ }
+ }
+ }
+
+ /// Calculate the four child rects for a given node
+ fn get_child_rects(
+ rect: &PictureBox2D,
+ result: &mut [PictureBox2D; 4],
+ ) {
+ let p0 = rect.min;
+ let p1 = rect.max;
+ let pc = p0 + rect.size() * 0.5;
+
+ *result = [
+ PictureBox2D::new(
+ p0,
+ pc,
+ ),
+ PictureBox2D::new(
+ PicturePoint::new(pc.x, p0.y),
+ PicturePoint::new(p1.x, pc.y),
+ ),
+ PictureBox2D::new(
+ PicturePoint::new(p0.x, pc.y),
+ PicturePoint::new(pc.x, p1.y),
+ ),
+ PictureBox2D::new(
+ pc,
+ p1,
+ ),
+ ];
+ }
+
+ /// Called during pre_update, to clear the current dependencies
+ fn clear(
+ &mut self,
+ rect: PictureBox2D,
+ ) {
+ self.rect = rect;
+
+ match self.kind {
+ TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => {
+ // Swap current dependencies to be the previous frame
+ mem::swap(prev_indices, curr_indices);
+ curr_indices.clear();
+ // Note that another frame has passed in the dirty bit trackers
+ *dirty_tracker = *dirty_tracker << 1;
+ *frames_since_modified += 1;
+ }
+ TileNodeKind::Node { ref mut children, .. } => {
+ let mut child_rects = [PictureBox2D::zero(); 4];
+ TileNode::get_child_rects(&rect, &mut child_rects);
+ assert_eq!(child_rects.len(), children.len());
+
+ for (child, rect) in children.iter_mut().zip(child_rects.iter()) {
+ child.clear(*rect);
+ }
+ }
+ }
+ }
+
+ /// Add a primitive dependency to this node
+ fn add_prim(
+ &mut self,
+ index: PrimitiveDependencyIndex,
+ prim_rect: &PictureBox2D,
+ ) {
+ match self.kind {
+ TileNodeKind::Leaf { ref mut curr_indices, .. } => {
+ curr_indices.push(index);
+ }
+ TileNodeKind::Node { ref mut children, .. } => {
+ for child in children.iter_mut() {
+ if child.rect.intersects(prim_rect) {
+ child.add_prim(index, prim_rect);
+ }
+ }
+ }
+ }
+ }
+
+ /// Apply a merge or split operation to this tile, if desired
+ fn maybe_merge_or_split(
+ &mut self,
+ level: i32,
+ curr_prims: &[PrimitiveDescriptor],
+ max_split_levels: i32,
+ ) {
+ // Determine if this tile wants to split or merge
+ let mut tile_mod = None;
+
+ fn get_dirty_frames(
+ dirty_tracker: u64,
+ frames_since_modified: usize,
+ ) -> Option<u32> {
+ // Only consider splitting or merging at least 64 frames since we last changed
+ if frames_since_modified > 64 {
+ // Each bit in the tracker is a frame that was recently invalidated
+ Some(dirty_tracker.count_ones())
+ } else {
+ None
+ }
+ }
+
+ match self.kind {
+ TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => {
+ // Only consider splitting if the tree isn't too deep.
+ if level < max_split_levels {
+ if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) {
+ // If the tile has invalidated > 50% of the recent number of frames, split.
+ if dirty_frames > 32 {
+ tile_mod = Some(TileModification::Split);
+ }
+ }
+ }
+ }
+ TileNodeKind::Node { ref children, .. } => {
+ // There's two conditions that cause a node to merge its children:
+ // (1) If _all_ the child nodes are constantly invalidating, then we are wasting
+ // CPU time tracking dependencies for each child, so merge them.
+ // (2) If _none_ of the child nodes are recently invalid, then the page content
+ // has probably changed, and we no longer need to track fine grained dependencies here.
+
+ let mut static_count = 0;
+ let mut changing_count = 0;
+
+ for child in children {
+ // Only consider merging nodes at the edge of the tree.
+ if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind {
+ if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) {
+ if dirty_frames == 0 {
+ // Hasn't been invalidated for some time
+ static_count += 1;
+ } else if dirty_frames == 64 {
+ // Is constantly being invalidated
+ changing_count += 1;
+ }
+ }
+ }
+
+ // Only merge if all the child tiles are in agreement. Otherwise, we have some
+ // that are invalidating / static, and it's worthwhile tracking dependencies for
+ // them individually.
+ if static_count == 4 || changing_count == 4 {
+ tile_mod = Some(TileModification::Merge);
+ }
+ }
+ }
+ }
+
+ match tile_mod {
+ Some(TileModification::Split) => {
+ // To split a node, take the current dependency index buffer for this node, and
+ // split it into child index buffers.
+ let curr_indices = match self.kind {
+ TileNodeKind::Node { .. } => {
+ unreachable!("bug - only leaves can split");
+ }
+ TileNodeKind::Leaf { ref mut curr_indices, .. } => {
+ curr_indices.take()
+ }
+ };
+
+ let mut child_rects = [PictureBox2D::zero(); 4];
+ TileNode::get_child_rects(&self.rect, &mut child_rects);
+
+ let mut child_indices = [
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ ];
+
+ // Step through the index buffer, and add primitives to each of the children
+ // that they intersect.
+ for index in curr_indices {
+ let prim = &curr_prims[index.0 as usize];
+ for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) {
+ if prim.prim_clip_box.intersects(child_rect) {
+ indices.push(index);
+ }
+ }
+ }
+
+ // Create the child nodes and switch from leaf -> node.
+ let children = child_indices
+ .iter_mut()
+ .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new())))
+ .collect();
+
+ self.kind = TileNodeKind::Node {
+ children,
+ };
+ }
+ Some(TileModification::Merge) => {
+ // Construct a merged index buffer by collecting the dependency index buffers
+ // from each child, and merging them into a de-duplicated index buffer.
+ let merged_indices = match self.kind {
+ TileNodeKind::Node { ref mut children, .. } => {
+ let mut merged_indices = Vec::new();
+
+ for child in children.iter() {
+ let child_indices = match child.kind {
+ TileNodeKind::Leaf { ref curr_indices, .. } => {
+ curr_indices
+ }
+ TileNodeKind::Node { .. } => {
+ unreachable!("bug: child is not a leaf");
+ }
+ };
+ merged_indices.extend_from_slice(child_indices);
+ }
+
+ merged_indices.sort();
+ merged_indices.dedup();
+
+ merged_indices
+ }
+ TileNodeKind::Leaf { .. } => {
+ unreachable!("bug - trying to merge a leaf");
+ }
+ };
+
+ // Switch from a node to a leaf, with the combined index buffer
+ self.kind = TileNodeKind::Leaf {
+ prev_indices: Vec::new(),
+ curr_indices: merged_indices,
+ dirty_tracker: 0,
+ frames_since_modified: 0,
+ };
+ }
+ None => {
+ // If this node didn't merge / split, then recurse into children
+ // to see if they want to split / merge.
+ if let TileNodeKind::Node { ref mut children, .. } = self.kind {
+ for child in children.iter_mut() {
+ child.maybe_merge_or_split(
+ level+1,
+ curr_prims,
+ max_split_levels,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /// Update the dirty state of this node, building the overall dirty rect
+ fn update_dirty_rects(
+ &mut self,
+ prev_prims: &[PrimitiveDescriptor],
+ curr_prims: &[PrimitiveDescriptor],
+ prim_comparer: &mut PrimitiveComparer,
+ dirty_rect: &mut PictureBox2D,
+ compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+ invalidation_reason: &mut Option<InvalidationReason>,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ match self.kind {
+ TileNodeKind::Node { ref mut children, .. } => {
+ for child in children.iter_mut() {
+ child.update_dirty_rects(
+ prev_prims,
+ curr_prims,
+ prim_comparer,
+ dirty_rect,
+ compare_cache,
+ invalidation_reason,
+ frame_context,
+ );
+ }
+ }
+ TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => {
+ // If the index buffers are of different length, they must be different
+ if prev_indices.len() == curr_indices.len() {
+ let mut prev_i0 = 0;
+ let mut prev_i1 = 0;
+ prim_comparer.reset();
+
+ // Walk each index buffer, comparing primitives
+ for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) {
+ let i0 = prev_index.0 as usize;
+ let i1 = curr_index.0 as usize;
+
+ // Advance the dependency arrays for each primitive (this handles
+ // prims that may be skipped by these index buffers).
+ for i in prev_i0 .. i0 {
+ prim_comparer.advance_prev(&prev_prims[i]);
+ }
+ for i in prev_i1 .. i1 {
+ prim_comparer.advance_curr(&curr_prims[i]);
+ }
+
+ // Compare the primitives, caching the result in a hash map
+ // to save comparisons in other tree nodes.
+ let key = PrimitiveComparisonKey {
+ prev_index: *prev_index,
+ curr_index: *curr_index,
+ };
+
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ let mut compare_detail = PrimitiveCompareResultDetail::Equal;
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ let prim_compare_result_detail =
+ if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+ Some(&mut compare_detail)
+ } else {
+ None
+ };
+
+ #[cfg(not(any(feature = "capture", feature = "replay")))]
+ let compare_detail = PrimitiveCompareResultDetail::Equal;
+ #[cfg(not(any(feature = "capture", feature = "replay")))]
+ let prim_compare_result_detail = None;
+
+ let prim_compare_result = *compare_cache
+ .entry(key)
+ .or_insert_with(|| {
+ let prev = &prev_prims[i0];
+ let curr = &curr_prims[i1];
+ prim_comparer.compare_prim(prev, curr, prim_compare_result_detail)
+ });
+
+ // If not the same, mark this node as dirty and update the dirty rect
+ if prim_compare_result != PrimitiveCompareResult::Equal {
+ if invalidation_reason.is_none() {
+ *invalidation_reason = Some(InvalidationReason::Content {
+ prim_compare_result,
+ prim_compare_result_detail: Some(compare_detail)
+ });
+ }
+ *dirty_rect = self.rect.union(dirty_rect);
+ *dirty_tracker = *dirty_tracker | 1;
+ break;
+ }
+
+ prev_i0 = i0;
+ prev_i1 = i1;
+ }
+ } else {
+ if invalidation_reason.is_none() {
+ // if and only if tile logging is enabled, do the expensive step of
+ // converting indices back to ItemUids and allocating old and new vectors
+ // to store them in.
+ #[cfg(any(feature = "capture", feature = "replay"))]
+ {
+ if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+ let old = prev_indices.iter().map( |i| prev_prims[i.0 as usize].prim_uid ).collect();
+ let new = curr_indices.iter().map( |i| curr_prims[i.0 as usize].prim_uid ).collect();
+ *invalidation_reason = Some(InvalidationReason::PrimCount {
+ old: Some(old),
+ new: Some(new) });
+ } else {
+ *invalidation_reason = Some(InvalidationReason::PrimCount {
+ old: None,
+ new: None });
+ }
+ }
+ #[cfg(not(any(feature = "capture", feature = "replay")))]
+ {
+ *invalidation_reason = Some(InvalidationReason::PrimCount {
+ old: None,
+ new: None });
+ }
+ }
+ *dirty_rect = self.rect.union(dirty_rect);
+ *dirty_tracker = *dirty_tracker | 1;
+ }
+ }
+ }
+ }
+}
+
+impl CompositeState {
+ // A helper function to destroy all native surfaces for a given list of tiles
+ pub fn destroy_native_tiles<'a, I: Iterator<Item = &'a mut Box<Tile>>>(
+ &mut self,
+ tiles_iter: I,
+ resource_cache: &mut ResourceCache,
+ ) {
+ // Any old tiles that remain after the loop above are going to be dropped. For
+ // simple composite mode, the texture cache handle will expire and be collected
+ // by the texture cache. For native compositor mode, we need to explicitly
+ // invoke a callback to the client to destroy that surface.
+ if let CompositorKind::Native { .. } = self.compositor_kind {
+ for tile in tiles_iter {
+ // Only destroy native surfaces that have been allocated. It's
+ // possible for display port tiles to be created that never
+ // come on screen, and thus never get a native surface allocated.
+ if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface {
+ if let Some(id) = id.take() {
+ resource_cache.destroy_compositor_tile(id);
+ }
+ }
+ }
+ }
+ }
+}
+
+pub fn get_raster_rects(
+ pic_rect: PictureRect,
+ map_to_raster: &SpaceMapper<PicturePixel, RasterPixel>,
+ map_to_world: &SpaceMapper<RasterPixel, WorldPixel>,
+ prim_bounding_rect: WorldRect,
+ device_pixel_scale: DevicePixelScale,
+) -> Option<(DeviceRect, DeviceRect)> {
+ let unclipped_raster_rect = map_to_raster.map(&pic_rect)?;
+
+ let unclipped = raster_rect_to_device_pixels(
+ unclipped_raster_rect,
+ device_pixel_scale,
+ );
+
+ let unclipped_world_rect = map_to_world.map(&unclipped_raster_rect)?;
+ let clipped_world_rect = unclipped_world_rect.intersection(&prim_bounding_rect)?;
+
+ // We don't have to be able to do the back-projection from world into raster.
+ // Rendering only cares one way, so if that fails, we fall back to the full rect.
+ let clipped_raster_rect = match map_to_world.unmap(&clipped_world_rect) {
+ Some(rect) => rect.intersection(&unclipped_raster_rect)?,
+ None => return Some((unclipped, unclipped)),
+ };
+
+ let clipped = raster_rect_to_device_pixels(
+ clipped_raster_rect,
+ device_pixel_scale,
+ );
+
+ // Ensure that we won't try to allocate a zero-sized clip render task.
+ if clipped.is_empty() {
+ return None;
+ }
+
+ Some((clipped, unclipped))
+}
diff --git a/gfx/wr/webrender/src/platform/macos/font.rs b/gfx/wr/webrender/src/platform/macos/font.rs
new file mode 100644
index 0000000000..919e3a0086
--- /dev/null
+++ b/gfx/wr/webrender/src/platform/macos/font.rs
@@ -0,0 +1,1040 @@
+/* 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 api::{ColorU, FontKey, FontRenderMode, FontSize, GlyphDimensions};
+use api::{FontInstanceFlags, FontVariation, NativeFontHandle};
+use core_foundation::{array::{CFArray, CFArrayRef}, data::CFData};
+use core_foundation::base::TCFType;
+use core_foundation::dictionary::CFDictionary;
+use core_foundation::number::{CFNumber, CFNumberRef};
+use core_foundation::string::{CFString, CFStringRef};
+use core_graphics::base::{kCGImageAlphaNoneSkipFirst, kCGImageAlphaPremultipliedFirst};
+use core_graphics::base::{kCGBitmapByteOrder32Little};
+use core_graphics::color_space::CGColorSpace;
+use core_graphics::context::CGContext;
+use core_graphics::context::{CGBlendMode, CGTextDrawingMode};
+use core_graphics::font::{CGFont, CGGlyph};
+use core_graphics::geometry::{CGAffineTransform, CGPoint, CGSize};
+use core_graphics::geometry::{CG_AFFINE_TRANSFORM_IDENTITY, CGRect};
+use core_text::{self, font_descriptor::CTFontDescriptorCreateCopyWithAttributes};
+use core_text::font::{CTFont, CTFontRef};
+use core_text::font_descriptor::{CTFontDescriptor, CTFontSymbolicTraits};
+use core_text::font_descriptor::{kCTFontDefaultOrientation, kCTFontColorGlyphsTrait};
+use euclid::default::Size2D;
+use crate::gamma_lut::{ColorLut, GammaLut};
+use crate::glyph_rasterizer::{FontInstance, FontTransform, GlyphKey};
+use crate::glyph_rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::internal_types::{FastHashMap, ResourceCacheError};
+use std::collections::hash_map::Entry;
+use std::sync::Arc;
+
+const INITIAL_CG_CONTEXT_SIDE_LENGTH: u32 = 32;
+
+// We prefer to create CTFonts from a CTFontDescriptor, but that doesn't work in the case
+// of hidden system fonts on recent macOS versions, so for those we will instead use a
+// native CGFont as the basis.
+enum DescOrFont {
+ Desc(CTFontDescriptor),
+ Font(CGFont),
+}
+
+pub struct FontContext {
+ desc_or_fonts: FastHashMap<FontKey, DescOrFont>,
+ // Table mapping a sized font key with variations to its instantiated CoreText font.
+ // We also cache the symbolic traits for the given CT font when it is instantiated.
+ // This avoids an expensive bottleneck accessing the symbolic traits every time we
+ // need to rasterize a glyph or access its dimensions.
+ ct_fonts: FastHashMap<(FontKey, FontSize, Vec<FontVariation>), (CTFont, CTFontSymbolicTraits)>,
+ #[allow(dead_code)]
+ graphics_context: GraphicsContext,
+ #[allow(dead_code)]
+ gamma_lut: GammaLut,
+}
+
+// core text is safe to use on multiple threads and non-shareable resources are
+// all hidden inside their font context.
+unsafe impl Send for FontContext {}
+
+struct GlyphMetrics {
+ rasterized_left: i32,
+ #[allow(dead_code)]
+ rasterized_descent: i32,
+ rasterized_ascent: i32,
+ rasterized_width: i32,
+ rasterized_height: i32,
+ advance: f32,
+}
+
+// There are a number of different OS prefs that control whether or not
+// requesting font smoothing actually results in subpixel AA. This gets even
+// murkier in newer macOS versions that deprecate subpixel AA, with the prefs
+// potentially interacting and overriding each other. In an attempt to future-
+// proof things against any new prefs or interpretation of those prefs in
+// future macOS versions, we do a check here to request font smoothing and see
+// what result it actually gives us much like Skia does. We need to check for
+// each of three potential results and process them in the font backend in
+// distinct ways:
+// 1) subpixel AA (differing RGB channels) with dilation
+// 2) grayscale AA (matching RGB channels) with dilation, a compatibility mode
+// 3) grayscale AA without dilation as if font smoothing was not requested
+// We can discern between case 1 and the rest by checking if the subpixels differ.
+// We can discern between cases 2 and 3 by rendering with and without smoothing
+// and comparing the two to determine if there was some dilation.
+// This returns the actual FontRenderMode needed to support each case, if any.
+fn determine_font_smoothing_mode() -> Option<FontRenderMode> {
+ let mut smooth_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ smooth_context.set_should_smooth_fonts(true);
+ smooth_context.set_should_antialias(true);
+ smooth_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+ let mut gray_context = CGContext::create_bitmap_context(
+ None,
+ 12,
+ 12,
+ 8,
+ 12 * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
+ );
+ gray_context.set_should_smooth_fonts(false);
+ gray_context.set_should_antialias(true);
+ gray_context.set_rgb_fill_color(1.0, 1.0, 1.0, 1.0);
+
+ // Autorelease pool for CTFont
+ objc::rc::autoreleasepool(|| {
+ // Lucida Grande 12 is the default fallback font in Firefox
+ let ct_font = core_text::font::new_from_name("Lucida Grande", 12.).unwrap();
+ let point = CGPoint { x: 0., y: 0. };
+ let glyph = 'X' as CGGlyph;
+ ct_font.draw_glyphs(&[glyph], &[point], smooth_context.clone());
+ ct_font.draw_glyphs(&[glyph], &[point], gray_context.clone());
+ });
+
+ let mut mode = None;
+ for (smooth, gray) in smooth_context.data().chunks(4).zip(gray_context.data().chunks(4)) {
+ if smooth[0] != smooth[1] || smooth[1] != smooth[2] {
+ return Some(FontRenderMode::Subpixel);
+ }
+ if smooth[0] != gray[0] || smooth[1] != gray[1] || smooth[2] != gray[2] {
+ mode = Some(FontRenderMode::Alpha);
+ }
+ }
+ return mode;
+}
+
+// We cache the font smoothing mode globally, rather than storing it in each FontContext,
+// to avoid having to determine this redundantly in each context and to avoid needing to
+// lock them to access this setting in prepare_font.
+lazy_static! {
+ static ref FONT_SMOOTHING_MODE: Option<FontRenderMode> = determine_font_smoothing_mode();
+}
+
+fn should_use_white_on_black(color: ColorU) -> bool {
+ let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
+ // These thresholds were determined on 10.12 by observing what CG does.
+ r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255
+}
+
+fn get_glyph_metrics(
+ ct_font: &CTFont,
+ transform: Option<&CGAffineTransform>,
+ glyph: CGGlyph,
+ x_offset: f64,
+ y_offset: f64,
+ extra_width: f64,
+) -> GlyphMetrics {
+ let mut bounds = ct_font.get_bounding_rects_for_glyphs(kCTFontDefaultOrientation, &[glyph]);
+
+ if bounds.origin.x.is_nan() || bounds.origin.y.is_nan() || bounds.size.width.is_nan() ||
+ bounds.size.height.is_nan()
+ {
+ // If an unexpected glyph index is requested, core text will return NaN values
+ // which causes us to do bad thing as the value is cast into an integer and
+ // overflow when expanding the bounds a few lines below.
+ // Instead we are better off returning zero-sized metrics because this special
+ // case is handled by the callers of this method.
+ return GlyphMetrics {
+ rasterized_left: 0,
+ rasterized_width: 0,
+ rasterized_height: 0,
+ rasterized_ascent: 0,
+ rasterized_descent: 0,
+ advance: 0.0,
+ };
+ }
+
+ let mut advance = CGSize { width: 0.0, height: 0.0 };
+ unsafe {
+ ct_font.get_advances_for_glyphs(kCTFontDefaultOrientation, &glyph, &mut advance, 1);
+ }
+
+ if bounds.size.width > 0.0 {
+ bounds.size.width += extra_width;
+ }
+ if advance.width > 0.0 {
+ advance.width += extra_width;
+ }
+
+ if let Some(transform) = transform {
+ bounds = bounds.apply_transform(transform);
+ }
+
+ // First round out to pixel boundaries
+ // CG Origin is bottom left
+ let mut left = bounds.origin.x.floor() as i32;
+ let mut bottom = bounds.origin.y.floor() as i32;
+ let mut right = (bounds.origin.x + bounds.size.width + x_offset).ceil() as i32;
+ let mut top = (bounds.origin.y + bounds.size.height + y_offset).ceil() as i32;
+
+ // Expand the bounds by 1 pixel, to give CG room for anti-aliasing.
+ // Note that this outset is to allow room for LCD smoothed glyphs. However, the correct outset
+ // is not currently known, as CG dilates the outlines by some percentage.
+ // This is taken from Skia.
+ left -= 1;
+ bottom -= 1;
+ right += 1;
+ top += 1;
+
+ let width = right - left;
+ let height = top - bottom;
+
+ GlyphMetrics {
+ rasterized_left: left,
+ rasterized_width: width,
+ rasterized_height: height,
+ rasterized_ascent: top,
+ rasterized_descent: -bottom,
+ advance: advance.width as f32,
+ }
+}
+
+#[link(name = "ApplicationServices", kind = "framework")]
+extern {
+ static kCTFontVariationAxisIdentifierKey: CFStringRef;
+ static kCTFontVariationAxisNameKey: CFStringRef;
+ static kCTFontVariationAxisMinimumValueKey: CFStringRef;
+ static kCTFontVariationAxisMaximumValueKey: CFStringRef;
+ static kCTFontVariationAxisDefaultValueKey: CFStringRef;
+ static kCTFontVariationAttribute: CFStringRef;
+
+ fn CTFontCopyVariationAxes(font: CTFontRef) -> CFArrayRef;
+}
+
+fn new_ct_font_with_variations(desc_or_font: &DescOrFont, size: f64, variations: &[FontVariation]) -> CTFont {
+ unsafe {
+ let ct_font = match desc_or_font {
+ DescOrFont::Desc(ct_font_desc) => core_text::font::new_from_descriptor(ct_font_desc, size),
+ DescOrFont::Font(cg_font) => core_text::font::new_from_CGFont(cg_font, size)
+ };
+ if variations.is_empty() {
+ return ct_font;
+ }
+ let axes_ref = CTFontCopyVariationAxes(ct_font.as_concrete_TypeRef());
+ if axes_ref.is_null() {
+ return ct_font;
+ }
+ let axes: CFArray<CFDictionary> = TCFType::wrap_under_create_rule(axes_ref);
+ // We collect the values with either number or string keys, depending whether
+ // we're going to instantiate the CTFont from a descriptor or a CGFont.
+ // It'd probably be better to switch the CGFont-related APIs to expect numbers,
+ // but that's left for a future cleanup.
+ let mut vals: Vec<(CFNumber, CFNumber)> = Vec::with_capacity(variations.len() as usize);
+ let mut vals_str: Vec<(CFString, CFNumber)> = Vec::with_capacity(variations.len() as usize);
+ for axis in axes.iter() {
+ if !axis.instance_of::<CFDictionary>() {
+ return ct_font;
+ }
+ let tag_val = match axis.find(kCTFontVariationAxisIdentifierKey as *const _) {
+ Some(tag_ptr) => {
+ let tag: CFNumber = TCFType::wrap_under_get_rule(*tag_ptr as CFNumberRef);
+ if !tag.instance_of::<CFNumber>() {
+ return ct_font;
+ }
+ match tag.to_i64() {
+ Some(val) => val,
+ None => return ct_font,
+ }
+ }
+ None => return ct_font,
+ };
+ let mut val = match variations.iter().find(|variation| (variation.tag as i64) == tag_val) {
+ Some(variation) => variation.value as f64,
+ None => continue,
+ };
+
+ let name: CFString = match axis.find(kCTFontVariationAxisNameKey as *const _) {
+ Some(name_ptr) => TCFType::wrap_under_get_rule(*name_ptr as CFStringRef),
+ None => return ct_font,
+ };
+ if !name.instance_of::<CFString>() {
+ return ct_font;
+ }
+
+ let min_val = match axis.find(kCTFontVariationAxisMinimumValueKey as *const _) {
+ Some(min_ptr) => {
+ let min: CFNumber = TCFType::wrap_under_get_rule(*min_ptr as CFNumberRef);
+ if !min.instance_of::<CFNumber>() {
+ return ct_font;
+ }
+ match min.to_f64() {
+ Some(val) => val,
+ None => return ct_font,
+ }
+ }
+ None => return ct_font,
+ };
+ let max_val = match axis.find(kCTFontVariationAxisMaximumValueKey as *const _) {
+ Some(max_ptr) => {
+ let max: CFNumber = TCFType::wrap_under_get_rule(*max_ptr as CFNumberRef);
+ if !max.instance_of::<CFNumber>() {
+ return ct_font;
+ }
+ match max.to_f64() {
+ Some(val) => val,
+ None => return ct_font,
+ }
+ }
+ None => return ct_font,
+ };
+ let def_val = match axis.find(kCTFontVariationAxisDefaultValueKey as *const _) {
+ Some(def_ptr) => {
+ let def: CFNumber = TCFType::wrap_under_get_rule(*def_ptr as CFNumberRef);
+ if !def.instance_of::<CFNumber>() {
+ return ct_font;
+ }
+ match def.to_f64() {
+ Some(val) => val,
+ None => return ct_font,
+ }
+ }
+ None => return ct_font,
+ };
+
+ val = val.max(min_val).min(max_val);
+ if val != def_val {
+ match desc_or_font {
+ DescOrFont::Font(_) => vals_str.push((name, CFNumber::from(val))),
+ DescOrFont::Desc(_) => vals.push((CFNumber::from(tag_val), CFNumber::from(val))),
+ }
+ }
+ }
+ match desc_or_font {
+ DescOrFont::Desc(ct_font_desc) => {
+ if vals.is_empty() {
+ return ct_font;
+ }
+ let vals_dict = CFDictionary::from_CFType_pairs(&vals);
+ let attrs_dict = CFDictionary::from_CFType_pairs(&[(CFString::wrap_under_get_rule(kCTFontVariationAttribute), vals_dict)]);
+ let ct_var_font_desc = create_copy_with_attributes(ct_font_desc, attrs_dict.to_untyped()).unwrap();
+ core_text::font::new_from_descriptor(&ct_var_font_desc, size)
+ }
+ DescOrFont::Font(cg_font) => {
+ if vals_str.is_empty() {
+ return ct_font;
+ }
+ let vals_dict = CFDictionary::from_CFType_pairs(&vals_str);
+ let cg_var_font = cg_font.create_copy_from_variations(&vals_dict).unwrap();
+ core_text::font::new_from_CGFont_with_variations(&cg_var_font, size, &vals_dict)
+ }
+ }
+ }
+}
+
+fn is_bitmap_font(traits: CTFontSymbolicTraits) -> bool {
+ (traits & kCTFontColorGlyphsTrait) != 0
+}
+
+impl FontContext {
+ pub fn new() -> Result<FontContext, ResourceCacheError> {
+ debug!("Test for subpixel AA support: {:?}", *FONT_SMOOTHING_MODE);
+
+ // Force CG to use sRGB color space to gamma correct.
+ let contrast = 0.0;
+ let gamma = 0.0;
+
+ Ok(FontContext {
+ desc_or_fonts: FastHashMap::default(),
+ ct_fonts: FastHashMap::default(),
+ graphics_context: GraphicsContext::new(),
+ gamma_lut: GammaLut::new(contrast, gamma, gamma),
+ })
+ }
+
+ pub fn has_font(&self, font_key: &FontKey) -> bool {
+ self.desc_or_fonts.contains_key(font_key)
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) {
+ if self.desc_or_fonts.contains_key(font_key) {
+ return;
+ }
+
+ assert_eq!(index, 0);
+ let data = CFData_wrapping_arc_vec(bytes);
+ let ct_font_desc = match create_font_descriptor(data) {
+ Err(_) => return,
+ Ok(desc) => desc,
+ };
+ self.desc_or_fonts.insert(*font_key, DescOrFont::Desc(ct_font_desc));
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) {
+ if self.desc_or_fonts.contains_key(font_key) {
+ return;
+ }
+
+ // there's no way great way to go from a CGFont to a CTFontDescriptor
+ // so we use the postscript name. Ideally NativeFontHandle would
+ // just use a CTFontDescriptor.
+ let name = native_font_handle.0.postscript_name();
+ // For "hidden" system fonts, whose names start with a period,
+ // we can't instantiate CTFonts via a descriptor. We're really
+ // supposed to use CTFontCreateUIFontForLanguage, but for now
+ // we just use the CGFont.
+ let desc_or_font = if name.to_string().starts_with('.') {
+ DescOrFont::Font(native_font_handle.0)
+ } else {
+ DescOrFont::Desc(core_text::font_descriptor::new_from_postscript_name(&name))
+ };
+
+ self.desc_or_fonts.insert(*font_key, desc_or_font);
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(_) = self.desc_or_fonts.remove(font_key) {
+ self.ct_fonts.retain(|k, _| k.0 != *font_key);
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Remove the CoreText font corresponding to this instance.
+ let size = FontSize::from_f64_px(instance.get_transformed_size());
+ self.ct_fonts.remove(&(instance.font_key, size, instance.variations.clone()));
+ }
+
+ fn get_ct_font(
+ &mut self,
+ font_key: FontKey,
+ size: f64,
+ variations: &[FontVariation],
+ ) -> Option<(CTFont, CTFontSymbolicTraits)> {
+ // Interacting with CoreText can create autorelease garbage.
+ objc::rc::autoreleasepool(|| {
+ match self.ct_fonts.entry((font_key, FontSize::from_f64_px(size), variations.to_vec())) {
+ Entry::Occupied(entry) => Some((*entry.get()).clone()),
+ Entry::Vacant(entry) => {
+ let desc_or_font = self.desc_or_fonts.get(&font_key)?;
+ let ct_font = new_ct_font_with_variations(desc_or_font, size, variations);
+ let traits = ct_font.symbolic_traits();
+ entry.insert((ct_font.clone(), traits));
+ Some((ct_font, traits))
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let character = ch as u16;
+ let mut glyph = 0;
+
+ self.get_ct_font(font_key, 16.0, &[])
+ .and_then(|(ct_font, _)| {
+ unsafe {
+ let result = ct_font.get_glyphs_for_characters(&character, &mut glyph, 1);
+
+ if result {
+ Some(glyph as u32)
+ } else {
+ None
+ }
+ }
+ })
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ self.get_ct_font(font.font_key, size, &font.variations)
+ .and_then(|(ct_font, traits)| {
+ let glyph = key.index() as CGGlyph;
+ let bitmap = is_bitmap_font(traits);
+ let (mut shape, (x_offset, y_offset)) = if bitmap {
+ (FontTransform::identity(), (0.0, 0.0))
+ } else {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+ let (strike_scale, pixel_step) = if bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+ let extra_strikes = font.get_extra_strikes(strike_scale);
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ None
+ } else {
+ Some(GlyphDimensions {
+ left: metrics.rasterized_left,
+ top: metrics.rasterized_ascent,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ advance: metrics.advance,
+ })
+ }
+ })
+ }
+
+ // Assumes the pixels here are linear values from CG
+ fn gamma_correct_pixels(
+ &self,
+ pixels: &mut Vec<u8>,
+ render_mode: FontRenderMode,
+ color: ColorU,
+ ) {
+ // Then convert back to gamma corrected values.
+ match render_mode {
+ FontRenderMode::Alpha => {
+ self.gamma_lut.preblend_grayscale(pixels, color);
+ }
+ FontRenderMode::Subpixel => {
+ self.gamma_lut.preblend(pixels, color);
+ }
+ _ => {} // Again, give mono untouched since only the alpha matters.
+ }
+ }
+
+ #[allow(dead_code)]
+ fn print_glyph_data(&mut self, data: &[u8], width: usize, height: usize) {
+ // Rust doesn't have step_by support on stable :(
+ println!("Width is: {:?} height: {:?}", width, height);
+ for i in 0 .. height {
+ let current_height = i * width * 4;
+
+ for pixel in data[current_height .. current_height + (width * 4)].chunks(4) {
+ let b = pixel[0];
+ let g = pixel[1];
+ let r = pixel[2];
+ let a = pixel[3];
+ print!("({}, {}, {}, {}) ", r, g, b, a);
+ }
+ println!();
+ }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ // Sanitize the render mode for font smoothing. If font smoothing is supported,
+ // then we just need to ensure the render mode is limited to what is supported.
+ // If font smoothing is actually disabled, then we need to fall back to grayscale.
+ if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) ||
+ font.render_mode == FontRenderMode::Subpixel {
+ match *FONT_SMOOTHING_MODE {
+ Some(mode) => {
+ font.render_mode = font.render_mode.limit_by(mode);
+ font.flags.insert(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ None => {
+ font.render_mode = font.render_mode.limit_by(FontRenderMode::Alpha);
+ font.flags.remove(FontInstanceFlags::FONT_SMOOTHING);
+ }
+ }
+ }
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(255, 255, 255, 255);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha => {
+ font.color = if font.flags.contains(FontInstanceFlags::FONT_SMOOTHING) {
+ // Only the G channel is used to index grayscale tables,
+ // so use R and B to preserve light/dark determination.
+ let ColorU { g, a, .. } = font.color.luminance_color().quantized_ceil();
+ let rb = if should_use_white_on_black(font.color) { 255 } else { 0 };
+ ColorU::new(rb, g, rb, a)
+ } else {
+ ColorU::new(255, 255, 255, 255)
+ };
+ }
+ FontRenderMode::Subpixel => {
+ // Quantization may change the light/dark determination, so quantize in the
+ // direction necessary to respect the threshold.
+ font.color = if should_use_white_on_black(font.color) {
+ font.color.quantized_ceil()
+ } else {
+ font.color.quantized_floor()
+ };
+ }
+ }
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ objc::rc::autoreleasepool(|| {
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let size = font.size.to_f64_px() * y_scale;
+ let (ct_font, traits) =
+ self.get_ct_font(font.font_key, size, &font.variations).ok_or(GlyphRasterError::LoadFailed)?;
+ let glyph_type = if is_bitmap_font(traits) {
+ GlyphType::Bitmap
+ } else {
+ GlyphType::Vector
+ };
+
+ let (mut shape, (x_offset, y_offset)) = match glyph_type {
+ GlyphType::Bitmap => (FontTransform::identity(), (0.0, 0.0)),
+ GlyphType::Vector => {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ }
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ }
+ let transform = if !shape.is_identity() || (tx, ty) != (0.0, 0.0) {
+ Some(CGAffineTransform {
+ a: shape.scale_x as f64,
+ b: -shape.skew_y as f64,
+ c: -shape.skew_x as f64,
+ d: shape.scale_y as f64,
+ tx: tx,
+ ty: -ty,
+ })
+ } else {
+ None
+ };
+
+ let glyph = key.index() as CGGlyph;
+ let (strike_scale, pixel_step) = if glyph_type == GlyphType::Bitmap {
+ (y_scale, 1.0)
+ } else {
+ (x_scale, y_scale / x_scale)
+ };
+
+ let extra_strikes = font.get_extra_strikes(strike_scale);
+ let metrics = get_glyph_metrics(
+ &ct_font,
+ transform.as_ref(),
+ glyph,
+ x_offset,
+ y_offset,
+ extra_strikes as f64 * pixel_step,
+ );
+ if metrics.rasterized_width == 0 || metrics.rasterized_height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let raster_size = Size2D::new(
+ metrics.rasterized_width as u32,
+ metrics.rasterized_height as u32
+ );
+
+ // If the font render mode is Alpha, we support two different ways to
+ // compute the grayscale mask, depending on the value of the platform
+ // options' font_smoothing flag:
+ // - Alpha + smoothing:
+ // We will recover a grayscale mask from a subpixel rasterization, in
+ // such a way that the result looks as close to subpixel text
+ // blending as we can make it. This involves gamma correction,
+ // luminance computations and preblending based on the text color,
+ // just like with the Subpixel render mode.
+ // - Alpha without smoothing:
+ // We will ask CoreGraphics to rasterize the text with font_smoothing
+ // off. This will cause it to use grayscale anti-aliasing with
+ // comparatively thin text. This method of text rendering is not
+ // gamma-aware.
+ //
+ // For subpixel rasterization, starting with macOS 10.11, CoreGraphics
+ // uses different glyph dilation based on the text color. Bright text
+ // uses less font dilation (looks thinner) than dark text.
+ // As a consequence, when we ask CG to rasterize with subpixel AA, we
+ // will render white-on-black text as opposed to black-on-white text if
+ // the text color brightness exceeds a certain threshold. This applies
+ // to both the Subpixel and the "Alpha + smoothing" modes, but not to
+ // the "Alpha without smoothing" and Mono modes.
+ let use_white_on_black = should_use_white_on_black(font.color);
+ let use_font_smoothing = font.flags.contains(FontInstanceFlags::FONT_SMOOTHING);
+ let (antialias, smooth, text_color, bg_color, bg_alpha, invert) = match glyph_type {
+ GlyphType::Bitmap => (true, false, 0.0, 0.0, 0.0, false),
+ GlyphType::Vector => {
+ match (font.render_mode, use_font_smoothing) {
+ (FontRenderMode::Subpixel, _) |
+ (FontRenderMode::Alpha, true) => if use_white_on_black {
+ (true, true, 1.0, 0.0, 1.0, false)
+ } else {
+ (true, true, 0.0, 1.0, 1.0, true)
+ },
+ (FontRenderMode::Alpha, false) => (true, false, 0.0, 1.0, 1.0, true),
+ (FontRenderMode::Mono, _) => (false, false, 0.0, 1.0, 1.0, true),
+ }
+ }
+ };
+
+ {
+ let cg_context = self.graphics_context.get_context(&raster_size, glyph_type);
+
+ // These are always true in Gecko, even for non-AA fonts
+ cg_context.set_allows_font_subpixel_positioning(true);
+ cg_context.set_should_subpixel_position_fonts(true);
+
+ // Don't quantize because we're doing it already.
+ cg_context.set_allows_font_subpixel_quantization(false);
+ cg_context.set_should_subpixel_quantize_fonts(false);
+
+ cg_context.set_should_smooth_fonts(smooth);
+ cg_context.set_should_antialias(antialias);
+
+ // Fill the background. This could be opaque white, opaque black, or
+ // transparency.
+ cg_context.set_rgb_fill_color(bg_color, bg_color, bg_color, bg_alpha);
+ let rect = CGRect {
+ origin: CGPoint { x: 0.0, y: 0.0 },
+ size: CGSize {
+ width: metrics.rasterized_width as f64,
+ height: metrics.rasterized_height as f64,
+ },
+ };
+
+ // Make sure we use the Copy blend mode, or else we'll get the Porter-Duff OVER
+ // operator, which can't clear to the transparent color!
+ cg_context.set_blend_mode(CGBlendMode::Copy);
+ cg_context.fill_rect(rect);
+ cg_context.set_blend_mode(CGBlendMode::Normal);
+
+ // Set the text color and draw the glyphs.
+ cg_context.set_rgb_fill_color(text_color, text_color, text_color, 1.0);
+ cg_context.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
+
+ // CG Origin is bottom left, WR is top left. Need -y offset
+ let mut draw_origin = CGPoint {
+ x: -metrics.rasterized_left as f64 + x_offset + tx,
+ y: metrics.rasterized_descent as f64 - y_offset - ty,
+ };
+
+ if let Some(transform) = transform {
+ cg_context.set_text_matrix(&transform);
+
+ draw_origin = draw_origin.apply_transform(&transform.invert());
+ } else {
+ // Make sure to reset this because some previous glyph rasterization might have
+ // changed it.
+ cg_context.set_text_matrix(&CG_AFFINE_TRANSFORM_IDENTITY);
+ }
+
+ ct_font.draw_glyphs(&[glyph], &[draw_origin], cg_context.clone());
+
+ // We'd like to render all the strikes in a single ct_font.draw_glyphs call,
+ // passing an array of glyph IDs and an array of origins, but unfortunately
+ // with some fonts, Core Text may inappropriately pixel-snap the rasterization,
+ // such that the strikes overprint instead of being offset. Rendering the
+ // strikes with individual draw_glyphs calls avoids this.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1633397 for details.)
+ for i in 1 ..= extra_strikes {
+ let origin = CGPoint {
+ x: draw_origin.x + i as f64 * pixel_step,
+ y: draw_origin.y,
+ };
+ ct_font.draw_glyphs(&[glyph], &[origin], cg_context.clone());
+ }
+ }
+
+ let mut rasterized_pixels = self.graphics_context
+ .get_rasterized_pixels(&raster_size, glyph_type);
+
+ if glyph_type == GlyphType::Vector {
+ // We rendered text into an opaque surface. The code below needs to
+ // ignore the current value of each pixel's alpha channel. But it's
+ // allowed to write to the alpha channel, because we're done calling
+ // CG functions now.
+
+ if smooth {
+ // Convert to linear space for subpixel AA.
+ // We explicitly do not do this for grayscale AA ("Alpha without
+ // smoothing" or Mono) because those rendering modes are not
+ // gamma-aware in CoreGraphics.
+ self.gamma_lut.coregraphics_convert_to_linear(
+ &mut rasterized_pixels,
+ );
+ }
+
+ for pixel in rasterized_pixels.chunks_mut(4) {
+ if invert {
+ pixel[0] = 255 - pixel[0];
+ pixel[1] = 255 - pixel[1];
+ pixel[2] = 255 - pixel[2];
+ }
+
+ // Set alpha to the value of the green channel. For grayscale
+ // text, all three channels have the same value anyway.
+ // For subpixel text, the mask's alpha only makes a difference
+ // when computing the destination alpha on destination pixels
+ // that are not completely opaque. Picking an alpha value
+ // that's somehow based on the mask at least ensures that text
+ // blending doesn't modify the destination alpha on pixels where
+ // the mask is entirely zero.
+ pixel[3] = pixel[1];
+ }
+
+ if smooth {
+ // Convert back from linear space into device space, and perform
+ // some "preblending" based on the text color.
+ // In Alpha + smoothing mode, this will also convert subpixel AA
+ // into grayscale AA.
+ self.gamma_correct_pixels(
+ &mut rasterized_pixels,
+ font.render_mode,
+ font.color,
+ );
+ }
+ }
+
+ Ok(RasterizedGlyph {
+ left: metrics.rasterized_left as f32,
+ top: metrics.rasterized_ascent as f32,
+ width: metrics.rasterized_width,
+ height: metrics.rasterized_height,
+ scale: match glyph_type {
+ GlyphType::Bitmap => y_scale.recip() as f32,
+ GlyphType::Vector => 1.0,
+ },
+ format: match glyph_type {
+ GlyphType::Bitmap => GlyphFormat::ColorBitmap,
+ GlyphType::Vector => font.get_glyph_format(),
+ },
+ bytes: rasterized_pixels,
+ })})
+ }
+}
+
+// Avoids taking locks by recycling Core Graphics contexts.
+#[allow(dead_code)]
+struct GraphicsContext {
+ vector_context: CGContext,
+ vector_context_size: Size2D<u32>,
+ bitmap_context: CGContext,
+ bitmap_context_size: Size2D<u32>,
+}
+
+impl GraphicsContext {
+ fn new() -> GraphicsContext {
+ let size = Size2D::new(INITIAL_CG_CONTEXT_SIDE_LENGTH, INITIAL_CG_CONTEXT_SIDE_LENGTH);
+ GraphicsContext {
+ vector_context: GraphicsContext::create_cg_context(&size, GlyphType::Vector),
+ vector_context_size: size,
+ bitmap_context: GraphicsContext::create_cg_context(&size, GlyphType::Bitmap),
+ bitmap_context_size: size,
+ }
+ }
+
+ #[allow(dead_code)]
+ fn get_context(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> &mut CGContext {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => {
+ (&mut self.vector_context, &mut self.vector_context_size)
+ }
+ GlyphType::Bitmap => {
+ (&mut self.bitmap_context, &mut self.bitmap_context_size)
+ }
+ };
+ let rounded_size = Size2D::new(size.width.next_power_of_two(),
+ size.height.next_power_of_two());
+ if rounded_size.width > cached_size.width || rounded_size.height > cached_size.height {
+ *cached_size = Size2D::new(u32::max(cached_size.width, rounded_size.width),
+ u32::max(cached_size.height, rounded_size.height));
+ *cached_context = GraphicsContext::create_cg_context(cached_size, glyph_type);
+ }
+ cached_context
+ }
+
+ #[allow(dead_code)]
+ fn get_rasterized_pixels(&mut self, size: &Size2D<u32>, glyph_type: GlyphType)
+ -> Vec<u8> {
+ let (cached_context, cached_size) = match glyph_type {
+ GlyphType::Vector => (&mut self.vector_context, &self.vector_context_size),
+ GlyphType::Bitmap => (&mut self.bitmap_context, &self.bitmap_context_size),
+ };
+ let cached_data = cached_context.data();
+ let cached_stride = cached_size.width as usize * 4;
+
+ let result_len = size.width as usize * size.height as usize * 4;
+ let mut result = Vec::with_capacity(result_len);
+ for y in (cached_size.height - size.height)..cached_size.height {
+ let cached_start = y as usize * cached_stride;
+ let cached_end = cached_start + size.width as usize * 4;
+ result.extend_from_slice(&cached_data[cached_start..cached_end]);
+ }
+ debug_assert_eq!(result.len(), result_len);
+ result
+ }
+
+ fn create_cg_context(size: &Size2D<u32>, glyph_type: GlyphType) -> CGContext {
+ // The result of rasterization, in all render modes, is going to be a
+ // BGRA surface with white text on transparency using premultiplied
+ // alpha. For subpixel text, the RGB values will be the mask value for
+ // the individual components. For bitmap glyphs, the RGB values will be
+ // the (premultiplied) color of the pixel. For Alpha and Mono, each
+ // pixel will have R==G==B==A at the end of this function.
+ // We access the color channels in little-endian order.
+ // The CGContext will create and own our pixel buffer.
+ // In the non-Bitmap cases, we will ask CoreGraphics to draw text onto
+ // an opaque background. In order to hit the most efficient path in CG
+ // for this, we will tell CG that the CGContext is opaque, by passing
+ // an "[...]AlphaNone[...]" context flag. This creates a slight
+ // contradiction to the way we use the buffer after CG is done with it,
+ // because we will convert it into text-on-transparency. But that's ok;
+ // we still get four bytes per pixel and CG won't mess with the alpha
+ // channel after we've stopped calling CG functions. We just need to
+ // make sure that we don't look at the alpha values of the pixels that
+ // we get from CG, and compute our own alpha value only from RGB.
+ // Note that CG requires kCGBitmapByteOrder32Little in order to do
+ // subpixel AA at all (which we need it to do in both Subpixel and
+ // Alpha+smoothing mode). But little-endian is what we want anyway, so
+ // this works out nicely.
+ let color_type = match glyph_type {
+ GlyphType::Vector => kCGImageAlphaNoneSkipFirst,
+ GlyphType::Bitmap => kCGImageAlphaPremultipliedFirst,
+ };
+
+ CGContext::create_bitmap_context(None,
+ size.width as usize,
+ size.height as usize,
+ 8,
+ size.width as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGBitmapByteOrder32Little | color_type)
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+enum GlyphType {
+ Vector,
+ Bitmap,
+}
+
+// This stuff should eventually migrate to upstream core-foundation
+#[allow(non_snake_case)]
+fn CFData_wrapping_arc_vec(buffer: Arc<Vec<u8>>) -> CFData {
+ use core_foundation::base::*;
+ use core_foundation::data::CFDataRef;
+ use std::os::raw::c_void;
+
+ extern "C" {
+ pub fn CFDataCreateWithBytesNoCopy(
+ allocator: CFAllocatorRef,
+ bytes: *const u8,
+ length: CFIndex,
+ allocator: CFAllocatorRef,
+ ) -> CFDataRef;
+ }
+ unsafe {
+ let ptr = (*buffer).as_ptr() as *const _;
+ let len = buffer.len().to_CFIndex();
+ let info = Arc::into_raw(buffer) as *mut c_void;
+
+ extern "C" fn deallocate(_: *mut c_void, info: *mut c_void) {
+ unsafe {
+ drop(Arc::from_raw(info as *mut Vec<u8>));
+ }
+ }
+
+ // CFAllocatorContext doesn't have nullable members so we transmute
+ let allocator = CFAllocator::new(CFAllocatorContext {
+ info: info,
+ version: 0,
+ retain: None,
+ reallocate: None,
+ release: None,
+ copyDescription: None,
+ allocate: None,
+ deallocate: Some(deallocate),
+ preferredSize: None,
+ });
+ let data_ref =
+ CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, ptr, len, allocator.as_CFTypeRef());
+ TCFType::wrap_under_create_rule(data_ref)
+ }
+}
+
+fn create_font_descriptor(cf_data: CFData) -> Result<CTFontDescriptor, ()> {
+ use core_text::font_descriptor::CTFontDescriptorRef;
+ use core_foundation::data::CFDataRef;
+ extern {
+ pub fn CTFontManagerCreateFontDescriptorFromData(data: CFDataRef) -> CTFontDescriptorRef;
+ }
+ unsafe {
+ let ct_font_descriptor_ref = CTFontManagerCreateFontDescriptorFromData(cf_data.as_concrete_TypeRef());
+ if ct_font_descriptor_ref.is_null() {
+ return Err(());
+ }
+ Ok(CTFontDescriptor::wrap_under_create_rule(ct_font_descriptor_ref))
+ }
+}
+
+fn create_copy_with_attributes(desc: &CTFontDescriptor, attr: CFDictionary) -> Result<CTFontDescriptor, ()> {
+ unsafe {
+ let ct_font_descriptor_ref = CTFontDescriptorCreateCopyWithAttributes(desc.as_concrete_TypeRef(), attr.as_concrete_TypeRef());
+ if ct_font_descriptor_ref.is_null() {
+ return Err(());
+ }
+ Ok(CTFontDescriptor::wrap_under_create_rule(ct_font_descriptor_ref))
+}
+}
diff --git a/gfx/wr/webrender/src/platform/unix/font.rs b/gfx/wr/webrender/src/platform/unix/font.rs
new file mode 100644
index 0000000000..aa02e9460a
--- /dev/null
+++ b/gfx/wr/webrender/src/platform/unix/font.rs
@@ -0,0 +1,1053 @@
+/* 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 api::{ColorU, GlyphDimensions, FontKey, FontRenderMode};
+use api::{FontInstancePlatformOptions, FontLCDFilter, FontHinting};
+use api::{FontInstanceFlags, FontVariation, NativeFontHandle};
+use freetype::freetype::{FT_BBox, FT_Outline_Translate, FT_Pixel_Mode, FT_Render_Mode};
+use freetype::freetype::{FT_Done_Face, FT_Error, FT_Get_Char_Index, FT_Int32};
+use freetype::freetype::{FT_Done_FreeType, FT_Library_SetLcdFilter, FT_Pos};
+use freetype::freetype::{FT_F26Dot6, FT_Face, FT_Glyph_Format, FT_Long, FT_UInt};
+use freetype::freetype::{FT_GlyphSlot, FT_LcdFilter, FT_New_Face, FT_New_Memory_Face};
+use freetype::freetype::{FT_Init_FreeType, FT_Load_Glyph, FT_Render_Glyph};
+use freetype::freetype::{FT_Library, FT_Outline_Get_CBox, FT_Set_Char_Size, FT_Select_Size};
+use freetype::freetype::{FT_Fixed, FT_Matrix, FT_Set_Transform, FT_String, FT_ULong, FT_Vector};
+use freetype::freetype::{FT_Err_Unimplemented_Feature, FT_MulFix, FT_Outline_Embolden};
+use freetype::freetype::{FT_LOAD_COLOR, FT_LOAD_DEFAULT, FT_LOAD_FORCE_AUTOHINT};
+use freetype::freetype::{FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, FT_LOAD_NO_AUTOHINT};
+use freetype::freetype::{FT_LOAD_NO_BITMAP, FT_LOAD_NO_HINTING};
+use freetype::freetype::{FT_FACE_FLAG_SCALABLE, FT_FACE_FLAG_FIXED_SIZES};
+use freetype::freetype::{FT_FACE_FLAG_MULTIPLE_MASTERS};
+use freetype::succeeded;
+use crate::glyph_rasterizer::{FontInstance, GlyphFormat, GlyphKey};
+use crate::glyph_rasterizer::{GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::internal_types::{FastHashMap, ResourceCacheError};
+#[cfg(any(not(target_os = "android"), feature = "no_static_freetype"))]
+use libc::{dlsym, RTLD_DEFAULT};
+use libc::free;
+use std::{cmp, mem, ptr, slice};
+use std::cmp::max;
+use std::collections::hash_map::Entry;
+use std::ffi::CString;
+use std::sync::Arc;
+
+// These constants are not present in the freetype
+// bindings due to bindgen not handling the way
+// the macros are defined.
+//const FT_LOAD_TARGET_NORMAL: FT_UInt = 0 << 16;
+const FT_LOAD_TARGET_LIGHT: FT_UInt = 1 << 16;
+const FT_LOAD_TARGET_MONO: FT_UInt = 2 << 16;
+const FT_LOAD_TARGET_LCD: FT_UInt = 3 << 16;
+const FT_LOAD_TARGET_LCD_V: FT_UInt = 4 << 16;
+
+#[repr(C)]
+struct FT_Var_Axis {
+ pub name: *mut FT_String,
+ pub minimum: FT_Fixed,
+ pub def: FT_Fixed,
+ pub maximum: FT_Fixed,
+ pub tag: FT_ULong,
+ pub strid: FT_UInt,
+}
+
+#[repr(C)]
+struct FT_Var_Named_Style {
+ pub coords: *mut FT_Fixed,
+ pub strid: FT_UInt,
+ pub psid: FT_UInt,
+}
+
+#[repr(C)]
+struct FT_MM_Var {
+ pub num_axis: FT_UInt,
+ pub num_designs: FT_UInt,
+ pub num_namedstyles: FT_UInt,
+ pub axis: *mut FT_Var_Axis,
+ pub namedstyle: *mut FT_Var_Named_Style,
+}
+
+#[inline]
+pub fn unimplemented(error: FT_Error) -> bool {
+ error == FT_Err_Unimplemented_Feature as FT_Error
+}
+
+// Use dlsym to check for symbols. If not available. just return an unimplemented error.
+#[cfg(any(not(target_os = "android"), feature = "no_static_freetype"))]
+macro_rules! ft_dyn_fn {
+ ($func_name:ident($($arg_name:ident:$arg_type:ty),*) -> FT_Error) => {
+ #[allow(non_snake_case)]
+ unsafe fn $func_name($($arg_name:$arg_type),*) -> FT_Error {
+ extern "C" fn unimpl_func($(_:$arg_type),*) -> FT_Error {
+ FT_Err_Unimplemented_Feature as FT_Error
+ }
+ lazy_static! {
+ static ref FUNC: unsafe extern "C" fn($($arg_type),*) -> FT_Error = {
+ unsafe {
+ let cname = CString::new(stringify!($func_name)).unwrap();
+ let ptr = dlsym(RTLD_DEFAULT, cname.as_ptr());
+ if !ptr.is_null() { mem::transmute(ptr) } else { unimpl_func }
+ }
+ };
+ }
+ (*FUNC)($($arg_name),*)
+ }
+ }
+}
+
+// On Android, just statically link in the symbols...
+#[cfg(all(target_os = "android", not(feature = "no_static_freetype")))]
+macro_rules! ft_dyn_fn {
+ ($($proto:tt)+) => { extern "C" { fn $($proto)+; } }
+}
+
+ft_dyn_fn!(FT_Get_MM_Var(face: FT_Face, desc: *mut *mut FT_MM_Var) -> FT_Error);
+ft_dyn_fn!(FT_Done_MM_Var(library: FT_Library, desc: *mut FT_MM_Var) -> FT_Error);
+ft_dyn_fn!(FT_Set_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error);
+ft_dyn_fn!(FT_Get_Var_Design_Coordinates(face: FT_Face, num_vals: FT_UInt, vals: *mut FT_Fixed) -> FT_Error);
+
+extern "C" {
+ fn FT_GlyphSlot_Embolden(slot: FT_GlyphSlot);
+}
+
+// Custom version of FT_GlyphSlot_Embolden to be less aggressive with outline
+// fonts than the default implementation in FreeType.
+#[no_mangle]
+pub extern "C" fn mozilla_glyphslot_embolden_less(slot: FT_GlyphSlot) {
+ if slot.is_null() {
+ return;
+ }
+
+ let slot_ = unsafe { &mut *slot };
+ let format = slot_.format;
+ if format != FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE {
+ // For non-outline glyphs, just fall back to FreeType's function.
+ unsafe { FT_GlyphSlot_Embolden(slot) };
+ return;
+ }
+
+ let face_ = unsafe { *slot_.face };
+
+ // FT_GlyphSlot_Embolden uses a divisor of 24 here; we'll be only half as
+ // bold.
+ let size_ = unsafe { *face_.size };
+ let strength =
+ unsafe { FT_MulFix(face_.units_per_EM as FT_Long,
+ size_.metrics.y_scale) / 48 };
+ unsafe { FT_Outline_Embolden(&mut slot_.outline, strength) };
+
+ // Adjust metrics to suit the fattened glyph.
+ if slot_.advance.x != 0 {
+ slot_.advance.x += strength;
+ }
+ if slot_.advance.y != 0 {
+ slot_.advance.y += strength;
+ }
+ slot_.metrics.width += strength;
+ slot_.metrics.height += strength;
+ slot_.metrics.horiAdvance += strength;
+ slot_.metrics.vertAdvance += strength;
+ slot_.metrics.horiBearingY += strength;
+}
+
+enum FontFile {
+ Pathname(CString),
+ Data(Arc<Vec<u8>>),
+}
+
+struct FontFace {
+ // Raw byte data has to live until the font is deleted, according to
+ // https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_New_Memory_Face
+ file: FontFile,
+ index: u32,
+ face: FT_Face,
+ mm_var: *mut FT_MM_Var,
+}
+
+impl Drop for FontFace {
+ fn drop(&mut self) {
+ unsafe {
+ if !self.mm_var.is_null() &&
+ unimplemented(FT_Done_MM_Var((*(*self.face).glyph).library, self.mm_var)) {
+ free(self.mm_var as _);
+ }
+
+ FT_Done_Face(self.face);
+ }
+ }
+}
+
+struct VariationFace(FT_Face);
+
+impl Drop for VariationFace {
+ fn drop(&mut self) {
+ unsafe { FT_Done_Face(self.0) };
+ }
+}
+
+fn new_ft_face(font_key: &FontKey, lib: FT_Library, file: &FontFile, index: u32) -> Option<FT_Face> {
+ unsafe {
+ let mut face: FT_Face = ptr::null_mut();
+ let result = match file {
+ FontFile::Pathname(ref cstr) => FT_New_Face(
+ lib,
+ cstr.as_ptr(),
+ index as FT_Long,
+ &mut face,
+ ),
+ FontFile::Data(ref bytes) => FT_New_Memory_Face(
+ lib,
+ bytes.as_ptr(),
+ bytes.len() as FT_Long,
+ index as FT_Long,
+ &mut face,
+ ),
+ };
+ if succeeded(result) && !face.is_null() {
+ Some(face)
+ } else {
+ warn!("WARN: webrender failed to load font");
+ debug!("font={:?}, result={:?}", font_key, result);
+ None
+ }
+ }
+}
+
+pub struct FontContext {
+ lib: FT_Library,
+ faces: FastHashMap<FontKey, FontFace>,
+ variations: FastHashMap<(FontKey, Vec<FontVariation>), VariationFace>,
+ lcd_extra_pixels: i64,
+}
+
+// FreeType resources are safe to move between threads as long as they
+// are not concurrently accessed. In our case, everything is hidden inside
+// a given FontContext so it is safe to move the latter between threads.
+unsafe impl Send for FontContext {}
+
+fn get_skew_bounds(bottom: i32, top: i32, skew_factor: f32, _vertical: bool) -> (f32, f32) {
+ let skew_min = ((bottom as f32 + 0.5) * skew_factor).floor();
+ let skew_max = ((top as f32 - 0.5) * skew_factor).ceil();
+ (skew_min, skew_max)
+}
+
+fn skew_bitmap(
+ bitmap: &[u8],
+ width: usize,
+ height: usize,
+ left: i32,
+ top: i32,
+ skew_factor: f32,
+ vertical: bool, // TODO: vertical skew not yet implemented!
+) -> (Vec<u8>, usize, i32) {
+ let stride = width * 4;
+ // Calculate the skewed horizontal offsets of the bottom and top of the glyph.
+ let (skew_min, skew_max) = get_skew_bounds(top - height as i32, top, skew_factor, vertical);
+ // Allocate enough extra width for the min/max skew offsets.
+ let skew_width = width + (skew_max - skew_min) as usize;
+ let mut skew_buffer = vec![0u8; skew_width * height * 4];
+ for y in 0 .. height {
+ // Calculate a skew offset at the vertical center of the current row.
+ let offset = (top as f32 - y as f32 - 0.5) * skew_factor - skew_min;
+ // Get a blend factor in 0..256 constant across all pixels in the row.
+ let blend = (offset.fract() * 256.0) as u32;
+ let src_row = y * stride;
+ let dest_row = (y * skew_width + offset.floor() as usize) * 4;
+ let mut prev_px = [0u32; 4];
+ for (src, dest) in
+ bitmap[src_row .. src_row + stride].chunks(4).zip(
+ skew_buffer[dest_row .. dest_row + stride].chunks_mut(4)
+ ) {
+ let px = [src[0] as u32, src[1] as u32, src[2] as u32, src[3] as u32];
+ // Blend current pixel with previous pixel based on blend factor.
+ let next_px = [px[0] * blend, px[1] * blend, px[2] * blend, px[3] * blend];
+ dest[0] = ((((px[0] << 8) - next_px[0]) + prev_px[0] + 128) >> 8) as u8;
+ dest[1] = ((((px[1] << 8) - next_px[1]) + prev_px[1] + 128) >> 8) as u8;
+ dest[2] = ((((px[2] << 8) - next_px[2]) + prev_px[2] + 128) >> 8) as u8;
+ dest[3] = ((((px[3] << 8) - next_px[3]) + prev_px[3] + 128) >> 8) as u8;
+ // Save the remainder for blending onto the next pixel.
+ prev_px = next_px;
+ }
+ // If the skew misaligns the final pixel, write out the remainder.
+ if blend > 0 {
+ let dest = &mut skew_buffer[dest_row + stride .. dest_row + stride + 4];
+ dest[0] = ((prev_px[0] + 128) >> 8) as u8;
+ dest[1] = ((prev_px[1] + 128) >> 8) as u8;
+ dest[2] = ((prev_px[2] + 128) >> 8) as u8;
+ dest[3] = ((prev_px[3] + 128) >> 8) as u8;
+ }
+ }
+ (skew_buffer, skew_width, left + skew_min as i32)
+}
+
+fn transpose_bitmap(bitmap: &[u8], width: usize, height: usize) -> Vec<u8> {
+ let mut transposed = vec![0u8; width * height * 4];
+ for (y, row) in bitmap.chunks(width * 4).enumerate() {
+ let mut offset = y * 4;
+ for src in row.chunks(4) {
+ transposed[offset .. offset + 4].copy_from_slice(src);
+ offset += height * 4;
+ }
+ }
+ transposed
+}
+
+fn flip_bitmap_x(bitmap: &mut [u8], width: usize, height: usize) {
+ assert!(bitmap.len() == width * height * 4);
+ let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) };
+ for row in pixels.chunks_mut(width) {
+ row.reverse();
+ }
+}
+
+fn flip_bitmap_y(bitmap: &mut [u8], width: usize, height: usize) {
+ assert!(bitmap.len() == width * height * 4);
+ let pixels = unsafe { slice::from_raw_parts_mut(bitmap.as_mut_ptr() as *mut u32, width * height) };
+ for y in 0 .. height / 2 {
+ let low_row = y * width;
+ let high_row = (height - 1 - y) * width;
+ for x in 0 .. width {
+ pixels.swap(low_row + x, high_row + x);
+ }
+ }
+}
+
+impl FontContext {
+ pub fn new() -> Result<FontContext, ResourceCacheError> {
+ let mut lib: FT_Library = ptr::null_mut();
+
+ // Using an LCD filter may add one full pixel to each side if support is built in.
+ // As of FreeType 2.8.1, an LCD filter is always used regardless of settings
+ // if support for the patent-encumbered LCD filter algorithms is not built in.
+ // Thus, the only reasonable way to guess padding is to unconditonally add it if
+ // subpixel AA is used.
+ let lcd_extra_pixels = 1;
+
+ let result = unsafe {
+ FT_Init_FreeType(&mut lib)
+ };
+
+ if succeeded(result) {
+ Ok(FontContext {
+ lib,
+ faces: FastHashMap::default(),
+ variations: FastHashMap::default(),
+ lcd_extra_pixels,
+ })
+ } else {
+ // TODO(gw): Provide detailed error values.
+ // Once this panic has been here for a while with no issues we should get rid of
+ // ResourceCacheError as this was the only place that could fail previously.
+ panic!("Failed to initialize FreeType - {}", result)
+ }
+ }
+
+ pub fn has_font(&self, font_key: &FontKey) -> bool {
+ self.faces.contains_key(font_key)
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, bytes: Arc<Vec<u8>>, index: u32) {
+ if !self.faces.contains_key(font_key) {
+ let file = FontFile::Data(bytes);
+ if let Some(face) = new_ft_face(font_key, self.lib, &file, index) {
+ self.faces.insert(*font_key, FontFace { file, index, face, mm_var: ptr::null_mut() });
+ }
+ }
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, native_font_handle: NativeFontHandle) {
+ if !self.faces.contains_key(font_key) {
+ let cstr = CString::new(native_font_handle.path.as_os_str().to_str().unwrap()).unwrap();
+ let file = FontFile::Pathname(cstr);
+ let index = native_font_handle.index;
+ if let Some(face) = new_ft_face(font_key, self.lib, &file, index) {
+ self.faces.insert(*font_key, FontFace { file, index, face, mm_var: ptr::null_mut() });
+ }
+ }
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if self.faces.remove(font_key).is_some() {
+ self.variations.retain(|k, _| k.0 != *font_key);
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Ensure we don't keep around excessive amounts of stale variations.
+ if !instance.variations.is_empty() {
+ self.variations.remove(&(instance.font_key, instance.variations.clone()));
+ }
+ }
+
+ fn get_ft_face(&mut self, font: &FontInstance) -> Option<FT_Face> {
+ if font.variations.is_empty() {
+ return Some(self.faces.get(&font.font_key)?.face);
+ }
+ match self.variations.entry((font.font_key, font.variations.clone())) {
+ Entry::Occupied(entry) => Some(entry.get().0),
+ Entry::Vacant(entry) => unsafe {
+ let normal_face = self.faces.get_mut(&font.font_key)?;
+ if ((*normal_face.face).face_flags & (FT_FACE_FLAG_MULTIPLE_MASTERS as FT_Long)) == 0 {
+ return Some(normal_face.face);
+ }
+ // Clone a new FT face and attempt to set the variation values on it.
+ // Leave unspecified values at the defaults.
+ let var_face = new_ft_face(&font.font_key, self.lib, &normal_face.file, normal_face.index)?;
+ if !normal_face.mm_var.is_null() ||
+ succeeded(FT_Get_MM_Var(normal_face.face, &mut normal_face.mm_var)) {
+ let mm_var = normal_face.mm_var;
+ let num_axis = (*mm_var).num_axis;
+ let mut coords: Vec<FT_Fixed> = Vec::with_capacity(num_axis as usize);
+
+ // Calling this before FT_Set_Var_Design_Coordinates avoids a bug with font variations
+ // not initialized properly in the font face, even if we ignore the result.
+ // See bug 1647035.
+ let mut tmp = [0; 16];
+ let res = FT_Get_Var_Design_Coordinates(
+ normal_face.face,
+ num_axis.min(16),
+ tmp.as_mut_ptr()
+ );
+ debug_assert!(succeeded(res));
+
+
+ for i in 0 .. num_axis {
+ let axis = (*mm_var).axis.offset(i as isize);
+ let mut value = (*axis).def;
+ for var in &font.variations {
+ if var.tag as FT_ULong == (*axis).tag {
+ value = (var.value * 65536.0 + 0.5) as FT_Fixed;
+ value = cmp::min(value, (*axis).maximum);
+ value = cmp::max(value, (*axis).minimum);
+ break;
+ }
+ }
+ coords.push(value);
+ }
+ let res = FT_Set_Var_Design_Coordinates(var_face, num_axis, coords.as_mut_ptr());
+ debug_assert!(succeeded(res));
+ }
+ entry.insert(VariationFace(var_face));
+ Some(var_face)
+ }
+ }
+ }
+
+ fn load_glyph(&mut self, font: &FontInstance, glyph: &GlyphKey) -> Option<(FT_GlyphSlot, f32)> {
+ let face = self.get_ft_face(font)?;
+
+ let mut load_flags = FT_LOAD_DEFAULT;
+ let FontInstancePlatformOptions { mut hinting, .. } = font.platform_options.unwrap_or_default();
+ // Disable hinting if there is a non-axis-aligned transform.
+ if font.synthetic_italics.is_enabled() ||
+ ((font.transform.scale_x != 0.0 || font.transform.scale_y != 0.0) &&
+ (font.transform.skew_x != 0.0 || font.transform.skew_y != 0.0)) {
+ hinting = FontHinting::None;
+ }
+ match (hinting, font.render_mode) {
+ (FontHinting::None, _) => load_flags |= FT_LOAD_NO_HINTING,
+ (FontHinting::Mono, _) => load_flags = FT_LOAD_TARGET_MONO,
+ (FontHinting::Light, _) => load_flags = FT_LOAD_TARGET_LIGHT,
+ (FontHinting::LCD, FontRenderMode::Subpixel) => {
+ load_flags = if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ FT_LOAD_TARGET_LCD_V
+ } else {
+ FT_LOAD_TARGET_LCD
+ };
+ if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) {
+ load_flags |= FT_LOAD_FORCE_AUTOHINT;
+ }
+ }
+ _ => {
+ if font.flags.contains(FontInstanceFlags::FORCE_AUTOHINT) {
+ load_flags |= FT_LOAD_FORCE_AUTOHINT;
+ }
+ }
+ }
+
+ if font.flags.contains(FontInstanceFlags::NO_AUTOHINT) {
+ load_flags |= FT_LOAD_NO_AUTOHINT;
+ }
+ if !font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS) {
+ load_flags |= FT_LOAD_NO_BITMAP;
+ }
+
+ load_flags |= FT_LOAD_COLOR;
+ load_flags |= FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH;
+
+ let (x_scale, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let req_size = font.size.to_f64_px();
+ let face_flags = unsafe { (*face).face_flags };
+ let mut result = if (face_flags & (FT_FACE_FLAG_FIXED_SIZES as FT_Long)) != 0 &&
+ (face_flags & (FT_FACE_FLAG_SCALABLE as FT_Long)) == 0 &&
+ (load_flags & FT_LOAD_NO_BITMAP) == 0 {
+ unsafe { FT_Set_Transform(face, ptr::null_mut(), ptr::null_mut()) };
+ self.choose_bitmap_size(face, req_size * y_scale)
+ } else {
+ let mut shape = font.transform.invert_scale(x_scale, y_scale);
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, y_scale * req_size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ };
+ let mut ft_shape = FT_Matrix {
+ xx: (shape.scale_x * 65536.0) as FT_Fixed,
+ xy: (shape.skew_x * -65536.0) as FT_Fixed,
+ yx: (shape.skew_y * -65536.0) as FT_Fixed,
+ yy: (shape.scale_y * 65536.0) as FT_Fixed,
+ };
+ // The delta vector for FT_Set_Transform is in units of 1/64 pixel.
+ let mut ft_delta = FT_Vector {
+ x: (tx * 64.0) as FT_F26Dot6,
+ y: (ty * -64.0) as FT_F26Dot6,
+ };
+ unsafe {
+ FT_Set_Transform(face, &mut ft_shape, &mut ft_delta);
+ FT_Set_Char_Size(
+ face,
+ (req_size * x_scale * 64.0 + 0.5) as FT_F26Dot6,
+ (req_size * y_scale * 64.0 + 0.5) as FT_F26Dot6,
+ 0,
+ 0,
+ )
+ }
+ };
+
+ if !succeeded(result) {
+ error!("Unable to set glyph size and transform: {}", result);
+ //let raw_error = unsafe { FT_Error_String(result) };
+ //if !raw_error.is_ptr() {
+ // error!("\tcode {:?}", CStr::from_ptr(raw_error));
+ //}
+ debug!(
+ "\t[{}] for size {:?} and scale {:?} from font {:?}",
+ glyph.index(),
+ req_size,
+ (x_scale, y_scale),
+ font.font_key,
+ );
+ return None;
+ }
+
+ result = unsafe { FT_Load_Glyph(face, glyph.index() as FT_UInt, load_flags as FT_Int32) };
+ if !succeeded(result) {
+ error!("Unable to load glyph: {}", result);
+ //let raw_error = unsafe { FT_Error_String(result) };
+ //if !raw_error.is_ptr() {
+ // error!("\tcode {:?}", CStr::from_ptr(raw_error));
+ //}
+ debug!(
+ "\t[{}] with flags {:?} from font {:?}",
+ glyph.index(),
+ load_flags,
+ font.font_key,
+ );
+ return None;
+ }
+
+ let slot = unsafe { (*face).glyph };
+ assert!(slot != ptr::null_mut());
+
+ if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ mozilla_glyphslot_embolden_less(slot);
+ }
+
+ let format = unsafe { (*slot).format };
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ let bitmap_size = unsafe { (*(*(*slot).face).size).metrics.y_ppem };
+ Some((slot, req_size as f32 / bitmap_size as f32))
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => Some((slot, 1.0)),
+ _ => {
+ error!("Unsupported format");
+ debug!("format={:?}", format);
+ None
+ }
+ }
+ }
+
+ fn pad_bounding_box(&self, font: &FontInstance, cbox: &mut FT_BBox) {
+ // Apply extra pixel of padding for subpixel AA, due to the filter.
+ if font.render_mode == FontRenderMode::Subpixel {
+ let padding = (self.lcd_extra_pixels * 64) as FT_Pos;
+ if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ cbox.yMin -= padding;
+ cbox.yMax += padding;
+ } else {
+ cbox.xMin -= padding;
+ cbox.xMax += padding;
+ }
+ }
+ }
+
+ // Get the bounding box for a glyph, accounting for sub-pixel positioning.
+ fn get_bounding_box(
+ &self,
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ glyph: &GlyphKey,
+ scale: f32,
+ ) -> FT_BBox {
+ // Get the estimated bounding box from FT (control points).
+ let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };
+
+ unsafe {
+ FT_Outline_Get_CBox(&(*slot).outline, &mut cbox);
+ }
+
+ // For spaces and other non-printable characters, early out.
+ if unsafe { (*slot).outline.n_contours } == 0 {
+ return cbox;
+ }
+
+ self.pad_bounding_box(font, &mut cbox);
+
+ // Offset the bounding box by subpixel positioning.
+ // Convert to 26.6 fixed point format for FT.
+ let (dx, dy) = font.get_subpx_offset(glyph);
+ let (dx, dy) = (
+ (dx / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ );
+ cbox.xMin += dx;
+ cbox.xMax += dx;
+ cbox.yMin += dy;
+ cbox.yMax += dy;
+
+ // Outset the box to device pixel boundaries
+ cbox.xMin &= !63;
+ cbox.yMin &= !63;
+ cbox.xMax = (cbox.xMax + 63) & !63;
+ cbox.yMax = (cbox.yMax + 63) & !63;
+
+ cbox
+ }
+
+ fn get_glyph_dimensions_impl(
+ &self,
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ glyph: &GlyphKey,
+ scale: f32,
+ use_transform: bool,
+ ) -> Option<GlyphDimensions> {
+ let format = unsafe { (*slot).format };
+ let (mut left, mut top, mut width, mut height) = match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ unsafe { (
+ (*slot).bitmap_left as i32,
+ (*slot).bitmap_top as i32,
+ (*slot).bitmap.width as i32,
+ (*slot).bitmap.rows as i32,
+ ) }
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ let cbox = self.get_bounding_box(slot, font, glyph, scale);
+ (
+ (cbox.xMin >> 6) as i32,
+ (cbox.yMax >> 6) as i32,
+ ((cbox.xMax - cbox.xMin) >> 6) as i32,
+ ((cbox.yMax - cbox.yMin) >> 6) as i32,
+ )
+ }
+ _ => return None,
+ };
+ let mut advance = unsafe { (*slot).metrics.horiAdvance as f32 / 64.0 };
+ if use_transform {
+ if scale != 1.0 {
+ let x0 = left as f32 * scale;
+ let x1 = width as f32 * scale + x0;
+ let y1 = top as f32 * scale;
+ let y0 = y1 - height as f32 * scale;
+ left = x0.round() as i32;
+ top = y1.round() as i32;
+ width = (x1.ceil() - x0.floor()) as i32;
+ height = (y1.ceil() - y0.floor()) as i32;
+ advance *= scale;
+ }
+ // An outline glyph's cbox would have already been transformed inside FT_Load_Glyph,
+ // so only handle bitmap glyphs which are not handled by FT_Load_Glyph.
+ if format == FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP {
+ if font.synthetic_italics.is_enabled() {
+ let (skew_min, skew_max) = get_skew_bounds(
+ top - height as i32,
+ top,
+ font.synthetic_italics.to_skew(),
+ font.flags.contains(FontInstanceFlags::VERTICAL),
+ );
+ left += skew_min as i32;
+ width += (skew_max - skew_min) as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ mem::swap(&mut width, &mut height);
+ mem::swap(&mut left, &mut top);
+ left -= width as i32;
+ top += height as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ left = -(left + width as i32);
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ top = -(top - height as i32);
+ }
+ }
+ }
+ Some(GlyphDimensions {
+ left,
+ top,
+ width,
+ height,
+ advance,
+ })
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let face = self.faces.get(&font_key)?.face;
+ unsafe {
+ let idx = FT_Get_Char_Index(face, ch as _);
+ if idx != 0 {
+ Some(idx)
+ } else {
+ None
+ }
+ }
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let slot = self.load_glyph(font, key);
+ slot.and_then(|(slot, scale)| self.get_glyph_dimensions_impl(slot, &font, key, scale, true))
+ }
+
+ fn choose_bitmap_size(&self, face: FT_Face, requested_size: f64) -> FT_Error {
+ let mut best_dist = unsafe { *(*face).available_sizes.offset(0) }.y_ppem as f64 / 64.0 - requested_size;
+ let mut best_size = 0;
+ let num_fixed_sizes = unsafe { (*face).num_fixed_sizes };
+ for i in 1 .. num_fixed_sizes {
+ // Distance is positive if strike is larger than desired size,
+ // or negative if smaller. If previously a found smaller strike,
+ // then prefer a larger strike. Otherwise, minimize distance.
+ let dist = unsafe { *(*face).available_sizes.offset(i as isize) }.y_ppem as f64 / 64.0 - requested_size;
+ if (best_dist < 0.0 && dist >= best_dist) || dist.abs() <= best_dist {
+ best_dist = dist;
+ best_size = i;
+ }
+ }
+ unsafe { FT_Select_Size(face, best_size) }
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => {
+ // We don't do any preblending with FreeType currently, so the color is not used.
+ font.color = ColorU::new(0xFF, 0xFF, 0xFF, 0xFF);
+ }
+ }
+ }
+
+ fn rasterize_glyph_outline(
+ &mut self,
+ slot: FT_GlyphSlot,
+ font: &FontInstance,
+ key: &GlyphKey,
+ scale: f32,
+ ) -> bool {
+ // Get the subpixel offsets in FT 26.6 format.
+ let (dx, dy) = font.get_subpx_offset(key);
+ let (dx, dy) = (
+ (dx / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ -(dy / scale as f64 * 64.0 + 0.5) as FT_Pos,
+ );
+
+ // Move the outline curves to be at the origin, taking
+ // into account the subpixel positioning.
+ unsafe {
+ let outline = &(*slot).outline;
+ let mut cbox = FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 };
+ FT_Outline_Get_CBox(outline, &mut cbox);
+ self.pad_bounding_box(font, &mut cbox);
+ FT_Outline_Translate(
+ outline,
+ dx - ((cbox.xMin + dx) & !63),
+ dy - ((cbox.yMin + dy) & !63),
+ );
+ }
+
+ if font.render_mode == FontRenderMode::Subpixel {
+ let FontInstancePlatformOptions { lcd_filter, .. } = font.platform_options.unwrap_or_default();
+ let filter = match lcd_filter {
+ FontLCDFilter::None => FT_LcdFilter::FT_LCD_FILTER_NONE,
+ FontLCDFilter::Default => FT_LcdFilter::FT_LCD_FILTER_DEFAULT,
+ FontLCDFilter::Light => FT_LcdFilter::FT_LCD_FILTER_LIGHT,
+ FontLCDFilter::Legacy => FT_LcdFilter::FT_LCD_FILTER_LEGACY,
+ };
+ unsafe { FT_Library_SetLcdFilter(self.lib, filter) };
+ }
+ let render_mode = match font.render_mode {
+ FontRenderMode::Mono => FT_Render_Mode::FT_RENDER_MODE_MONO,
+ FontRenderMode::Alpha => FT_Render_Mode::FT_RENDER_MODE_NORMAL,
+ FontRenderMode::Subpixel => if font.flags.contains(FontInstanceFlags::LCD_VERTICAL) {
+ FT_Render_Mode::FT_RENDER_MODE_LCD_V
+ } else {
+ FT_Render_Mode::FT_RENDER_MODE_LCD
+ },
+ };
+ let result = unsafe { FT_Render_Glyph(slot, render_mode) };
+ if !succeeded(result) {
+ error!("Unable to rasterize");
+ debug!(
+ "{:?} with {:?}, {:?}",
+ key,
+ render_mode,
+ result
+ );
+ false
+ } else {
+ true
+ }
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ let (slot, scale) = self.load_glyph(font, key).ok_or(GlyphRasterError::LoadFailed)?;
+
+ // Get dimensions of the glyph, to see if we need to rasterize it.
+ // Don't apply scaling to the dimensions, as the glyph cache needs to know the actual
+ // footprint of the glyph.
+ let dimensions = self.get_glyph_dimensions_impl(slot, font, key, scale, false)
+ .ok_or(GlyphRasterError::LoadFailed)?;
+ let GlyphDimensions { mut left, mut top, width, height, .. } = dimensions;
+
+ // For spaces and other non-printable characters, early out.
+ if width == 0 || height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let format = unsafe { (*slot).format };
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {}
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ if !self.rasterize_glyph_outline(slot, font, key, scale) {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+ }
+ _ => {
+ error!("Unsupported format");
+ debug!("format={:?}", format);
+ return Err(GlyphRasterError::LoadFailed);
+ }
+ };
+
+ debug!(
+ "Rasterizing {:?} as {:?} with dimensions {:?}",
+ key,
+ font.render_mode,
+ dimensions
+ );
+
+ let bitmap = unsafe { &(*slot).bitmap };
+ let pixel_mode = unsafe { mem::transmute(bitmap.pixel_mode as u32) };
+ let (mut actual_width, mut actual_height) = match pixel_mode {
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD => {
+ assert!(bitmap.width % 3 == 0);
+ ((bitmap.width / 3) as usize, bitmap.rows as usize)
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => {
+ assert!(bitmap.rows % 3 == 0);
+ (bitmap.width as usize, (bitmap.rows / 3) as usize)
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_MONO |
+ FT_Pixel_Mode::FT_PIXEL_MODE_GRAY |
+ FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => {
+ (bitmap.width as usize, bitmap.rows as usize)
+ }
+ _ => panic!("Unsupported mode"),
+ };
+
+ // If we need padding, we will need to expand the buffer size.
+ let (buffer_width, buffer_height, padding) = if font.use_texture_padding() {
+ (actual_width + 2, actual_height + 2, 1)
+ } else {
+ (actual_width, actual_height, 0)
+ };
+
+ let mut final_buffer = vec![0u8; buffer_width * buffer_height * 4];
+
+ // Extract the final glyph from FT format into BGRA8 format, which is
+ // what WR expects.
+ let subpixel_bgr = font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR);
+ let mut src_row = bitmap.buffer;
+ let mut dest = 4 * padding * (padding + buffer_width);
+ let actual_end = final_buffer.len() - 4 * padding * (buffer_width + 1);
+ while dest < actual_end {
+ let mut src = src_row;
+ let row_end = dest + actual_width * 4;
+ match pixel_mode {
+ FT_Pixel_Mode::FT_PIXEL_MODE_MONO => {
+ while dest < row_end {
+ // Cast the byte to signed so that we can left shift each bit into
+ // the top bit, then right shift to fill out the bits with 0s or 1s.
+ let mut byte: i8 = unsafe { *src as i8 };
+ src = unsafe { src.offset(1) };
+ let byte_end = cmp::min(row_end, dest + 8 * 4);
+ while dest < byte_end {
+ let alpha = (byte >> 7) as u8;
+ final_buffer[dest + 0] = alpha;
+ final_buffer[dest + 1] = alpha;
+ final_buffer[dest + 2] = alpha;
+ final_buffer[dest + 3] = alpha;
+ dest += 4;
+ byte <<= 1;
+ }
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_GRAY => {
+ while dest < row_end {
+ let alpha = unsafe { *src };
+ final_buffer[dest + 0] = alpha;
+ final_buffer[dest + 1] = alpha;
+ final_buffer[dest + 2] = alpha;
+ final_buffer[dest + 3] = alpha;
+ src = unsafe { src.offset(1) };
+ dest += 4;
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD => {
+ while dest < row_end {
+ let (mut r, g, mut b) = unsafe { (*src, *src.offset(1), *src.offset(2)) };
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ final_buffer[dest + 0] = b;
+ final_buffer[dest + 1] = g;
+ final_buffer[dest + 2] = r;
+ final_buffer[dest + 3] = max(max(b, g), r);
+ src = unsafe { src.offset(3) };
+ dest += 4;
+ }
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V => {
+ while dest < row_end {
+ let (mut r, g, mut b) =
+ unsafe { (*src, *src.offset(bitmap.pitch as isize), *src.offset((2 * bitmap.pitch) as isize)) };
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ final_buffer[dest + 0] = b;
+ final_buffer[dest + 1] = g;
+ final_buffer[dest + 2] = r;
+ final_buffer[dest + 3] = max(max(b, g), r);
+ src = unsafe { src.offset(1) };
+ dest += 4;
+ }
+ src_row = unsafe { src_row.offset((2 * bitmap.pitch) as isize) };
+ }
+ FT_Pixel_Mode::FT_PIXEL_MODE_BGRA => {
+ // The source is premultiplied BGRA data.
+ let dest_slice = &mut final_buffer[dest .. row_end];
+ let src_slice = unsafe { slice::from_raw_parts(src, dest_slice.len()) };
+ dest_slice.copy_from_slice(src_slice);
+ }
+ _ => panic!("Unsupported mode"),
+ }
+ src_row = unsafe { src_row.offset(bitmap.pitch as isize) };
+ dest = row_end + 8 * padding;
+ }
+
+ if font.use_texture_padding() {
+ left -= padding as i32;
+ top += padding as i32;
+ actual_width = buffer_width;
+ actual_height = buffer_height;
+ }
+
+ match format {
+ FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP => {
+ if font.synthetic_italics.is_enabled() {
+ let (skew_buffer, skew_width, skew_left) = skew_bitmap(
+ &final_buffer,
+ actual_width,
+ actual_height,
+ left,
+ top,
+ font.synthetic_italics.to_skew(),
+ font.flags.contains(FontInstanceFlags::VERTICAL),
+ );
+ final_buffer = skew_buffer;
+ actual_width = skew_width;
+ left = skew_left;
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ final_buffer = transpose_bitmap(&final_buffer, actual_width, actual_height);
+ mem::swap(&mut actual_width, &mut actual_height);
+ mem::swap(&mut left, &mut top);
+ left -= actual_width as i32;
+ top += actual_height as i32;
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ flip_bitmap_x(&mut final_buffer, actual_width, actual_height);
+ left = -(left + actual_width as i32);
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ flip_bitmap_y(&mut final_buffer, actual_width, actual_height);
+ top = -(top - actual_height as i32);
+ }
+ }
+ FT_Glyph_Format::FT_GLYPH_FORMAT_OUTLINE => {
+ unsafe {
+ left += (*slot).bitmap_left;
+ top += (*slot).bitmap_top - height as i32;
+ }
+ }
+ _ => {}
+ }
+
+ let glyph_format = match (pixel_mode, format) {
+ (FT_Pixel_Mode::FT_PIXEL_MODE_LCD, _) |
+ (FT_Pixel_Mode::FT_PIXEL_MODE_LCD_V, _) => font.get_subpixel_glyph_format(),
+ (FT_Pixel_Mode::FT_PIXEL_MODE_BGRA, _) => GlyphFormat::ColorBitmap,
+ (_, FT_Glyph_Format::FT_GLYPH_FORMAT_BITMAP) => GlyphFormat::Bitmap,
+ _ => font.get_alpha_glyph_format(),
+ };
+
+ Ok(RasterizedGlyph {
+ left: left as f32,
+ top: top as f32,
+ width: actual_width as i32,
+ height: actual_height as i32,
+ scale,
+ format: glyph_format,
+ bytes: final_buffer,
+ })
+ }
+}
+
+impl Drop for FontContext {
+ fn drop(&mut self) {
+ self.variations.clear();
+ self.faces.clear();
+ unsafe {
+ FT_Done_FreeType(self.lib);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/platform/windows/font.rs b/gfx/wr/webrender/src/platform/windows/font.rs
new file mode 100644
index 0000000000..49ec039017
--- /dev/null
+++ b/gfx/wr/webrender/src/platform/windows/font.rs
@@ -0,0 +1,603 @@
+/* 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 api::{FontInstanceFlags, FontKey, FontRenderMode, FontVariation};
+use api::{ColorU, GlyphDimensions, NativeFontHandle};
+use dwrote;
+use crate::gamma_lut::ColorLut;
+use crate::glyph_rasterizer::{FontInstance, FontTransform, GlyphKey};
+use crate::internal_types::{FastHashMap, FastHashSet, ResourceCacheError};
+use crate::glyph_rasterizer::{GlyphFormat, GlyphRasterError, GlyphRasterResult, RasterizedGlyph};
+use crate::gamma_lut::GammaLut;
+use std::borrow::Borrow;
+use std::collections::hash_map::Entry;
+use std::hash::{Hash, Hasher};
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+use api::FontInstancePlatformOptions;
+use std::mem;
+
+lazy_static! {
+ static ref DEFAULT_FONT_DESCRIPTOR: dwrote::FontDescriptor = dwrote::FontDescriptor {
+ family_name: "Arial".to_owned(),
+ weight: dwrote::FontWeight::Regular,
+ stretch: dwrote::FontStretch::Normal,
+ style: dwrote::FontStyle::Normal,
+ };
+}
+
+type CachedFontKey = Arc<Path>;
+
+// A cached dwrote font file that is shared among all faces.
+// Each face holds a CachedFontKey to keep track of how many users of the font there are.
+struct CachedFont {
+ key: CachedFontKey,
+ file: dwrote::FontFile,
+}
+
+// FontFile contains a ComPtr<IDWriteFontFile>, but DWrite font files are threadsafe.
+unsafe impl Send for CachedFont {}
+
+impl PartialEq for CachedFont {
+ fn eq(&self, other: &CachedFont) -> bool {
+ self.key == other.key
+ }
+}
+impl Eq for CachedFont {}
+
+impl Hash for CachedFont {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.key.hash(state);
+ }
+}
+
+impl Borrow<Path> for CachedFont {
+ fn borrow(&self) -> &Path {
+ &*self.key
+ }
+}
+
+lazy_static! {
+ // This is effectively a weak map of dwrote FontFiles. CachedFonts are entered into the
+ // cache when there are any FontFaces using them. CachedFonts are removed from the cache
+ // when there are no more FontFaces using them at all.
+ static ref FONT_CACHE: Mutex<FastHashSet<CachedFont>> = Mutex::new(FastHashSet::default());
+}
+
+struct FontFace {
+ cached: Option<CachedFontKey>,
+ file: dwrote::FontFile,
+ index: u32,
+ face: dwrote::FontFace,
+}
+
+pub struct FontContext {
+ fonts: FastHashMap<FontKey, FontFace>,
+ variations: FastHashMap<(FontKey, dwrote::DWRITE_FONT_SIMULATIONS, Vec<FontVariation>), dwrote::FontFace>,
+ gamma_luts: FastHashMap<(u16, u8), GammaLut>,
+}
+
+// DirectWrite is safe to use on multiple threads and non-shareable resources are
+// all hidden inside their font context.
+unsafe impl Send for FontContext {}
+
+fn dwrite_texture_type(render_mode: FontRenderMode) -> dwrote::DWRITE_TEXTURE_TYPE {
+ match render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_TEXTURE_ALIASED_1x1,
+ FontRenderMode::Alpha |
+ FontRenderMode::Subpixel => dwrote::DWRITE_TEXTURE_CLEARTYPE_3x1,
+ }
+}
+
+fn dwrite_measure_mode(
+ font: &FontInstance,
+ bitmaps: bool,
+) -> dwrote::DWRITE_MEASURING_MODE {
+ if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
+ dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC
+ } else {
+ match font.render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_MEASURING_MODE_GDI_CLASSIC,
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => dwrote::DWRITE_MEASURING_MODE_NATURAL,
+ }
+ }
+}
+
+fn dwrite_render_mode(
+ font_face: &dwrote::FontFace,
+ font: &FontInstance,
+ em_size: f32,
+ measure_mode: dwrote::DWRITE_MEASURING_MODE,
+ bitmaps: bool,
+) -> dwrote::DWRITE_RENDERING_MODE {
+ let dwrite_render_mode = match font.render_mode {
+ FontRenderMode::Mono => dwrote::DWRITE_RENDERING_MODE_ALIASED,
+ FontRenderMode::Alpha | FontRenderMode::Subpixel => {
+ if bitmaps || font.flags.contains(FontInstanceFlags::FORCE_GDI) {
+ dwrote::DWRITE_RENDERING_MODE_GDI_CLASSIC
+ } else if font.flags.contains(FontInstanceFlags::FORCE_SYMMETRIC) {
+ dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC
+ } else if font.flags.contains(FontInstanceFlags::NO_SYMMETRIC) {
+ dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL
+ } else {
+ font_face.get_recommended_rendering_mode_default_params(em_size, 1.0, measure_mode)
+ }
+ }
+ };
+
+ if dwrite_render_mode == dwrote::DWRITE_RENDERING_MODE_OUTLINE {
+ // Outline mode is not supported
+ return dwrote::DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC;
+ }
+
+ dwrite_render_mode
+}
+
+fn is_bitmap_font(font: &FontInstance) -> bool {
+ // If bitmaps are requested, then treat as a bitmap font to disable transforms.
+ // If mono AA is requested, let that take priority over using bitmaps.
+ font.render_mode != FontRenderMode::Mono &&
+ font.flags.contains(FontInstanceFlags::EMBEDDED_BITMAPS)
+}
+
+impl FontContext {
+ pub fn new() -> Result<FontContext, ResourceCacheError> {
+ Ok(FontContext {
+ fonts: FastHashMap::default(),
+ variations: FastHashMap::default(),
+ gamma_luts: FastHashMap::default(),
+ })
+ }
+
+ pub fn has_font(&self, font_key: &FontKey) -> bool {
+ self.fonts.contains_key(font_key)
+ }
+
+ fn add_font_descriptor(&mut self, font_key: &FontKey, desc: &dwrote::FontDescriptor) {
+ let system_fc = dwrote::FontCollection::get_system(false);
+ if let Some(font) = system_fc.get_font_from_descriptor(desc) {
+ let face = font.create_font_face();
+ let file = face.get_files().pop().unwrap();
+ let index = face.get_index();
+ self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
+ }
+ }
+
+ pub fn add_raw_font(&mut self, font_key: &FontKey, data: Arc<Vec<u8>>, index: u32) {
+ if self.fonts.contains_key(font_key) {
+ return;
+ }
+
+ if let Some(file) = dwrote::FontFile::new_from_data(data) {
+ if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ self.fonts.insert(*font_key, FontFace { cached: None, file, index, face });
+ return;
+ }
+ }
+ // XXX add_raw_font needs to have a way to return an error
+ debug!("DWrite WR failed to load font from data, using Arial instead");
+ self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
+ }
+
+ pub fn add_native_font(&mut self, font_key: &FontKey, font_handle: NativeFontHandle) {
+ if self.fonts.contains_key(font_key) {
+ return;
+ }
+
+ let index = font_handle.index;
+ let mut cache = FONT_CACHE.lock().unwrap();
+ // Check to see if the font is already in the cache. If so, reuse it.
+ if let Some(font) = cache.get(font_handle.path.as_path()) {
+ if let Ok(face) = font.file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ self.fonts.insert(
+ *font_key,
+ FontFace { cached: Some(font.key.clone()), file: font.file.clone(), index, face },
+ );
+ return;
+ }
+ }
+ if let Some(file) = dwrote::FontFile::new_from_path(&font_handle.path) {
+ // The font is not in the cache yet, so try to create the font and insert it in the cache.
+ if let Ok(face) = file.create_face(index, dwrote::DWRITE_FONT_SIMULATIONS_NONE) {
+ let key: CachedFontKey = font_handle.path.into();
+ self.fonts.insert(
+ *font_key,
+ FontFace { cached: Some(key.clone()), file: file.clone(), index, face },
+ );
+ cache.insert(CachedFont { key, file });
+ return;
+ }
+ }
+
+ // XXX add_native_font needs to have a way to return an error
+ debug!("DWrite WR failed to load font from path, using Arial instead");
+ self.add_font_descriptor(font_key, &DEFAULT_FONT_DESCRIPTOR);
+ }
+
+ pub fn delete_font(&mut self, font_key: &FontKey) {
+ if let Some(face) = self.fonts.remove(font_key) {
+ self.variations.retain(|k, _| k.0 != *font_key);
+ // Check if this was a cached font.
+ if let Some(key) = face.cached {
+ let mut cache = FONT_CACHE.lock().unwrap();
+ // If there are only two references left, that means only this face and
+ // the cache are using the font. So remove it from the cache.
+ if Arc::strong_count(&key) == 2 {
+ cache.remove(&*key);
+ }
+ }
+ }
+ }
+
+ pub fn delete_font_instance(&mut self, instance: &FontInstance) {
+ // Ensure we don't keep around excessive amounts of stale variations.
+ if !instance.variations.is_empty() {
+ let sims = if instance.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ dwrote::DWRITE_FONT_SIMULATIONS_BOLD
+ } else {
+ dwrote::DWRITE_FONT_SIMULATIONS_NONE
+ };
+ self.variations.remove(&(instance.font_key, sims, instance.variations.clone()));
+ }
+ }
+
+ // Assumes RGB format from dwrite, which is 3 bytes per pixel as dwrite
+ // doesn't output an alpha value via GlyphRunAnalysis::CreateAlphaTexture
+ #[allow(dead_code)]
+ fn print_glyph_data(&self, data: &[u8], width: usize, height: usize) {
+ // Rust doesn't have step_by support on stable :(
+ for i in 0 .. height {
+ let current_height = i * width * 3;
+
+ for pixel in data[current_height .. current_height + (width * 3)].chunks(3) {
+ let r = pixel[0];
+ let g = pixel[1];
+ let b = pixel[2];
+ print!("({}, {}, {}) ", r, g, b,);
+ }
+ println!();
+ }
+ }
+
+ fn get_font_face(
+ &mut self,
+ font: &FontInstance,
+ ) -> &dwrote::FontFace {
+ if !font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) &&
+ font.variations.is_empty() {
+ return &self.fonts.get(&font.font_key).unwrap().face;
+ }
+ let sims = if font.flags.contains(FontInstanceFlags::SYNTHETIC_BOLD) {
+ dwrote::DWRITE_FONT_SIMULATIONS_BOLD
+ } else {
+ dwrote::DWRITE_FONT_SIMULATIONS_NONE
+ };
+ match self.variations.entry((font.font_key, sims, font.variations.clone())) {
+ Entry::Occupied(entry) => entry.into_mut(),
+ Entry::Vacant(entry) => {
+ let normal_face = self.fonts.get(&font.font_key).unwrap();
+ if !font.variations.is_empty() {
+ if let Some(var_face) = normal_face.face.create_font_face_with_variations(
+ sims,
+ &font.variations.iter().map(|var| {
+ dwrote::DWRITE_FONT_AXIS_VALUE {
+ // OpenType tags are big-endian, but DWrite wants little-endian.
+ axisTag: var.tag.swap_bytes(),
+ value: var.value,
+ }
+ }).collect::<Vec<_>>(),
+ ) {
+ return entry.insert(var_face);
+ }
+ }
+ let var_face = normal_face.file
+ .create_face(normal_face.index, sims)
+ .unwrap_or_else(|_| normal_face.face.clone());
+ entry.insert(var_face)
+ }
+ }
+ }
+
+ fn create_glyph_analysis(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ size: f32,
+ transform: Option<dwrote::DWRITE_MATRIX>,
+ bitmaps: bool,
+ ) -> Result<(dwrote::GlyphRunAnalysis, dwrote::DWRITE_TEXTURE_TYPE, dwrote::RECT), dwrote::HRESULT> {
+ let face = self.get_font_face(font);
+ let glyph = key.index() as u16;
+ let advance = 0.0f32;
+ let offset = dwrote::GlyphOffset {
+ advanceOffset: 0.0,
+ ascenderOffset: 0.0,
+ };
+
+ let glyph_run = dwrote::DWRITE_GLYPH_RUN {
+ fontFace: unsafe { face.as_ptr() },
+ fontEmSize: size, // size in DIPs (1/96", same as CSS pixels)
+ glyphCount: 1,
+ glyphIndices: &glyph,
+ glyphAdvances: &advance,
+ glyphOffsets: &offset,
+ isSideways: 0,
+ bidiLevel: 0,
+ };
+
+ let dwrite_measure_mode = dwrite_measure_mode(font, bitmaps);
+ let dwrite_render_mode = dwrite_render_mode(
+ face,
+ font,
+ size,
+ dwrite_measure_mode,
+ bitmaps,
+ );
+
+ let analysis = dwrote::GlyphRunAnalysis::create(
+ &glyph_run,
+ 1.0,
+ transform,
+ dwrite_render_mode,
+ dwrite_measure_mode,
+ 0.0,
+ 0.0,
+ )?;
+ let texture_type = dwrite_texture_type(font.render_mode);
+ let bounds = analysis.get_alpha_texture_bounds(texture_type)?;
+ // If the bounds are empty, then we might not be able to render the glyph with cleartype.
+ // Try again with aliased rendering to check if that works instead.
+ if font.render_mode != FontRenderMode::Mono &&
+ (bounds.left == bounds.right || bounds.top == bounds.bottom) {
+ let analysis2 = dwrote::GlyphRunAnalysis::create(
+ &glyph_run,
+ 1.0,
+ transform,
+ dwrote::DWRITE_RENDERING_MODE_ALIASED,
+ dwrite_measure_mode,
+ 0.0,
+ 0.0,
+ )?;
+ let bounds2 = analysis2.get_alpha_texture_bounds(dwrote::DWRITE_TEXTURE_ALIASED_1x1)?;
+ if bounds2.left != bounds2.right && bounds2.top != bounds2.bottom {
+ return Ok((analysis2, dwrote::DWRITE_TEXTURE_ALIASED_1x1, bounds2));
+ }
+ }
+ Ok((analysis, texture_type, bounds))
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ let face = &self.fonts.get(&font_key).unwrap().face;
+ let indices = face.get_glyph_indices(&[ch as u32]);
+ indices.first().map(|idx| *idx as u32)
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ key: &GlyphKey,
+ ) -> Option<GlyphDimensions> {
+ let (size, _, bitmaps, transform) = Self::get_glyph_parameters(font, key);
+ let (_, _, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps).ok()?;
+
+ let width = (bounds.right - bounds.left) as i32;
+ let height = (bounds.bottom - bounds.top) as i32;
+
+ // Alpha texture bounds can sometimes return an empty rect
+ // Such as for spaces
+ if width == 0 || height == 0 {
+ return None;
+ }
+
+ let face = self.get_font_face(font);
+ face.get_design_glyph_metrics(&[key.index() as u16], false)
+ .first()
+ .map(|metrics| {
+ let em_size = size / 16.;
+ let design_units_per_pixel = face.metrics().metrics0().designUnitsPerEm as f32 / 16. as f32;
+ let scaled_design_units_to_pixels = em_size / design_units_per_pixel;
+ let advance = metrics.advanceWidth as f32 * scaled_design_units_to_pixels;
+
+ GlyphDimensions {
+ left: bounds.left,
+ top: -bounds.top,
+ width,
+ height,
+ advance,
+ }
+ })
+ }
+
+ // DWrite ClearType gives us values in RGB, but WR expects BGRA.
+ fn convert_to_bgra(
+ &self,
+ pixels: &[u8],
+ width: usize,
+ height: usize,
+ texture_type: dwrote::DWRITE_TEXTURE_TYPE,
+ render_mode: FontRenderMode,
+ bitmaps: bool,
+ subpixel_bgr: bool,
+ texture_padding: bool,
+ ) -> Vec<u8> {
+ let (buffer_width, buffer_height, padding) = if texture_padding {
+ (width + 2, height + 2, 1)
+ } else {
+ (width, height, 0)
+ };
+
+ let buffer_length = buffer_width * buffer_height * 4;
+ let mut bgra_pixels: Vec<u8> = vec![0; buffer_length];
+
+ match (texture_type, render_mode, bitmaps) {
+ (dwrote::DWRITE_TEXTURE_ALIASED_1x1, _, _) => {
+ assert!(width * height == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ let alpha = pixels[i];
+ i += 1;
+ bgra_pixels[offset + 0] = alpha;
+ bgra_pixels[offset + 1] = alpha;
+ bgra_pixels[offset + 2] = alpha;
+ bgra_pixels[offset + 3] = alpha;
+ }
+ }
+ }
+ (_, FontRenderMode::Subpixel, false) => {
+ assert!(width * height * 3 == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ let (mut r, g, mut b) = (pixels[i + 0], pixels[i + 1], pixels[i + 2]);
+ if subpixel_bgr {
+ mem::swap(&mut r, &mut b);
+ }
+ i += 3;
+ bgra_pixels[offset + 0] = b;
+ bgra_pixels[offset + 1] = g;
+ bgra_pixels[offset + 2] = r;
+ bgra_pixels[offset + 3] = 0xff;
+ }
+ }
+ }
+ _ => {
+ assert!(width * height * 3 == pixels.len());
+ let mut i = 0;
+ for row in padding .. height + padding {
+ let row_offset = row * buffer_width;
+ for col in padding .. width + padding {
+ let offset = (row_offset + col) * 4;
+ // Only take the G channel, as its closest to D2D
+ let alpha = pixels[i + 1] as u8;
+ i += 3;
+ bgra_pixels[offset + 0] = alpha;
+ bgra_pixels[offset + 1] = alpha;
+ bgra_pixels[offset + 2] = alpha;
+ bgra_pixels[offset + 3] = alpha;
+ }
+ }
+ }
+ };
+ bgra_pixels
+ }
+
+ pub fn prepare_font(font: &mut FontInstance) {
+ match font.render_mode {
+ FontRenderMode::Mono => {
+ // In mono mode the color of the font is irrelevant.
+ font.color = ColorU::new(255, 255, 255, 255);
+ // Subpixel positioning is disabled in mono mode.
+ font.disable_subpixel_position();
+ }
+ FontRenderMode::Alpha => {
+ font.color = font.color.luminance_color().quantize();
+ }
+ FontRenderMode::Subpixel => {
+ font.color = font.color.quantize();
+ }
+ }
+ }
+
+ fn get_glyph_parameters(font: &FontInstance, key: &GlyphKey) -> (f32, f64, bool, Option<dwrote::DWRITE_MATRIX>) {
+ let (_, y_scale) = font.transform.compute_scale().unwrap_or((1.0, 1.0));
+ let scaled_size = font.size.to_f64_px() * y_scale;
+ let bitmaps = is_bitmap_font(font);
+ let (mut shape, (mut x_offset, mut y_offset)) = if bitmaps {
+ (FontTransform::identity(), (0.0, 0.0))
+ } else {
+ (font.transform.invert_scale(y_scale, y_scale), font.get_subpx_offset(key))
+ };
+ if font.flags.contains(FontInstanceFlags::FLIP_X) {
+ shape = shape.flip_x();
+ }
+ if font.flags.contains(FontInstanceFlags::FLIP_Y) {
+ shape = shape.flip_y();
+ }
+ if font.flags.contains(FontInstanceFlags::TRANSPOSE) {
+ shape = shape.swap_xy();
+ }
+ let (mut tx, mut ty) = (0.0, 0.0);
+ if font.synthetic_italics.is_enabled() {
+ let (shape_, (tx_, ty_)) = font.synthesize_italics(shape, scaled_size);
+ shape = shape_;
+ tx = tx_;
+ ty = ty_;
+ };
+ x_offset += tx;
+ y_offset += ty;
+ let transform = if !shape.is_identity() || (x_offset, y_offset) != (0.0, 0.0) {
+ Some(dwrote::DWRITE_MATRIX {
+ m11: shape.scale_x,
+ m12: shape.skew_y,
+ m21: shape.skew_x,
+ m22: shape.scale_y,
+ dx: x_offset as f32,
+ dy: y_offset as f32,
+ })
+ } else {
+ None
+ };
+ (scaled_size as f32, y_scale, bitmaps, transform)
+ }
+
+ pub fn rasterize_glyph(&mut self, font: &FontInstance, key: &GlyphKey) -> GlyphRasterResult {
+ let (size, y_scale, bitmaps, transform) = Self::get_glyph_parameters(font, key);
+ let (analysis, texture_type, bounds) = self.create_glyph_analysis(font, key, size, transform, bitmaps)
+ .or(Err(GlyphRasterError::LoadFailed))?;
+ let width = (bounds.right - bounds.left) as i32;
+ let height = (bounds.bottom - bounds.top) as i32;
+ // Alpha texture bounds can sometimes return an empty rect
+ // Such as for spaces
+ if width == 0 || height == 0 {
+ return Err(GlyphRasterError::LoadFailed);
+ }
+
+ let pixels = analysis.create_alpha_texture(texture_type, bounds).or(Err(GlyphRasterError::LoadFailed))?;
+ let mut bgra_pixels = self.convert_to_bgra(&pixels, width as usize, height as usize,
+ texture_type, font.render_mode, bitmaps,
+ font.flags.contains(FontInstanceFlags::SUBPIXEL_BGR),
+ font.use_texture_padding());
+
+ let FontInstancePlatformOptions { gamma, contrast, cleartype_level, .. } =
+ font.platform_options.unwrap_or_default();
+ let gamma_lut = self.gamma_luts
+ .entry((gamma, contrast))
+ .or_insert_with(||
+ GammaLut::new(
+ contrast as f32 / 100.0,
+ gamma as f32 / 100.0,
+ gamma as f32 / 100.0,
+ ));
+ if bitmaps || texture_type == dwrote::DWRITE_TEXTURE_ALIASED_1x1 ||
+ font.render_mode != FontRenderMode::Subpixel {
+ gamma_lut.preblend(&mut bgra_pixels, font.color);
+ } else {
+ gamma_lut.preblend_scaled(&mut bgra_pixels, font.color, cleartype_level);
+ }
+
+ let format = if bitmaps {
+ GlyphFormat::Bitmap
+ } else if texture_type == dwrote::DWRITE_TEXTURE_ALIASED_1x1 {
+ font.get_alpha_glyph_format()
+ } else {
+ font.get_glyph_format()
+ };
+
+ let padding = if font.use_texture_padding() { 1 } else { 0 };
+ Ok(RasterizedGlyph {
+ left: (bounds.left - padding) as f32,
+ top: (-bounds.top + padding) as f32,
+ width: width + padding * 2,
+ height: height + padding * 2,
+ scale: (if bitmaps { y_scale.recip() } else { 1.0 }) as f32,
+ format,
+ bytes: bgra_pixels,
+ })
+ }
+}
diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs
new file mode 100644
index 0000000000..0e61370830
--- /dev/null
+++ b/gfx/wr/webrender/src/prepare.rs
@@ -0,0 +1,1889 @@
+/* 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/. */
+
+//! # Prepare pass
+//!
+//! TODO: document this!
+
+use std::{cmp, u32};
+use api::{PremultipliedColorF, PropertyBinding, GradientStop, ExtendMode};
+use api::{BoxShadowClipMode, LineOrientation, BorderStyle, ClipMode};
+use api::units::*;
+use euclid::Scale;
+use euclid::approxeq::ApproxEq;
+use smallvec::SmallVec;
+use crate::image_tiling::{self, Repetition};
+use crate::border::{get_max_scale_for_border, build_border_instances};
+use crate::clip::{ClipStore};
+use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
+use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainInstance, ClipItemKind};
+use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
+use crate::gpu_cache::{GpuCacheHandle, GpuDataRequest};
+use crate::gpu_types::{BrushFlags};
+use crate::internal_types::{FastHashMap, PlaneSplitAnchor};
+use crate::picture::{PicturePrimitive, SliceId, TileCacheLogger, ClusterFlags, SurfaceRenderTasks};
+use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, TileCacheInstance};
+use crate::prim_store::gradient::{GRADIENT_FP_STOPS, GradientCacheKey, GradientStopKey};
+use crate::prim_store::gradient::LinearGradientPrimitive;
+use crate::prim_store::line_dec::MAX_LINE_DECORATION_RESOLUTION;
+use crate::prim_store::*;
+use crate::render_backend::DataStores;
+use crate::render_task_cache::{RenderTaskCacheKeyKind, RenderTaskCacheEntryHandle};
+use crate::render_task_cache::{RenderTaskCacheKey, to_cache_size, RenderTaskParent};
+use crate::render_task::{RenderTaskKind, RenderTask};
+use crate::segment::SegmentBuilder;
+use crate::space::SpaceMapper;
+use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
+use crate::util::{clamp_to_scale_factor, pack_as_float, raster_rect_to_device_pixels};
+use crate::visibility::{compute_conservative_visible_rect, PrimitiveVisibility, VisibilityState, PrimitiveVisibilityMask};
+
+
+const MAX_MASK_SIZE: f32 = 4096.0;
+
+const MIN_BRUSH_SPLIT_AREA: f32 = 128.0 * 128.0;
+
+
+pub fn prepare_primitives(
+ store: &mut PrimitiveStore,
+ prim_list: &mut PrimitiveList,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_cache_log: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+) {
+ profile_scope!("prepare_primitives");
+ for (cluster_index, cluster) in prim_list.clusters.iter_mut().enumerate() {
+ if !cluster.flags.contains(ClusterFlags::IS_VISIBLE) {
+ continue;
+ }
+ profile_scope!("cluster");
+ pic_state.map_local_to_pic.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ frame_state.surfaces[pic_context.surface_index.0].opaque_rect = PictureRect::zero();
+
+ for (idx, prim_instance) in (&mut prim_list.prim_instances[cluster.prim_range()]).iter_mut().enumerate() {
+ let prim_instance_index = cluster.prim_range.start + idx;
+
+ // First check for coarse visibility (if this primitive was completely off-screen)
+ match prim_instance.vis.state {
+ VisibilityState::Unset => {
+ panic!("bug: invalid vis state");
+ }
+ VisibilityState::Culled => {
+ continue;
+ }
+ VisibilityState::Coarse { ref rect_in_pic_space } => {
+ // The original coarse state was calculated during the initial visibility pass.
+ // However, it's possible that the dirty rect has got smaller, if tiles were not
+ // dirty. Intersecting with the dirty rect here eliminates preparing any primitives
+ // outside the dirty rect, and reduces the size of any off-screen surface allocations
+ // for clip masks / render tasks that we make.
+
+ // Clear the current visibiilty mask, and build a more detailed one based on the dirty rect
+ // regions below.
+ let dirty_region = frame_state.current_dirty_region();
+ let mut visibility_mask = PrimitiveVisibilityMask::empty();
+
+ for dirty_region in &dirty_region.dirty_rects {
+ if rect_in_pic_space.intersects(&dirty_region.rect_in_pic_space) {
+ visibility_mask.include(dirty_region.visibility_mask);
+ }
+ }
+
+ // Check again if the prim is now visible after considering the current dirty regions.
+ if visibility_mask.is_empty() {
+ prim_instance.clear_visibility();
+ continue;
+ } else {
+ prim_instance.vis.state = VisibilityState::Detailed {
+ visibility_mask,
+ }
+ }
+ }
+ VisibilityState::Detailed { .. } => {
+ // Was already set to detailed (picture caching disabled or a root element)
+ }
+ }
+
+ let plane_split_anchor = PlaneSplitAnchor::new(cluster_index, prim_instance_index);
+
+ if prepare_prim_for_render(
+ store,
+ prim_instance,
+ cluster,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ plane_split_anchor,
+ data_stores,
+ scratch,
+ tile_cache_log,
+ tile_caches,
+ ) {
+ frame_state.num_visible_primitives += 1;
+ } else {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ if !cluster.opaque_rect.is_empty() {
+ let surface = &mut frame_state.surfaces[pic_context.surface_index.0];
+
+ if let Some(cluster_opaque_rect) = surface.map_local_to_surface.map_inner_bounds(&cluster.opaque_rect) {
+ surface.opaque_rect = crate::util::conservative_union_rect(&surface.opaque_rect, &cluster_opaque_rect);
+ }
+ }
+ }
+}
+
+fn prepare_prim_for_render(
+ store: &mut PrimitiveStore,
+ prim_instance: &mut PrimitiveInstance,
+ cluster: &mut PrimitiveCluster,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ plane_split_anchor: PlaneSplitAnchor,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+ tile_cache_log: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+) -> bool {
+ profile_scope!("prepare_prim_for_render");
+
+ // If we have dependencies, we need to prepare them first, in order
+ // to know the actual rect of this primitive.
+ // For example, scrolling may affect the location of an item in
+ // local space, which may force us to render this item on a larger
+ // picture target, if being composited.
+ let pic_info = {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index ,.. } => {
+ let pic = &mut store.pictures[pic_index.0];
+
+ match pic.take_context(
+ pic_index,
+ pic_context.surface_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ pic_context.surface_index,
+ &pic_context.subpixel_mode,
+ frame_state,
+ frame_context,
+ scratch,
+ tile_cache_log,
+ tile_caches,
+ ) {
+ Some(info) => Some(info),
+ None => {
+ if prim_instance.is_chased() {
+ println!("\tculled for carrying an invisible composite filter");
+ }
+
+ return false;
+ }
+ }
+ }
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::Rectangle { .. } |
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::ImageBorder { .. } |
+ PrimitiveInstanceKind::YuvImage { .. } |
+ PrimitiveInstanceKind::Image { .. } |
+ PrimitiveInstanceKind::LinearGradient { .. } |
+ PrimitiveInstanceKind::RadialGradient { .. } |
+ PrimitiveInstanceKind::ConicGradient { .. } |
+ PrimitiveInstanceKind::Clear { .. } |
+ PrimitiveInstanceKind::Backdrop { .. } => {
+ None
+ }
+ }
+ };
+
+ let is_passthrough = match pic_info {
+ Some((pic_context_for_children, mut pic_state_for_children, mut prim_list)) => {
+ let is_passthrough = pic_context_for_children.is_passthrough;
+
+ prepare_primitives(
+ store,
+ &mut prim_list,
+ &pic_context_for_children,
+ &mut pic_state_for_children,
+ frame_context,
+ frame_state,
+ data_stores,
+ scratch,
+ tile_cache_log,
+ tile_caches,
+ );
+
+ // Restore the dependencies (borrow check dance)
+ store.pictures[pic_context_for_children.pic_index.0]
+ .restore_context(
+ prim_list,
+ pic_context_for_children,
+ pic_state_for_children,
+ frame_state,
+ );
+
+ is_passthrough
+ }
+ None => {
+ false
+ }
+ };
+
+ let prim_rect = data_stores.get_local_prim_rect(
+ prim_instance,
+ store,
+ );
+
+ if !is_passthrough {
+ if !update_clip_task(
+ prim_instance,
+ &prim_rect.origin,
+ cluster.spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ store,
+ data_stores,
+ scratch,
+ ) {
+ if prim_instance.is_chased() {
+ println!("\tconsidered invisible");
+ }
+ return false;
+ }
+
+ if prim_instance.is_chased() {
+ println!("\tconsidered visible and ready with local pos {:?}", prim_rect.origin);
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ {
+ prim_instance.prepared_frame_id = frame_state.rg_builder.frame_id();
+ }
+
+ prepare_interned_prim_for_render(
+ store,
+ prim_instance,
+ cluster,
+ plane_split_anchor,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ data_stores,
+ scratch,
+ );
+
+ true
+}
+
+/// Prepare an interned primitive for rendering, by requesting
+/// resources, render tasks etc. This is equivalent to the
+/// prepare_prim_for_render_inner call for old style primitives.
+fn prepare_interned_prim_for_render(
+ store: &mut PrimitiveStore,
+ prim_instance: &mut PrimitiveInstance,
+ cluster: &mut PrimitiveCluster,
+ plane_split_anchor: PlaneSplitAnchor,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+) {
+ let prim_spatial_node_index = cluster.spatial_node_index;
+ let is_chased = prim_instance.is_chased();
+ let device_pixel_scale = frame_state.surfaces[pic_context.surface_index.0].device_pixel_scale;
+ let mut is_opaque = false;
+
+ match &mut prim_instance.kind {
+ PrimitiveInstanceKind::LineDecoration { data_handle, ref mut cache_handle, .. } => {
+ profile_scope!("LineDecoration");
+ let prim_data = &mut data_stores.line_decoration[*data_handle];
+ let common_data = &mut prim_data.common;
+ let line_dec_data = &mut prim_data.kind;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ line_dec_data.update(common_data, frame_state);
+
+ // Work out the device pixel size to be used to cache this line decoration.
+ if is_chased {
+ println!("\tline decoration key={:?}", line_dec_data.cache_key);
+ }
+
+ // If we have a cache key, it's a wavy / dashed / dotted line. Otherwise, it's
+ // a simple solid line.
+ if let Some(cache_key) = line_dec_data.cache_key.as_ref() {
+ // TODO(gw): Do we ever need / want to support scales for text decorations
+ // based on the current transform?
+ let scale_factor = Scale::new(1.0) * device_pixel_scale;
+ let mut task_size = (LayoutSize::from_au(cache_key.size) * scale_factor).ceil().to_i32();
+ if task_size.width > MAX_LINE_DECORATION_RESOLUTION as i32 ||
+ task_size.height > MAX_LINE_DECORATION_RESOLUTION as i32 {
+ let max_extent = cmp::max(task_size.width, task_size.height);
+ let task_scale_factor = Scale::new(MAX_LINE_DECORATION_RESOLUTION as f32 / max_extent as f32);
+ task_size = (LayoutSize::from_au(cache_key.size) * scale_factor * task_scale_factor)
+ .ceil().to_i32();
+ }
+
+ // Request a pre-rendered image task.
+ // TODO(gw): This match is a bit untidy, but it should disappear completely
+ // once the prepare_prims and batching are unified. When that
+ // happens, we can use the cache handle immediately, and not need
+ // to temporarily store it in the primitive instance.
+ *cache_handle = Some(frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: task_size,
+ kind: RenderTaskCacheKeyKind::LineDecoration(cache_key.clone()),
+ },
+ frame_state.gpu_cache,
+ frame_state.rg_builder,
+ None,
+ false,
+ RenderTaskParent::Surface(pic_context.surface_index),
+ frame_state.surfaces,
+ |rg_builder| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_line_decoration(
+ cache_key.style,
+ cache_key.orientation,
+ cache_key.wavy_line_thickness.to_f32_px(),
+ LayoutSize::from_au(cache_key.size),
+ ),
+ ))
+ }
+ ));
+ }
+ }
+ PrimitiveInstanceKind::TextRun { run_index, data_handle, .. } => {
+ profile_scope!("TextRun");
+ let prim_data = &mut data_stores.text_run[*data_handle];
+ let run = &mut store.text_runs[*run_index];
+
+ prim_data.common.may_need_repetition = false;
+
+ // The glyph transform has to match `glyph_transform` in "ps_text_run" shader.
+ // It's relative to the rasterizing space of a glyph.
+ let transform = frame_context.spatial_tree
+ .get_relative_transform(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ )
+ .into_fast_transform();
+ let prim_offset = prim_data.common.prim_rect.origin.to_vector() - run.reference_frame_relative_offset;
+
+ let pic = &store.pictures[pic_context.pic_index.0];
+ let surface = &frame_state.surfaces[pic_context.surface_index.0];
+ let prim_info = &prim_instance.vis;
+ let root_scaling_factor = match pic.raster_config {
+ Some(ref raster_config) => raster_config.root_scaling_factor,
+ None => 1.0
+ };
+
+ run.request_resources(
+ prim_offset,
+ prim_info.clip_chain.pic_clip_rect,
+ &prim_data.font,
+ &prim_data.glyphs,
+ &transform.to_transform().with_destination::<_>(),
+ surface,
+ prim_spatial_node_index,
+ root_scaling_factor,
+ &pic_context.subpixel_mode,
+ frame_state.resource_cache,
+ frame_state.gpu_cache,
+ frame_context.spatial_tree,
+ scratch,
+ );
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state);
+ }
+ PrimitiveInstanceKind::Clear { data_handle, .. } => {
+ profile_scope!("Clear");
+ let prim_data = &mut data_stores.prim[*data_handle];
+
+ prim_data.common.may_need_repetition = false;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state, frame_context.scene_properties);
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, ref mut cache_handles, .. } => {
+ profile_scope!("NormalBorder");
+ let prim_data = &mut data_stores.normal_border[*data_handle];
+ let common_data = &mut prim_data.common;
+ let border_data = &mut prim_data.kind;
+
+ common_data.may_need_repetition =
+ matches!(border_data.border.top.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.right.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.bottom.style, BorderStyle::Dotted | BorderStyle::Dashed) ||
+ matches!(border_data.border.left.style, BorderStyle::Dotted | BorderStyle::Dashed);
+
+
+ // Update the template this instance references, which may refresh the GPU
+ // cache with any shared template data.
+ border_data.update(common_data, frame_state);
+
+ // TODO(gw): For now, the scale factors to rasterize borders at are
+ // based on the true world transform of the primitive. When
+ // raster roots with local scale are supported in future,
+ // that will need to be accounted for here.
+ let scale = frame_context
+ .spatial_tree
+ .get_world_transform(prim_spatial_node_index)
+ .scale_factors();
+
+ // Scale factors are normalized to a power of 2 to reduce the number of
+ // resolution changes.
+ // For frames with a changing scale transform round scale factors up to
+ // nearest power-of-2 boundary so that we don't keep having to redraw
+ // the content as it scales up and down. Rounding up to nearest
+ // power-of-2 boundary ensures we never scale up, only down --- avoiding
+ // jaggies. It also ensures we never scale down by more than a factor of
+ // 2, avoiding bad downscaling quality.
+ let scale_width = clamp_to_scale_factor(scale.0, false);
+ let scale_height = clamp_to_scale_factor(scale.1, false);
+ // Pick the maximum dimension as scale
+ let world_scale = LayoutToWorldScale::new(scale_width.max(scale_height));
+ let mut scale = world_scale * device_pixel_scale;
+ let max_scale = get_max_scale_for_border(border_data);
+ scale.0 = scale.0.min(max_scale.0);
+
+ // For each edge and corner, request the render task by content key
+ // from the render task cache. This ensures that the render task for
+ // this segment will be available for batching later in the frame.
+ let mut handles: SmallVec<[RenderTaskCacheEntryHandle; 8]> = SmallVec::new();
+
+ for segment in &border_data.border_segments {
+ // Update the cache key device size based on requested scale.
+ let cache_size = to_cache_size(segment.local_task_size, &mut scale);
+ let cache_key = RenderTaskCacheKey {
+ kind: RenderTaskCacheKeyKind::BorderSegment(segment.cache_key.clone()),
+ size: cache_size,
+ };
+
+ handles.push(frame_state.resource_cache.request_render_task(
+ cache_key,
+ frame_state.gpu_cache,
+ frame_state.rg_builder,
+ None,
+ false, // TODO(gw): We don't calculate opacity for borders yet!
+ RenderTaskParent::Surface(pic_context.surface_index),
+ frame_state.surfaces,
+ |rg_builder| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ cache_size,
+ RenderTaskKind::new_border_segment(
+ build_border_instances(
+ &segment.cache_key,
+ cache_size,
+ &border_data.border,
+ scale,
+ )
+ ),
+ ))
+ }
+ ));
+ }
+
+ *cache_handles = scratch
+ .border_cache_handles
+ .extend(handles);
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ profile_scope!("ImageBorder");
+ let prim_data = &mut data_stores.image_border[*data_handle];
+
+ // TODO: get access to the ninepatch and to check whwther we need support
+ // for repetitions in the shader.
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.kind.update(&mut prim_data.common, frame_state);
+ }
+ PrimitiveInstanceKind::Rectangle { data_handle, segment_instance_index, color_binding_index, .. } => {
+ profile_scope!("Rectangle");
+ let prim_data = &mut data_stores.prim[*data_handle];
+ prim_data.common.may_need_repetition = false;
+
+ if *color_binding_index != ColorBindingIndex::INVALID {
+ match store.color_bindings[*color_binding_index] {
+ PropertyBinding::Binding(..) => {
+ // We explicitly invalidate the gpu cache
+ // if the color is animating.
+ let gpu_cache_handle =
+ if *segment_instance_index == SegmentInstanceIndex::INVALID {
+ None
+ } else if *segment_instance_index == SegmentInstanceIndex::UNUSED {
+ Some(&prim_data.common.gpu_cache_handle)
+ } else {
+ Some(&scratch.segment_instances[*segment_instance_index].gpu_cache_handle)
+ };
+ if let Some(gpu_cache_handle) = gpu_cache_handle {
+ frame_state.gpu_cache.invalidate(gpu_cache_handle);
+ }
+ }
+ PropertyBinding::Value(..) => {},
+ }
+ }
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(
+ frame_state,
+ frame_context.scene_properties,
+ );
+
+ is_opaque = prim_data.common.opacity.is_opaque;
+
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ prim_data.kind.write_prim_gpu_blocks(
+ request,
+ frame_context.scene_properties,
+ );
+ }
+ );
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, segment_instance_index, .. } => {
+ profile_scope!("YuvImage");
+ let prim_data = &mut data_stores.yuv_image[*data_handle];
+ let common_data = &mut prim_data.common;
+ let yuv_image_data = &mut prim_data.kind;
+ is_opaque = true;
+
+ common_data.may_need_repetition = false;
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ yuv_image_data.update(common_data, frame_state);
+
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ yuv_image_data.write_prim_gpu_blocks(request);
+ }
+ );
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+ profile_scope!("Image");
+ let prim_data = &mut data_stores.image[*data_handle];
+ let common_data = &mut prim_data.common;
+ let image_data = &mut prim_data.kind;
+
+ if image_data.stretch_size.width >= common_data.prim_rect.size.width &&
+ image_data.stretch_size.height >= common_data.prim_rect.size.height {
+
+ common_data.may_need_repetition = false;
+ }
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ image_data.update(
+ common_data,
+ pic_context.surface_index,
+ frame_state,
+ );
+
+ // common_data.opacity.is_opaque is computed in the above update call.
+ is_opaque = common_data.opacity.is_opaque;
+
+ let image_instance = &mut store.images[*image_instance_index];
+
+ write_segment(
+ image_instance.segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ image_data.write_prim_gpu_blocks(request);
+ },
+ );
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, gradient_index, .. } => {
+ profile_scope!("LinearGradient");
+ let prim_data = &mut data_stores.linear_grad[*data_handle];
+ let gradient = &mut store.linear_gradients[*gradient_index];
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state);
+
+ if prim_data.stretch_size.width >= prim_data.common.prim_rect.size.width &&
+ prim_data.stretch_size.height >= prim_data.common.prim_rect.size.height {
+
+ prim_data.common.may_need_repetition = false;
+ }
+
+ if prim_data.supports_caching {
+ let gradient_size = (prim_data.end_point - prim_data.start_point).to_size();
+
+ // Calculate what the range of the gradient is that covers this
+ // primitive. These values are included in the cache key. The
+ // size of the gradient task is the length of a texture cache
+ // region, for maximum accuracy, and a minimal size on the
+ // axis that doesn't matter.
+ let (size, orientation, prim_start_offset, prim_end_offset) =
+ if prim_data.start_point.x.approx_eq(&prim_data.end_point.x) {
+ let prim_start_offset = -prim_data.start_point.y / gradient_size.height;
+ let prim_end_offset = (prim_data.common.prim_rect.size.height - prim_data.start_point.y)
+ / gradient_size.height;
+ let size = DeviceIntSize::new(16, TEXTURE_REGION_DIMENSIONS);
+ (size, LineOrientation::Vertical, prim_start_offset, prim_end_offset)
+ } else {
+ let prim_start_offset = -prim_data.start_point.x / gradient_size.width;
+ let prim_end_offset = (prim_data.common.prim_rect.size.width - prim_data.start_point.x)
+ / gradient_size.width;
+ let size = DeviceIntSize::new(TEXTURE_REGION_DIMENSIONS, 16);
+ (size, LineOrientation::Horizontal, prim_start_offset, prim_end_offset)
+ };
+
+ // Build the cache key, including information about the stops.
+ let mut stops = vec![GradientStopKey::empty(); prim_data.stops.len()];
+
+ // Reverse the stops as required, same as the gradient builder does
+ // for the slow path.
+ if prim_data.reverse_stops {
+ for (src, dest) in prim_data.stops.iter().rev().zip(stops.iter_mut()) {
+ let stop = GradientStop {
+ offset: 1.0 - src.offset,
+ color: src.color,
+ };
+ *dest = stop.into();
+ }
+ } else {
+ for (src, dest) in prim_data.stops.iter().zip(stops.iter_mut()) {
+ *dest = (*src).into();
+ }
+ }
+
+ gradient.cache_segments.clear();
+
+ // emit render task caches and image rectangles to draw a gradient
+ // with offsets from start_offset to end_offset.
+ //
+ // the primitive is covered by a gradient that ranges from
+ // prim_start_offset to prim_end_offset.
+ //
+ // when clamping, these two pairs of offsets will always be the same.
+ // when repeating, however, we march across the primitive, blitting
+ // copies of the gradient along the way. each copy has a range from
+ // 0.0 to 1.0 (assuming it's fully visible), but where it appears on
+ // the primitive changes as we go. this position is also expressed
+ // as an offset: gradient_offset_base. that is, in terms of stops,
+ // we draw a gradient from start_offset to end_offset. its actual
+ // location on the primitive is at start_offset + gradient_offset_base.
+ //
+ // either way, we need a while-loop to draw the gradient as well
+ // because it might have more than 4 stops (the maximum of a cached
+ // segment) and/or hard stops. so we have a walk-within-the-walk from
+ // start_offset to end_offset caching up to GRADIENT_FP_STOPS stops at a
+ // time.
+ fn emit_segments(start_offset: f32, // start and end offset together are
+ end_offset: f32, // always a subrange of 0..1
+ gradient_offset_base: f32,
+ prim_start_offset: f32, // the offsets of the entire gradient as it
+ prim_end_offset: f32, // covers the entire primitive.
+ prim_origin_in: LayoutPoint,
+ prim_size_in: LayoutSize,
+ task_size: DeviceIntSize,
+ is_opaque: bool,
+ stops: &[GradientStopKey],
+ orientation: LineOrientation,
+ frame_state: &mut FrameBuildingState,
+ gradient: &mut LinearGradientPrimitive,
+ parent_surface_index: SurfaceIndex,
+ ) {
+ // these prints are used to generate documentation examples, so
+ // leaving them in but commented out:
+ //println!("emit_segments call:");
+ //println!("\tstart_offset: {}, end_offset: {}", start_offset, end_offset);
+ //println!("\tprim_start_offset: {}, prim_end_offset: {}", prim_start_offset, prim_end_offset);
+ //println!("\tgradient_offset_base: {}", gradient_offset_base);
+ let mut first_stop = 0;
+ // look for an inclusive range of stops [first_stop, last_stop].
+ // once first_stop points at (or past) the last stop, we're done.
+ while first_stop < stops.len()-1 {
+
+ // if the entire sub-gradient starts at an offset that's past the
+ // segment's end offset, we're done.
+ if stops[first_stop].offset > end_offset {
+ return;
+ }
+
+ // accumulate stops until we have GRADIENT_FP_STOPS of them, or we hit
+ // a hard stop:
+ let mut last_stop = first_stop;
+ let mut hard_stop = false; // did we stop on a hard stop?
+ while last_stop < stops.len()-1 &&
+ last_stop - first_stop + 1 < GRADIENT_FP_STOPS
+ {
+ if stops[last_stop+1].offset == stops[last_stop].offset {
+ hard_stop = true;
+ break;
+ }
+
+ last_stop = last_stop + 1;
+ }
+
+ let num_stops = last_stop - first_stop + 1;
+
+ // repeated hard stops at the same offset, skip
+ if num_stops == 0 {
+ first_stop = last_stop + 1;
+ continue;
+ }
+
+ // if the last_stop offset is before start_offset, the segment's not visible:
+ if stops[last_stop].offset < start_offset {
+ first_stop = if hard_stop { last_stop+1 } else { last_stop };
+ continue;
+ }
+
+ let segment_start_point = start_offset.max(stops[first_stop].offset);
+ let segment_end_point = end_offset .min(stops[last_stop ].offset);
+
+ let mut segment_stops = [GradientStopKey::empty(); GRADIENT_FP_STOPS];
+ for i in 0..num_stops {
+ segment_stops[i] = stops[first_stop + i];
+ }
+
+ let cache_key = GradientCacheKey {
+ orientation,
+ start_stop_point: VectorKey {
+ x: segment_start_point,
+ y: segment_end_point,
+ },
+ stops: segment_stops,
+ };
+
+ let mut prim_origin = prim_origin_in;
+ let mut prim_size = prim_size_in;
+
+ // the primitive is covered by a segment from overall_start to
+ // overall_end; scale and shift based on the length of the actual
+ // segment that we're drawing:
+ let inv_length = 1.0 / ( prim_end_offset - prim_start_offset );
+ if orientation == LineOrientation::Horizontal {
+ prim_origin.x += ( segment_start_point + gradient_offset_base - prim_start_offset )
+ * inv_length * prim_size.width;
+ prim_size.width *= ( segment_end_point - segment_start_point )
+ * inv_length; // 2 gradient_offset_bases cancel out
+ } else {
+ prim_origin.y += ( segment_start_point + gradient_offset_base - prim_start_offset )
+ * inv_length * prim_size.height;
+ prim_size.height *= ( segment_end_point - segment_start_point )
+ * inv_length; // 2 gradient_offset_bases cancel out
+ }
+
+ // <= 0 can happen if a hardstop lands exactly on an edge
+ if prim_size.area() > 0.0 {
+ let local_rect = LayoutRect::new( prim_origin, prim_size );
+
+ // documentation example traces:
+ //println!("\t\tcaching from offset {} to {}", segment_start_point, segment_end_point);
+ //println!("\t\tand blitting to {:?}", local_rect);
+
+ // Request the render task each frame.
+ gradient.cache_segments.push(
+ CachedGradientSegment {
+ handle: frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: task_size,
+ kind: RenderTaskCacheKeyKind::Gradient(cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.rg_builder,
+ None,
+ is_opaque,
+ RenderTaskParent::Surface(parent_surface_index),
+ frame_state.surfaces,
+ |rg_builder| {
+ rg_builder.add().init(RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::new_gradient(
+ segment_stops,
+ orientation,
+ segment_start_point,
+ segment_end_point,
+ ),
+ ))
+ }),
+ local_rect: local_rect,
+ }
+ );
+ }
+
+ // if ending on a hardstop, skip past it for the start of the next run:
+ first_stop = if hard_stop { last_stop + 1 } else { last_stop };
+ }
+ }
+
+ if prim_data.extend_mode == ExtendMode::Clamp ||
+ ( prim_start_offset >= 0.0 && prim_end_offset <= 1.0 ) // repeat doesn't matter
+ {
+ // To support clamping, we need to make sure that quads are emitted for the
+ // segments before and after the 0.0...1.0 range of offsets. emit_segments
+ // can handle that by duplicating the first and last point if necessary:
+ if prim_start_offset < 0.0 {
+ stops.insert(0, GradientStopKey {
+ offset: prim_start_offset,
+ color : stops[0].color
+ });
+ }
+
+ if prim_end_offset > 1.0 {
+ stops.push( GradientStopKey {
+ offset: prim_end_offset,
+ color : stops[stops.len()-1].color
+ });
+ }
+
+ emit_segments(prim_start_offset, prim_end_offset,
+ 0.0,
+ prim_start_offset, prim_end_offset,
+ prim_data.common.prim_rect.origin,
+ prim_data.common.prim_rect.size,
+ size,
+ prim_data.stops_opacity.is_opaque,
+ &stops,
+ orientation,
+ frame_state,
+ gradient,
+ pic_context.surface_index,
+ );
+ }
+ else
+ {
+ let mut segment_start_point = prim_start_offset;
+ while segment_start_point < prim_end_offset {
+
+ // gradient stops are expressed in the range 0.0 ... 1.0, so to blit
+ // a copy of the gradient, snap to the integer just before the offset
+ // we want ...
+ let gradient_offset_base = segment_start_point.floor();
+ // .. and then draw from a start offset in range 0 to 1 ...
+ let repeat_start = segment_start_point - gradient_offset_base;
+ // .. up to the next integer, but clamped to the primitive's real
+ // end offset:
+ let repeat_end = (gradient_offset_base + 1.0).min(prim_end_offset) - gradient_offset_base;
+
+ emit_segments(repeat_start, repeat_end,
+ gradient_offset_base,
+ prim_start_offset, prim_end_offset,
+ prim_data.common.prim_rect.origin,
+ prim_data.common.prim_rect.size,
+ size,
+ prim_data.stops_opacity.is_opaque,
+ &stops,
+ orientation,
+ frame_state,
+ gradient,
+ pic_context.surface_index,
+ );
+
+ segment_start_point = repeat_end + gradient_offset_base;
+ }
+ }
+ }
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ prim_data.common.may_need_repetition = false;
+
+ gradient.visible_tiles_range = decompose_repeated_primitive(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ &mut |_, mut request| {
+ request.push([
+ prim_data.start_point.x,
+ prim_data.start_point.y,
+ prim_data.end_point.x,
+ prim_data.end_point.y,
+ ]);
+ request.push([
+ pack_as_float(prim_data.extend_mode as u32),
+ prim_data.stretch_size.width,
+ prim_data.stretch_size.height,
+ 0.0,
+ ]);
+ }
+ );
+
+ if gradient.visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("RadialGradient");
+ let prim_data = &mut data_stores.radial_grad[*data_handle];
+
+ if prim_data.stretch_size.width >= prim_data.common.prim_rect.size.width &&
+ prim_data.stretch_size.height >= prim_data.common.prim_rect.size.height {
+
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ prim_data.common.may_need_repetition = false;
+ }
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state);
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_primitive(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ &mut |_, mut request| {
+ request.push([
+ prim_data.center.x,
+ prim_data.center.y,
+ prim_data.params.start_radius,
+ prim_data.params.end_radius,
+ ]);
+ request.push([
+ prim_data.params.ratio_xy,
+ pack_as_float(prim_data.extend_mode as u32),
+ prim_data.stretch_size.width,
+ prim_data.stretch_size.height,
+ ]);
+ },
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, ref mut visible_tiles_range, .. } => {
+ profile_scope!("ConicGradient");
+ let prim_data = &mut data_stores.conic_grad[*data_handle];
+
+ if prim_data.stretch_size.width >= prim_data.common.prim_rect.size.width &&
+ prim_data.stretch_size.height >= prim_data.common.prim_rect.size.height {
+
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ prim_data.common.may_need_repetition = false;
+ }
+
+ // Update the template this instane references, which may refresh the GPU
+ // cache with any shared template data.
+ prim_data.update(frame_state);
+
+ if prim_data.tile_spacing != LayoutSize::zero() {
+ prim_data.common.may_need_repetition = false;
+
+ *visible_tiles_range = decompose_repeated_primitive(
+ &prim_instance.vis,
+ &prim_data.common.prim_rect,
+ prim_spatial_node_index,
+ &prim_data.stretch_size,
+ &prim_data.tile_spacing,
+ frame_state,
+ &mut scratch.gradient_tiles,
+ &frame_context.spatial_tree,
+ &mut |_, mut request| {
+ request.push([
+ prim_data.center.x,
+ prim_data.center.y,
+ prim_data.params.start_offset,
+ prim_data.params.end_offset,
+ ]);
+ request.push([
+ prim_data.params.angle,
+ pack_as_float(prim_data.extend_mode as u32),
+ prim_data.stretch_size.width,
+ prim_data.stretch_size.height,
+ ]);
+ },
+ );
+
+ if visible_tiles_range.is_empty() {
+ prim_instance.clear_visibility();
+ }
+ }
+
+ // TODO(gw): Consider whether it's worth doing segment building
+ // for gradient primitives.
+ }
+ PrimitiveInstanceKind::Picture { pic_index, segment_instance_index, .. } => {
+ profile_scope!("Picture");
+ let pic = &mut store.pictures[pic_index.0];
+
+ if pic.prepare_for_render(
+ frame_context,
+ frame_state,
+ data_stores,
+ ) {
+ if let Some(ref mut splitter) = pic_state.plane_splitter {
+ PicturePrimitive::add_split_plane(
+ splitter,
+ frame_context.spatial_tree,
+ prim_spatial_node_index,
+ pic.precise_local_rect,
+ &prim_instance.vis.combined_local_clip_rect,
+ frame_state.current_dirty_region().combined,
+ plane_split_anchor,
+ );
+ }
+
+ // If this picture uses segments, ensure the GPU cache is
+ // up to date with segment local rects.
+ // TODO(gw): This entire match statement above can now be
+ // refactored into prepare_interned_prim_for_render.
+ if pic.can_use_segments() {
+ write_segment(
+ *segment_instance_index,
+ frame_state,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ |request| {
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ -1.0, // -ve means use prim rect for stretch size
+ 0.0,
+ 0.0,
+ 0.0,
+ ]);
+ }
+ );
+ }
+ } else {
+ prim_instance.clear_visibility();
+ }
+ }
+ PrimitiveInstanceKind::Backdrop { data_handle } => {
+ profile_scope!("Backdrop");
+ let backdrop_pic_index = data_stores.backdrop[*data_handle].kind.pic_index;
+
+ // Setup a dependency on the backdrop picture to ensure it is rendered prior to rendering this primitive.
+ let backdrop_surface_index = store.pictures[backdrop_pic_index.0].raster_config.as_ref().unwrap().surface_index;
+ if let Some(ref backdrop_tasks) = frame_state.surfaces[backdrop_surface_index.0].render_tasks {
+ // This is untidy / code duplication but matches existing behavior and will be
+ // removed in follow up patches to this bug to rework how backdrop-filter works.
+ let backdrop_task_id = match backdrop_tasks {
+ SurfaceRenderTasks::Tiled(..) => unreachable!(),
+ SurfaceRenderTasks::Simple(id) => *id,
+ SurfaceRenderTasks::Chained { port_task_id, .. } => *port_task_id,
+ };
+
+ frame_state.add_child_render_task(
+ pic_context.surface_index,
+ backdrop_task_id,
+ );
+ } else {
+ if prim_instance.is_chased() {
+ println!("\tBackdrop primitive culled because backdrop task was not assigned render tasks");
+ }
+ prim_instance.clear_visibility();
+ }
+ }
+ };
+
+ // If the primitive is opaque, see if it can contribut to it's picture surface's opaque rect.
+
+ is_opaque = is_opaque && {
+ let clip = prim_instance.vis.clip_task_index;
+ clip == ClipTaskIndex::INVALID
+ };
+
+ is_opaque = is_opaque && !frame_context.spatial_tree.is_relative_transform_complex(
+ prim_spatial_node_index,
+ pic_context.raster_spatial_node_index,
+ );
+
+ if is_opaque {
+ let prim_local_rect = data_stores.get_local_prim_rect(
+ prim_instance,
+ store,
+ );
+ cluster.opaque_rect = crate::util::conservative_union_rect(&cluster.opaque_rect, &prim_local_rect);
+ }
+}
+
+
+fn write_segment<F>(
+ segment_instance_index: SegmentInstanceIndex,
+ frame_state: &mut FrameBuildingState,
+ segments: &mut SegmentStorage,
+ segment_instances: &mut SegmentInstanceStorage,
+ f: F,
+) where F: Fn(&mut GpuDataRequest) {
+ debug_assert_ne!(segment_instance_index, SegmentInstanceIndex::INVALID);
+ if segment_instance_index != SegmentInstanceIndex::UNUSED {
+ let segment_instance = &mut segment_instances[segment_instance_index];
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut segment_instance.gpu_cache_handle) {
+ let segments = &segments[segment_instance.segments_range];
+
+ f(&mut request);
+
+ for segment in segments {
+ request.write_segment(
+ segment.local_rect,
+ [0.0; 4],
+ );
+ }
+ }
+ }
+}
+
+fn decompose_repeated_primitive(
+ prim_vis: &PrimitiveVisibility,
+ prim_local_rect: &LayoutRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ stretch_size: &LayoutSize,
+ tile_spacing: &LayoutSize,
+ frame_state: &mut FrameBuildingState,
+ gradient_tiles: &mut GradientTileStorage,
+ spatial_tree: &SpatialTree,
+ callback: &mut dyn FnMut(&LayoutRect, GpuDataRequest),
+) -> GradientTileRange {
+ let mut visible_tiles = Vec::new();
+
+ // Tighten the clip rect because decomposing the repeated image can
+ // produce primitives that are partially covering the original image
+ // rect and we want to clip these extra parts out.
+ let tight_clip_rect = prim_vis
+ .combined_local_clip_rect
+ .intersection(prim_local_rect).unwrap();
+
+ let visible_rect = compute_conservative_visible_rect(
+ &prim_vis.clip_chain,
+ frame_state.current_dirty_region().combined,
+ prim_spatial_node_index,
+ spatial_tree,
+ );
+ let stride = *stretch_size + *tile_spacing;
+
+ let repetitions = image_tiling::repetitions(prim_local_rect, &visible_rect, stride);
+ for Repetition { origin, .. } in repetitions {
+ let mut handle = GpuCacheHandle::new();
+ let rect = LayoutRect {
+ origin,
+ size: *stretch_size,
+ };
+
+ if let Some(request) = frame_state.gpu_cache.request(&mut handle) {
+ callback(&rect, request);
+ }
+
+ visible_tiles.push(VisibleGradientTile {
+ local_rect: rect,
+ local_clip_rect: tight_clip_rect,
+ handle
+ });
+ }
+
+ // At this point if we don't have tiles to show it means we could probably
+ // have done a better a job at culling during an earlier stage.
+ // Clearing the screen rect has the effect of "culling out" the primitive
+ // from the point of view of the batch builder, and ensures we don't hit
+ // assertions later on because we didn't request any image.
+ if visible_tiles.is_empty() {
+ GradientTileRange::empty()
+ } else {
+ gradient_tiles.extend(visible_tiles)
+ }
+}
+
+
+fn update_clip_task_for_brush(
+ instance: &PrimitiveInstance,
+ prim_origin: &LayoutPoint,
+ prim_spatial_node_index: SpatialNodeIndex,
+ root_spatial_node_index: SpatialNodeIndex,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &PrimitiveStore,
+ data_stores: &mut DataStores,
+ segments_store: &mut SegmentStorage,
+ segment_instances_store: &mut SegmentInstanceStorage,
+ clip_mask_instances: &mut Vec<ClipMaskKind>,
+ unclipped: &DeviceRect,
+ device_pixel_scale: DevicePixelScale,
+) -> Option<ClipTaskIndex> {
+ let segments = match instance.kind {
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::Clear { .. } |
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::Backdrop { .. } => {
+ return None;
+ }
+ PrimitiveInstanceKind::Image { image_instance_index, .. } => {
+ let segment_instance_index = prim_store
+ .images[image_instance_index]
+ .segment_instance_index;
+
+ if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::Picture { segment_instance_index, .. } => {
+ // Pictures may not support segment rendering at all (INVALID)
+ // or support segment rendering but choose not to due to size
+ // or some other factor (UNUSED).
+ if segment_instance_index == SegmentInstanceIndex::UNUSED ||
+ segment_instance_index == SegmentInstanceIndex::INVALID {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::YuvImage { segment_instance_index, .. } |
+ PrimitiveInstanceKind::Rectangle { segment_instance_index, .. } => {
+ debug_assert!(segment_instance_index != SegmentInstanceIndex::INVALID);
+
+ if segment_instance_index == SegmentInstanceIndex::UNUSED {
+ return None;
+ }
+
+ let segment_instance = &segment_instances_store[segment_instance_index];
+
+ &segments_store[segment_instance.segments_range]
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let border_data = &data_stores.image_border[data_handle].kind;
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ border_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ let border_data = &data_stores.normal_border[data_handle].kind;
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ border_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
+ let prim_data = &data_stores.linear_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let prim_data = &data_stores.radial_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let prim_data = &data_stores.conic_grad[data_handle];
+
+ // TODO: This is quite messy - once we remove legacy primitives we
+ // can change this to be a tuple match on (instance, template)
+ if prim_data.brush_segments.is_empty() {
+ return None;
+ }
+
+ prim_data.brush_segments.as_slice()
+ }
+ };
+
+ // If there are no segments, early out to avoid setting a valid
+ // clip task instance location below.
+ if segments.is_empty() {
+ return None;
+ }
+
+ // Set where in the clip mask instances array the clip mask info
+ // can be found for this primitive. Each segment will push the
+ // clip mask information for itself in update_clip_task below.
+ let clip_task_index = ClipTaskIndex(clip_mask_instances.len() as _);
+
+ // If we only built 1 segment, there is no point in re-running
+ // the clip chain builder. Instead, just use the clip chain
+ // instance that was built for the main primitive. This is a
+ // significant optimization for the common case.
+ if segments.len() == 1 {
+ let clip_mask_kind = update_brush_segment_clip_task(
+ &segments[0],
+ Some(&instance.vis.clip_chain),
+ frame_state.current_dirty_region().combined,
+ root_spatial_node_index,
+ pic_context.surface_index,
+ pic_state,
+ frame_context,
+ frame_state,
+ &mut data_stores.clip,
+ unclipped,
+ device_pixel_scale,
+ );
+ clip_mask_instances.push(clip_mask_kind);
+ } else {
+ let dirty_world_rect = frame_state.current_dirty_region().combined;
+
+ for segment in segments {
+ // Build a clip chain for the smaller segment rect. This will
+ // often manage to eliminate most/all clips, and sometimes
+ // clip the segment completely.
+ frame_state.clip_store.set_active_clips_from_clip_chain(
+ &instance.vis.clip_chain,
+ prim_spatial_node_index,
+ &frame_context.spatial_tree,
+ );
+
+ let segment_clip_chain = frame_state
+ .clip_store
+ .build_clip_chain_instance(
+ segment.local_rect.translate(prim_origin.to_vector()),
+ &pic_state.map_local_to_pic,
+ &pic_state.map_pic_to_world,
+ &frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ device_pixel_scale,
+ &dirty_world_rect,
+ &mut data_stores.clip,
+ false,
+ instance.is_chased(),
+ );
+
+ let clip_mask_kind = update_brush_segment_clip_task(
+ &segment,
+ segment_clip_chain.as_ref(),
+ frame_state.current_dirty_region().combined,
+ root_spatial_node_index,
+ pic_context.surface_index,
+ pic_state,
+ frame_context,
+ frame_state,
+ &mut data_stores.clip,
+ unclipped,
+ device_pixel_scale,
+ );
+ clip_mask_instances.push(clip_mask_kind);
+ }
+ }
+
+ Some(clip_task_index)
+}
+
+pub fn update_clip_task(
+ instance: &mut PrimitiveInstance,
+ prim_origin: &LayoutPoint,
+ prim_spatial_node_index: SpatialNodeIndex,
+ root_spatial_node_index: SpatialNodeIndex,
+ pic_context: &PictureContext,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &mut PrimitiveStore,
+ data_stores: &mut DataStores,
+ scratch: &mut PrimitiveScratchBuffer,
+) -> bool {
+ let device_pixel_scale = frame_state.surfaces[pic_context.surface_index.0].device_pixel_scale;
+
+ if instance.is_chased() {
+ println!("\tupdating clip task with pic rect {:?}", instance.vis.clip_chain.pic_clip_rect);
+ }
+
+ // Get the device space rect for the primitive if it was unclipped.
+ let unclipped = match get_unclipped_device_rect(
+ instance.vis.clip_chain.pic_clip_rect,
+ &pic_state.map_pic_to_raster,
+ device_pixel_scale,
+ ) {
+ Some(rect) => rect,
+ None => return false,
+ };
+
+ build_segments_if_needed(
+ instance,
+ frame_state,
+ prim_store,
+ data_stores,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ );
+
+ // First try to render this primitive's mask using optimized brush rendering.
+ instance.vis.clip_task_index = if let Some(clip_task_index) = update_clip_task_for_brush(
+ instance,
+ prim_origin,
+ prim_spatial_node_index,
+ root_spatial_node_index,
+ pic_context,
+ pic_state,
+ frame_context,
+ frame_state,
+ prim_store,
+ data_stores,
+ &mut scratch.segments,
+ &mut scratch.segment_instances,
+ &mut scratch.clip_mask_instances,
+ &unclipped,
+ device_pixel_scale,
+ ) {
+ if instance.is_chased() {
+ println!("\tsegment tasks have been created for clipping: {:?}", clip_task_index);
+ }
+ clip_task_index
+ } else if instance.vis.clip_chain.needs_mask {
+ // Get a minimal device space rect, clipped to the screen that we
+ // need to allocate for the clip mask, as well as interpolated
+ // snap offsets.
+ let unadjusted_device_rect = match get_clipped_device_rect(
+ &unclipped,
+ &pic_state.map_raster_to_world,
+ frame_state.current_dirty_region().combined,
+ device_pixel_scale,
+ ) {
+ Some(device_rect) => device_rect,
+ None => return false,
+ };
+
+ let (device_rect, device_pixel_scale) = adjust_mask_scale_for_max_size(
+ unadjusted_device_rect,
+ device_pixel_scale,
+ );
+ let clip_task_id = RenderTaskKind::new_mask(
+ device_rect,
+ instance.vis.clip_chain.clips_range,
+ root_spatial_node_index,
+ frame_state.clip_store,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ frame_state.rg_builder,
+ &mut data_stores.clip,
+ device_pixel_scale,
+ frame_context.fb_config,
+ frame_state.surfaces,
+ );
+ if instance.is_chased() {
+ println!("\tcreated task {:?} with device rect {:?}",
+ clip_task_id, device_rect);
+ }
+ // Set the global clip mask instance for this primitive.
+ let clip_task_index = ClipTaskIndex(scratch.clip_mask_instances.len() as _);
+ scratch.clip_mask_instances.push(ClipMaskKind::Mask(clip_task_id));
+ instance.vis.clip_task_index = clip_task_index;
+ frame_state.add_child_render_task(
+ pic_context.surface_index,
+ clip_task_id,
+ );
+ clip_task_index
+ } else {
+ if instance.is_chased() {
+ println!("\tno mask is needed");
+ }
+ ClipTaskIndex::INVALID
+ };
+
+ true
+}
+
+/// Write out to the clip mask instances array the correct clip mask
+/// config for this segment.
+pub fn update_brush_segment_clip_task(
+ segment: &BrushSegment,
+ clip_chain: Option<&ClipChainInstance>,
+ world_clip_rect: WorldRect,
+ root_spatial_node_index: SpatialNodeIndex,
+ surface_index: SurfaceIndex,
+ pic_state: &mut PictureState,
+ frame_context: &FrameBuildingContext,
+ frame_state: &mut FrameBuildingState,
+ clip_data_store: &mut ClipDataStore,
+ unclipped: &DeviceRect,
+ device_pixel_scale: DevicePixelScale,
+) -> ClipMaskKind {
+ let clip_chain = match clip_chain {
+ Some(chain) => chain,
+ None => return ClipMaskKind::Clipped,
+ };
+ if !clip_chain.needs_mask ||
+ (!segment.may_need_clip_mask && !clip_chain.has_non_local_clips) {
+ return ClipMaskKind::None;
+ }
+
+ let segment_world_rect = match pic_state.map_pic_to_world.map(&clip_chain.pic_clip_rect) {
+ Some(rect) => rect,
+ None => return ClipMaskKind::Clipped,
+ };
+
+ let segment_world_rect = match segment_world_rect.intersection(&world_clip_rect) {
+ Some(rect) => rect,
+ None => return ClipMaskKind::Clipped,
+ };
+
+ // Get a minimal device space rect, clipped to the screen that we
+ // need to allocate for the clip mask, as well as interpolated
+ // snap offsets.
+ let device_rect = match get_clipped_device_rect(
+ unclipped,
+ &pic_state.map_raster_to_world,
+ segment_world_rect,
+ device_pixel_scale,
+ ) {
+ Some(info) => info,
+ None => {
+ return ClipMaskKind::Clipped;
+ }
+ };
+
+ let (device_rect, device_pixel_scale) = adjust_mask_scale_for_max_size(device_rect, device_pixel_scale);
+
+ let clip_task_id = RenderTaskKind::new_mask(
+ device_rect,
+ clip_chain.clips_range,
+ root_spatial_node_index,
+ frame_state.clip_store,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ frame_state.rg_builder,
+ clip_data_store,
+ device_pixel_scale,
+ frame_context.fb_config,
+ frame_state.surfaces,
+ );
+
+ frame_state.add_child_render_task(
+ surface_index,
+ clip_task_id,
+ );
+ ClipMaskKind::Mask(clip_task_id)
+}
+
+
+fn write_brush_segment_description(
+ prim_local_rect: LayoutRect,
+ prim_local_clip_rect: LayoutRect,
+ clip_chain: &ClipChainInstance,
+ segment_builder: &mut SegmentBuilder,
+ clip_store: &ClipStore,
+ data_stores: &DataStores,
+) -> bool {
+ // If the brush is small, we want to skip building segments
+ // and just draw it as a single primitive with clip mask.
+ if prim_local_rect.size.area() < MIN_BRUSH_SPLIT_AREA {
+ return false;
+ }
+
+ segment_builder.initialize(
+ prim_local_rect,
+ None,
+ prim_local_clip_rect
+ );
+
+ // Segment the primitive on all the local-space clip sources that we can.
+ for i in 0 .. clip_chain.clips_range.count {
+ let clip_instance = clip_store
+ .get_instance_from_range(&clip_chain.clips_range, i);
+ let clip_node = &data_stores.clip[clip_instance.handle];
+
+ // If this clip item is positioned by another positioning node, its relative position
+ // could change during scrolling. This means that we would need to resegment. Instead
+ // of doing that, only segment with clips that have the same positioning node.
+ // TODO(mrobinson, #2858): It may make sense to include these nodes, resegmenting only
+ // when necessary while scrolling.
+ if !clip_instance.flags.contains(ClipNodeFlags::SAME_SPATIAL_NODE) {
+ continue;
+ }
+
+ let (local_clip_rect, radius, mode) = match clip_node.item.kind {
+ ClipItemKind::RoundedRectangle { rect, radius, mode } => {
+ (rect, Some(radius), mode)
+ }
+ ClipItemKind::Rectangle { rect, mode } => {
+ (rect, None, mode)
+ }
+ ClipItemKind::BoxShadow { ref source } => {
+ // For inset box shadows, we can clip out any
+ // pixels that are inside the shadow region
+ // and are beyond the inner rect, as they can't
+ // be affected by the blur radius.
+ let inner_clip_mode = match source.clip_mode {
+ BoxShadowClipMode::Outset => None,
+ BoxShadowClipMode::Inset => Some(ClipMode::ClipOut),
+ };
+
+ // Push a region into the segment builder where the
+ // box-shadow can have an effect on the result. This
+ // ensures clip-mask tasks get allocated for these
+ // pixel regions, even if no other clips affect them.
+ segment_builder.push_mask_region(
+ source.prim_shadow_rect,
+ source.prim_shadow_rect.inflate(
+ -0.5 * source.original_alloc_size.width,
+ -0.5 * source.original_alloc_size.height,
+ ),
+ inner_clip_mode,
+ );
+
+ continue;
+ }
+ ClipItemKind::Image { .. } => {
+ // If we encounter an image mask, bail out from segment building.
+ // It's not possible to know which parts of the primitive are affected
+ // by the mask (without inspecting the pixels). We could do something
+ // better here in the future if it ever shows up as a performance issue
+ // (for instance, at least segment based on the bounding rect of the
+ // image mask if it's non-repeating).
+ return false;
+ }
+ };
+
+ segment_builder.push_clip_rect(local_clip_rect, radius, mode);
+ }
+
+ true
+}
+
+fn build_segments_if_needed(
+ instance: &mut PrimitiveInstance,
+ frame_state: &mut FrameBuildingState,
+ prim_store: &mut PrimitiveStore,
+ data_stores: &DataStores,
+ segments_store: &mut SegmentStorage,
+ segment_instances_store: &mut SegmentInstanceStorage,
+) {
+ let prim_clip_chain = &instance.vis.clip_chain;
+
+ // Usually, the primitive rect can be found from information
+ // in the instance and primitive template.
+ let prim_local_rect = data_stores.get_local_prim_rect(
+ instance,
+ prim_store,
+ );
+
+ let segment_instance_index = match instance.kind {
+ PrimitiveInstanceKind::Rectangle { ref mut segment_instance_index, .. } |
+ PrimitiveInstanceKind::YuvImage { ref mut segment_instance_index, .. } => {
+ segment_instance_index
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+ let image_data = &data_stores.image[data_handle].kind;
+ let image_instance = &mut prim_store.images[image_instance_index];
+ //Note: tiled images don't support automatic segmentation,
+ // they strictly produce one segment per visible tile instead.
+ if frame_state
+ .resource_cache
+ .get_image_properties(image_data.key)
+ .and_then(|properties| properties.tiling)
+ .is_some()
+ {
+ image_instance.segment_instance_index = SegmentInstanceIndex::UNUSED;
+ return;
+ }
+ &mut image_instance.segment_instance_index
+ }
+ PrimitiveInstanceKind::Picture { ref mut segment_instance_index, pic_index, .. } => {
+ let pic = &mut prim_store.pictures[pic_index.0];
+
+ // If this picture supports segment rendering
+ if pic.can_use_segments() {
+ // If the segments have been invalidated, ensure the current
+ // index of segments is invalid. This ensures that the segment
+ // building logic below will be run.
+ if !pic.segments_are_valid {
+ *segment_instance_index = SegmentInstanceIndex::INVALID;
+ pic.segments_are_valid = true;
+ }
+
+ segment_instance_index
+ } else {
+ return;
+ }
+ }
+ PrimitiveInstanceKind::TextRun { .. } |
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::ImageBorder { .. } |
+ PrimitiveInstanceKind::Clear { .. } |
+ PrimitiveInstanceKind::LinearGradient { .. } |
+ PrimitiveInstanceKind::RadialGradient { .. } |
+ PrimitiveInstanceKind::ConicGradient { .. } |
+ PrimitiveInstanceKind::LineDecoration { .. } |
+ PrimitiveInstanceKind::Backdrop { .. } => {
+ // These primitives don't support / need segments.
+ return;
+ }
+ };
+
+ if *segment_instance_index == SegmentInstanceIndex::INVALID {
+ let mut segments: SmallVec<[BrushSegment; 8]> = SmallVec::new();
+
+ if write_brush_segment_description(
+ prim_local_rect,
+ instance.clip_set.local_clip_rect,
+ prim_clip_chain,
+ &mut frame_state.segment_builder,
+ frame_state.clip_store,
+ data_stores,
+ ) {
+ frame_state.segment_builder.build(|segment| {
+ segments.push(
+ BrushSegment::new(
+ segment.rect.translate(-prim_local_rect.origin.to_vector()),
+ segment.has_mask,
+ segment.edge_flags,
+ [0.0; 4],
+ BrushFlags::PERSPECTIVE_INTERPOLATION,
+ ),
+ );
+ });
+ }
+
+ // If only a single segment is produced, there is no benefit to writing
+ // a segment instance array. Instead, just use the main primitive rect
+ // written into the GPU cache.
+ // TODO(gw): This is (sortof) a bandaid - due to a limitation in the current
+ // brush encoding, we can only support a total of up to 2^16 segments.
+ // This should be (more than) enough for any real world case, so for
+ // now we can handle this by skipping cases where we were generating
+ // segments where there is no benefit. The long term / robust fix
+ // for this is to move the segment building to be done as a more
+ // limited nine-patch system during scene building, removing arbitrary
+ // segmentation during frame-building (see bug #1617491).
+ if segments.len() <= 1 {
+ *segment_instance_index = SegmentInstanceIndex::UNUSED;
+ } else {
+ let segments_range = segments_store.extend(segments);
+
+ let instance = SegmentedInstance {
+ segments_range,
+ gpu_cache_handle: GpuCacheHandle::new(),
+ };
+
+ *segment_instance_index = segment_instances_store.push(instance);
+ };
+ }
+}
+
+/// Retrieve the exact unsnapped device space rectangle for a primitive.
+fn get_unclipped_device_rect(
+ prim_rect: PictureRect,
+ map_to_raster: &SpaceMapper<PicturePixel, RasterPixel>,
+ device_pixel_scale: DevicePixelScale,
+) -> Option<DeviceRect> {
+ let raster_rect = map_to_raster.map(&prim_rect)?;
+ let world_rect = raster_rect * Scale::new(1.0);
+ Some(world_rect * device_pixel_scale)
+}
+
+/// Given an unclipped device rect, try to find a minimal device space
+/// rect to allocate a clip mask for, by clipping to the screen. This
+/// function is very similar to picture::get_raster_rects. It is far from
+/// ideal, and should be refactored as part of the support for setting
+/// scale per-raster-root.
+fn get_clipped_device_rect(
+ unclipped: &DeviceRect,
+ map_to_world: &SpaceMapper<RasterPixel, WorldPixel>,
+ world_clip_rect: WorldRect,
+ device_pixel_scale: DevicePixelScale,
+) -> Option<DeviceRect> {
+ let unclipped_raster_rect = {
+ let world_rect = *unclipped * Scale::new(1.0);
+ let raster_rect = world_rect * device_pixel_scale.inverse();
+
+ raster_rect.cast_unit()
+ };
+
+ let unclipped_world_rect = map_to_world.map(&unclipped_raster_rect)?;
+
+ let clipped_world_rect = unclipped_world_rect.intersection(&world_clip_rect)?;
+
+ let clipped_raster_rect = map_to_world.unmap(&clipped_world_rect)?;
+
+ let clipped_raster_rect = clipped_raster_rect.intersection(&unclipped_raster_rect)?;
+
+ // Ensure that we won't try to allocate a zero-sized clip render task.
+ if clipped_raster_rect.is_empty() {
+ return None;
+ }
+
+ let clipped = raster_rect_to_device_pixels(
+ clipped_raster_rect,
+ device_pixel_scale,
+ );
+
+ Some(clipped)
+}
+
+// Ensures that the size of mask render tasks are within MAX_MASK_SIZE.
+fn adjust_mask_scale_for_max_size(device_rect: DeviceRect, device_pixel_scale: DevicePixelScale) -> (DeviceRect, DevicePixelScale) {
+ if device_rect.width() > MAX_MASK_SIZE || device_rect.height() > MAX_MASK_SIZE {
+ // round_out will grow by 1 integer pixel if origin is on a
+ // fractional position, so keep that margin for error with -1:
+ let scale = (MAX_MASK_SIZE - 1.0) /
+ f32::max(device_rect.width(), device_rect.height());
+ let new_device_pixel_scale = device_pixel_scale * Scale::new(scale);
+ let new_device_rect = (device_rect.to_f32() * Scale::new(scale))
+ .round_out();
+ (new_device_rect, new_device_pixel_scale)
+ } else {
+ (device_rect, device_pixel_scale)
+ }
+}
diff --git a/gfx/wr/webrender/src/prim_store/backdrop.rs b/gfx/wr/webrender/src/prim_store/backdrop.rs
new file mode 100644
index 0000000000..c45bf78eef
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/backdrop.rs
@@ -0,0 +1,98 @@
+/* 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 api::units::*;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{
+ InternablePrimitive, PictureIndex, PrimitiveInstanceKind, PrimKey, PrimTemplate,
+ PrimTemplateCommonData, PrimitiveStore, RectangleKey,
+};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, MallocSizeOf, Hash)]
+pub struct Backdrop {
+ pub pic_index: PictureIndex,
+ pub spatial_node_index: SpatialNodeIndex,
+ pub border_rect: RectangleKey,
+}
+
+impl From<Backdrop> for BackdropData {
+ fn from(backdrop: Backdrop) -> Self {
+ BackdropData {
+ pic_index: backdrop.pic_index,
+ spatial_node_index: backdrop.spatial_node_index,
+ border_rect: backdrop.border_rect.into(),
+ }
+ }
+}
+
+pub type BackdropKey = PrimKey<Backdrop>;
+
+impl BackdropKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ backdrop: Backdrop,
+ ) -> Self {
+ BackdropKey {
+ common: info.into(),
+ kind: backdrop,
+ }
+ }
+}
+
+impl InternDebug for BackdropKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct BackdropData {
+ pub pic_index: PictureIndex,
+ pub spatial_node_index: SpatialNodeIndex,
+ pub border_rect: LayoutRect,
+}
+
+pub type BackdropTemplate = PrimTemplate<BackdropData>;
+
+impl From<BackdropKey> for BackdropTemplate {
+ fn from(backdrop: BackdropKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(backdrop.common);
+
+ BackdropTemplate {
+ common,
+ kind: backdrop.kind.into(),
+ }
+ }
+}
+
+pub type BackdropDataHandle = InternHandle<Backdrop>;
+
+impl Internable for Backdrop {
+ type Key = BackdropKey;
+ type StoreData = BackdropTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_BACKDROPS;
+}
+
+impl InternablePrimitive for Backdrop {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> BackdropKey {
+ BackdropKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: BackdropKey,
+ data_handle: BackdropDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::Backdrop {
+ data_handle,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/prim_store/borders.rs b/gfx/wr/webrender/src/prim_store/borders.rs
new file mode 100644
index 0000000000..561f4a8ada
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/borders.rs
@@ -0,0 +1,376 @@
+/* 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 api::{NormalBorder, PremultipliedColorF, Shadow, RasterSpace};
+use api::units::*;
+use crate::border::create_border_segments;
+use crate::border::NormalBorderAu;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::{FrameBuildingState};
+use crate::gpu_cache::{GpuCache, GpuDataRequest};
+use crate::intern;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{
+ BorderSegmentInfo, BrushSegment, NinePatchDescriptor, PrimKey,
+ PrimTemplate, PrimTemplateCommonData,
+ PrimitiveInstanceKind, PrimitiveOpacity,
+ PrimitiveStore, InternablePrimitive,
+};
+use crate::resource_cache::{ImageRequest, ResourceCache};
+
+use super::storage;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct NormalBorderPrim {
+ pub border: NormalBorderAu,
+ pub widths: LayoutSideOffsetsAu,
+}
+
+pub type NormalBorderKey = PrimKey<NormalBorderPrim>;
+
+impl NormalBorderKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ normal_border: NormalBorderPrim,
+ ) -> Self {
+ NormalBorderKey {
+ common: info.into(),
+ kind: normal_border,
+ }
+ }
+}
+
+impl intern::InternDebug for NormalBorderKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct NormalBorderData {
+ pub brush_segments: Vec<BrushSegment>,
+ pub border_segments: Vec<BorderSegmentInfo>,
+ pub border: NormalBorder,
+ pub widths: LayoutSideOffsets,
+}
+
+impl NormalBorderData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request, common.prim_rect.size);
+ self.write_segment_gpu_blocks(request);
+ }
+
+ common.opacity = PrimitiveOpacity::translucent();
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ prim_size: LayoutSize
+ ) {
+ // Border primitives currently used for
+ // image borders, and run through the
+ // normal brush_image shader.
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ prim_size.width,
+ prim_size.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ fn write_segment_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ ) {
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+}
+
+pub type NormalBorderTemplate = PrimTemplate<NormalBorderData>;
+
+impl From<NormalBorderKey> for NormalBorderTemplate {
+ fn from(key: NormalBorderKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(key.common);
+
+ let mut border: NormalBorder = key.kind.border.into();
+ let widths = LayoutSideOffsets::from_au(key.kind.widths);
+
+ // FIXME(emilio): Is this the best place to do this?
+ border.normalize(&widths);
+
+ let mut brush_segments = Vec::new();
+ let mut border_segments = Vec::new();
+
+ create_border_segments(
+ common.prim_rect.size,
+ &border,
+ &widths,
+ &mut border_segments,
+ &mut brush_segments,
+ );
+
+ NormalBorderTemplate {
+ common,
+ kind: NormalBorderData {
+ brush_segments,
+ border_segments,
+ border,
+ widths,
+ }
+ }
+ }
+}
+
+pub type NormalBorderDataHandle = intern::Handle<NormalBorderPrim>;
+
+impl intern::Internable for NormalBorderPrim {
+ type Key = NormalBorderKey;
+ type StoreData = NormalBorderTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_NORMAL_BORDERS;
+}
+
+impl InternablePrimitive for NormalBorderPrim {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> NormalBorderKey {
+ NormalBorderKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: NormalBorderKey,
+ data_handle: NormalBorderDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::NormalBorder {
+ data_handle,
+ cache_handles: storage::Range::empty(),
+ }
+ }
+}
+
+impl CreateShadow for NormalBorderPrim {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ let border = self.border.with_color(shadow.color.into());
+ NormalBorderPrim {
+ border,
+ widths: self.widths,
+ }
+ }
+}
+
+impl IsVisible for NormalBorderPrim {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct ImageBorder {
+ #[ignore_malloc_size_of = "Arc"]
+ pub request: ImageRequest,
+ pub nine_patch: NinePatchDescriptor,
+}
+
+pub type ImageBorderKey = PrimKey<ImageBorder>;
+
+impl ImageBorderKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ image_border: ImageBorder,
+ ) -> Self {
+ ImageBorderKey {
+ common: info.into(),
+ kind: image_border,
+ }
+ }
+}
+
+impl intern::InternDebug for ImageBorderKey {}
+
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ImageBorderData {
+ #[ignore_malloc_size_of = "Arc"]
+ pub request: ImageRequest,
+ pub brush_segments: Vec<BrushSegment>,
+}
+
+impl ImageBorderData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request, &common.prim_rect.size);
+ self.write_segment_gpu_blocks(request);
+ }
+
+ let image_properties = frame_state
+ .resource_cache
+ .get_image_properties(self.request.key);
+
+ common.opacity = if let Some(image_properties) = image_properties {
+ PrimitiveOpacity {
+ is_opaque: image_properties.descriptor.is_opaque(),
+ }
+ } else {
+ PrimitiveOpacity::opaque()
+ }
+ }
+
+ pub fn request_resources(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ ) {
+ resource_cache.request_image(
+ self.request,
+ gpu_cache,
+ );
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ prim_size: &LayoutSize,
+ ) {
+ // Border primitives currently used for
+ // image borders, and run through the
+ // normal brush_image shader.
+ request.push(PremultipliedColorF::WHITE);
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ prim_size.width,
+ prim_size.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+
+ fn write_segment_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ ) {
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+}
+
+pub type ImageBorderTemplate = PrimTemplate<ImageBorderData>;
+
+impl From<ImageBorderKey> for ImageBorderTemplate {
+ fn from(key: ImageBorderKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(key.common);
+
+ let brush_segments = key.kind.nine_patch.create_segments(common.prim_rect.size);
+ ImageBorderTemplate {
+ common,
+ kind: ImageBorderData {
+ request: key.kind.request,
+ brush_segments,
+ }
+ }
+ }
+}
+
+pub type ImageBorderDataHandle = intern::Handle<ImageBorder>;
+
+impl intern::Internable for ImageBorder {
+ type Key = ImageBorderKey;
+ type StoreData = ImageBorderTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_IMAGE_BORDERS;
+}
+
+impl InternablePrimitive for ImageBorder {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ImageBorderKey {
+ ImageBorderKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: ImageBorderKey,
+ data_handle: ImageBorderDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::ImageBorder {
+ data_handle
+ }
+ }
+}
+
+impl IsVisible for ImageBorder {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<NormalBorderPrim>(), 84, "NormalBorderPrim size changed");
+ assert_eq!(mem::size_of::<NormalBorderTemplate>(), 216, "NormalBorderTemplate size changed");
+ assert_eq!(mem::size_of::<NormalBorderKey>(), 104, "NormalBorderKey size changed");
+ assert_eq!(mem::size_of::<ImageBorder>(), 84, "ImageBorder size changed");
+ assert_eq!(mem::size_of::<ImageBorderTemplate>(), 80, "ImageBorderTemplate size changed");
+ assert_eq!(mem::size_of::<ImageBorderKey>(), 104, "ImageBorderKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/gradient.rs b/gfx/wr/webrender/src/prim_store/gradient.rs
new file mode 100644
index 0000000000..da623cf212
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/gradient.rs
@@ -0,0 +1,1031 @@
+/* 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 api::{
+ ColorF, ColorU, ExtendMode, GradientStop,
+ PremultipliedColorF, LineOrientation,
+};
+use api::units::{LayoutPoint, LayoutRect, LayoutSize, LayoutVector2D};
+use crate::scene_building::IsVisible;
+use euclid::approxeq::ApproxEq;
+use crate::frame_builder::FrameBuildingState;
+use crate::gpu_cache::{GpuCacheHandle, GpuDataRequest};
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{BrushSegment, CachedGradientSegment, GradientTileRange, VectorKey};
+use crate::prim_store::{PrimitiveInstanceKind, PrimitiveOpacity};
+use crate::prim_store::{PrimKeyCommonData, PrimTemplateCommonData, PrimitiveStore};
+use crate::prim_store::{NinePatchDescriptor, PointKey, SizeKey, InternablePrimitive};
+use std::{hash, ops::{Deref, DerefMut}};
+use crate::util::pack_as_float;
+use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
+
+/// The maximum number of stops a gradient may have to use the fast path.
+pub const GRADIENT_FP_STOPS: usize = 4;
+
+/// A hashable gradient stop that can be used in primitive keys.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+pub struct GradientStopKey {
+ pub offset: f32,
+ pub color: ColorU,
+}
+
+impl GradientStopKey {
+ pub fn empty() -> Self {
+ GradientStopKey {
+ offset: 0.0,
+ color: ColorU::new(0, 0, 0, 0),
+ }
+ }
+}
+
+impl Into<GradientStopKey> for GradientStop {
+ fn into(self) -> GradientStopKey {
+ GradientStopKey {
+ offset: self.offset,
+ color: self.color.into(),
+ }
+ }
+}
+
+// Convert `stop_keys` into a vector of `GradientStop`s, which is a more
+// convenient representation for the current gradient builder. Compute the
+// minimum stop alpha along the way.
+fn stops_and_min_alpha(stop_keys: &[GradientStopKey]) -> (Vec<GradientStop>, f32) {
+ let mut min_alpha: f32 = 1.0;
+ let stops = stop_keys.iter().map(|stop_key| {
+ let color: ColorF = stop_key.color.into();
+ min_alpha = min_alpha.min(color.a);
+
+ GradientStop {
+ offset: stop_key.offset,
+ color,
+ }
+ }).collect();
+
+ (stops, min_alpha)
+}
+
+impl Eq for GradientStopKey {}
+
+impl hash::Hash for GradientStopKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.offset.to_bits().hash(state);
+ self.color.hash(state);
+ }
+}
+
+/// Identifying key for a linear gradient.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct LinearGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub start_point: PointKey,
+ pub end_point: PointKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub reverse_stops: bool,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl LinearGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ linear_grad: LinearGradient,
+ ) -> Self {
+ LinearGradientKey {
+ common: info.into(),
+ extend_mode: linear_grad.extend_mode,
+ start_point: linear_grad.start_point,
+ end_point: linear_grad.end_point,
+ stretch_size: linear_grad.stretch_size,
+ tile_spacing: linear_grad.tile_spacing,
+ stops: linear_grad.stops,
+ reverse_stops: linear_grad.reverse_stops,
+ nine_patch: linear_grad.nine_patch,
+ }
+ }
+}
+
+impl InternDebug for LinearGradientKey {}
+
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GradientCacheKey {
+ pub orientation: LineOrientation,
+ pub start_stop_point: VectorKey,
+ pub stops: [GradientStopKey; GRADIENT_FP_STOPS],
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct LinearGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub start_point: LayoutPoint,
+ pub end_point: LayoutPoint,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub brush_segments: Vec<BrushSegment>,
+ pub reverse_stops: bool,
+ pub stops_handle: GpuCacheHandle,
+ /// If true, this gradient can be drawn via the fast path
+ /// (cache gradient, and draw as image).
+ pub supports_caching: bool,
+}
+
+impl Deref for LinearGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for LinearGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<LinearGradientKey> for LinearGradientTemplate {
+ fn from(item: LinearGradientKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+
+ // Check if we can draw this gradient via a fast path by caching the
+ // gradient in a smaller task, and drawing as an image.
+ // TODO(gw): Aim to reduce the constraints on fast path gradients in future,
+ // although this catches the vast majority of gradients on real pages.
+ let mut supports_caching =
+ // Gradient must cover entire primitive
+ item.tile_spacing.w + item.stretch_size.w >= common.prim_rect.size.width &&
+ item.tile_spacing.h + item.stretch_size.h >= common.prim_rect.size.height &&
+ // Must be a vertical or horizontal gradient
+ (item.start_point.x.approx_eq(&item.end_point.x) ||
+ item.start_point.y.approx_eq(&item.end_point.y)) &&
+ // Fast path not supported on segmented (border-image) gradients.
+ item.nine_patch.is_none();
+
+ // if we support caching and the gradient uses repeat, we might potentially
+ // emit a lot of quads to cover the primitive. each quad will still cover
+ // the entire gradient along the other axis, so the effect is linear in
+ // display resolution, not quadratic (unlike say a tiny background image
+ // tiling the display). in addition, excessive minification may lead to
+ // texture trashing. so use the minification as a proxy heuristic for both
+ // cases.
+ //
+ // note that the actual number of quads may be further increased due to
+ // hard-stops and/or more than GRADIENT_FP_STOPS stops per gradient.
+ if supports_caching && item.extend_mode == ExtendMode::Repeat {
+ let single_repeat_size =
+ if item.start_point.x.approx_eq(&item.end_point.x) {
+ item.end_point.y - item.start_point.y
+ } else {
+ item.end_point.x - item.start_point.x
+ };
+ let downscaling = single_repeat_size as f32 / TEXTURE_REGION_DIMENSIONS as f32;
+ if downscaling < 0.1 {
+ // if a single copy of the gradient is this small relative to its baked
+ // gradient cache, we have bad texture caching and/or too many quads.
+ supports_caching = false;
+ }
+ }
+
+ let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size);
+ }
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ LinearGradientTemplate {
+ common,
+ extend_mode: item.extend_mode,
+ start_point: item.start_point.into(),
+ end_point: item.end_point.into(),
+ stretch_size: item.stretch_size.into(),
+ tile_spacing: item.tile_spacing.into(),
+ stops_opacity,
+ stops,
+ brush_segments,
+ reverse_stops: item.reverse_stops,
+ stops_handle: GpuCacheHandle::new(),
+ supports_caching,
+ }
+ }
+}
+
+fn get_gradient_opacity(
+ prim_rect: LayoutRect,
+ stretch_size: LayoutSize,
+ tile_spacing: LayoutSize,
+ stops_opacity: PrimitiveOpacity,
+) -> PrimitiveOpacity {
+ // If the coverage of the gradient extends to or beyond
+ // the primitive rect, then the opacity can be determined
+ // by the colors of the stops. If we have tiling / spacing
+ // then we just assume the gradient is translucent for now.
+ // (In the future we could consider segmenting in some cases).
+ let stride = stretch_size + tile_spacing;
+ if stride.width >= prim_rect.size.width && stride.height >= prim_rect.size.height {
+ stops_opacity
+ } else {
+ PrimitiveOpacity::translucent()
+ }
+}
+
+impl LinearGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(mut request) =
+ frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ // write_prim_gpu_blocks
+ request.push([
+ self.start_point.x,
+ self.start_point.y,
+ self.end_point.x,
+ self.end_point.y,
+ ]);
+ request.push([
+ pack_as_float(self.extend_mode as u32),
+ self.stretch_size.width,
+ self.stretch_size.height,
+ 0.0,
+ ]);
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.stops_handle) {
+ GradientGpuBlockBuilder::build(
+ self.reverse_stops,
+ &mut request,
+ &self.stops,
+ );
+ }
+
+ self.opacity = get_gradient_opacity(
+ self.common.prim_rect,
+ self.stretch_size,
+ self.tile_spacing,
+ self.stops_opacity,
+ );
+ }
+}
+
+pub type LinearGradientDataHandle = InternHandle<LinearGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LinearGradient {
+ pub extend_mode: ExtendMode,
+ pub start_point: PointKey,
+ pub end_point: PointKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub reverse_stops: bool,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl Internable for LinearGradient {
+ type Key = LinearGradientKey;
+ type StoreData = LinearGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINEAR_GRADIENTS;
+}
+
+impl InternablePrimitive for LinearGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> LinearGradientKey {
+ LinearGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: LinearGradientKey,
+ data_handle: LinearGradientDataHandle,
+ prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ let gradient_index = prim_store.linear_gradients.push(LinearGradientPrimitive {
+ cache_segments: Vec::new(),
+ visible_tiles_range: GradientTileRange::empty(),
+ });
+
+ PrimitiveInstanceKind::LinearGradient {
+ data_handle,
+ gradient_index,
+ }
+ }
+}
+
+impl IsVisible for LinearGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct LinearGradientPrimitive {
+ pub cache_segments: Vec<CachedGradientSegment>,
+ pub visible_tiles_range: GradientTileRange,
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Hashable radial gradient parameters, for use during prim interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct RadialGradientParams {
+ pub start_radius: f32,
+ pub end_radius: f32,
+ pub ratio_xy: f32,
+}
+
+impl Eq for RadialGradientParams {}
+
+impl hash::Hash for RadialGradientParams {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.start_radius.to_bits().hash(state);
+ self.end_radius.to_bits().hash(state);
+ self.ratio_xy.to_bits().hash(state);
+ }
+}
+
+/// Identifying key for a radial gradient.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct RadialGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: RadialGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl RadialGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ radial_grad: RadialGradient,
+ ) -> Self {
+ RadialGradientKey {
+ common: info.into(),
+ extend_mode: radial_grad.extend_mode,
+ center: radial_grad.center,
+ params: radial_grad.params,
+ stretch_size: radial_grad.stretch_size,
+ stops: radial_grad.stops,
+ tile_spacing: radial_grad.tile_spacing,
+ nine_patch: radial_grad.nine_patch,
+ }
+ }
+}
+
+impl InternDebug for RadialGradientKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct RadialGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: LayoutPoint,
+ pub params: RadialGradientParams,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub brush_segments: Vec<BrushSegment>,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub stops_handle: GpuCacheHandle,
+}
+
+impl Deref for RadialGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for RadialGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<RadialGradientKey> for RadialGradientTemplate {
+ fn from(item: RadialGradientKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size);
+ }
+
+ let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ RadialGradientTemplate {
+ common,
+ center: item.center.into(),
+ extend_mode: item.extend_mode,
+ params: item.params,
+ stretch_size: item.stretch_size.into(),
+ tile_spacing: item.tile_spacing.into(),
+ brush_segments,
+ stops_opacity,
+ stops,
+ stops_handle: GpuCacheHandle::new(),
+ }
+ }
+}
+
+impl RadialGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(mut request) =
+ frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ // write_prim_gpu_blocks
+ request.push([
+ self.center.x,
+ self.center.y,
+ self.params.start_radius,
+ self.params.end_radius,
+ ]);
+ request.push([
+ self.params.ratio_xy,
+ pack_as_float(self.extend_mode as u32),
+ self.stretch_size.width,
+ self.stretch_size.height,
+ ]);
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.stops_handle) {
+ GradientGpuBlockBuilder::build(
+ false,
+ &mut request,
+ &self.stops,
+ );
+ }
+
+ self.opacity = get_gradient_opacity(
+ self.common.prim_rect,
+ self.stretch_size,
+ self.tile_spacing,
+ self.stops_opacity,
+ );
+ }
+}
+
+pub type RadialGradientDataHandle = InternHandle<RadialGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RadialGradient {
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: RadialGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl Internable for RadialGradient {
+ type Key = RadialGradientKey;
+ type StoreData = RadialGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_RADIAL_GRADIENTS;
+}
+
+impl InternablePrimitive for RadialGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> RadialGradientKey {
+ RadialGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: RadialGradientKey,
+ data_handle: RadialGradientDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::RadialGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ }
+}
+
+impl IsVisible for RadialGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Conic gradients
+
+/// Hashable conic gradient parameters, for use during prim interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct ConicGradientParams {
+ pub angle: f32, // in radians
+ pub start_offset: f32,
+ pub end_offset: f32,
+}
+
+impl Eq for ConicGradientParams {}
+
+impl hash::Hash for ConicGradientParams {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.angle.to_bits().hash(state);
+ self.start_offset.to_bits().hash(state);
+ self.end_offset.to_bits().hash(state);
+ }
+}
+
+/// Identifying key for a line decoration.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, Hash, MallocSizeOf)]
+pub struct ConicGradientKey {
+ pub common: PrimKeyCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: ConicGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl ConicGradientKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ conic_grad: ConicGradient,
+ ) -> Self {
+ ConicGradientKey {
+ common: info.into(),
+ extend_mode: conic_grad.extend_mode,
+ center: conic_grad.center,
+ params: conic_grad.params,
+ stretch_size: conic_grad.stretch_size,
+ stops: conic_grad.stops,
+ tile_spacing: conic_grad.tile_spacing,
+ nine_patch: conic_grad.nine_patch,
+ }
+ }
+}
+
+impl InternDebug for ConicGradientKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct ConicGradientTemplate {
+ pub common: PrimTemplateCommonData,
+ pub extend_mode: ExtendMode,
+ pub center: LayoutPoint,
+ pub params: ConicGradientParams,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub brush_segments: Vec<BrushSegment>,
+ pub stops_opacity: PrimitiveOpacity,
+ pub stops: Vec<GradientStop>,
+ pub stops_handle: GpuCacheHandle,
+}
+
+impl Deref for ConicGradientTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl DerefMut for ConicGradientTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<ConicGradientKey> for ConicGradientTemplate {
+ fn from(item: ConicGradientKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ let mut brush_segments = Vec::new();
+
+ if let Some(ref nine_patch) = item.nine_patch {
+ brush_segments = nine_patch.create_segments(common.prim_rect.size);
+ }
+
+ let (stops, min_alpha) = stops_and_min_alpha(&item.stops);
+
+ // Save opacity of the stops for use in
+ // selecting which pass this gradient
+ // should be drawn in.
+ let stops_opacity = PrimitiveOpacity::from_alpha(min_alpha);
+
+ ConicGradientTemplate {
+ common,
+ center: item.center.into(),
+ extend_mode: item.extend_mode,
+ params: item.params,
+ stretch_size: item.stretch_size.into(),
+ tile_spacing: item.tile_spacing.into(),
+ brush_segments,
+ stops_opacity,
+ stops,
+ stops_handle: GpuCacheHandle::new(),
+ }
+ }
+}
+
+impl ConicGradientTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(mut request) =
+ frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ // write_prim_gpu_blocks
+ request.push([
+ self.center.x,
+ self.center.y,
+ self.params.start_offset,
+ self.params.end_offset,
+ ]);
+ request.push([
+ self.params.angle,
+ pack_as_float(self.extend_mode as u32),
+ self.stretch_size.width,
+ self.stretch_size.height,
+ ]);
+
+ // write_segment_gpu_blocks
+ for segment in &self.brush_segments {
+ // has to match VECS_PER_SEGMENT
+ request.write_segment(
+ segment.local_rect,
+ segment.extra_data,
+ );
+ }
+ }
+
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.stops_handle) {
+ GradientGpuBlockBuilder::build(
+ false,
+ &mut request,
+ &self.stops,
+ );
+ }
+
+ self.opacity = get_gradient_opacity(
+ self.common.prim_rect,
+ self.stretch_size,
+ self.tile_spacing,
+ self.stops_opacity,
+ );
+ }
+}
+
+pub type ConicGradientDataHandle = InternHandle<ConicGradient>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ConicGradient {
+ pub extend_mode: ExtendMode,
+ pub center: PointKey,
+ pub params: ConicGradientParams,
+ pub stretch_size: SizeKey,
+ pub stops: Vec<GradientStopKey>,
+ pub tile_spacing: SizeKey,
+ pub nine_patch: Option<Box<NinePatchDescriptor>>,
+}
+
+impl Internable for ConicGradient {
+ type Key = ConicGradientKey;
+ type StoreData = ConicGradientTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CONIC_GRADIENTS;
+}
+
+impl InternablePrimitive for ConicGradient {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ConicGradientKey {
+ ConicGradientKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: ConicGradientKey,
+ data_handle: ConicGradientDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::ConicGradient {
+ data_handle,
+ visible_tiles_range: GradientTileRange::empty(),
+ }
+ }
+}
+
+impl IsVisible for ConicGradient {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// The gradient entry index for the first color stop
+pub const GRADIENT_DATA_FIRST_STOP: usize = 0;
+// The gradient entry index for the last color stop
+pub const GRADIENT_DATA_LAST_STOP: usize = GRADIENT_DATA_SIZE - 1;
+
+// The start of the gradient data table
+pub const GRADIENT_DATA_TABLE_BEGIN: usize = GRADIENT_DATA_FIRST_STOP + 1;
+// The exclusive bound of the gradient data table
+pub const GRADIENT_DATA_TABLE_END: usize = GRADIENT_DATA_LAST_STOP;
+// The number of entries in the gradient data table.
+pub const GRADIENT_DATA_TABLE_SIZE: usize = 128;
+
+// The number of entries in a gradient data: GRADIENT_DATA_TABLE_SIZE + first stop entry + last stop entry
+pub const GRADIENT_DATA_SIZE: usize = GRADIENT_DATA_TABLE_SIZE + 2;
+
+/// An entry in a gradient data table representing a segment of the gradient
+/// color space.
+#[derive(Debug, Copy, Clone)]
+#[repr(C)]
+struct GradientDataEntry {
+ start_color: PremultipliedColorF,
+ end_color: PremultipliedColorF,
+}
+
+impl GradientDataEntry {
+ fn white() -> Self {
+ Self {
+ start_color: PremultipliedColorF::WHITE,
+ end_color: PremultipliedColorF::WHITE,
+ }
+ }
+}
+
+// TODO(gw): Tidy this up to be a free function / module?
+struct GradientGpuBlockBuilder {}
+
+impl GradientGpuBlockBuilder {
+ /// Generate a color ramp filling the indices in [start_idx, end_idx) and interpolating
+ /// from start_color to end_color.
+ fn fill_colors(
+ start_idx: usize,
+ end_idx: usize,
+ start_color: &PremultipliedColorF,
+ end_color: &PremultipliedColorF,
+ entries: &mut [GradientDataEntry; GRADIENT_DATA_SIZE],
+ ) {
+ // Calculate the color difference for individual steps in the ramp.
+ let inv_steps = 1.0 / (end_idx - start_idx) as f32;
+ let step_r = (end_color.r - start_color.r) * inv_steps;
+ let step_g = (end_color.g - start_color.g) * inv_steps;
+ let step_b = (end_color.b - start_color.b) * inv_steps;
+ let step_a = (end_color.a - start_color.a) * inv_steps;
+
+ let mut cur_color = *start_color;
+
+ // Walk the ramp writing start and end colors for each entry.
+ for index in start_idx .. end_idx {
+ let entry = &mut entries[index];
+ entry.start_color = cur_color;
+ cur_color.r += step_r;
+ cur_color.g += step_g;
+ cur_color.b += step_b;
+ cur_color.a += step_a;
+ entry.end_color = cur_color;
+ }
+ }
+
+ /// Compute an index into the gradient entry table based on a gradient stop offset. This
+ /// function maps offsets from [0, 1] to indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END].
+ #[inline]
+ fn get_index(offset: f32) -> usize {
+ (offset.max(0.0).min(1.0) * GRADIENT_DATA_TABLE_SIZE as f32 +
+ GRADIENT_DATA_TABLE_BEGIN as f32)
+ .round() as usize
+ }
+
+ // Build the gradient data from the supplied stops, reversing them if necessary.
+ fn build(
+ reverse_stops: bool,
+ request: &mut GpuDataRequest,
+ src_stops: &[GradientStop],
+ ) {
+ // Preconditions (should be ensured by DisplayListBuilder):
+ // * we have at least two stops
+ // * first stop has offset 0.0
+ // * last stop has offset 1.0
+ let mut src_stops = src_stops.into_iter();
+ let mut cur_color = match src_stops.next() {
+ Some(stop) => {
+ debug_assert_eq!(stop.offset, 0.0);
+ stop.color.premultiplied()
+ }
+ None => {
+ error!("Zero gradient stops found!");
+ PremultipliedColorF::BLACK
+ }
+ };
+
+ // A table of gradient entries, with two colors per entry, that specify the start and end color
+ // within the segment of the gradient space represented by that entry. To lookup a gradient result,
+ // first the entry index is calculated to determine which two colors to interpolate between, then
+ // the offset within that entry bucket is used to interpolate between the two colors in that entry.
+ // This layout preserves hard stops, as the end color for a given entry can differ from the start
+ // color for the following entry, despite them being adjacent. Colors are stored within in BGRA8
+ // format for texture upload. This table requires the gradient color stops to be normalized to the
+ // range [0, 1]. The first and last entries hold the first and last color stop colors respectively,
+ // while the entries in between hold the interpolated color stop values for the range [0, 1].
+ let mut entries = [GradientDataEntry::white(); GRADIENT_DATA_SIZE];
+
+ if reverse_stops {
+ // Fill in the first entry (for reversed stops) with the first color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_LAST_STOP,
+ GRADIENT_DATA_LAST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ );
+
+ // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
+ // of gradient stops. Each iteration of a loop will fill the indices in [next_idx, cur_idx). The
+ // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
+ let mut cur_idx = GRADIENT_DATA_TABLE_END;
+ for next in src_stops {
+ let next_color = next.color.premultiplied();
+ let next_idx = Self::get_index(1.0 - next.offset);
+
+ if next_idx < cur_idx {
+ GradientGpuBlockBuilder::fill_colors(
+ next_idx,
+ cur_idx,
+ &next_color,
+ &cur_color,
+ &mut entries,
+ );
+ cur_idx = next_idx;
+ }
+
+ cur_color = next_color;
+ }
+ if cur_idx != GRADIENT_DATA_TABLE_BEGIN {
+ error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
+ }
+
+ // Fill in the last entry (for reversed stops) with the last color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_FIRST_STOP,
+ GRADIENT_DATA_FIRST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ );
+ } else {
+ // Fill in the first entry with the first color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_FIRST_STOP,
+ GRADIENT_DATA_FIRST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ );
+
+ // Fill in the center of the gradient table, generating a color ramp between each consecutive pair
+ // of gradient stops. Each iteration of a loop will fill the indices in [cur_idx, next_idx). The
+ // loop will then fill indices in [GRADIENT_DATA_TABLE_BEGIN, GRADIENT_DATA_TABLE_END).
+ let mut cur_idx = GRADIENT_DATA_TABLE_BEGIN;
+ for next in src_stops {
+ let next_color = next.color.premultiplied();
+ let next_idx = Self::get_index(next.offset);
+
+ if next_idx > cur_idx {
+ GradientGpuBlockBuilder::fill_colors(
+ cur_idx,
+ next_idx,
+ &cur_color,
+ &next_color,
+ &mut entries,
+ );
+ cur_idx = next_idx;
+ }
+
+ cur_color = next_color;
+ }
+ if cur_idx != GRADIENT_DATA_TABLE_END {
+ error!("Gradient stops abruptly at {}, auto-completing to white", cur_idx);
+ }
+
+ // Fill in the last entry with the last color stop
+ GradientGpuBlockBuilder::fill_colors(
+ GRADIENT_DATA_LAST_STOP,
+ GRADIENT_DATA_LAST_STOP + 1,
+ &cur_color,
+ &cur_color,
+ &mut entries,
+ );
+ }
+
+ for entry in entries.iter() {
+ request.push(entry.start_color);
+ request.push(entry.end_color);
+ }
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<LinearGradient>(), 72, "LinearGradient size changed");
+ assert_eq!(mem::size_of::<LinearGradientTemplate>(), 120, "LinearGradientTemplate size changed");
+ assert_eq!(mem::size_of::<LinearGradientKey>(), 88, "LinearGradientKey size changed");
+
+ assert_eq!(mem::size_of::<RadialGradient>(), 72, "RadialGradient size changed");
+ assert_eq!(mem::size_of::<RadialGradientTemplate>(), 128, "RadialGradientTemplate size changed");
+ assert_eq!(mem::size_of::<RadialGradientKey>(), 96, "RadialGradientKey size changed");
+
+ assert_eq!(mem::size_of::<ConicGradient>(), 72, "ConicGradient size changed");
+ assert_eq!(mem::size_of::<ConicGradientTemplate>(), 128, "ConicGradientTemplate size changed");
+ assert_eq!(mem::size_of::<ConicGradientKey>(), 96, "ConicGradientKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/image.rs b/gfx/wr/webrender/src/prim_store/image.rs
new file mode 100644
index 0000000000..8ce62abebd
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/image.rs
@@ -0,0 +1,521 @@
+/* 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 api::{
+ AlphaType, ColorDepth, ColorF, ColorU, RasterSpace,
+ ImageKey as ApiImageKey, ImageRendering,
+ PremultipliedColorF, Shadow, YuvColorSpace, ColorRange, YuvFormat,
+};
+use api::units::*;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::FrameBuildingState;
+use crate::gpu_cache::{GpuCache, GpuDataRequest};
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::{LayoutPrimitiveInfo};
+use crate::picture::SurfaceIndex;
+use crate::prim_store::{
+ EdgeAaSegmentMask, PrimitiveInstanceKind,
+ PrimitiveOpacity, PrimKey,
+ PrimTemplate, PrimTemplateCommonData, PrimitiveStore, SegmentInstanceIndex,
+ SizeKey, InternablePrimitive,
+};
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{BlitSource, RenderTask};
+use crate::render_task_cache::{
+ RenderTaskCacheEntryHandle, RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskParent
+};
+use crate::resource_cache::{ImageRequest, ResourceCache};
+use crate::util::pack_as_float;
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct VisibleImageTile {
+ pub tile_offset: TileOffset,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+// Key that identifies a unique (partial) image that is being
+// stored in the render task cache.
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageCacheKey {
+ pub request: ImageRequest,
+ pub texel_rect: Option<DeviceIntRect>,
+}
+
+/// Instance specific fields for an image primitive. These are
+/// currently stored in a separate array to avoid bloating the
+/// size of PrimitiveInstance. In the future, we should be able
+/// to remove this and store the information inline, by:
+/// (a) Removing opacity collapse / binding support completely.
+/// Once we have general picture caching, we don't need this.
+/// (b) Change visible_tiles to use Storage in the primitive
+/// scratch buffer. This will reduce the size of the
+/// visible_tiles field here, and save memory allocation
+/// when image tiling is used. I've left it as a Vec for
+/// now to reduce the number of changes, and because image
+/// tiling is very rare on real pages.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ImageInstance {
+ pub segment_instance_index: SegmentInstanceIndex,
+ pub tight_local_clip_rect: LayoutRect,
+ pub visible_tiles: Vec<VisibleImageTile>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, PartialEq, MallocSizeOf, Hash)]
+pub struct Image {
+ pub key: ApiImageKey,
+ pub stretch_size: SizeKey,
+ pub tile_spacing: SizeKey,
+ pub color: ColorU,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+}
+
+pub type ImageKey = PrimKey<Image>;
+
+impl ImageKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ image: Image,
+ ) -> Self {
+ ImageKey {
+ common: info.into(),
+ kind: image,
+ }
+ }
+}
+
+impl InternDebug for ImageKey {}
+
+// Where to find the texture data for an image primitive.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub enum ImageSource {
+ // A normal image - just reference the texture cache.
+ Default,
+ // An image that is pre-rendered into the texture cache
+ // via a render task.
+ Cache {
+ size: DeviceIntSize,
+ handle: Option<RenderTaskCacheEntryHandle>,
+ },
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct ImageData {
+ pub key: ApiImageKey,
+ pub stretch_size: LayoutSize,
+ pub tile_spacing: LayoutSize,
+ pub color: ColorF,
+ pub source: ImageSource,
+ pub image_rendering: ImageRendering,
+ pub alpha_type: AlphaType,
+}
+
+impl From<Image> for ImageData {
+ fn from(image: Image) -> Self {
+ ImageData {
+ key: image.key,
+ color: image.color.into(),
+ stretch_size: image.stretch_size.into(),
+ tile_spacing: image.tile_spacing.into(),
+ source: ImageSource::Default,
+ image_rendering: image.image_rendering,
+ alpha_type: image.alpha_type,
+ }
+ }
+}
+
+impl ImageData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ parent_surface: SurfaceIndex,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(&mut request);
+ }
+
+ common.opacity = {
+ let image_properties = frame_state
+ .resource_cache
+ .get_image_properties(self.key);
+
+ match image_properties {
+ Some(image_properties) => {
+ let is_tiled = image_properties.tiling.is_some();
+
+ if self.tile_spacing != LayoutSize::zero() && !is_tiled {
+ self.source = ImageSource::Cache {
+ // Size in device-pixels we need to allocate in render task cache.
+ size: image_properties.descriptor.size.to_i32(),
+ handle: None,
+ };
+ }
+
+ let mut is_opaque = image_properties.descriptor.is_opaque();
+ let request = ImageRequest {
+ key: self.key,
+ rendering: self.image_rendering,
+ tile: None,
+ };
+
+ // Every frame, for cached items, we need to request the render
+ // task cache item. The closure will be invoked on the first
+ // time through, and any time the render task output has been
+ // evicted from the texture cache.
+ match self.source {
+ ImageSource::Cache { ref mut size, ref mut handle } => {
+ let padding = DeviceIntSideOffsets::new(
+ 0,
+ (self.tile_spacing.width * size.width as f32 / self.stretch_size.width) as i32,
+ (self.tile_spacing.height * size.height as f32 / self.stretch_size.height) as i32,
+ 0,
+ );
+
+ size.width += padding.horizontal();
+ size.height += padding.vertical();
+
+ is_opaque &= padding == DeviceIntSideOffsets::zero();
+
+ let image_cache_key = ImageCacheKey {
+ request,
+ texel_rect: None,
+ };
+ let target_kind = if image_properties.descriptor.format.bytes_per_pixel() == 1 {
+ RenderTargetKind::Alpha
+ } else {
+ RenderTargetKind::Color
+ };
+
+ // Request a pre-rendered image task.
+ *handle = Some(frame_state.resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: *size,
+ kind: RenderTaskCacheKeyKind::Image(image_cache_key),
+ },
+ frame_state.gpu_cache,
+ frame_state.rg_builder,
+ None,
+ image_properties.descriptor.is_opaque(),
+ RenderTaskParent::Surface(parent_surface),
+ frame_state.surfaces,
+ |rg_builder| {
+ // Create a task to blit from the texture cache to
+ // a normal transient render task surface. This will
+ // copy only the sub-rect, if specified.
+ // TODO: figure out if/when we can do a blit instead.
+ let cache_to_target_task_id = RenderTask::new_scaling_with_padding(
+ BlitSource::Image { key: image_cache_key },
+ rg_builder,
+ target_kind,
+ *size,
+ padding,
+ );
+
+ // Create a task to blit the rect from the child render
+ // task above back into the right spot in the persistent
+ // render target cache.
+ RenderTask::new_blit(
+ *size,
+ BlitSource::RenderTask {
+ task_id: cache_to_target_task_id,
+ },
+ rg_builder,
+ )
+ }
+ ));
+ }
+ ImageSource::Default => {}
+ }
+
+ if is_opaque {
+ PrimitiveOpacity::from_alpha(self.color.a)
+ } else {
+ PrimitiveOpacity::translucent()
+ }
+ }
+ None => {
+ PrimitiveOpacity::opaque()
+ }
+ }
+ };
+ }
+
+ pub fn write_prim_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ // Images are drawn as a white color, modulated by the total
+ // opacity coming from any collapsed property bindings.
+ // Size has to match `VECS_PER_SPECIFIC_BRUSH` from `brush_image.glsl` exactly.
+ request.push(self.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ self.stretch_size.width + self.tile_spacing.width,
+ self.stretch_size.height + self.tile_spacing.height,
+ 0.0,
+ 0.0,
+ ]);
+ }
+}
+
+pub type ImageTemplate = PrimTemplate<ImageData>;
+
+impl From<ImageKey> for ImageTemplate {
+ fn from(image: ImageKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(image.common);
+
+ ImageTemplate {
+ common,
+ kind: image.kind.into(),
+ }
+ }
+}
+
+pub type ImageDataHandle = InternHandle<Image>;
+
+impl Internable for Image {
+ type Key = ImageKey;
+ type StoreData = ImageTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_IMAGES;
+}
+
+impl InternablePrimitive for Image {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> ImageKey {
+ ImageKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: ImageKey,
+ data_handle: ImageDataHandle,
+ prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ // TODO(gw): Refactor this to not need a separate image
+ // instance (see ImageInstance struct).
+ let image_instance_index = prim_store.images.push(ImageInstance {
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ tight_local_clip_rect: LayoutRect::zero(),
+ visible_tiles: Vec::new(),
+ });
+
+ PrimitiveInstanceKind::Image {
+ data_handle,
+ image_instance_index,
+ is_compositor_surface: false,
+ }
+ }
+}
+
+impl CreateShadow for Image {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ Image {
+ tile_spacing: self.tile_spacing,
+ stretch_size: self.stretch_size,
+ key: self.key,
+ image_rendering: self.image_rendering,
+ alpha_type: self.alpha_type,
+ color: shadow.color.into(),
+ }
+ }
+}
+
+impl IsVisible for Image {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct YuvImage {
+ pub color_depth: ColorDepth,
+ pub yuv_key: [ApiImageKey; 3],
+ pub format: YuvFormat,
+ pub color_space: YuvColorSpace,
+ pub color_range: ColorRange,
+ pub image_rendering: ImageRendering,
+}
+
+pub type YuvImageKey = PrimKey<YuvImage>;
+
+impl YuvImageKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ yuv_image: YuvImage,
+ ) -> Self {
+ YuvImageKey {
+ common: info.into(),
+ kind: yuv_image,
+ }
+ }
+}
+
+impl InternDebug for YuvImageKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct YuvImageData {
+ pub color_depth: ColorDepth,
+ pub yuv_key: [ApiImageKey; 3],
+ pub format: YuvFormat,
+ pub color_space: YuvColorSpace,
+ pub color_range: ColorRange,
+ pub image_rendering: ImageRendering,
+}
+
+impl From<YuvImage> for YuvImageData {
+ fn from(image: YuvImage) -> Self {
+ YuvImageData {
+ color_depth: image.color_depth,
+ yuv_key: image.yuv_key,
+ format: image.format,
+ color_space: image.color_space,
+ color_range: image.color_range,
+ image_rendering: image.image_rendering,
+ }
+ }
+}
+
+impl YuvImageData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(&mut request);
+ };
+
+ // YUV images never have transparency
+ common.opacity = PrimitiveOpacity::opaque();
+ }
+
+ pub fn request_resources(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ ) {
+ let channel_num = self.format.get_plane_num();
+ debug_assert!(channel_num <= 3);
+ for channel in 0 .. channel_num {
+ resource_cache.request_image(
+ ImageRequest {
+ key: self.yuv_key[channel],
+ rendering: self.image_rendering,
+ tile: None,
+ },
+ gpu_cache,
+ );
+ }
+ }
+
+ pub fn write_prim_gpu_blocks(&self, request: &mut GpuDataRequest) {
+ request.push([
+ self.color_depth.rescaling_factor(),
+ pack_as_float(self.color_space as u32),
+ pack_as_float(self.format as u32),
+ 0.0
+ ]);
+ }
+}
+
+pub type YuvImageTemplate = PrimTemplate<YuvImageData>;
+
+impl From<YuvImageKey> for YuvImageTemplate {
+ fn from(image: YuvImageKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(image.common);
+
+ YuvImageTemplate {
+ common,
+ kind: image.kind.into(),
+ }
+ }
+}
+
+pub type YuvImageDataHandle = InternHandle<YuvImage>;
+
+impl Internable for YuvImage {
+ type Key = YuvImageKey;
+ type StoreData = YuvImageTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_YUV_IMAGES;
+}
+
+impl InternablePrimitive for YuvImage {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> YuvImageKey {
+ YuvImageKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ _key: YuvImageKey,
+ data_handle: YuvImageDataHandle,
+ _prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::YuvImage {
+ data_handle,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ is_compositor_surface: false,
+ }
+ }
+}
+
+impl IsVisible for YuvImage {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<Image>(), 32, "Image size changed");
+ assert_eq!(mem::size_of::<ImageTemplate>(), 92, "ImageTemplate size changed");
+ assert_eq!(mem::size_of::<ImageKey>(), 52, "ImageKey size changed");
+ assert_eq!(mem::size_of::<YuvImage>(), 32, "YuvImage size changed");
+ assert_eq!(mem::size_of::<YuvImageTemplate>(), 60, "YuvImageTemplate size changed");
+ assert_eq!(mem::size_of::<YuvImageKey>(), 52, "YuvImageKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/interned.rs b/gfx/wr/webrender/src/prim_store/interned.rs
new file mode 100644
index 0000000000..50337103c3
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/interned.rs
@@ -0,0 +1,14 @@
+/* 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/. */
+
+// list of all interned primitives to match enumerate_interners!
+
+pub use crate::prim_store::backdrop::Backdrop;
+pub use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+pub use crate::prim_store::image::{Image, YuvImage};
+pub use crate::prim_store::line_dec::{LineDecoration};
+pub use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
+pub use crate::prim_store::picture::Picture;
+pub use crate::prim_store::text_run::TextRun;
+
diff --git a/gfx/wr/webrender/src/prim_store/line_dec.rs b/gfx/wr/webrender/src/prim_store/line_dec.rs
new file mode 100644
index 0000000000..e176ead4a6
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/line_dec.rs
@@ -0,0 +1,257 @@
+/* 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 api::{
+ ColorF, ColorU, RasterSpace,
+ LineOrientation, LineStyle, PremultipliedColorF, Shadow,
+};
+use api::units::*;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::{FrameBuildingState};
+use crate::gpu_cache::GpuDataRequest;
+use crate::intern;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::prim_store::{
+ PrimKey, PrimTemplate, PrimTemplateCommonData,
+ InternablePrimitive, PrimitiveStore,
+};
+use crate::prim_store::PrimitiveInstanceKind;
+
+/// Maximum resolution in device pixels at which line decorations are rasterized.
+pub const MAX_LINE_DECORATION_RESOLUTION: u32 = 4096;
+
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecorationCacheKey {
+ pub style: LineStyle,
+ pub orientation: LineOrientation,
+ pub wavy_line_thickness: Au,
+ pub size: LayoutSizeAu,
+}
+
+/// Identifying key for a line decoration.
+#[derive(Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecoration {
+ // If the cache_key is Some(..) it is a line decoration
+ // that relies on a render task (e.g. wavy). If the
+ // cache key is None, it uses a fast path to draw the
+ // line decoration as a solid rect.
+ pub cache_key: Option<LineDecorationCacheKey>,
+ pub color: ColorU,
+}
+
+pub type LineDecorationKey = PrimKey<LineDecoration>;
+
+impl LineDecorationKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ line_dec: LineDecoration,
+ ) -> Self {
+ LineDecorationKey {
+ common: info.into(),
+ kind: line_dec,
+ }
+ }
+}
+
+impl intern::InternDebug for LineDecorationKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct LineDecorationData {
+ pub cache_key: Option<LineDecorationCacheKey>,
+ pub color: ColorF,
+}
+
+impl LineDecorationData {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ common: &mut PrimTemplateCommonData,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ if let Some(ref mut request) = frame_state.gpu_cache.request(&mut common.gpu_cache_handle) {
+ self.write_prim_gpu_blocks(request);
+ }
+ }
+
+ fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest
+ ) {
+ match self.cache_key.as_ref() {
+ Some(cache_key) => {
+ request.push(self.color.premultiplied());
+ request.push(PremultipliedColorF::WHITE);
+ request.push([
+ cache_key.size.width.to_f32_px(),
+ cache_key.size.height.to_f32_px(),
+ 0.0,
+ 0.0,
+ ]);
+ }
+ None => {
+ request.push(self.color.premultiplied());
+ }
+ }
+ }
+}
+
+pub type LineDecorationTemplate = PrimTemplate<LineDecorationData>;
+
+impl From<LineDecorationKey> for LineDecorationTemplate {
+ fn from(line_dec: LineDecorationKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(line_dec.common);
+ LineDecorationTemplate {
+ common,
+ kind: LineDecorationData {
+ cache_key: line_dec.kind.cache_key,
+ color: line_dec.kind.color.into(),
+ }
+ }
+ }
+}
+
+pub type LineDecorationDataHandle = intern::Handle<LineDecoration>;
+
+impl intern::Internable for LineDecoration {
+ type Key = LineDecorationKey;
+ type StoreData = LineDecorationTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_LINE_DECORATIONS;
+}
+
+impl InternablePrimitive for LineDecoration {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> LineDecorationKey {
+ LineDecorationKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ _key: LineDecorationKey,
+ data_handle: LineDecorationDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ PrimitiveInstanceKind::LineDecoration {
+ data_handle,
+ cache_handle: None,
+ }
+ }
+}
+
+impl CreateShadow for LineDecoration {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> Self {
+ LineDecoration {
+ color: shadow.color.into(),
+ cache_key: self.cache_key.clone(),
+ }
+ }
+}
+
+impl IsVisible for LineDecoration {
+ fn is_visible(&self) -> bool {
+ self.color.a > 0
+ }
+}
+
+/// Choose the decoration mask tile size for a given line.
+///
+/// Given a line with overall size `rect_size` and the given `orientation`,
+/// return the dimensions of a single mask tile for the decoration pattern
+/// described by `style` and `wavy_line_thickness`.
+///
+/// If `style` is `Solid`, no mask tile is necessary; return `None`. The other
+/// styles each have their own characteristic periods of repetition, so for each
+/// one, this function returns a `LayoutSize` with the right aspect ratio and
+/// whose specific size is convenient for the `cs_line_decoration.glsl` fragment
+/// shader to work with. The shader uses a local coordinate space in which the
+/// tile fills a rectangle with one corner at the origin, and with the size this
+/// function returns.
+///
+/// The returned size is not necessarily in pixels; device scaling and other
+/// concerns can still affect the actual task size.
+///
+/// Regardless of whether `orientation` is `Vertical` or `Horizontal`, the
+/// `width` and `height` of the returned size are always horizontal and
+/// vertical, respectively.
+pub fn get_line_decoration_size(
+ rect_size: &LayoutSize,
+ orientation: LineOrientation,
+ style: LineStyle,
+ wavy_line_thickness: f32,
+) -> Option<LayoutSize> {
+ let h = match orientation {
+ LineOrientation::Horizontal => rect_size.height,
+ LineOrientation::Vertical => rect_size.width,
+ };
+
+ // TODO(gw): The formulae below are based on the existing gecko and line
+ // shader code. They give reasonable results for most inputs,
+ // but could definitely do with a detailed pass to get better
+ // quality on a wider range of inputs!
+ // See nsCSSRendering::PaintDecorationLine in Gecko.
+
+ let (parallel, perpendicular) = match style {
+ LineStyle::Solid => {
+ return None;
+ }
+ LineStyle::Dashed => {
+ let dash_length = (3.0 * h).min(64.0).max(1.0);
+
+ (2.0 * dash_length, 4.0)
+ }
+ LineStyle::Dotted => {
+ let diameter = h.min(64.0).max(1.0);
+ let period = 2.0 * diameter;
+
+ (period, diameter)
+ }
+ LineStyle::Wavy => {
+ let line_thickness = wavy_line_thickness.max(1.0);
+ let slope_length = h - line_thickness;
+ let flat_length = ((line_thickness - 1.0) * 2.0).max(1.0);
+ let approx_period = 2.0 * (slope_length + flat_length);
+
+ (approx_period, h)
+ }
+ };
+
+ Some(match orientation {
+ LineOrientation::Horizontal => LayoutSize::new(parallel, perpendicular),
+ LineOrientation::Vertical => LayoutSize::new(perpendicular, parallel),
+ })
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<LineDecoration>(), 20, "LineDecoration size changed");
+ assert_eq!(mem::size_of::<LineDecorationTemplate>(), 60, "LineDecorationTemplate size changed");
+ assert_eq!(mem::size_of::<LineDecorationKey>(), 40, "LineDecorationKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/mod.rs b/gfx/wr/webrender/src/prim_store/mod.rs
new file mode 100644
index 0000000000..ddb7517abc
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -0,0 +1,1364 @@
+/* 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 api::{BorderRadius, ClipMode, ColorF, ColorU, RasterSpace};
+use api::{ImageRendering, RepeatMode, PrimitiveFlags};
+use api::{PremultipliedColorF, PropertyBinding, Shadow};
+use api::{PrimitiveKeyKind};
+use api::units::*;
+use euclid::{SideOffsets2D, Size2D};
+use malloc_size_of::MallocSizeOf;
+use crate::segment::EdgeAaSegmentMask;
+use crate::border::BorderSegmentCacheKey;
+use crate::clip::{ClipChainId, ClipSet};
+use crate::debug_item::DebugItem;
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::FrameBuildingState;
+use crate::glyph_rasterizer::GlyphKey;
+use crate::gpu_cache::{GpuCacheAddress, GpuCacheHandle, GpuDataRequest};
+use crate::gpu_types::{BrushFlags};
+use crate::intern;
+use crate::picture::PicturePrimitive;
+#[cfg(debug_assertions)]
+use crate::render_backend::{FrameId};
+use crate::render_task_graph::RenderTaskId;
+use crate::render_task_cache::RenderTaskCacheEntryHandle;
+use crate::resource_cache::ImageProperties;
+use crate::scene::SceneProperties;
+use std::{hash, ops, u32, usize};
+#[cfg(debug_assertions)]
+use std::sync::atomic::{AtomicUsize, Ordering};
+use crate::util::Recycler;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::visibility::PrimitiveVisibility;
+
+pub mod backdrop;
+pub mod borders;
+pub mod gradient;
+pub mod image;
+pub mod line_dec;
+pub mod picture;
+pub mod text_run;
+pub mod interned;
+
+mod storage;
+
+use backdrop::BackdropDataHandle;
+use borders::{ImageBorderDataHandle, NormalBorderDataHandle};
+use gradient::{LinearGradientPrimitive, LinearGradientDataHandle, RadialGradientDataHandle, ConicGradientDataHandle};
+use image::{ImageDataHandle, ImageInstance, YuvImageDataHandle};
+use line_dec::LineDecorationDataHandle;
+use picture::PictureDataHandle;
+use text_run::{TextRunDataHandle, TextRunPrimitive};
+
+pub const VECS_PER_SEGMENT: usize = 2;
+
+/// Counter for unique primitive IDs for debug tracing.
+#[cfg(debug_assertions)]
+static NEXT_PRIM_ID: AtomicUsize = AtomicUsize::new(0);
+
+#[cfg(debug_assertions)]
+static PRIM_CHASE_ID: AtomicUsize = AtomicUsize::new(usize::MAX);
+
+#[cfg(debug_assertions)]
+pub fn register_prim_chase_id(id: PrimitiveDebugId) {
+ PRIM_CHASE_ID.store(id.0, Ordering::SeqCst);
+}
+
+#[cfg(not(debug_assertions))]
+pub fn register_prim_chase_id(_: PrimitiveDebugId) {
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf)]
+pub struct PrimitiveOpacity {
+ pub is_opaque: bool,
+}
+
+impl PrimitiveOpacity {
+ pub fn opaque() -> PrimitiveOpacity {
+ PrimitiveOpacity { is_opaque: true }
+ }
+
+ pub fn translucent() -> PrimitiveOpacity {
+ PrimitiveOpacity { is_opaque: false }
+ }
+
+ pub fn from_alpha(alpha: f32) -> PrimitiveOpacity {
+ PrimitiveOpacity {
+ is_opaque: alpha >= 1.0,
+ }
+ }
+
+ pub fn combine(self, other: PrimitiveOpacity) -> PrimitiveOpacity {
+ PrimitiveOpacity{
+ is_opaque: self.is_opaque && other.is_opaque
+ }
+ }
+}
+
+/// For external images, it's not possible to know the
+/// UV coords of the image (or the image data itself)
+/// until the render thread receives the frame and issues
+/// callbacks to the client application. For external
+/// images that are visible, a DeferredResolve is created
+/// that is stored in the frame. This allows the render
+/// thread to iterate this list and update any changed
+/// texture data and update the UV rect. Any filtering
+/// is handled externally for NativeTexture external
+/// images.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DeferredResolve {
+ pub address: GpuCacheAddress,
+ pub image_properties: ImageProperties,
+ pub rendering: ImageRendering,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ClipTaskIndex(pub u16);
+
+impl ClipTaskIndex {
+ pub const INVALID: ClipTaskIndex = ClipTaskIndex(0);
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, MallocSizeOf, Ord, PartialOrd)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureIndex(pub usize);
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct RectangleKey {
+ pub x: f32,
+ pub y: f32,
+ pub w: f32,
+ pub h: f32,
+}
+
+impl RectangleKey {
+ pub fn intersects(&self, other: &Self) -> bool {
+ self.x < other.x + other.w
+ && other.x < self.x + self.w
+ && self.y < other.y + other.h
+ && other.y < self.y + self.h
+ }
+}
+
+impl Eq for RectangleKey {}
+
+impl hash::Hash for RectangleKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x.to_bits().hash(state);
+ self.y.to_bits().hash(state);
+ self.w.to_bits().hash(state);
+ self.h.to_bits().hash(state);
+ }
+}
+
+impl From<RectangleKey> for LayoutRect {
+ fn from(key: RectangleKey) -> LayoutRect {
+ LayoutRect {
+ origin: LayoutPoint::new(key.x, key.y),
+ size: LayoutSize::new(key.w, key.h),
+ }
+ }
+}
+
+impl From<RectangleKey> for WorldRect {
+ fn from(key: RectangleKey) -> WorldRect {
+ WorldRect {
+ origin: WorldPoint::new(key.x, key.y),
+ size: WorldSize::new(key.w, key.h),
+ }
+ }
+}
+
+impl From<LayoutRect> for RectangleKey {
+ fn from(rect: LayoutRect) -> RectangleKey {
+ RectangleKey {
+ x: rect.origin.x,
+ y: rect.origin.y,
+ w: rect.size.width,
+ h: rect.size.height,
+ }
+ }
+}
+
+impl From<PictureRect> for RectangleKey {
+ fn from(rect: PictureRect) -> RectangleKey {
+ RectangleKey {
+ x: rect.origin.x,
+ y: rect.origin.y,
+ w: rect.size.width,
+ h: rect.size.height,
+ }
+ }
+}
+
+impl From<WorldRect> for RectangleKey {
+ fn from(rect: WorldRect) -> RectangleKey {
+ RectangleKey {
+ x: rect.origin.x,
+ y: rect.origin.y,
+ w: rect.size.width,
+ h: rect.size.height,
+ }
+ }
+}
+
+/// A hashable SideOffset2D that can be used in primitive keys.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct SideOffsetsKey {
+ pub top: f32,
+ pub right: f32,
+ pub bottom: f32,
+ pub left: f32,
+}
+
+impl Eq for SideOffsetsKey {}
+
+impl hash::Hash for SideOffsetsKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.top.to_bits().hash(state);
+ self.right.to_bits().hash(state);
+ self.bottom.to_bits().hash(state);
+ self.left.to_bits().hash(state);
+ }
+}
+
+impl From<SideOffsetsKey> for LayoutSideOffsets {
+ fn from(key: SideOffsetsKey) -> LayoutSideOffsets {
+ LayoutSideOffsets::new(
+ key.top,
+ key.right,
+ key.bottom,
+ key.left,
+ )
+ }
+}
+
+impl<U> From<SideOffsets2D<f32, U>> for SideOffsetsKey {
+ fn from(offsets: SideOffsets2D<f32, U>) -> SideOffsetsKey {
+ SideOffsetsKey {
+ top: offsets.top,
+ right: offsets.right,
+ bottom: offsets.bottom,
+ left: offsets.left,
+ }
+ }
+}
+
+/// A hashable size for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct SizeKey {
+ w: f32,
+ h: f32,
+}
+
+impl Eq for SizeKey {}
+
+impl hash::Hash for SizeKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.w.to_bits().hash(state);
+ self.h.to_bits().hash(state);
+ }
+}
+
+impl From<SizeKey> for LayoutSize {
+ fn from(key: SizeKey) -> LayoutSize {
+ LayoutSize::new(key.w, key.h)
+ }
+}
+
+impl<U> From<Size2D<f32, U>> for SizeKey {
+ fn from(size: Size2D<f32, U>) -> SizeKey {
+ SizeKey {
+ w: size.width,
+ h: size.height,
+ }
+ }
+}
+
+/// A hashable vec for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Debug, Clone, MallocSizeOf, PartialEq)]
+pub struct VectorKey {
+ pub x: f32,
+ pub y: f32,
+}
+
+impl Eq for VectorKey {}
+
+impl hash::Hash for VectorKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x.to_bits().hash(state);
+ self.y.to_bits().hash(state);
+ }
+}
+
+impl From<VectorKey> for LayoutVector2D {
+ fn from(key: VectorKey) -> LayoutVector2D {
+ LayoutVector2D::new(key.x, key.y)
+ }
+}
+
+impl From<VectorKey> for WorldVector2D {
+ fn from(key: VectorKey) -> WorldVector2D {
+ WorldVector2D::new(key.x, key.y)
+ }
+}
+
+impl From<LayoutVector2D> for VectorKey {
+ fn from(vec: LayoutVector2D) -> VectorKey {
+ VectorKey {
+ x: vec.x,
+ y: vec.y,
+ }
+ }
+}
+
+impl From<WorldVector2D> for VectorKey {
+ fn from(vec: WorldVector2D) -> VectorKey {
+ VectorKey {
+ x: vec.x,
+ y: vec.y,
+ }
+ }
+}
+
+/// A hashable point for using as a key during primitive interning.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
+pub struct PointKey {
+ pub x: f32,
+ pub y: f32,
+}
+
+impl Eq for PointKey {}
+
+impl hash::Hash for PointKey {
+ fn hash<H: hash::Hasher>(&self, state: &mut H) {
+ self.x.to_bits().hash(state);
+ self.y.to_bits().hash(state);
+ }
+}
+
+impl From<PointKey> for LayoutPoint {
+ fn from(key: PointKey) -> LayoutPoint {
+ LayoutPoint::new(key.x, key.y)
+ }
+}
+
+impl From<LayoutPoint> for PointKey {
+ fn from(p: LayoutPoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+impl From<PicturePoint> for PointKey {
+ fn from(p: PicturePoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+impl From<WorldPoint> for PointKey {
+ fn from(p: WorldPoint) -> PointKey {
+ PointKey {
+ x: p.x,
+ y: p.y,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimKeyCommonData {
+ pub flags: PrimitiveFlags,
+ pub prim_rect: RectangleKey,
+}
+
+impl From<&LayoutPrimitiveInfo> for PrimKeyCommonData {
+ fn from(info: &LayoutPrimitiveInfo) -> Self {
+ PrimKeyCommonData {
+ flags: info.flags,
+ prim_rect: info.rect.into(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimKey<T: MallocSizeOf> {
+ pub common: PrimKeyCommonData,
+ pub kind: T,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PrimitiveKey {
+ pub common: PrimKeyCommonData,
+ pub kind: PrimitiveKeyKind,
+}
+
+impl PrimitiveKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ kind: PrimitiveKeyKind,
+ ) -> Self {
+ PrimitiveKey {
+ common: info.into(),
+ kind,
+ }
+ }
+}
+
+impl intern::InternDebug for PrimitiveKey {}
+
+/// The shared information for a given primitive. This is interned and retained
+/// both across frames and display lists, by comparing the matching PrimitiveKey.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub enum PrimitiveTemplateKind {
+ Rectangle {
+ color: PropertyBinding<ColorF>,
+ },
+ Clear,
+}
+
+impl PrimitiveTemplateKind {
+ /// Write any GPU blocks for the primitive template to the given request object.
+ pub fn write_prim_gpu_blocks(
+ &self,
+ request: &mut GpuDataRequest,
+ scene_properties: &SceneProperties,
+ ) {
+ match *self {
+ PrimitiveTemplateKind::Clear => {
+ // Opaque black with operator dest out
+ request.push(PremultipliedColorF::BLACK);
+ }
+ PrimitiveTemplateKind::Rectangle { ref color, .. } => {
+ request.push(scene_properties.resolve_color(color).premultiplied())
+ }
+ }
+ }
+}
+
+/// Construct the primitive template data from a primitive key. This
+/// is invoked when a primitive key is created and the interner
+/// doesn't currently contain a primitive with this key.
+impl From<PrimitiveKeyKind> for PrimitiveTemplateKind {
+ fn from(kind: PrimitiveKeyKind) -> Self {
+ match kind {
+ PrimitiveKeyKind::Clear => {
+ PrimitiveTemplateKind::Clear
+ }
+ PrimitiveKeyKind::Rectangle { color, .. } => {
+ PrimitiveTemplateKind::Rectangle {
+ color: color.into(),
+ }
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PrimTemplateCommonData {
+ pub flags: PrimitiveFlags,
+ pub may_need_repetition: bool,
+ pub prim_rect: LayoutRect,
+ pub opacity: PrimitiveOpacity,
+ /// The GPU cache handle for a primitive template. Since this structure
+ /// is retained across display lists by interning, this GPU cache handle
+ /// also remains valid, which reduces the number of updates to the GPU
+ /// cache when a new display list is processed.
+ pub gpu_cache_handle: GpuCacheHandle,
+}
+
+impl PrimTemplateCommonData {
+ pub fn with_key_common(common: PrimKeyCommonData) -> Self {
+ PrimTemplateCommonData {
+ flags: common.flags,
+ may_need_repetition: true,
+ prim_rect: common.prim_rect.into(),
+ gpu_cache_handle: GpuCacheHandle::new(),
+ opacity: PrimitiveOpacity::translucent(),
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PrimTemplate<T> {
+ pub common: PrimTemplateCommonData,
+ pub kind: T,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PrimitiveTemplate {
+ pub common: PrimTemplateCommonData,
+ pub kind: PrimitiveTemplateKind,
+}
+
+impl ops::Deref for PrimitiveTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl ops::DerefMut for PrimitiveTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<PrimitiveKey> for PrimitiveTemplate {
+ fn from(item: PrimitiveKey) -> Self {
+ PrimitiveTemplate {
+ common: PrimTemplateCommonData::with_key_common(item.common),
+ kind: item.kind.into(),
+ }
+ }
+}
+
+impl PrimitiveTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ scene_properties: &SceneProperties,
+ ) {
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ self.kind.write_prim_gpu_blocks(&mut request, scene_properties);
+ }
+
+ self.opacity = match self.kind {
+ PrimitiveTemplateKind::Clear => {
+ PrimitiveOpacity::translucent()
+ }
+ PrimitiveTemplateKind::Rectangle { ref color, .. } => {
+ PrimitiveOpacity::from_alpha(scene_properties.resolve_color(color).a)
+ }
+ };
+ }
+}
+
+type PrimitiveDataHandle = intern::Handle<PrimitiveKeyKind>;
+
+impl intern::Internable for PrimitiveKeyKind {
+ type Key = PrimitiveKey;
+ type StoreData = PrimitiveTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_PRIMITIVES;
+}
+
+impl InternablePrimitive for PrimitiveKeyKind {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> PrimitiveKey {
+ PrimitiveKey::new(info, self)
+ }
+
+ fn make_instance_kind(
+ key: PrimitiveKey,
+ data_handle: PrimitiveDataHandle,
+ prim_store: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ match key.kind {
+ PrimitiveKeyKind::Clear => {
+ PrimitiveInstanceKind::Clear {
+ data_handle
+ }
+ }
+ PrimitiveKeyKind::Rectangle { color, .. } => {
+ let color_binding_index = match color {
+ PropertyBinding::Binding(..) => {
+ prim_store.color_bindings.push(color)
+ }
+ PropertyBinding::Value(..) => ColorBindingIndex::INVALID,
+ };
+ PrimitiveInstanceKind::Rectangle {
+ data_handle,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ color_binding_index,
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct VisibleMaskImageTile {
+ pub tile_offset: TileOffset,
+ pub tile_rect: LayoutRect,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct VisibleGradientTile {
+ pub handle: GpuCacheHandle,
+ pub local_rect: LayoutRect,
+ pub local_clip_rect: LayoutRect,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct CachedGradientSegment {
+ pub handle: RenderTaskCacheEntryHandle,
+ pub local_rect: LayoutRect,
+}
+
+/// Information about how to cache a border segment,
+/// along with the current render task cache entry.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, MallocSizeOf)]
+pub struct BorderSegmentInfo {
+ pub local_task_size: LayoutSize,
+ pub cache_key: BorderSegmentCacheKey,
+}
+
+/// Represents the visibility state of a segment (wrt clip masks).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Debug, Clone)]
+pub enum ClipMaskKind {
+ /// The segment has a clip mask, specified by the render task.
+ Mask(RenderTaskId),
+ /// The segment has no clip mask.
+ None,
+ /// The segment is made invisible / clipped completely.
+ Clipped,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf)]
+pub struct BrushSegment {
+ pub local_rect: LayoutRect,
+ pub may_need_clip_mask: bool,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub extra_data: [f32; 4],
+ pub brush_flags: BrushFlags,
+}
+
+impl BrushSegment {
+ pub fn new(
+ local_rect: LayoutRect,
+ may_need_clip_mask: bool,
+ edge_flags: EdgeAaSegmentMask,
+ extra_data: [f32; 4],
+ brush_flags: BrushFlags,
+ ) -> Self {
+ Self {
+ local_rect,
+ may_need_clip_mask,
+ edge_flags,
+ extra_data,
+ brush_flags,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct ClipRect {
+ rect: LayoutRect,
+ mode: f32,
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct ClipCorner {
+ rect: LayoutRect,
+ outer_radius_x: f32,
+ outer_radius_y: f32,
+ inner_radius_x: f32,
+ inner_radius_y: f32,
+}
+
+impl ClipCorner {
+ fn uniform(rect: LayoutRect, outer_radius: f32, inner_radius: f32) -> ClipCorner {
+ ClipCorner {
+ rect,
+ outer_radius_x: outer_radius,
+ outer_radius_y: outer_radius,
+ inner_radius_x: inner_radius,
+ inner_radius_y: inner_radius,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipData {
+ rect: ClipRect,
+ top_left: ClipCorner,
+ top_right: ClipCorner,
+ bottom_left: ClipCorner,
+ bottom_right: ClipCorner,
+}
+
+impl ClipData {
+ pub fn rounded_rect(size: LayoutSize, radii: &BorderRadius, mode: ClipMode) -> ClipData {
+ // TODO(gw): For simplicity, keep most of the clip GPU structs the
+ // same as they were, even though the origin is now always
+ // zero, since they are in the clip's local space. In future,
+ // we could reduce the GPU cache size of ClipData.
+ let rect = LayoutRect::new(
+ LayoutPoint::zero(),
+ size,
+ );
+
+ ClipData {
+ rect: ClipRect {
+ rect,
+ mode: mode as u32 as f32,
+ },
+ top_left: ClipCorner {
+ rect: LayoutRect::new(
+ LayoutPoint::new(rect.origin.x, rect.origin.y),
+ LayoutSize::new(radii.top_left.width, radii.top_left.height),
+ ),
+ outer_radius_x: radii.top_left.width,
+ outer_radius_y: radii.top_left.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ top_right: ClipCorner {
+ rect: LayoutRect::new(
+ LayoutPoint::new(
+ rect.origin.x + rect.size.width - radii.top_right.width,
+ rect.origin.y,
+ ),
+ LayoutSize::new(radii.top_right.width, radii.top_right.height),
+ ),
+ outer_radius_x: radii.top_right.width,
+ outer_radius_y: radii.top_right.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ bottom_left: ClipCorner {
+ rect: LayoutRect::new(
+ LayoutPoint::new(
+ rect.origin.x,
+ rect.origin.y + rect.size.height - radii.bottom_left.height,
+ ),
+ LayoutSize::new(radii.bottom_left.width, radii.bottom_left.height),
+ ),
+ outer_radius_x: radii.bottom_left.width,
+ outer_radius_y: radii.bottom_left.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ bottom_right: ClipCorner {
+ rect: LayoutRect::new(
+ LayoutPoint::new(
+ rect.origin.x + rect.size.width - radii.bottom_right.width,
+ rect.origin.y + rect.size.height - radii.bottom_right.height,
+ ),
+ LayoutSize::new(radii.bottom_right.width, radii.bottom_right.height),
+ ),
+ outer_radius_x: radii.bottom_right.width,
+ outer_radius_y: radii.bottom_right.height,
+ inner_radius_x: 0.0,
+ inner_radius_y: 0.0,
+ },
+ }
+ }
+
+ pub fn uniform(size: LayoutSize, radius: f32, mode: ClipMode) -> ClipData {
+ // TODO(gw): For simplicity, keep most of the clip GPU structs the
+ // same as they were, even though the origin is now always
+ // zero, since they are in the clip's local space. In future,
+ // we could reduce the GPU cache size of ClipData.
+ let rect = LayoutRect::new(
+ LayoutPoint::zero(),
+ size,
+ );
+
+ ClipData {
+ rect: ClipRect {
+ rect,
+ mode: mode as u32 as f32,
+ },
+ top_left: ClipCorner::uniform(
+ LayoutRect::new(
+ LayoutPoint::new(rect.origin.x, rect.origin.y),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ top_right: ClipCorner::uniform(
+ LayoutRect::new(
+ LayoutPoint::new(rect.origin.x + rect.size.width - radius, rect.origin.y),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ bottom_left: ClipCorner::uniform(
+ LayoutRect::new(
+ LayoutPoint::new(rect.origin.x, rect.origin.y + rect.size.height - radius),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ bottom_right: ClipCorner::uniform(
+ LayoutRect::new(
+ LayoutPoint::new(
+ rect.origin.x + rect.size.width - radius,
+ rect.origin.y + rect.size.height - radius,
+ ),
+ LayoutSize::new(radius, radius),
+ ),
+ radius,
+ 0.0,
+ ),
+ }
+ }
+}
+
+/// A hashable descriptor for nine-patches, used by image and
+/// gradient borders.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct NinePatchDescriptor {
+ pub width: i32,
+ pub height: i32,
+ pub slice: DeviceIntSideOffsets,
+ pub fill: bool,
+ pub repeat_horizontal: RepeatMode,
+ pub repeat_vertical: RepeatMode,
+ pub outset: SideOffsetsKey,
+ pub widths: SideOffsetsKey,
+}
+
+impl IsVisible for PrimitiveKeyKind {
+ // Return true if the primary primitive is visible.
+ // Used to trivially reject non-visible primitives.
+ // TODO(gw): Currently, primitives other than those
+ // listed here are handled before the
+ // add_primitive() call. In the future
+ // we should move the logic for all other
+ // primitive types to use this.
+ fn is_visible(&self) -> bool {
+ match *self {
+ PrimitiveKeyKind::Clear => {
+ true
+ }
+ PrimitiveKeyKind::Rectangle { ref color, .. } => {
+ match *color {
+ PropertyBinding::Value(value) => value.a > 0,
+ PropertyBinding::Binding(..) => true,
+ }
+ }
+ }
+ }
+}
+
+impl CreateShadow for PrimitiveKeyKind {
+ // Create a clone of this PrimitiveContainer, applying whatever
+ // changes are necessary to the primitive to support rendering
+ // it as part of the supplied shadow.
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ _: bool,
+ _: RasterSpace,
+ ) -> PrimitiveKeyKind {
+ match *self {
+ PrimitiveKeyKind::Rectangle { .. } => {
+ PrimitiveKeyKind::Rectangle {
+ color: PropertyBinding::Value(shadow.color.into()),
+ }
+ }
+ PrimitiveKeyKind::Clear => {
+ panic!("bug: this prim is not supported in shadow contexts");
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveDebugId(pub usize);
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum PrimitiveInstanceKind {
+ /// Direct reference to a Picture
+ Picture {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PictureDataHandle,
+ pic_index: PictureIndex,
+ segment_instance_index: SegmentInstanceIndex,
+ },
+ /// A run of glyphs, with associated font parameters.
+ TextRun {
+ /// Handle to the common interned data for this primitive.
+ data_handle: TextRunDataHandle,
+ /// Index to the per instance scratch data for this primitive.
+ run_index: TextRunIndex,
+ },
+ /// A line decoration. cache_handle refers to a cached render
+ /// task handle, if this line decoration is not a simple solid.
+ LineDecoration {
+ /// Handle to the common interned data for this primitive.
+ data_handle: LineDecorationDataHandle,
+ // TODO(gw): For now, we need to store some information in
+ // the primitive instance that is created during
+ // prepare_prims and read during the batching pass.
+ // Once we unify the prepare_prims and batching to
+ // occur at the same time, we can remove most of
+ // the things we store here in the instance, and
+ // use them directly. This will remove cache_handle,
+ // but also the opacity, clip_task_id etc below.
+ cache_handle: Option<RenderTaskCacheEntryHandle>,
+ },
+ NormalBorder {
+ /// Handle to the common interned data for this primitive.
+ data_handle: NormalBorderDataHandle,
+ cache_handles: storage::Range<RenderTaskCacheEntryHandle>,
+ },
+ ImageBorder {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ImageBorderDataHandle,
+ },
+ Rectangle {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PrimitiveDataHandle,
+ segment_instance_index: SegmentInstanceIndex,
+ color_binding_index: ColorBindingIndex,
+ },
+ YuvImage {
+ /// Handle to the common interned data for this primitive.
+ data_handle: YuvImageDataHandle,
+ segment_instance_index: SegmentInstanceIndex,
+ is_compositor_surface: bool,
+ },
+ Image {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ImageDataHandle,
+ image_instance_index: ImageInstanceIndex,
+ is_compositor_surface: bool,
+ },
+ LinearGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: LinearGradientDataHandle,
+ gradient_index: LinearGradientIndex,
+ },
+ RadialGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: RadialGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ ConicGradient {
+ /// Handle to the common interned data for this primitive.
+ data_handle: ConicGradientDataHandle,
+ visible_tiles_range: GradientTileRange,
+ },
+ /// Clear out a rect, used for special effects.
+ Clear {
+ /// Handle to the common interned data for this primitive.
+ data_handle: PrimitiveDataHandle,
+ },
+ /// Render a portion of a specified backdrop.
+ Backdrop {
+ data_handle: BackdropDataHandle,
+ },
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveInstance {
+ /// Identifies the kind of primitive this
+ /// instance is, and references to where
+ /// the relevant information for the primitive
+ /// can be found.
+ pub kind: PrimitiveInstanceKind,
+
+ #[cfg(debug_assertions)]
+ pub id: PrimitiveDebugId,
+
+ /// The last frame ID (of the `RenderTaskGraph`) this primitive
+ /// was prepared for rendering in.
+ #[cfg(debug_assertions)]
+ pub prepared_frame_id: FrameId,
+
+ /// All information and state related to clip(s) for this primitive
+ pub clip_set: ClipSet,
+
+ /// Information related to the current visibility state of this
+ /// primitive.
+ // TODO(gw): Currently built each frame, but can be retained.
+ // TODO(gw): Remove clipped_world_rect (use tile bounds to determine vis flags)
+ pub vis: PrimitiveVisibility,
+}
+
+impl PrimitiveInstance {
+ pub fn new(
+ local_clip_rect: LayoutRect,
+ kind: PrimitiveInstanceKind,
+ clip_chain_id: ClipChainId,
+ ) -> Self {
+ PrimitiveInstance {
+ kind,
+ #[cfg(debug_assertions)]
+ prepared_frame_id: FrameId::INVALID,
+ #[cfg(debug_assertions)]
+ id: PrimitiveDebugId(NEXT_PRIM_ID.fetch_add(1, Ordering::Relaxed)),
+ vis: PrimitiveVisibility::new(),
+ clip_set: ClipSet {
+ local_clip_rect,
+ clip_chain_id,
+ },
+ }
+ }
+
+ // Reset any pre-frame state for this primitive.
+ pub fn reset(&mut self) {
+ self.vis.reset();
+ }
+
+ pub fn clear_visibility(&mut self) {
+ self.vis.reset();
+ }
+
+ #[cfg(debug_assertions)]
+ pub fn is_chased(&self) -> bool {
+ PRIM_CHASE_ID.load(Ordering::SeqCst) == self.id.0
+ }
+
+ #[cfg(not(debug_assertions))]
+ pub fn is_chased(&self) -> bool {
+ false
+ }
+
+ pub fn uid(&self) -> intern::ItemUid {
+ match &self.kind {
+ PrimitiveInstanceKind::Clear { data_handle, .. } |
+ PrimitiveInstanceKind::Rectangle { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::Image { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::Picture { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+ data_handle.uid()
+ }
+ PrimitiveInstanceKind::Backdrop { data_handle, .. } => {
+ data_handle.uid()
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[derive(Debug)]
+pub struct SegmentedInstance {
+ pub gpu_cache_handle: GpuCacheHandle,
+ pub segments_range: SegmentsRange,
+}
+
+pub type GlyphKeyStorage = storage::Storage<GlyphKey>;
+pub type TextRunIndex = storage::Index<TextRunPrimitive>;
+pub type TextRunStorage = storage::Storage<TextRunPrimitive>;
+pub type ColorBindingIndex = storage::Index<PropertyBinding<ColorU>>;
+pub type ColorBindingStorage = storage::Storage<PropertyBinding<ColorU>>;
+pub type BorderHandleStorage = storage::Storage<RenderTaskCacheEntryHandle>;
+pub type SegmentStorage = storage::Storage<BrushSegment>;
+pub type SegmentsRange = storage::Range<BrushSegment>;
+pub type SegmentInstanceStorage = storage::Storage<SegmentedInstance>;
+pub type SegmentInstanceIndex = storage::Index<SegmentedInstance>;
+pub type ImageInstanceStorage = storage::Storage<ImageInstance>;
+pub type ImageInstanceIndex = storage::Index<ImageInstance>;
+pub type GradientTileStorage = storage::Storage<VisibleGradientTile>;
+pub type GradientTileRange = storage::Range<VisibleGradientTile>;
+pub type LinearGradientIndex = storage::Index<LinearGradientPrimitive>;
+pub type LinearGradientStorage = storage::Storage<LinearGradientPrimitive>;
+
+/// Contains various vecs of data that is used only during frame building,
+/// where we want to recycle the memory each new display list, to avoid constantly
+/// re-allocating and moving memory around. Written during primitive preparation,
+/// and read during batching.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveScratchBuffer {
+ /// Contains a list of clip mask instance parameters
+ /// per segment generated.
+ pub clip_mask_instances: Vec<ClipMaskKind>,
+
+ /// List of glyphs keys that are allocated by each
+ /// text run instance.
+ pub glyph_keys: GlyphKeyStorage,
+
+ /// List of render task handles for border segment instances
+ /// that have been added this frame.
+ pub border_cache_handles: BorderHandleStorage,
+
+ /// A list of brush segments that have been built for this scene.
+ pub segments: SegmentStorage,
+
+ /// A list of segment ranges and GPU cache handles for prim instances
+ /// that have opted into segment building. In future, this should be
+ /// removed in favor of segment building during primitive interning.
+ pub segment_instances: SegmentInstanceStorage,
+
+ /// A list of visible tiles that tiled gradients use to store
+ /// per-tile information.
+ pub gradient_tiles: GradientTileStorage,
+
+ /// List of debug display items for rendering.
+ pub debug_items: Vec<DebugItem>,
+}
+
+impl Default for PrimitiveScratchBuffer {
+ fn default() -> Self {
+ PrimitiveScratchBuffer {
+ clip_mask_instances: Vec::new(),
+ glyph_keys: GlyphKeyStorage::new(0),
+ border_cache_handles: BorderHandleStorage::new(0),
+ segments: SegmentStorage::new(0),
+ segment_instances: SegmentInstanceStorage::new(0),
+ gradient_tiles: GradientTileStorage::new(0),
+ debug_items: Vec::new(),
+ }
+ }
+}
+
+impl PrimitiveScratchBuffer {
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.clip_mask_instances);
+ self.glyph_keys.recycle(recycler);
+ self.border_cache_handles.recycle(recycler);
+ self.segments.recycle(recycler);
+ self.segment_instances.recycle(recycler);
+ self.gradient_tiles.recycle(recycler);
+ recycler.recycle_vec(&mut self.debug_items);
+ }
+
+ pub fn begin_frame(&mut self) {
+ // Clear the clip mask tasks for the beginning of the frame. Append
+ // a single kind representing no clip mask, at the ClipTaskIndex::INVALID
+ // location.
+ self.clip_mask_instances.clear();
+ self.clip_mask_instances.push(ClipMaskKind::None);
+
+ self.border_cache_handles.clear();
+
+ // TODO(gw): As in the previous code, the gradient tiles store GPU cache
+ // handles that are cleared (and thus invalidated + re-uploaded)
+ // every frame. This maintains the existing behavior, but we
+ // should fix this in the future to retain handles.
+ self.gradient_tiles.clear();
+
+ self.debug_items.clear();
+ }
+
+ #[allow(dead_code)]
+ pub fn push_debug_rect(
+ &mut self,
+ rect: DeviceRect,
+ outer_color: ColorF,
+ inner_color: ColorF,
+ ) {
+ self.debug_items.push(DebugItem::Rect {
+ rect,
+ outer_color,
+ inner_color,
+ });
+ }
+
+ #[allow(dead_code)]
+ pub fn push_debug_string(
+ &mut self,
+ position: DevicePoint,
+ color: ColorF,
+ msg: String,
+ ) {
+ self.debug_items.push(DebugItem::Text {
+ position,
+ color,
+ msg,
+ });
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct PrimitiveStoreStats {
+ picture_count: usize,
+ text_run_count: usize,
+ image_count: usize,
+ linear_gradient_count: usize,
+ color_binding_count: usize,
+}
+
+impl PrimitiveStoreStats {
+ pub fn empty() -> Self {
+ PrimitiveStoreStats {
+ picture_count: 0,
+ text_run_count: 0,
+ image_count: 0,
+ linear_gradient_count: 0,
+ color_binding_count: 0,
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveStore {
+ pub pictures: Vec<PicturePrimitive>,
+ pub text_runs: TextRunStorage,
+ pub linear_gradients: LinearGradientStorage,
+
+ /// A list of image instances. These are stored separately as
+ /// storing them inline in the instance makes the structure bigger
+ /// for other types.
+ pub images: ImageInstanceStorage,
+
+ /// animated color bindings for this primitive.
+ pub color_bindings: ColorBindingStorage,
+}
+
+impl PrimitiveStore {
+ pub fn new(stats: &PrimitiveStoreStats) -> PrimitiveStore {
+ PrimitiveStore {
+ pictures: Vec::with_capacity(stats.picture_count),
+ text_runs: TextRunStorage::new(stats.text_run_count),
+ images: ImageInstanceStorage::new(stats.image_count),
+ color_bindings: ColorBindingStorage::new(stats.color_binding_count),
+ linear_gradients: LinearGradientStorage::new(stats.linear_gradient_count),
+ }
+ }
+
+ pub fn get_stats(&self) -> PrimitiveStoreStats {
+ PrimitiveStoreStats {
+ picture_count: self.pictures.len(),
+ text_run_count: self.text_runs.len(),
+ image_count: self.images.len(),
+ linear_gradient_count: self.linear_gradients.len(),
+ color_binding_count: self.color_bindings.len(),
+ }
+ }
+
+ #[allow(unused)]
+ pub fn print_picture_tree(&self, root: PictureIndex) {
+ use crate::print_tree::PrintTree;
+ let mut pt = PrintTree::new("picture tree");
+ self.pictures[root.0].print(&self.pictures, root, &mut pt);
+ }
+
+ /// Returns the total count of primitive instances contained in pictures.
+ pub fn prim_count(&self) -> usize {
+ let mut prim_count = 0;
+ for pic in &self.pictures {
+ prim_count += pic.prim_list.prim_instances.len();
+ }
+ prim_count
+ }
+}
+
+/// Trait for primitives that are directly internable.
+/// see SceneBuilder::add_primitive<P>
+pub trait InternablePrimitive: intern::Internable<InternData = ()> + Sized {
+ /// Build a new key from self with `info`.
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> Self::Key;
+
+ fn make_instance_kind(
+ key: Self::Key,
+ data_handle: intern::Handle<Self>,
+ prim_store: &mut PrimitiveStore,
+ reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind;
+}
+
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<PrimitiveInstance>(), 152, "PrimitiveInstance size changed");
+ assert_eq!(mem::size_of::<PrimitiveInstanceKind>(), 24, "PrimitiveInstanceKind size changed");
+ assert_eq!(mem::size_of::<PrimitiveTemplate>(), 56, "PrimitiveTemplate size changed");
+ assert_eq!(mem::size_of::<PrimitiveTemplateKind>(), 28, "PrimitiveTemplateKind size changed");
+ assert_eq!(mem::size_of::<PrimitiveKey>(), 36, "PrimitiveKey size changed");
+ assert_eq!(mem::size_of::<PrimitiveKeyKind>(), 16, "PrimitiveKeyKind size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/picture.rs b/gfx/wr/webrender/src/prim_store/picture.rs
new file mode 100644
index 0000000000..d0815cdac8
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/picture.rs
@@ -0,0 +1,322 @@
+/* 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 api::{
+ ColorU, MixBlendMode, FilterPrimitiveInput, FilterPrimitiveKind, ColorSpace,
+ PropertyBinding, PropertyBindingId, CompositeOperator,
+};
+use api::units::{Au, LayoutVector2D};
+use crate::scene_building::IsVisible;
+use crate::filterdata::SFilterData;
+use crate::intern::ItemUid;
+use crate::intern::{Internable, InternDebug, Handle as InternHandle};
+use crate::internal_types::{LayoutPrimitiveInfo, Filter};
+use crate::picture::PictureCompositeMode;
+use crate::prim_store::{
+ PrimitiveInstanceKind, PrimitiveStore, VectorKey,
+ InternablePrimitive,
+};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum CompositeOperatorKey {
+ Over,
+ In,
+ Out,
+ Atop,
+ Xor,
+ Lighter,
+ Arithmetic([Au; 4]),
+}
+
+impl From<CompositeOperator> for CompositeOperatorKey {
+ fn from(operator: CompositeOperator) -> Self {
+ match operator {
+ CompositeOperator::Over => CompositeOperatorKey::Over,
+ CompositeOperator::In => CompositeOperatorKey::In,
+ CompositeOperator::Out => CompositeOperatorKey::Out,
+ CompositeOperator::Atop => CompositeOperatorKey::Atop,
+ CompositeOperator::Xor => CompositeOperatorKey::Xor,
+ CompositeOperator::Lighter => CompositeOperatorKey::Lighter,
+ CompositeOperator::Arithmetic(k_vals) => {
+ let k_vals = [
+ Au::from_f32_px(k_vals[0]),
+ Au::from_f32_px(k_vals[1]),
+ Au::from_f32_px(k_vals[2]),
+ Au::from_f32_px(k_vals[3]),
+ ];
+ CompositeOperatorKey::Arithmetic(k_vals)
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum FilterPrimitiveKey {
+ Identity(ColorSpace, FilterPrimitiveInput),
+ Flood(ColorSpace, ColorU),
+ Blend(ColorSpace, MixBlendMode, FilterPrimitiveInput, FilterPrimitiveInput),
+ Blur(ColorSpace, Au, Au, FilterPrimitiveInput),
+ Opacity(ColorSpace, Au, FilterPrimitiveInput),
+ ColorMatrix(ColorSpace, [Au; 20], FilterPrimitiveInput),
+ DropShadow(ColorSpace, (VectorKey, Au, ColorU), FilterPrimitiveInput),
+ ComponentTransfer(ColorSpace, FilterPrimitiveInput, Vec<SFilterData>),
+ Offset(ColorSpace, FilterPrimitiveInput, VectorKey),
+ Composite(ColorSpace, FilterPrimitiveInput, FilterPrimitiveInput, CompositeOperatorKey),
+}
+
+/// Represents a hashable description of how a picture primitive
+/// will be composited into its parent.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, MallocSizeOf, PartialEq, Hash, Eq)]
+pub enum PictureCompositeKey {
+ // No visual compositing effect
+ Identity,
+
+ // FilterOp
+ Blur(Au, Au),
+ Brightness(Au),
+ Contrast(Au),
+ Grayscale(Au),
+ HueRotate(Au),
+ Invert(Au),
+ Opacity(Au),
+ OpacityBinding(PropertyBindingId, Au),
+ Saturate(Au),
+ Sepia(Au),
+ DropShadows(Vec<(VectorKey, Au, ColorU)>),
+ ColorMatrix([Au; 20]),
+ SrgbToLinear,
+ LinearToSrgb,
+ ComponentTransfer(ItemUid),
+ Flood(ColorU),
+ SvgFilter(Vec<FilterPrimitiveKey>),
+
+ // MixBlendMode
+ Multiply,
+ Screen,
+ Overlay,
+ Darken,
+ Lighten,
+ ColorDodge,
+ ColorBurn,
+ HardLight,
+ SoftLight,
+ Difference,
+ Exclusion,
+ Hue,
+ Saturation,
+ Color,
+ Luminosity,
+}
+
+impl From<Option<PictureCompositeMode>> for PictureCompositeKey {
+ fn from(mode: Option<PictureCompositeMode>) -> Self {
+ match mode {
+ Some(PictureCompositeMode::MixBlend(mode)) => {
+ match mode {
+ MixBlendMode::Normal => PictureCompositeKey::Identity,
+ MixBlendMode::Multiply => PictureCompositeKey::Multiply,
+ MixBlendMode::Screen => PictureCompositeKey::Screen,
+ MixBlendMode::Overlay => PictureCompositeKey::Overlay,
+ MixBlendMode::Darken => PictureCompositeKey::Darken,
+ MixBlendMode::Lighten => PictureCompositeKey::Lighten,
+ MixBlendMode::ColorDodge => PictureCompositeKey::ColorDodge,
+ MixBlendMode::ColorBurn => PictureCompositeKey::ColorBurn,
+ MixBlendMode::HardLight => PictureCompositeKey::HardLight,
+ MixBlendMode::SoftLight => PictureCompositeKey::SoftLight,
+ MixBlendMode::Difference => PictureCompositeKey::Difference,
+ MixBlendMode::Exclusion => PictureCompositeKey::Exclusion,
+ MixBlendMode::Hue => PictureCompositeKey::Hue,
+ MixBlendMode::Saturation => PictureCompositeKey::Saturation,
+ MixBlendMode::Color => PictureCompositeKey::Color,
+ MixBlendMode::Luminosity => PictureCompositeKey::Luminosity,
+ }
+ }
+ Some(PictureCompositeMode::Filter(op)) => {
+ match op {
+ Filter::Blur(width, height) =>
+ PictureCompositeKey::Blur(Au::from_f32_px(width), Au::from_f32_px(height)),
+ Filter::Brightness(value) => PictureCompositeKey::Brightness(Au::from_f32_px(value)),
+ Filter::Contrast(value) => PictureCompositeKey::Contrast(Au::from_f32_px(value)),
+ Filter::Grayscale(value) => PictureCompositeKey::Grayscale(Au::from_f32_px(value)),
+ Filter::HueRotate(value) => PictureCompositeKey::HueRotate(Au::from_f32_px(value)),
+ Filter::Invert(value) => PictureCompositeKey::Invert(Au::from_f32_px(value)),
+ Filter::Saturate(value) => PictureCompositeKey::Saturate(Au::from_f32_px(value)),
+ Filter::Sepia(value) => PictureCompositeKey::Sepia(Au::from_f32_px(value)),
+ Filter::SrgbToLinear => PictureCompositeKey::SrgbToLinear,
+ Filter::LinearToSrgb => PictureCompositeKey::LinearToSrgb,
+ Filter::Identity => PictureCompositeKey::Identity,
+ Filter::DropShadows(ref shadows) => {
+ PictureCompositeKey::DropShadows(
+ shadows.iter().map(|shadow| {
+ (shadow.offset.into(), Au::from_f32_px(shadow.blur_radius), shadow.color.into())
+ }).collect()
+ )
+ }
+ Filter::Opacity(binding, _) => {
+ match binding {
+ PropertyBinding::Value(value) => {
+ PictureCompositeKey::Opacity(Au::from_f32_px(value))
+ }
+ PropertyBinding::Binding(key, default) => {
+ PictureCompositeKey::OpacityBinding(key.id, Au::from_f32_px(default))
+ }
+ }
+ }
+ Filter::ColorMatrix(values) => {
+ let mut quantized_values: [Au; 20] = [Au(0); 20];
+ for (value, result) in values.iter().zip(quantized_values.iter_mut()) {
+ *result = Au::from_f32_px(*value);
+ }
+ PictureCompositeKey::ColorMatrix(quantized_values)
+ }
+ Filter::ComponentTransfer => unreachable!(),
+ Filter::Flood(color) => PictureCompositeKey::Flood(color.into()),
+ }
+ }
+ Some(PictureCompositeMode::ComponentTransferFilter(handle)) => {
+ PictureCompositeKey::ComponentTransfer(handle.uid())
+ }
+ Some(PictureCompositeMode::SvgFilter(filter_primitives, filter_data)) => {
+ PictureCompositeKey::SvgFilter(filter_primitives.into_iter().map(|primitive| {
+ match primitive.kind {
+ FilterPrimitiveKind::Identity(identity) => FilterPrimitiveKey::Identity(primitive.color_space, identity.input),
+ FilterPrimitiveKind::Blend(blend) => FilterPrimitiveKey::Blend(primitive.color_space, blend.mode, blend.input1, blend.input2),
+ FilterPrimitiveKind::Flood(flood) => FilterPrimitiveKey::Flood(primitive.color_space, flood.color.into()),
+ FilterPrimitiveKind::Blur(blur) =>
+ FilterPrimitiveKey::Blur(primitive.color_space, Au::from_f32_px(blur.width), Au::from_f32_px(blur.height), blur.input),
+ FilterPrimitiveKind::Opacity(opacity) =>
+ FilterPrimitiveKey::Opacity(primitive.color_space, Au::from_f32_px(opacity.opacity), opacity.input),
+ FilterPrimitiveKind::ColorMatrix(color_matrix) => {
+ let mut quantized_values: [Au; 20] = [Au(0); 20];
+ for (value, result) in color_matrix.matrix.iter().zip(quantized_values.iter_mut()) {
+ *result = Au::from_f32_px(*value);
+ }
+ FilterPrimitiveKey::ColorMatrix(primitive.color_space, quantized_values, color_matrix.input)
+ }
+ FilterPrimitiveKind::DropShadow(drop_shadow) => {
+ FilterPrimitiveKey::DropShadow(
+ primitive.color_space,
+ (
+ drop_shadow.shadow.offset.into(),
+ Au::from_f32_px(drop_shadow.shadow.blur_radius),
+ drop_shadow.shadow.color.into(),
+ ),
+ drop_shadow.input,
+ )
+ }
+ FilterPrimitiveKind::ComponentTransfer(component_transfer) =>
+ FilterPrimitiveKey::ComponentTransfer(primitive.color_space, component_transfer.input, filter_data.clone()),
+ FilterPrimitiveKind::Offset(info) =>
+ FilterPrimitiveKey::Offset(primitive.color_space, info.input, info.offset.into()),
+ FilterPrimitiveKind::Composite(info) =>
+ FilterPrimitiveKey::Composite(primitive.color_space, info.input1, info.input2, info.operator.into()),
+ }
+ }).collect())
+ }
+ Some(PictureCompositeMode::Blit(_)) |
+ Some(PictureCompositeMode::TileCache { .. }) |
+ None => {
+ PictureCompositeKey::Identity
+ }
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct Picture {
+ pub composite_mode_key: PictureCompositeKey,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct PictureKey {
+ pub composite_mode_key: PictureCompositeKey,
+}
+
+impl PictureKey {
+ pub fn new(
+ pic: Picture,
+ ) -> Self {
+ PictureKey {
+ composite_mode_key: pic.composite_mode_key,
+ }
+ }
+}
+
+impl InternDebug for PictureKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PictureData;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct PictureTemplate;
+
+impl From<PictureKey> for PictureTemplate {
+ fn from(_: PictureKey) -> Self {
+ PictureTemplate
+ }
+}
+
+pub type PictureDataHandle = InternHandle<Picture>;
+
+impl Internable for Picture {
+ type Key = PictureKey;
+ type StoreData = PictureTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_PICTURES;
+}
+
+impl InternablePrimitive for Picture {
+ fn into_key(
+ self,
+ _: &LayoutPrimitiveInfo,
+ ) -> PictureKey {
+ PictureKey::new(self)
+ }
+
+ fn make_instance_kind(
+ _key: PictureKey,
+ _: PictureDataHandle,
+ _: &mut PrimitiveStore,
+ _reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ // Should never be hit as this method should not be
+ // called for pictures.
+ unreachable!();
+ }
+}
+
+impl IsVisible for Picture {
+ fn is_visible(&self) -> bool {
+ true
+ }
+}
+
+#[test]
+#[cfg(target_pointer_width = "64")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<Picture>(), 88, "Picture size changed");
+ assert_eq!(mem::size_of::<PictureTemplate>(), 0, "PictureTemplate size changed");
+ assert_eq!(mem::size_of::<PictureKey>(), 88, "PictureKey size changed");
+}
diff --git a/gfx/wr/webrender/src/prim_store/storage.rs b/gfx/wr/webrender/src/prim_store/storage.rs
new file mode 100644
index 0000000000..a928192cd9
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/storage.rs
@@ -0,0 +1,134 @@
+/* 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 std::{iter::Extend, ops, marker::PhantomData, u32};
+use crate::util::Recycler;
+
+#[derive(Debug, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Index<T>(u32, PhantomData<T>);
+
+// We explicitly implement Copy + Clone instead of using #[derive(Copy, Clone)]
+// because we don't want to require that T implements Clone + Copy.
+impl<T> Clone for Index<T> {
+ fn clone(&self) -> Self { *self }
+}
+
+impl<T> Copy for Index<T> {}
+
+impl<T> PartialEq for Index<T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+
+impl<T> Index<T> {
+ fn new(idx: usize) -> Self {
+ debug_assert!(idx < u32::max_value() as usize);
+ Index(idx as u32, PhantomData)
+ }
+
+ pub const INVALID: Index<T> = Index(u32::MAX, PhantomData);
+ pub const UNUSED: Index<T> = Index(u32::MAX-1, PhantomData);
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct Range<T> {
+ pub start: Index<T>,
+ pub end: Index<T>,
+}
+
+// We explicitly implement Copy + Clone instead of using #[derive(Copy, Clone)]
+// because we don't want to require that T implements Clone + Copy.
+impl<T> Clone for Range<T> {
+ fn clone(&self) -> Self {
+ Range { start: self.start, end: self.end }
+ }
+}
+impl<T> Copy for Range<T> {}
+
+impl<T> Range<T> {
+ /// Create an empty `Range`
+ pub fn empty() -> Self {
+ Range {
+ start: Index::new(0),
+ end: Index::new(0),
+ }
+ }
+
+ /// Check for an empty `Range`
+ pub fn is_empty(self) -> bool {
+ self.start.0 >= self.end.0
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct Storage<T> {
+ data: Vec<T>,
+}
+
+impl<T> Storage<T> {
+ pub fn new(initial_capacity: usize) -> Self {
+ Storage {
+ data: Vec::with_capacity(initial_capacity),
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.data.len()
+ }
+
+ pub fn clear(&mut self) {
+ self.data.clear();
+ }
+
+ pub fn push(&mut self, t: T) -> Index<T> {
+ let index = self.data.len();
+ self.data.push(t);
+ Index(index as u32, PhantomData)
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ recycler.recycle_vec(&mut self.data);
+ }
+
+ pub fn extend<II: IntoIterator<Item=T>>(&mut self, iter: II) -> Range<T> {
+ let start = Index::new(self.data.len());
+ self.data.extend(iter);
+ let end = Index::new(self.data.len());
+ Range { start, end }
+ }
+}
+
+impl<T> ops::Index<Index<T>> for Storage<T> {
+ type Output = T;
+ fn index(&self, index: Index<T>) -> &Self::Output {
+ &self.data[index.0 as usize]
+ }
+}
+
+impl<T> ops::IndexMut<Index<T>> for Storage<T> {
+ fn index_mut(&mut self, index: Index<T>) -> &mut Self::Output {
+ &mut self.data[index.0 as usize]
+ }
+}
+
+impl<T> ops::Index<Range<T>> for Storage<T> {
+ type Output = [T];
+ fn index(&self, index: Range<T>) -> &Self::Output {
+ let start = index.start.0 as _;
+ let end = index.end.0 as _;
+ &self.data[start..end]
+ }
+}
+
+impl<T> ops::IndexMut<Range<T>> for Storage<T> {
+ fn index_mut(&mut self, index: Range<T>) -> &mut Self::Output {
+ let start = index.start.0 as _;
+ let end = index.end.0 as _;
+ &mut self.data[start..end]
+ }
+}
diff --git a/gfx/wr/webrender/src/prim_store/text_run.rs b/gfx/wr/webrender/src/prim_store/text_run.rs
new file mode 100644
index 0000000000..534c1ef680
--- /dev/null
+++ b/gfx/wr/webrender/src/prim_store/text_run.rs
@@ -0,0 +1,492 @@
+/* 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 api::{ColorF, FontInstanceFlags, GlyphInstance, RasterSpace, Shadow};
+use api::units::{LayoutToWorldTransform, LayoutVector2D, PictureRect};
+use crate::scene_building::{CreateShadow, IsVisible};
+use crate::frame_builder::FrameBuildingState;
+use crate::glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
+use crate::gpu_cache::GpuCache;
+use crate::intern;
+use crate::internal_types::LayoutPrimitiveInfo;
+use crate::picture::{SubpixelMode, SurfaceInfo};
+use crate::prim_store::{PrimitiveOpacity, PrimitiveScratchBuffer};
+use crate::prim_store::{PrimitiveStore, PrimKeyCommonData, PrimTemplateCommonData};
+use crate::renderer::{MAX_VERTEX_TEXTURE_WIDTH};
+use crate::resource_cache::{ResourceCache};
+use crate::util::{MatrixHelpers};
+use crate::prim_store::{InternablePrimitive, PrimitiveInstanceKind};
+use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, ROOT_SPATIAL_NODE_INDEX};
+use crate::space::SpaceSnapper;
+use crate::util::PrimaryArc;
+
+use std::ops;
+use std::sync::Arc;
+
+use super::storage;
+
+/// A run of glyphs, with associated font information.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
+pub struct TextRunKey {
+ pub common: PrimKeyCommonData,
+ pub font: FontInstance,
+ pub glyphs: PrimaryArc<Vec<GlyphInstance>>,
+ pub shadow: bool,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl TextRunKey {
+ pub fn new(
+ info: &LayoutPrimitiveInfo,
+ text_run: TextRun,
+ ) -> Self {
+ TextRunKey {
+ common: info.into(),
+ font: text_run.font,
+ glyphs: PrimaryArc(text_run.glyphs),
+ shadow: text_run.shadow,
+ requested_raster_space: text_run.requested_raster_space,
+ }
+ }
+}
+
+impl intern::InternDebug for TextRunKey {}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(MallocSizeOf)]
+pub struct TextRunTemplate {
+ pub common: PrimTemplateCommonData,
+ pub font: FontInstance,
+ #[ignore_malloc_size_of = "Measured via PrimaryArc"]
+ pub glyphs: Arc<Vec<GlyphInstance>>,
+}
+
+impl ops::Deref for TextRunTemplate {
+ type Target = PrimTemplateCommonData;
+ fn deref(&self) -> &Self::Target {
+ &self.common
+ }
+}
+
+impl ops::DerefMut for TextRunTemplate {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.common
+ }
+}
+
+impl From<TextRunKey> for TextRunTemplate {
+ fn from(item: TextRunKey) -> Self {
+ let common = PrimTemplateCommonData::with_key_common(item.common);
+ TextRunTemplate {
+ common,
+ font: item.font,
+ glyphs: item.glyphs.0,
+ }
+ }
+}
+
+impl TextRunTemplate {
+ /// Update the GPU cache for a given primitive template. This may be called multiple
+ /// times per frame, by each primitive reference that refers to this interned
+ /// template. The initial request call to the GPU cache ensures that work is only
+ /// done if the cache entry is invalid (due to first use or eviction).
+ pub fn update(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ self.write_prim_gpu_blocks(frame_state);
+ self.opacity = PrimitiveOpacity::translucent();
+ }
+
+ fn write_prim_gpu_blocks(
+ &mut self,
+ frame_state: &mut FrameBuildingState,
+ ) {
+ // corresponds to `fetch_glyph` in the shaders
+ if let Some(mut request) = frame_state.gpu_cache.request(&mut self.common.gpu_cache_handle) {
+ request.push(ColorF::from(self.font.color).premultiplied());
+ // this is the only case where we need to provide plain color to GPU
+ let bg_color = ColorF::from(self.font.bg_color);
+ request.push([bg_color.r, bg_color.g, bg_color.b, 1.0]);
+
+ let mut gpu_block = [0.0; 4];
+ for (i, src) in self.glyphs.iter().enumerate() {
+ // Two glyphs are packed per GPU block.
+
+ if (i & 1) == 0 {
+ gpu_block[0] = src.point.x;
+ gpu_block[1] = src.point.y;
+ } else {
+ gpu_block[2] = src.point.x;
+ gpu_block[3] = src.point.y;
+ request.push(gpu_block);
+ }
+ }
+
+ // Ensure the last block is added in the case
+ // of an odd number of glyphs.
+ if (self.glyphs.len() & 1) != 0 {
+ request.push(gpu_block);
+ }
+
+ assert!(request.current_used_block_num() <= MAX_VERTEX_TEXTURE_WIDTH);
+ }
+ }
+}
+
+pub type TextRunDataHandle = intern::Handle<TextRun>;
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextRun {
+ pub font: FontInstance,
+ #[ignore_malloc_size_of = "Measured via PrimaryArc"]
+ pub glyphs: Arc<Vec<GlyphInstance>>,
+ pub shadow: bool,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl intern::Internable for TextRun {
+ type Key = TextRunKey;
+ type StoreData = TextRunTemplate;
+ type InternData = ();
+ const PROFILE_COUNTER: usize = crate::profiler::INTERNED_TEXT_RUNS;
+}
+
+impl InternablePrimitive for TextRun {
+ fn into_key(
+ self,
+ info: &LayoutPrimitiveInfo,
+ ) -> TextRunKey {
+ TextRunKey::new(
+ info,
+ self,
+ )
+ }
+
+ fn make_instance_kind(
+ key: TextRunKey,
+ data_handle: TextRunDataHandle,
+ prim_store: &mut PrimitiveStore,
+ reference_frame_relative_offset: LayoutVector2D,
+ ) -> PrimitiveInstanceKind {
+ let run_index = prim_store.text_runs.push(TextRunPrimitive {
+ used_font: key.font.clone(),
+ glyph_keys_range: storage::Range::empty(),
+ reference_frame_relative_offset,
+ snapped_reference_frame_relative_offset: reference_frame_relative_offset,
+ shadow: key.shadow,
+ raster_scale: 1.0,
+ requested_raster_space: key.requested_raster_space,
+ });
+
+ PrimitiveInstanceKind::TextRun{ data_handle, run_index }
+ }
+}
+
+impl CreateShadow for TextRun {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ blur_is_noop: bool,
+ current_raster_space: RasterSpace,
+ ) -> Self {
+ let mut font = FontInstance {
+ color: shadow.color.into(),
+ ..self.font.clone()
+ };
+ if shadow.blur_radius > 0.0 {
+ font.disable_subpixel_aa();
+ }
+
+ let requested_raster_space = if blur_is_noop {
+ current_raster_space
+ } else {
+ RasterSpace::Local(1.0)
+ };
+
+ TextRun {
+ font,
+ glyphs: self.glyphs.clone(),
+ shadow: true,
+ requested_raster_space,
+ }
+ }
+}
+
+impl IsVisible for TextRun {
+ fn is_visible(&self) -> bool {
+ self.font.color.a > 0
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct TextRunPrimitive {
+ pub used_font: FontInstance,
+ pub glyph_keys_range: storage::Range<GlyphKey>,
+ pub reference_frame_relative_offset: LayoutVector2D,
+ pub snapped_reference_frame_relative_offset: LayoutVector2D,
+ pub shadow: bool,
+ pub raster_scale: f32,
+ pub requested_raster_space: RasterSpace,
+}
+
+impl TextRunPrimitive {
+ pub fn update_font_instance(
+ &mut self,
+ specified_font: &FontInstance,
+ surface: &SurfaceInfo,
+ spatial_node_index: SpatialNodeIndex,
+ transform: &LayoutToWorldTransform,
+ subpixel_mode: &SubpixelMode,
+ raster_space: RasterSpace,
+ prim_rect: PictureRect,
+ root_scaling_factor: f32,
+ spatial_tree: &SpatialTree,
+ ) -> bool {
+ // If local raster space is specified, include that in the scale
+ // of the glyphs that get rasterized.
+ // TODO(gw): Once we support proper local space raster modes, this
+ // will implicitly be part of the device pixel ratio for
+ // the (cached) local space surface, and so this code
+ // will no longer be required.
+ let raster_scale = raster_space.local_scale().unwrap_or(1.0).max(0.001);
+
+ // root_scaling_factor is used to scale very large pictures that establish
+ // a raster root back to something sane, thus scale the device size accordingly.
+ // to the shader it looks like a change in DPI which it already supports.
+ let dps = surface.device_pixel_scale.0 * root_scaling_factor;
+ let font_size = specified_font.size.to_f32_px();
+ let mut device_font_size = font_size * dps * raster_scale;
+
+ // Check there is a valid transform that doesn't exceed the font size limit.
+ // Ensure the font is supposed to be rasterized in screen-space.
+ // Only support transforms that can be coerced to simple 2D transforms.
+ // Add texture padding to the rasterized glyph buffer when one anticipates
+ // the glyph will need to be scaled when rendered.
+ let (use_subpixel_aa, transform_glyphs, texture_padding, oversized) = if raster_space != RasterSpace::Screen ||
+ transform.has_perspective_component() || !transform.has_2d_inverse()
+ {
+ (false, false, true, device_font_size > FONT_SIZE_LIMIT)
+ } else if transform.exceeds_2d_scale((FONT_SIZE_LIMIT / device_font_size) as f64) {
+ (false, false, true, true)
+ } else {
+ (true, !transform.is_simple_2d_translation(), false, false)
+ };
+
+ let font_transform = if transform_glyphs {
+ // Get the font transform matrix (skew / scale) from the complete transform.
+ // Fold in the device pixel scale.
+ self.raster_scale = 1.0;
+ FontTransform::from(transform)
+ } else {
+ if oversized {
+ // Font sizes larger than the limit need to be scaled, thus can't use subpixels.
+ // In this case we adjust the font size and raster space to ensure
+ // we rasterize at the limit, to minimize the amount of scaling.
+ let limited_raster_scale = FONT_SIZE_LIMIT / (font_size * dps);
+ device_font_size = FONT_SIZE_LIMIT;
+
+ // Record the raster space the text needs to be snapped in. The original raster
+ // scale would have been too big.
+ self.raster_scale = limited_raster_scale;
+ } else {
+ // Record the raster space the text needs to be snapped in. We may have changed
+ // from RasterSpace::Screen due to a transform with perspective or without a 2d
+ // inverse, or it may have been RasterSpace::Local all along.
+ self.raster_scale = raster_scale;
+ }
+
+ // Rasterize the glyph without any transform
+ FontTransform::identity()
+ };
+
+ // TODO(aosmond): Snapping really ought to happen during scene building
+ // as much as possible. This will allow clips to be already adjusted
+ // based on the snapping requirements of the primitive. This may affect
+ // complex clips that create a different task, and when we rasterize
+ // glyphs without the transform (because the shader doesn't have the
+ // snap offsets to adjust its clip). These rects are fairly conservative
+ // to begin with and do not appear to be causing significant issues at
+ // this time.
+ self.snapped_reference_frame_relative_offset = if transform_glyphs {
+ // Don't touch the reference frame relative offset. We'll let the
+ // shader do the snapping in device pixels.
+ self.reference_frame_relative_offset
+ } else {
+ // There may be an animation, so snap the reference frame relative
+ // offset such that it excludes the impact, if any.
+ let snap_to_device = SpaceSnapper::new_with_target(
+ surface.raster_spatial_node_index,
+ spatial_node_index,
+ surface.device_pixel_scale,
+ spatial_tree,
+ );
+ snap_to_device.snap_point(&self.reference_frame_relative_offset.to_point()).to_vector()
+ };
+
+ let mut flags = specified_font.flags;
+ if transform_glyphs {
+ flags |= FontInstanceFlags::TRANSFORM_GLYPHS;
+ }
+ if texture_padding {
+ flags |= FontInstanceFlags::TEXTURE_PADDING;
+ }
+
+ // If the transform or device size is different, then the caller of
+ // this method needs to know to rebuild the glyphs.
+ let cache_dirty =
+ self.used_font.transform != font_transform ||
+ self.used_font.size != device_font_size.into() ||
+ self.used_font.flags != flags;
+
+ // Construct used font instance from the specified font instance
+ self.used_font = FontInstance {
+ transform: font_transform,
+ size: device_font_size.into(),
+ flags,
+ ..specified_font.clone()
+ };
+
+ // If subpixel AA is disabled due to the backing surface the glyphs
+ // are being drawn onto, disable it (unless we are using the
+ // specifial subpixel mode that estimates background color).
+ let mut allow_subpixel = match subpixel_mode {
+ SubpixelMode::Allow => true,
+ SubpixelMode::Deny => false,
+ SubpixelMode::Conditional { allowed_rect, excluded_rects } => {
+ // Conditional mode allows subpixel AA to be enabled for this
+ // text run, so long as it doesn't intersect with any of the
+ // cutout rectangles in the list, and it's inside the allowed rect.
+ allowed_rect.contains_rect(&prim_rect) &&
+ excluded_rects.iter().all(|rect| !rect.intersects(&prim_rect))
+ }
+ };
+
+ // If we are using special estimated background subpixel blending, then
+ // we can allow it regardless of what the surface says.
+ allow_subpixel |= self.used_font.bg_color.a != 0;
+
+ // If using local space glyphs, we don't want subpixel AA.
+ if !allow_subpixel || !use_subpixel_aa {
+ self.used_font.disable_subpixel_aa();
+
+ // Disable subpixel positioning for oversized glyphs to avoid
+ // thrashing the glyph cache with many subpixel variations of
+ // big glyph textures. A possible subpixel positioning error
+ // is small relative to the maximum font size and thus should
+ // not be very noticeable.
+ if oversized {
+ self.used_font.disable_subpixel_position();
+ }
+ }
+
+ cache_dirty
+ }
+
+ /// Gets the raster space to use when rendering this primitive.
+ /// Usually this would be the requested raster space. However, if
+ /// the primitive's spatial node or one of its ancestors is being pinch zoomed
+ /// then we round it. This prevents us rasterizing glyphs for every minor
+ /// change in zoom level, as that would be too expensive.
+ fn get_raster_space_for_prim(
+ &self,
+ prim_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> RasterSpace {
+ let prim_spatial_node = &spatial_tree.spatial_nodes[prim_spatial_node_index.0 as usize];
+ if prim_spatial_node.is_ancestor_or_self_zooming {
+ let scale_factors = spatial_tree
+ .get_relative_transform(prim_spatial_node_index, ROOT_SPATIAL_NODE_INDEX)
+ .scale_factors();
+
+ // Round the scale up to the nearest power of 2, but don't exceed 8.
+ let scale = scale_factors.0.max(scale_factors.1).min(8.0);
+ let rounded_up = 2.0f32.powf(scale.log2().ceil());
+
+ RasterSpace::Local(rounded_up)
+ } else {
+ self.requested_raster_space
+ }
+ }
+
+ pub fn request_resources(
+ &mut self,
+ prim_offset: LayoutVector2D,
+ prim_rect: PictureRect,
+ specified_font: &FontInstance,
+ glyphs: &[GlyphInstance],
+ transform: &LayoutToWorldTransform,
+ surface: &SurfaceInfo,
+ spatial_node_index: SpatialNodeIndex,
+ root_scaling_factor: f32,
+ subpixel_mode: &SubpixelMode,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ spatial_tree: &SpatialTree,
+ scratch: &mut PrimitiveScratchBuffer,
+ ) {
+ let raster_space = self.get_raster_space_for_prim(
+ spatial_node_index,
+ spatial_tree,
+ );
+
+ let cache_dirty = self.update_font_instance(
+ specified_font,
+ surface,
+ spatial_node_index,
+ transform,
+ subpixel_mode,
+ raster_space,
+ prim_rect,
+ root_scaling_factor,
+ spatial_tree,
+ );
+
+ if self.glyph_keys_range.is_empty() || cache_dirty {
+ let subpx_dir = self.used_font.get_subpx_dir();
+
+ let dps = surface.device_pixel_scale.0 * root_scaling_factor;
+ let transform = match raster_space {
+ RasterSpace::Local(scale) => FontTransform::new(scale * dps, 0.0, 0.0, scale * dps),
+ RasterSpace::Screen => self.used_font.transform.scale(dps),
+ };
+
+ self.glyph_keys_range = scratch.glyph_keys.extend(
+ glyphs.iter().map(|src| {
+ let src_point = src.point + prim_offset;
+ let device_offset = transform.transform(&src_point);
+ GlyphKey::new(src.index, device_offset, subpx_dir)
+ }));
+ }
+
+ resource_cache.request_glyphs(
+ self.used_font.clone(),
+ &scratch.glyph_keys[self.glyph_keys_range],
+ gpu_cache,
+ );
+ }
+}
+
+/// These are linux only because FontInstancePlatformOptions varies in size by platform.
+#[test]
+#[cfg(target_os = "linux")]
+fn test_struct_sizes() {
+ use std::mem;
+ // The sizes of these structures are critical for performance on a number of
+ // talos stress tests. If you get a failure here on CI, there's two possibilities:
+ // (a) You made a structure smaller than it currently is. Great work! Update the
+ // test expectations and move on.
+ // (b) You made a structure larger. This is not necessarily a problem, but should only
+ // be done with care, and after checking if talos performance regresses badly.
+ assert_eq!(mem::size_of::<TextRun>(), 64, "TextRun size changed");
+ assert_eq!(mem::size_of::<TextRunTemplate>(), 80, "TextRunTemplate size changed");
+ assert_eq!(mem::size_of::<TextRunKey>(), 80, "TextRunKey size changed");
+ assert_eq!(mem::size_of::<TextRunPrimitive>(), 80, "TextRunPrimitive size changed");
+}
diff --git a/gfx/wr/webrender/src/print_tree.rs b/gfx/wr/webrender/src/print_tree.rs
new file mode 100644
index 0000000000..bfcb82fa2a
--- /dev/null
+++ b/gfx/wr/webrender/src/print_tree.rs
@@ -0,0 +1,105 @@
+/* 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 std::io::{stdout, Stdout, Write};
+
+/// A struct that makes it easier to print out a pretty tree of data, which
+/// can be visually scanned more easily.
+pub struct PrintTree<W>
+where
+ W: Write
+{
+ /// The current level of recursion.
+ level: u32,
+
+ /// An item which is queued up, so that we can determine if we need
+ /// a mid-tree prefix or a branch ending prefix.
+ queued_item: Option<String>,
+
+ /// The sink to print to.
+ sink: W,
+}
+
+/// A trait that makes it easy to describe a pretty tree of data,
+/// regardless of the printing destination, to either print it
+/// directly to stdout, or serialize it as in the debugger
+pub trait PrintTreePrinter {
+ fn new_level(&mut self, title: String);
+ fn end_level(&mut self);
+ fn add_item(&mut self, text: String);
+}
+
+impl PrintTree<Stdout> {
+ pub fn new(title: &str) -> Self {
+ PrintTree::new_with_sink(title, stdout())
+ }
+}
+
+impl<W> PrintTree<W>
+where
+ W: Write
+{
+ pub fn new_with_sink(title: &str, mut sink: W) -> Self {
+ writeln!(sink, "\u{250c} {}", title).unwrap();
+ PrintTree {
+ level: 1,
+ queued_item: None,
+ sink,
+ }
+ }
+
+ fn print_level_prefix(&mut self) {
+ for _ in 0 .. self.level {
+ write!(self.sink, "\u{2502} ").unwrap();
+ }
+ }
+
+ fn flush_queued_item(&mut self, prefix: &str) {
+ if let Some(queued_item) = self.queued_item.take() {
+ self.print_level_prefix();
+ writeln!(self.sink, "{} {}", prefix, queued_item).unwrap();
+ }
+ }
+}
+
+// The default `println!` based printer
+impl<W> PrintTreePrinter for PrintTree<W>
+where
+ W: Write
+{
+ /// Descend one level in the tree with the given title.
+ fn new_level(&mut self, title: String) {
+ self.flush_queued_item("\u{251C}\u{2500}");
+
+ self.print_level_prefix();
+ writeln!(self.sink, "\u{251C}\u{2500} {}", title).unwrap();
+
+ self.level = self.level + 1;
+ }
+
+ /// Ascend one level in the tree.
+ fn end_level(&mut self) {
+ self.flush_queued_item("\u{2514}\u{2500}");
+ self.level = self.level - 1;
+ }
+
+ /// Add an item to the current level in the tree.
+ fn add_item(&mut self, text: String) {
+ self.flush_queued_item("\u{251C}\u{2500}");
+ self.queued_item = Some(text);
+ }
+}
+
+impl<W> Drop for PrintTree<W>
+where
+ W: Write
+{
+ fn drop(&mut self) {
+ self.flush_queued_item("\u{9492}\u{9472}");
+ }
+}
+
+pub trait PrintableTree {
+ fn print_with<T: PrintTreePrinter>(&self, pt: &mut T);
+}
diff --git a/gfx/wr/webrender/src/profiler.rs b/gfx/wr/webrender/src/profiler.rs
new file mode 100644
index 0000000000..c22dcfe830
--- /dev/null
+++ b/gfx/wr/webrender/src/profiler.rs
@@ -0,0 +1,1656 @@
+/* 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/. */
+
+//! # Overlay profiler
+//!
+//! ## Profiler UI string syntax
+//!
+//! Comma-separated list of of tokens with trailing and leading spaces trimmed.
+//! Each tokens can be:
+//! - A counter name with an optional prefix. The name corresponds to the displayed name (see the
+//! counters vector below.
+//! - By default (no prefix) the counter is shown as average + max over half a second.
+//! - With a '#' prefix the counter is shown as a graph.
+//! - With a '*' prefix the counter is shown as a change indicator.
+//! - Some special counters such as GPU time queries have specific visualizations ignoring prefixes.
+//! - A preset name to append the preset to the UI (see PROFILER_PRESETS).
+//! - An empty token to insert a bit of vertical space.
+//! - A '|' token to start a new column.
+//! - A '_' token to start a new row.
+
+use api::{ColorF, ColorU};
+use crate::renderer::DebugRenderer;
+use crate::device::query::GpuTimer;
+use euclid::{Point2D, Rect, Size2D, vec2, default};
+use crate::internal_types::FastHashMap;
+use crate::renderer::{MAX_VERTEX_TEXTURE_WIDTH, wr_has_been_initialized};
+use api::units::DeviceIntSize;
+use std::collections::vec_deque::VecDeque;
+use std::fmt::{Write, Debug};
+use std::f32;
+use std::ffi::CStr;
+use std::ops::Range;
+use std::time::Duration;
+use time::precise_time_ns;
+
+macro_rules! set_text {
+ ($dst:expr, $($arg:tt)*) => {
+ $dst.clear();
+ write!($dst, $($arg)*).unwrap();
+ };
+}
+
+const GRAPH_WIDTH: f32 = 1024.0;
+const GRAPH_HEIGHT: f32 = 320.0;
+const GRAPH_PADDING: f32 = 8.0;
+const GRAPH_FRAME_HEIGHT: f32 = 16.0;
+const PROFILE_SPACING: f32 = 15.0;
+const PROFILE_PADDING: f32 = 10.0;
+const BACKGROUND_COLOR: ColorU = ColorU { r: 20, g: 20, b: 20, a: 220 };
+
+const ONE_SECOND_NS: u64 = 1_000_000_000;
+
+/// Profiler UI string presets. Defined in the profiler UI string syntax, can contain other presets.
+static PROFILER_PRESETS: &'static[(&'static str, &'static str)] = &[
+ // Default view, doesn't show everything, but still shows quite a bit.
+ (&"Default", &"FPS,|,Slow indicators,_,Time graphs,|,Frame times, ,Transaction times, ,Frame stats, ,Memory, ,Interners,_,GPU time queries"),
+ // Smaller, less intrusive overview
+ (&"Compact", &"FPS, ,Frame times, ,Frame stats"),
+ // Even less intrusive, only slow transactions and frame indicators.
+ (&"Slow indicators", &"*Slow transaction,*Slow frame"),
+
+ // Counters:
+
+ // Timing information for per layout transaction stages.
+ (&"Transaction times", &"DisplayList,Scene building,Content send,API send"),
+ // Timing information for per-frame stages.
+ (&"Frame times", &"Frame CPU total,Frame building,Visibility,Prepare,Batching,Glyph resolve,Texture cache update,Renderer,GPU"),
+ // Stats about the content of the frame.
+ (&"Frame stats", &"Primitives,Visible primitives,Draw calls,Vertices,Color passes,Alpha passes,Rendered picture tiles,Rasterized glyphs"),
+ // Texture cache allocation stats.
+ (&"Texture cache stats", &"Texture cache RGBA8 linear pixels, Texture cache RGBA8 linear textures, Texture cache RGBA8 glyphs pixels, Texture cache RGBA8 glyphs textures, Texture cache A8 glyphs pixels, Texture cache A8 glyphs textures, Texture cache A8 pixels, Texture cache A8 textures, Texture cache A16 pixels, Texture cache A16 textures, Texture cache RGBA8 nearest pixels, Texture cache RGBA8 nearest textures, Texture cache shared mem, Texture cache standalone mem"),
+
+ // Graphs:
+
+ // Graph overview of time spent in WebRender's main stages.
+ (&"Time graphs", &"#DisplayList,#Scene building,#Blob rasterization, ,#Frame CPU total,#Frame building,#Renderer,#Texture cache update, ,#GPU"),
+ // Useful when investigating render backend bottlenecks.
+ (&"Backend graphs", &"#Frame building, #Visibility, #Prepare, #Batching, #Glyph resolve"),
+ // Useful when investigating renderer bottlenecks.
+ (&"Renderer graphs", &"#Rendered picture tiles,#Draw calls,#Rasterized glyphs,#Texture uploads,#Texture uploads mem, ,#Texture cache update,#Renderer,"),
+
+ // Misc:
+
+ (&"Memory", &"Image templates,Image templates mem,Font templates,Font templates mem,DisplayList mem,Picture tiles mem"),
+ (&"Interners", "Interned primitives,Interned clips,Interned pictures,Interned text runs,Interned normal borders,Interned image borders,Interned images,Interned YUV images,Interned line decorations,Interned linear gradients,Interned radial gradients,Interned conic gradients,Interned filter data,Interned backdrops"),
+ // Gpu sampler queries (need the pref gfx.webrender.debug.gpu-sampler-queries).
+ (&"GPU samplers", &"Alpha targets samplers,Transparent pass samplers,Opaque pass samplers,Total samplers"),
+];
+
+fn find_preset(name: &str) -> Option<&'static str> {
+ for preset in PROFILER_PRESETS {
+ if preset.0 == name {
+ return Some(preset.1);
+ }
+ }
+
+ None
+}
+
+// The indices here must match the PROFILE_COUNTERS array (checked at runtime).
+pub const FRAME_BUILDING_TIME: usize = 0;
+pub const FRAME_VISIBILITY_TIME: usize = 1;
+pub const FRAME_PREPARE_TIME: usize = 2;
+pub const FRAME_BATCHING_TIME: usize = 3;
+
+pub const RENDERER_TIME: usize = 4;
+pub const TOTAL_FRAME_CPU_TIME: usize = 5;
+pub const GPU_TIME: usize = 6;
+
+pub const CONTENT_SEND_TIME: usize = 7;
+pub const API_SEND_TIME: usize = 8;
+
+pub const DISPLAY_LIST_BUILD_TIME: usize = 9;
+pub const DISPLAY_LIST_MEM: usize = 10;
+
+pub const SCENE_BUILD_TIME: usize = 11;
+
+pub const RASTERIZED_BLOBS: usize = 12;
+pub const RASTERIZED_BLOB_TILES: usize = 13;
+pub const RASTERIZED_BLOBS_PX: usize = 14;
+pub const BLOB_RASTERIZATION_TIME: usize = 15;
+
+pub const RASTERIZED_GLYPHS: usize = 16;
+pub const GLYPH_RESOLVE_TIME: usize = 17;
+
+pub const DRAW_CALLS: usize = 18;
+pub const VERTICES: usize = 19;
+pub const PRIMITIVES: usize = 20;
+pub const VISIBLE_PRIMITIVES: usize = 21;
+
+pub const USED_TARGETS: usize = 22;
+pub const CREATED_TARGETS: usize = 23;
+pub const PICTURE_CACHE_SLICES: usize = 24;
+
+pub const COLOR_PASSES: usize = 25;
+pub const ALPHA_PASSES: usize = 26;
+pub const PICTURE_TILES: usize = 27;
+pub const PICTURE_TILES_MEM: usize = 28;
+pub const RENDERED_PICTURE_TILES: usize = 29;
+pub const TEXTURE_UPLOADS: usize = 30;
+pub const TEXTURE_UPLOADS_MEM: usize = 31;
+
+pub const FONT_TEMPLATES: usize = 32;
+pub const FONT_TEMPLATES_MEM: usize = 33;
+pub const IMAGE_TEMPLATES: usize = 34;
+pub const IMAGE_TEMPLATES_MEM: usize = 35;
+
+pub const GPU_CACHE_ROWS_TOTAL: usize = 36;
+pub const GPU_CACHE_ROWS_UPDATED: usize = 37;
+pub const GPU_CACHE_BLOCKS_TOTAL: usize = 38;
+pub const GPU_CACHE_BLOCKS_UPDATED: usize = 39;
+pub const GPU_CACHE_BLOCKS_SAVED: usize = 40;
+
+pub const TEXTURE_CACHE_A8_PIXELS: usize = 41;
+pub const TEXTURE_CACHE_A8_TEXTURES: usize = 42;
+pub const TEXTURE_CACHE_A16_PIXELS: usize = 43;
+pub const TEXTURE_CACHE_A16_TEXTURES: usize = 44;
+pub const TEXTURE_CACHE_RGBA8_LINEAR_PIXELS: usize = 45;
+pub const TEXTURE_CACHE_RGBA8_LINEAR_TEXTURES: usize = 46;
+pub const TEXTURE_CACHE_RGBA8_NEAREST_PIXELS: usize = 47;
+pub const TEXTURE_CACHE_RGBA8_NEAREST_TEXTURES: usize = 48;
+pub const TEXTURE_CACHE_SHARED_MEM: usize = 49;
+pub const TEXTURE_CACHE_STANDALONE_MEM: usize = 50;
+
+pub const SLOW_FRAME: usize = 51;
+pub const SLOW_TXN: usize = 52;
+
+pub const GPU_CACHE_UPLOAD_TIME: usize = 53;
+pub const TEXTURE_CACHE_UPLOAD_TIME: usize = 54;
+
+pub const FRAME_TIME: usize = 55;
+
+pub const ALPHA_TARGETS_SAMPLERS: usize = 56;
+pub const TRANSPARENT_PASS_SAMPLERS: usize = 57;
+pub const OPAQUE_PASS_SAMPLERS: usize = 58;
+pub const TOTAL_SAMPLERS: usize = 59;
+
+pub const INTERNED_PRIMITIVES: usize = 60;
+pub const INTERNED_CLIPS: usize = 61;
+pub const INTERNED_TEXT_RUNS: usize = 62;
+pub const INTERNED_NORMAL_BORDERS: usize = 63;
+pub const INTERNED_IMAGE_BORDERS: usize = 64;
+pub const INTERNED_IMAGES: usize = 65;
+pub const INTERNED_YUV_IMAGES: usize = 66;
+pub const INTERNED_LINE_DECORATIONS: usize = 67;
+pub const INTERNED_LINEAR_GRADIENTS: usize = 68;
+pub const INTERNED_RADIAL_GRADIENTS: usize = 69;
+pub const INTERNED_CONIC_GRADIENTS: usize = 70;
+pub const INTERNED_PICTURES: usize = 71;
+pub const INTERNED_FILTER_DATA: usize = 72;
+pub const INTERNED_BACKDROPS: usize = 73;
+
+pub const TEXTURE_CACHE_RGBA8_GLYPHS_PIXELS: usize = 74;
+pub const TEXTURE_CACHE_RGBA8_GLYPHS_TEXTURES: usize = 75;
+pub const TEXTURE_CACHE_A8_GLYPHS_PIXELS: usize = 76;
+pub const TEXTURE_CACHE_A8_GLYPHS_TEXTURES: usize = 77;
+
+pub const NUM_PROFILER_EVENTS: usize = 78;
+
+pub struct Profiler {
+ counters: Vec<Counter>,
+ gpu_frames: GpuFrameCollection,
+
+ start: u64,
+ avg_over_period: u64,
+ num_graph_samples: usize,
+
+ // For FPS computation. Updated in update().
+ frame_timestamps_within_last_second: Vec<u64>,
+
+ ui: Vec<Item>,
+}
+
+impl Profiler {
+ pub fn new() -> Self {
+
+ fn float(name: &'static str, unit: &'static str, index: usize, expected: Expected<f64>) -> CounterDescriptor {
+ CounterDescriptor { name, unit, show_as: ShowAs::Float, index, expected }
+ }
+
+ fn int(name: &'static str, unit: &'static str, index: usize, expected: Expected<i64>) -> CounterDescriptor {
+ CounterDescriptor { name, unit, show_as: ShowAs::Int, index, expected: expected.into_float() }
+ }
+
+ // Not in the list below:
+ // - "GPU time queries" shows the details of the GPU time queries if selected as a graph.
+ // - "GPU cache bars" shows some info about the GPU cache.
+
+ // TODO: This should be a global variable but to keep things readable we need to be able to
+ // use match in const fn which isn't supported by the current rustc version in gecko's build
+ // system.
+ let profile_counters = &[
+ float("Frame building", "ms", FRAME_BUILDING_TIME, expected(0.0..6.0).avg(0.0..3.0)),
+
+ float("Visibility", "ms", FRAME_VISIBILITY_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+ float("Prepare", "ms", FRAME_PREPARE_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+ float("Batching", "ms", FRAME_BATCHING_TIME, expected(0.0..3.0).avg(0.0..2.0)),
+
+ float("Renderer", "ms", RENDERER_TIME, expected(0.0..8.0).avg(0.0..5.0)),
+ float("Frame CPU total", "ms", TOTAL_FRAME_CPU_TIME, expected(0.0..15.0).avg(0.0..6.0)),
+ float("GPU", "ms", GPU_TIME, expected(0.0..15.0).avg(0.0..8.0)),
+
+ float("Content send", "ms", CONTENT_SEND_TIME, expected(0.0..1.0).avg(0.0..1.0)),
+ float("API send", "ms", API_SEND_TIME, expected(0.0..1.0).avg(0.0..0.4)),
+ float("DisplayList", "ms", DISPLAY_LIST_BUILD_TIME, expected(0.0..5.0).avg(0.0..3.0)),
+ float("DisplayList mem", "MB", DISPLAY_LIST_MEM, expected(0.0..20.0)),
+ float("Scene building", "ms", SCENE_BUILD_TIME, expected(0.0..4.0).avg(0.0..3.0)),
+
+ int("Rasterized blobs", "", RASTERIZED_BLOBS, expected(0..15)),
+ int("Rasterized blob tiles", "", RASTERIZED_BLOB_TILES, expected(0..15)),
+ int("Rasterized blob pixels", "px", RASTERIZED_BLOBS_PX, expected(0..300_000)),
+ float("Blob rasterization", "ms", BLOB_RASTERIZATION_TIME, expected(0.0..8.0)),
+
+ int("Rasterized glyphs", "", RASTERIZED_GLYPHS, expected(0..15)),
+ float("Glyph resolve", "ms", GLYPH_RESOLVE_TIME, expected(0.0..4.0)),
+
+ int("Draw calls", "", DRAW_CALLS, expected(1..120).avg(1..90)),
+ int("Vertices", "", VERTICES, expected(10..5000)),
+ int("Primitives", "", PRIMITIVES, expected(10..5000)),
+ int("Visible primitives", "", VISIBLE_PRIMITIVES, expected(1..5000)),
+
+ int("Used targets", "", USED_TARGETS, expected(1..4)),
+ int("Created targets", "", CREATED_TARGETS, expected(0..3)),
+ int("Picture cache slices", "", PICTURE_CACHE_SLICES, expected(0..5)),
+
+ int("Color passes", "", COLOR_PASSES, expected(1..4)),
+ int("Alpha passes", "", ALPHA_PASSES, expected(0..3)),
+ int("Picture tiles", "", PICTURE_TILES, expected(0..15)),
+ float("Picture tiles mem", "MB", PICTURE_TILES_MEM, expected(0.0..150.0)),
+ int("Rendered picture tiles", "", RENDERED_PICTURE_TILES, expected(0..5)),
+ int("Texture uploads", "", TEXTURE_UPLOADS, expected(0..10)),
+ float("Texture uploads mem", "MB", TEXTURE_UPLOADS_MEM, expected(0.0..10.0)),
+
+ int("Font templates", "", FONT_TEMPLATES, expected(0..40)),
+ float("Font templates mem", "MB", FONT_TEMPLATES_MEM, expected(0.0..20.0)),
+ int("Image templates", "", IMAGE_TEMPLATES, expected(0..100)),
+ float("Image templates mem", "MB", IMAGE_TEMPLATES_MEM, expected(0.0..50.0)),
+
+ int("GPU cache rows total", "", GPU_CACHE_ROWS_TOTAL, expected(1..50)),
+ int("GPU cache rows updated", "", GPU_CACHE_ROWS_UPDATED, expected(0..25)),
+ int("GPU blocks total", "", GPU_CACHE_BLOCKS_TOTAL, expected(1..65_000)),
+ int("GPU blocks updated", "", GPU_CACHE_BLOCKS_UPDATED, expected(0..1000)),
+ int("GPU blocks saved", "", GPU_CACHE_BLOCKS_SAVED, expected(0..50_000)),
+
+ int("Texture cache A8 pixels", "px", TEXTURE_CACHE_A8_PIXELS, expected(0..1_000_000)),
+ int("Texture cache A8 textures", "", TEXTURE_CACHE_A8_TEXTURES, expected(0..2)),
+ int("Texture cache A16 pixels", "px", TEXTURE_CACHE_A16_PIXELS, expected(0..260_000)),
+ int("Texture cache A16 textures", "", TEXTURE_CACHE_A16_TEXTURES, expected(0..2)),
+ int("Texture cache RGBA8 linear pixels", "px", TEXTURE_CACHE_RGBA8_LINEAR_PIXELS, expected(0..8_000_000)),
+ int("Texture cache RGBA8 linear textures", "", TEXTURE_CACHE_RGBA8_LINEAR_TEXTURES, expected(0..3)),
+ int("Texture cache RGBA8 nearest pixels", "px", TEXTURE_CACHE_RGBA8_NEAREST_PIXELS, expected(0..260_000)),
+ int("Texture cache RGBA8 nearest textures", "", TEXTURE_CACHE_RGBA8_NEAREST_TEXTURES, expected(0..2)),
+ float("Texture cache shared mem", "MB", TEXTURE_CACHE_SHARED_MEM, expected(0.0..100.0)),
+ float("Texture cache standalone mem", "MB", TEXTURE_CACHE_STANDALONE_MEM, expected(0.0..100.0)),
+
+
+ float("Slow frame", "", SLOW_FRAME, expected(0.0..0.0)),
+ float("Slow transaction", "", SLOW_TXN, expected(0.0..0.0)),
+
+ float("GPU cache upload", "ms", GPU_CACHE_UPLOAD_TIME, expected(0.0..2.0)),
+ float("Texture cache update", "ms", TEXTURE_CACHE_UPLOAD_TIME, expected(0.0..3.0)),
+
+ float("Frame", "ms", FRAME_TIME, Expected::none()),
+
+ float("Alpha targets samplers", "%", ALPHA_TARGETS_SAMPLERS, Expected::none()),
+ float("Transparent pass samplers", "%", TRANSPARENT_PASS_SAMPLERS, Expected::none()),
+ float("Opaque pass samplers", "%", OPAQUE_PASS_SAMPLERS, Expected::none()),
+ float("Total samplers", "%", TOTAL_SAMPLERS, Expected::none()),
+
+ int("Interned primitives", "", INTERNED_PRIMITIVES, Expected::none()),
+ int("Interned clips", "", INTERNED_CLIPS, Expected::none()),
+ int("Interned text runs", "", INTERNED_TEXT_RUNS, Expected::none()),
+ int("Interned normal borders", "", INTERNED_NORMAL_BORDERS, Expected::none()),
+ int("Interned image borders", "", INTERNED_IMAGE_BORDERS, Expected::none()),
+ int("Interned images", "", INTERNED_IMAGES, Expected::none()),
+ int("Interned YUV images", "", INTERNED_YUV_IMAGES, Expected::none()),
+ int("Interned line decorations", "", INTERNED_LINE_DECORATIONS, Expected::none()),
+ int("Interned linear gradients", "", INTERNED_LINEAR_GRADIENTS, Expected::none()),
+ int("Interned radial gradients", "", INTERNED_RADIAL_GRADIENTS, Expected::none()),
+ int("Interned conic gradients", "", INTERNED_CONIC_GRADIENTS, Expected::none()),
+ int("Interned pictures", "", INTERNED_PICTURES, Expected::none()),
+ int("Interned filter data", "", INTERNED_FILTER_DATA, Expected::none()),
+ int("Interned backdrops", "", INTERNED_BACKDROPS, Expected::none()),
+
+ int("Texture cache RGBA8 glyphs pixels", "px", TEXTURE_CACHE_RGBA8_GLYPHS_PIXELS, expected(0..4_000_000)),
+ int("Texture cache RGBA8 glyphs textures", "", TEXTURE_CACHE_RGBA8_GLYPHS_TEXTURES, expected(0..2)),
+ int("Texture cache A8 glyphs pixels", "px", TEXTURE_CACHE_A8_GLYPHS_PIXELS, expected(0..4_000_000)),
+ int("Texture cache A8 glyphs textures", "", TEXTURE_CACHE_A8_GLYPHS_TEXTURES, expected(0..2)),
+ ];
+
+
+ let mut counters = Vec::with_capacity(profile_counters.len());
+
+ for (idx, descriptor) in profile_counters.iter().enumerate() {
+ debug_assert_eq!(descriptor.index, idx);
+ counters.push(Counter::new(descriptor));
+ }
+
+ Profiler {
+ gpu_frames: GpuFrameCollection::new(),
+
+ counters,
+ start: precise_time_ns(),
+ avg_over_period: ONE_SECOND_NS / 2,
+
+ num_graph_samples: 500, // Would it be useful to control this via a pref?
+ frame_timestamps_within_last_second: Vec::new(),
+ ui: Vec::new(),
+ }
+ }
+
+ /// Sum a few counters and if the total amount is larger than a threshold, update
+ /// a specific counter.
+ ///
+ /// This is useful to monitor slow frame and slow transactions.
+ fn update_slow_event(&mut self, dst_counter: usize, counters: &[usize], threshold: f64) {
+ let mut total = 0.0;
+ for &counter in counters {
+ if self.counters[counter].value.is_finite() {
+ total += self.counters[counter].value;
+ }
+ }
+
+ if total > threshold {
+ self.counters[dst_counter].set(total);
+ }
+ }
+
+ // Call at the end of every frame, after setting the counter values and before drawing the counters.
+ pub fn update(&mut self) {
+ let now = precise_time_ns();
+ let update_avg = (now - self.start) > self.avg_over_period;
+ if update_avg {
+ self.start = now;
+ }
+ let one_second_ago = now - ONE_SECOND_NS;
+ self.frame_timestamps_within_last_second.retain(|t| *t > one_second_ago);
+ self.frame_timestamps_within_last_second.push(now);
+
+ self.update_slow_event(
+ SLOW_FRAME,
+ &[TOTAL_FRAME_CPU_TIME],
+ 15.0,
+ );
+ self.update_slow_event(
+ SLOW_TXN,
+ &[DISPLAY_LIST_BUILD_TIME, CONTENT_SEND_TIME, SCENE_BUILD_TIME],
+ 80.0
+ );
+
+ for counter in &mut self.counters {
+ counter.update(update_avg);
+ }
+ }
+
+ pub fn set_gpu_time_queries(&mut self, gpu_queries: Vec<GpuTimer>) {
+ let mut gpu_time_ns = 0;
+ for sample in &gpu_queries {
+ gpu_time_ns += sample.time_ns;
+ }
+
+ self.gpu_frames.push(gpu_time_ns, gpu_queries);
+
+ self.counters[GPU_TIME].set_f64(ns_to_ms(gpu_time_ns));
+ }
+
+ // Find the index of a counter by its name.
+ pub fn index_of(&self, name: &str) -> Option<usize> {
+ self.counters.iter().position(|counter| counter.name == name)
+ }
+
+ // Define the profiler UI, see comment about the syntax at the top of this file.
+ pub fn set_ui(&mut self, names: &str) {
+ let mut selection = Vec::new();
+
+ self.append_to_ui(&mut selection, names);
+
+ if selection == self.ui {
+ return;
+ }
+
+ for counter in &mut self.counters {
+ counter.disable_graph();
+ }
+
+ for item in &selection {
+ if let Item::Graph(idx) = item {
+ self.counters[*idx].enable_graph(self.num_graph_samples);
+ }
+ }
+
+ self.ui = selection;
+ }
+
+ fn append_to_ui(&mut self, selection: &mut Vec<Item>, names: &str) {
+ // Group successive counters together.
+ fn flush_counters(counters: &mut Vec<usize>, selection: &mut Vec<Item>) {
+ if !counters.is_empty() {
+ selection.push(Item::Counters(std::mem::take(counters)))
+ }
+ }
+
+ let mut counters = Vec::new();
+
+ for name in names.split(",") {
+ let name = name.trim();
+ let is_graph = name.starts_with("#");
+ let is_indicator = name.starts_with("*");
+ let name = if is_graph || is_indicator {
+ &name[1..]
+ } else {
+ name
+ };
+ // See comment about the ui string syntax at the top of this file.
+ match name {
+ "" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Space);
+ }
+ "|" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Column);
+ }
+ "_" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Row);
+ }
+ "FPS" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Fps);
+ }
+ "GPU time queries" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::GpuTimeQueries);
+ }
+ "GPU cache bars" => {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::GpuCacheBars);
+ }
+ _ => {
+ if let Some(idx) = self.index_of(name) {
+ if is_graph {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::Graph(idx));
+ } else if is_indicator {
+ flush_counters(&mut counters, selection);
+ selection.push(Item::ChangeIndicator(idx));
+ } else {
+ counters.push(idx);
+ }
+ } else if let Some(preset_str) = find_preset(name) {
+ flush_counters(&mut counters, selection);
+ self.append_to_ui(selection, preset_str);
+ } else {
+ selection.push(Item::Text(format!("Unknonw counter: {}", name)));
+ }
+ }
+ }
+ }
+
+ flush_counters(&mut counters, selection);
+ }
+
+ pub fn set_counters(&mut self, counters: &mut TransactionProfile) {
+ for (id, evt) in counters.events.iter_mut().enumerate() {
+ if let Event::Value(val) = *evt {
+ self.counters[id].set(val);
+ }
+ *evt = Event::None;
+ }
+ }
+
+ pub fn get(&self, id: usize) -> Option<f64> {
+ self.counters[id].get()
+ }
+
+ fn draw_counters(
+ counters: &[Counter],
+ selected: &[usize],
+ mut x: f32, mut y: f32,
+ text_buffer: &mut String,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let line_height = debug_renderer.line_height();
+
+ x += PROFILE_PADDING;
+ y += PROFILE_PADDING;
+ let origin = default::Point2D::new(x, y);
+ y += line_height * 0.5;
+
+ let mut total_rect = Rect::zero();
+
+ let mut color_index = 0;
+ let colors = [
+ // Regular values,
+ ColorU::new(255, 255, 255, 255),
+ ColorU::new(255, 255, 0, 255),
+ // Unexpected values,
+ ColorU::new(255, 80, 0, 255),
+ ColorU::new(255, 0, 0, 255),
+ ];
+
+ for idx in selected {
+ // If The index is invalid, add some vertical space.
+ let counter = &counters[*idx];
+
+ let rect = debug_renderer.add_text(
+ x, y,
+ counter.name,
+ colors[color_index],
+ None,
+ );
+ color_index = (color_index + 1) % 2;
+
+ total_rect = total_rect.union(&rect);
+ y += line_height;
+ }
+
+ color_index = 0;
+ x = total_rect.max_x() + 60.0;
+ y = origin.y + line_height * 0.5;
+
+ for idx in selected {
+ let counter = &counters[*idx];
+ let expected_offset = if counter.has_unexpected_avg_max() { 2 } else { 0 };
+
+ counter.write_value(text_buffer);
+
+ let rect = debug_renderer.add_text(
+ x,
+ y,
+ &text_buffer,
+ colors[color_index + expected_offset],
+ None,
+ );
+ color_index = (color_index + 1) % 2;
+
+ total_rect = total_rect.union(&rect);
+ y += line_height;
+ }
+
+ total_rect = total_rect
+ .union(&Rect { origin, size: Size2D::new(1.0, 1.0) })
+ .inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ debug_renderer.add_quad(
+ total_rect.min_x(),
+ total_rect.min_y(),
+ total_rect.max_x(),
+ total_rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ total_rect
+ }
+
+ fn draw_graph(
+ counter: &Counter,
+ x: f32,
+ y: f32,
+ text_buffer: &mut String,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let graph = counter.graph.as_ref().unwrap();
+
+ let max_samples = graph.values.capacity() as f32;
+
+ let size = Size2D::new(max_samples, 100.0);
+ let line_height = debug_renderer.line_height();
+ let graph_rect = Rect::new(Point2D::new(x + PROFILE_PADDING, y + PROFILE_PADDING), size);
+ let mut rect = graph_rect.inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ let stats = graph.stats();
+
+ let text_color = ColorU::new(255, 255, 0, 255);
+ let text_origin = rect.origin + vec2(rect.size.width, 25.0);
+ set_text!(text_buffer, "{} ({})", counter.name, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y,
+ if counter.unit == "" { counter.name } else { text_buffer },
+ ColorU::new(0, 255, 0, 255),
+ None,
+ );
+
+ set_text!(text_buffer, "Samples: {}", stats.samples);
+
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ if stats.samples > 0 {
+ set_text!(text_buffer, "Min: {:.2} {}", stats.min, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 2.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ set_text!(text_buffer, "Avg: {:.2} {}", stats.avg, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 3.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+
+ set_text!(text_buffer, "Max: {:.2} {}", stats.max, counter.unit);
+ debug_renderer.add_text(
+ text_origin.x,
+ text_origin.y + line_height * 4.0,
+ text_buffer,
+ text_color,
+ None,
+ );
+ }
+
+ rect.size.width += 220.0;
+ debug_renderer.add_quad(
+ rect.min_x(),
+ rect.min_y(),
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ let bx1 = graph_rect.max_x();
+ let by1 = graph_rect.max_y();
+
+ let w = graph_rect.size.width / max_samples;
+ let h = graph_rect.size.height;
+
+ let color_t0 = ColorU::new(0, 255, 0, 255);
+ let color_b0 = ColorU::new(0, 180, 0, 255);
+
+ let color_t2 = ColorU::new(255, 0, 0, 255);
+ let color_b2 = ColorU::new(180, 0, 0, 255);
+
+ for (index, sample) in graph.values.iter().enumerate() {
+ if !sample.is_finite() {
+ // NAN means no sample this frame.
+ continue;
+ }
+ let sample = *sample as f32;
+ let x1 = bx1 - index as f32 * w;
+ let x0 = x1 - w;
+
+ let y0 = by1 - (sample / stats.max as f32) as f32 * h;
+ let y1 = by1;
+
+ let (color_top, color_bottom) = if counter.is_unexpected_value(sample as f64) {
+ (color_t2, color_b2)
+ } else {
+ (color_t0, color_b0)
+ };
+
+ debug_renderer.add_quad(x0, y0, x1, y1, color_top, color_bottom);
+ }
+
+ rect
+ }
+
+
+ fn draw_change_indicator(
+ counter: &Counter,
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer
+ ) -> default::Rect<f32> {
+ let height = 10.0;
+ let width = 20.0;
+
+ // Draw the indicator red instead of blue if is is not within expected ranges.
+ let color = if counter.has_unexpected_value() || counter.has_unexpected_avg_max() {
+ ColorU::new(255, 20, 20, 255)
+ } else {
+ ColorU::new(0, 100, 250, 255)
+ };
+
+ let tx = counter.change_indicator as f32 * width;
+ debug_renderer.add_quad(
+ x,
+ y,
+ x + 15.0 * width,
+ y + height,
+ ColorU::new(0, 0, 0, 150),
+ ColorU::new(0, 0, 0, 150),
+ );
+
+ debug_renderer.add_quad(
+ x + tx,
+ y,
+ x + tx + width,
+ y + height,
+ color,
+ ColorU::new(25, 25, 25, 255),
+ );
+
+ Rect {
+ origin: Point2D::new(x, y),
+ size: Size2D::new(15.0 * width + 20.0, height),
+ }
+ }
+
+ fn draw_bar(
+ label: &str,
+ label_color: ColorU,
+ counters: &[(ColorU, usize)],
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let x = x + 8.0;
+ let y = y + 24.0;
+ let text_rect = debug_renderer.add_text(
+ x, y,
+ label,
+ label_color,
+ None,
+ );
+
+ let x_base = text_rect.max_x() + 10.0;
+ let width = 300.0;
+ let total_value = counters.last().unwrap().1;
+ let scale = width / total_value as f32;
+ let mut x_current = x_base;
+
+ for &(color, counter) in counters {
+ let x_stop = x_base + counter as f32 * scale;
+ debug_renderer.add_quad(
+ x_current,
+ text_rect.origin.y,
+ x_stop,
+ text_rect.max_y(),
+ color,
+ color,
+ );
+ x_current = x_stop;
+
+ }
+
+ let mut total_rect = text_rect;
+ total_rect.size.width += width + 10.0;
+
+ total_rect
+ }
+
+ fn draw_gpu_cache_bars(&self, x: f32, mut y: f32, text_buffer: &mut String, debug_renderer: &mut DebugRenderer) -> default::Rect<f32> {
+ let color_updated = ColorU::new(0xFF, 0, 0, 0xFF);
+ let color_free = ColorU::new(0, 0, 0xFF, 0xFF);
+ let color_saved = ColorU::new(0, 0xFF, 0, 0xFF);
+
+ let updated_blocks = self.get(GPU_CACHE_BLOCKS_UPDATED).unwrap_or(0.0) as usize;
+ let saved_blocks = self.get(GPU_CACHE_BLOCKS_SAVED).unwrap_or(0.0) as usize;
+ let allocated_blocks = self.get(GPU_CACHE_BLOCKS_TOTAL).unwrap_or(0.0) as usize;
+ let allocated_rows = self.get(GPU_CACHE_ROWS_TOTAL).unwrap_or(0.0) as usize;
+ let updated_rows = self.get(GPU_CACHE_ROWS_UPDATED).unwrap_or(0.0) as usize;
+ let requested_blocks = updated_blocks + saved_blocks;
+ let total_blocks = allocated_rows * MAX_VERTEX_TEXTURE_WIDTH;
+
+ set_text!(text_buffer, "GPU cache rows ({}):", allocated_rows);
+
+ let rect0 = Profiler::draw_bar(
+ text_buffer,
+ ColorU::new(0xFF, 0xFF, 0xFF, 0xFF),
+ &[
+ (color_updated, updated_rows),
+ (color_free, allocated_rows),
+ ],
+ x, y,
+ debug_renderer,
+ );
+
+ y = rect0.max_y();
+
+ let rect1 = Profiler::draw_bar(
+ "GPU cache blocks",
+ ColorU::new(0xFF, 0xFF, 0, 0xFF),
+ &[
+ (color_updated, updated_blocks),
+ (color_saved, requested_blocks),
+ (color_free, allocated_blocks),
+ (ColorU::new(0, 0, 0, 0xFF), total_blocks),
+ ],
+ x, y,
+ debug_renderer,
+ );
+
+ let total_rect = rect0.union(&rect1).inflate(10.0, 10.0);
+ debug_renderer.add_quad(
+ total_rect.origin.x,
+ total_rect.origin.y,
+ total_rect.origin.x + total_rect.size.width,
+ total_rect.origin.y + total_rect.size.height,
+ ColorF::new(0.1, 0.1, 0.1, 0.8).into(),
+ ColorF::new(0.2, 0.2, 0.2, 0.8).into(),
+ );
+
+ total_rect
+ }
+
+ fn draw_gpu_time_queries(
+ time_queries: &GpuFrameCollection,
+ x: f32, y: f32,
+ debug_renderer: &mut DebugRenderer,
+ ) -> default::Rect<f32> {
+ let mut has_data = false;
+ for frame in &time_queries.frames {
+ if !frame.samples.is_empty() {
+ has_data = true;
+ break;
+ }
+ }
+
+ if !has_data {
+ return Rect::zero();
+ }
+
+ let graph_rect = Rect::new(
+ Point2D::new(x + GRAPH_PADDING, y + GRAPH_PADDING),
+ Size2D::new(GRAPH_WIDTH, GRAPH_HEIGHT),
+ );
+ let bounding_rect = graph_rect.inflate(GRAPH_PADDING, GRAPH_PADDING);
+
+ debug_renderer.add_quad(
+ bounding_rect.origin.x,
+ bounding_rect.origin.y,
+ bounding_rect.origin.x + bounding_rect.size.width,
+ bounding_rect.origin.y + bounding_rect.size.height,
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ let w = graph_rect.size.width;
+ let mut y0 = graph_rect.origin.y;
+
+ let mut max_time = time_queries.frames
+ .iter()
+ .max_by_key(|f| f.total_time)
+ .unwrap()
+ .total_time as f32;
+
+ // If the max time is lower than 16ms, fix the scale
+ // at 16ms so that the graph is easier to interpret.
+ let baseline_ns = 16_000_000.0; // 16ms
+ max_time = max_time.max(baseline_ns);
+
+ let mut tags_present = FastHashMap::default();
+
+ for frame in &time_queries.frames {
+ let y1 = y0 + GRAPH_FRAME_HEIGHT;
+
+ let mut current_ns = 0;
+ for sample in &frame.samples {
+ let x0 = graph_rect.origin.x + w * current_ns as f32 / max_time;
+ current_ns += sample.time_ns;
+ let x1 = graph_rect.origin.x + w * current_ns as f32 / max_time;
+ let mut bottom_color = sample.tag.color;
+ bottom_color.a *= 0.5;
+
+ debug_renderer.add_quad(
+ x0,
+ y0,
+ x1,
+ y1,
+ sample.tag.color.into(),
+ bottom_color.into(),
+ );
+
+ tags_present.insert(sample.tag.label, sample.tag.color);
+ }
+
+ y0 = y1;
+ }
+
+ // If the max time is higher than 16ms, show a vertical line at the
+ // 16ms mark.
+ if max_time > baseline_ns {
+ let x = graph_rect.origin.x + w * baseline_ns as f32 / max_time;
+ let height = time_queries.frames.len() as f32 * GRAPH_FRAME_HEIGHT;
+
+ debug_renderer.add_quad(
+ x,
+ graph_rect.origin.y,
+ x + 4.0,
+ graph_rect.origin.y + height,
+ ColorU::new(120, 00, 00, 150),
+ ColorU::new(120, 00, 00, 100),
+ );
+ }
+
+
+ // Add a legend to see which color correspond to what primitive.
+ const LEGEND_SIZE: f32 = 20.0;
+ const PADDED_LEGEND_SIZE: f32 = 25.0;
+ if !tags_present.is_empty() {
+ debug_renderer.add_quad(
+ bounding_rect.max_x() + GRAPH_PADDING,
+ bounding_rect.origin.y,
+ bounding_rect.max_x() + GRAPH_PADDING + 200.0,
+ bounding_rect.origin.y + tags_present.len() as f32 * PADDED_LEGEND_SIZE + GRAPH_PADDING,
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+ }
+
+ for (i, (label, &color)) in tags_present.iter().enumerate() {
+ let x0 = bounding_rect.origin.x + bounding_rect.size.width + GRAPH_PADDING * 2.0;
+ let y0 = bounding_rect.origin.y + GRAPH_PADDING + i as f32 * PADDED_LEGEND_SIZE;
+
+ debug_renderer.add_quad(
+ x0, y0, x0 + LEGEND_SIZE, y0 + LEGEND_SIZE,
+ color.into(),
+ color.into(),
+ );
+
+ debug_renderer.add_text(
+ x0 + PADDED_LEGEND_SIZE,
+ y0 + LEGEND_SIZE * 0.75,
+ label,
+ ColorU::new(255, 255, 0, 255),
+ None,
+ );
+ }
+
+ bounding_rect
+ }
+
+ pub fn draw_profile(
+ &mut self,
+ _frame_index: u64,
+ debug_renderer: &mut DebugRenderer,
+ device_size: DeviceIntSize,
+ ) {
+ let x_start = 20.0;
+ let mut y_start = 150.0;
+ let default_column_width = 400.0;
+
+ // set_text!(..) into this string instead of using format!(..) to avoid
+ // unnecessary allocations.
+ let mut text_buffer = String::with_capacity(32);
+
+ let mut column_width = default_column_width;
+ let mut max_y = y_start;
+
+ let mut x = x_start;
+ let mut y = y_start;
+
+ for elt in &self.ui {
+ let rect = match elt {
+ Item::Counters(indices) => {
+ Profiler::draw_counters(&self.counters, &indices, x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::Graph(idx) => {
+ Profiler::draw_graph(&self.counters[*idx], x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::ChangeIndicator(idx) => {
+ Profiler::draw_change_indicator(&self.counters[*idx], x, y, debug_renderer)
+ }
+ Item::GpuTimeQueries => {
+ Profiler::draw_gpu_time_queries(&self.gpu_frames, x, y, debug_renderer)
+ }
+ Item::GpuCacheBars => {
+ self.draw_gpu_cache_bars(x, y, &mut text_buffer, debug_renderer)
+ }
+ Item::Text(text) => {
+ let p = 10.0;
+ let mut rect = debug_renderer.add_text(
+ x + p,
+ y + p,
+ &text,
+ ColorU::new(255, 255, 255, 255),
+ None,
+ );
+ rect = rect.inflate(p, p);
+
+ debug_renderer.add_quad(
+ rect.origin.x,
+ rect.origin.y,
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ rect
+ }
+ Item::Fps => {
+ let fps = self.frame_timestamps_within_last_second.len();
+ set_text!(&mut text_buffer, "{} fps", fps);
+ let mut rect = debug_renderer.add_text(
+ x + PROFILE_PADDING,
+ y + PROFILE_PADDING + 5.0,
+ &text_buffer,
+ ColorU::new(255, 255, 255, 255),
+ None,
+ );
+ rect = rect.inflate(PROFILE_PADDING, PROFILE_PADDING);
+
+ debug_renderer.add_quad(
+ rect.min_x(),
+ rect.min_y(),
+ rect.max_x(),
+ rect.max_y(),
+ BACKGROUND_COLOR,
+ BACKGROUND_COLOR,
+ );
+
+ rect
+ }
+ Item::Space => {
+ Rect { origin: Point2D::new(x, y), size: Size2D::new(0.0, PROFILE_SPACING) }
+ }
+ Item::Column => {
+ max_y = max_y.max(y);
+ x += column_width + PROFILE_SPACING;
+ y = y_start;
+ column_width = default_column_width;
+
+ continue;
+ }
+ Item::Row => {
+ max_y = max_y.max(y);
+ y_start = max_y + PROFILE_SPACING;
+ y = y_start;
+ x = x_start;
+ column_width = default_column_width;
+
+ continue;
+ }
+ };
+
+ column_width = column_width.max(rect.size.width);
+ y = rect.max_y();
+
+ if y > device_size.height as f32 - 100.0 {
+ max_y = max_y.max(y);
+ x += column_width + PROFILE_SPACING;
+ y = y_start;
+ column_width = default_column_width;
+ }
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn dump_stats(&self, sink: &mut dyn std::io::Write) -> std::io::Result<()> {
+ for counter in &self.counters {
+ if counter.value.is_finite() {
+ writeln!(sink, "{} {:?}{}", counter.name, counter.value, counter.unit)?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// Defines the interface for hooking up an external profiler to WR.
+pub trait ProfilerHooks : Send + Sync {
+ /// Called at the beginning of a profile scope. The label must
+ /// be a C string (null terminated).
+ fn begin_marker(&self, label: &CStr);
+
+ /// Called at the end of a profile scope. The label must
+ /// be a C string (null terminated).
+ fn end_marker(&self, label: &CStr);
+
+ /// Called to mark an event happening. The label must
+ /// be a C string (null terminated).
+ fn event_marker(&self, label: &CStr);
+
+ /// Called with a duration to indicate a text marker that just ended. Text
+ /// markers allow different types of entries to be recorded on the same row
+ /// in the timeline, by adding labels to the entry.
+ ///
+ /// This variant is also useful when the caller only wants to record events
+ /// longer than a certain threshold, and thus they don't know in advance
+ /// whether the event will qualify.
+ fn add_text_marker(&self, label: &CStr, text: &str, duration: Duration);
+
+ /// Returns true if the current thread is being profiled.
+ fn thread_is_being_profiled(&self) -> bool;
+}
+
+/// The current global profiler callbacks, if set by embedder.
+pub static mut PROFILER_HOOKS: Option<&'static dyn ProfilerHooks> = None;
+
+/// Set the profiler callbacks, or None to disable the profiler.
+/// This function must only ever be called before any WR instances
+/// have been created, or the hooks will not be set.
+pub fn set_profiler_hooks(hooks: Option<&'static dyn ProfilerHooks>) {
+ if !wr_has_been_initialized() {
+ unsafe {
+ PROFILER_HOOKS = hooks;
+ }
+ }
+}
+
+/// A simple RAII style struct to manage a profile scope.
+pub struct ProfileScope {
+ name: &'static CStr,
+}
+
+/// Records a marker of the given duration that just ended.
+pub fn add_text_marker(label: &CStr, text: &str, duration: Duration) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.add_text_marker(label, text, duration);
+ }
+ }
+}
+
+/// Records a marker of the given duration that just ended.
+pub fn add_event_marker(label: &CStr) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.event_marker(label);
+ }
+ }
+}
+
+/// Returns true if the current thread is being profiled.
+pub fn thread_is_being_profiled() -> bool {
+ unsafe {
+ PROFILER_HOOKS.map_or(false, |h| h.thread_is_being_profiled())
+ }
+}
+
+impl ProfileScope {
+ /// Begin a new profile scope
+ pub fn new(name: &'static CStr) -> Self {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.begin_marker(name);
+ }
+ }
+
+ ProfileScope {
+ name,
+ }
+ }
+}
+
+impl Drop for ProfileScope {
+ fn drop(&mut self) {
+ unsafe {
+ if let Some(ref hooks) = PROFILER_HOOKS {
+ hooks.end_marker(self.name);
+ }
+ }
+ }
+}
+
+/// A helper macro to define profile scopes.
+macro_rules! profile_marker {
+ ($string:expr) => {
+ let _scope = $crate::profiler::ProfileScope::new(cstr!($string));
+ };
+}
+
+#[derive(Debug, Clone)]
+pub struct GpuProfileTag {
+ pub label: &'static str,
+ pub color: ColorF,
+}
+
+/// Ranges of expected value for a profile counter.
+#[derive(Clone, Debug)]
+pub struct Expected<T> {
+ pub range: Option<Range<T>>,
+ pub avg: Option<Range<T>>,
+}
+
+impl<T> Expected<T> {
+ const fn none() -> Self {
+ Expected {
+ range: None,
+ avg: None,
+ }
+ }
+}
+
+const fn expected<T>(range: Range<T>) -> Expected<T> {
+ Expected {
+ range: Some(range),
+ avg: None,
+ }
+}
+
+impl Expected<f64> {
+ const fn avg(mut self, avg: Range<f64>) -> Self {
+ self.avg = Some(avg);
+ self
+ }
+}
+
+impl Expected<i64> {
+ const fn avg(mut self, avg: Range<i64>) -> Self {
+ self.avg = Some(avg);
+ self
+ }
+
+ fn into_float(self) -> Expected<f64> {
+ Expected {
+ range: match self.range {
+ Some(r) => Some(r.start as f64 .. r.end as f64),
+ None => None,
+ },
+ avg: match self.avg {
+ Some(r) => Some(r.start as f64 .. r.end as f64),
+ None => None,
+ },
+ }
+ }
+}
+
+pub struct CounterDescriptor {
+ pub name: &'static str,
+ pub unit: &'static str,
+ pub index: usize,
+ pub show_as: ShowAs,
+ pub expected: Expected<f64>,
+}
+
+#[derive(Debug)]
+pub struct Counter {
+ pub name: &'static str,
+ pub unit: &'static str,
+ pub show_as: ShowAs,
+ pub expected: Expected<f64>,
+
+ ///
+ value: f64,
+ /// Number of samples in the current time slice.
+ num_samples: u64,
+ /// Sum of the values recorded during the current time slice.
+ sum: f64,
+ /// The max value in in-progress time slice.
+ next_max: f64,
+ /// The max value of the previous time slice (displayed).
+ max: f64,
+ /// The average value of the previous time slice (displayed).
+ avg: f64,
+ /// Incremented when the counter changes.
+ change_indicator: u8,
+
+ /// Only used to check that the constants match the real index.
+ index: usize,
+
+ graph: Option<Graph>,
+}
+
+impl Counter {
+ pub fn new(descriptor: &CounterDescriptor) -> Self {
+ Counter {
+ name: descriptor.name,
+ unit: descriptor.unit,
+ show_as: descriptor.show_as,
+ expected: descriptor.expected.clone(),
+ index: descriptor.index,
+ value: std::f64::NAN,
+ num_samples: 0,
+ sum: 0.0,
+ next_max: 0.0,
+ max: 0.0,
+ avg: 0.0,
+ change_indicator: 0,
+ graph: None,
+ }
+ }
+ pub fn set_f64(&mut self, val: f64) {
+ self.value = val;
+ }
+
+ pub fn set<T>(&mut self, val: T) where T: Into<f64> {
+ self.set_f64(val.into());
+ }
+
+ pub fn get(&self) -> Option<f64> {
+ if self.value.is_finite() {
+ Some(self.value)
+ } else {
+ None
+ }
+ }
+
+ pub fn write_value(&self, output: &mut String) {
+ match self.show_as {
+ ShowAs::Float => {
+ set_text!(output, "{:.2} {} (max: {:.2})", self.avg, self.unit, self.max);
+ }
+ ShowAs::Int => {
+ set_text!(output, "{:.0} {} (max: {:.0})", self.avg.round(), self.unit, self.max.round());
+ }
+ }
+ }
+
+ pub fn enable_graph(&mut self, max_samples: usize) {
+ if self.graph.is_some() {
+ return;
+ }
+
+ self.graph = Some(Graph::new(max_samples));
+ }
+
+ pub fn disable_graph(&mut self) {
+ self.graph = None;
+ }
+
+ pub fn is_unexpected_value(&self, value: f64) -> bool {
+ if let Some(range) = &self.expected.range {
+ return value.is_finite() && value >= range.end;
+ }
+
+ false
+ }
+
+ pub fn has_unexpected_value(&self) -> bool {
+ self.is_unexpected_value(self.value)
+ }
+
+ pub fn has_unexpected_avg_max(&self) -> bool {
+ if let Some(range) = &self.expected.range {
+ if self.max.is_finite() && self.max >= range.end {
+ return true;
+ }
+ }
+
+ if let Some(range) = &self.expected.avg {
+ if self.avg < range.start || self.avg >= range.end {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ fn update(&mut self, update_avg: bool) {
+ let updated = self.value.is_finite();
+ if updated {
+ self.next_max = self.next_max.max(self.value);
+ self.sum += self.value;
+ self.num_samples += 1;
+ self.change_indicator = (self.change_indicator + 1) % 15;
+ }
+
+ if let Some(graph) = &mut self.graph {
+ graph.set(self.value);
+ }
+
+ self.value = std::f64::NAN;
+
+ if update_avg && self.num_samples > 0 {
+ self.avg = self.sum / self.num_samples as f64;
+ self.max = self.next_max;
+ self.sum = 0.0;
+ self.num_samples = 0;
+ self.next_max = std::f64::MIN;
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum Event {
+ Start(f64),
+ Value(f64),
+ None,
+}
+
+// std::convert::From/TryFrom can't deal with integer to f64 so we roll our own...
+pub trait EventValue {
+ fn into_f64(self) -> f64;
+}
+
+impl EventValue for f64 { fn into_f64(self) -> f64 { self } }
+impl EventValue for f32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for u32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for i32 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for u64 { fn into_f64(self) -> f64 { self as f64 } }
+impl EventValue for usize { fn into_f64(self) -> f64 { self as f64 } }
+
+/// A container for profiling information that moves along the rendering pipeline
+/// and is handed off to the profiler at the end.
+pub struct TransactionProfile {
+ pub events: Vec<Event>,
+}
+
+impl TransactionProfile {
+ pub fn new() -> Self {
+ TransactionProfile {
+ events: vec![Event::None; NUM_PROFILER_EVENTS],
+ }
+ }
+
+ pub fn start_time(&mut self, id: usize) {
+ let ms = ns_to_ms(precise_time_ns());
+ self.events[id] = Event::Start(ms);
+ }
+
+ pub fn end_time(&mut self, id: usize) -> f64 {
+ self.end_time_if_started(id).unwrap()
+ }
+
+ /// Similar to end_time, but doesn't panic if not matched with start_time.
+ pub fn end_time_if_started(&mut self, id: usize) -> Option<f64> {
+ if let Event::Start(start) = self.events[id] {
+ let time = ns_to_ms(precise_time_ns()) - start;
+ self.events[id] = Event::Value(time);
+
+ Some(time)
+ } else {
+ None
+ }
+ }
+
+ pub fn set<T>(&mut self, id: usize, value: T) where T: EventValue {
+ self.set_f64(id, value.into_f64());
+ }
+
+
+ pub fn set_f64(&mut self, id: usize, value: f64) {
+ self.events[id] = Event::Value(value);
+ }
+
+ pub fn get(&self, id: usize) -> Option<f64> {
+ if let Event::Value(val) = self.events[id] {
+ Some(val)
+ } else {
+ None
+ }
+ }
+
+ pub fn get_or(&self, id: usize, or: f64) -> f64 {
+ self.get(id).unwrap_or(or)
+ }
+
+ pub fn add<T>(&mut self, id: usize, n: T) where T: EventValue {
+ let n = n.into_f64();
+
+ let evt = &mut self.events[id];
+
+ let val = match *evt {
+ Event::Value(v) => v + n,
+ Event::None => n,
+ Event::Start(..) => { panic!(); }
+ };
+
+ *evt = Event::Value(val);
+ }
+
+ pub fn inc(&mut self, id: usize) {
+ self.add(id, 1.0);
+ }
+
+ pub fn take(&mut self) -> Self {
+ TransactionProfile {
+ events: std::mem::take(&mut self.events),
+ }
+ }
+
+ pub fn take_and_reset(&mut self) -> Self {
+ let events = std::mem::take(&mut self.events);
+
+ *self = TransactionProfile::new();
+
+ TransactionProfile { events }
+ }
+
+ pub fn merge(&mut self, other: &mut Self) {
+ for i in 0..self.events.len() {
+ match (self.events[i], other.events[i]) {
+ (Event::Value(v1), Event::Value(v2)) => {
+ self.events[i] = Event::Value(v1.max(v2));
+ }
+ (Event::Value(_), _) => {}
+ (_, Event::Value(v2)) => {
+ self.events[i] = Event::Value(v2);
+ }
+ (Event::None, evt) => {
+ self.events[i] = evt;
+ }
+ (Event::Start(..), Event::Start(s)) => {
+ self.events[i] = Event::Start(s);
+ }
+ _=> {}
+ }
+ other.events[i] = Event::None;
+ }
+ }
+
+ pub fn clear(&mut self) {
+ for evt in &mut self.events {
+ *evt = Event::None;
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct GraphStats {
+ pub min: f64,
+ pub avg: f64,
+ pub max: f64,
+ pub sum: f64,
+ pub samples: usize,
+}
+
+#[derive(Debug)]
+pub struct Graph {
+ values: VecDeque<f64>,
+}
+
+impl Graph {
+ fn new(max_samples: usize) -> Self {
+ let mut values = VecDeque::new();
+ values.reserve(max_samples);
+
+ Graph { values }
+ }
+
+ fn set(&mut self, val: f64) {
+ if self.values.len() == self.values.capacity() {
+ self.values.pop_back();
+ }
+ self.values.push_front(val);
+ }
+
+ pub fn stats(&self) -> GraphStats {
+ let mut stats = GraphStats {
+ min: f64::MAX,
+ avg: 0.0,
+ max: -f64::MAX,
+ sum: 0.0,
+ samples: 0,
+ };
+
+ let mut samples = 0;
+ for value in &self.values {
+ if value.is_finite() {
+ stats.min = stats.min.min(*value);
+ stats.max = stats.max.max(*value);
+ stats.sum += *value;
+ samples += 1;
+ }
+ }
+
+ if samples > 0 {
+ stats.avg = stats.sum / samples as f64;
+ stats.samples = samples;
+ }
+
+ stats
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ShowAs {
+ Float,
+ Int,
+}
+
+struct GpuFrame {
+ total_time: u64,
+ samples: Vec<GpuTimer>,
+}
+
+struct GpuFrameCollection {
+ frames: VecDeque<GpuFrame>,
+}
+
+impl GpuFrameCollection {
+ fn new() -> Self {
+ GpuFrameCollection {
+ frames: VecDeque::new(),
+ }
+ }
+
+ fn push(&mut self, total_time: u64, samples: Vec<GpuTimer>) {
+ if self.frames.len() == 20 {
+ self.frames.pop_back();
+ }
+ self.frames.push_front(GpuFrame {
+ total_time,
+ samples,
+ });
+ }
+}
+
+pub fn ns_to_ms(ns: u64) -> f64 {
+ ns as f64 / 1_000_000.0
+}
+
+pub fn bytes_to_mb(bytes: usize) -> f64 {
+ bytes as f64 / 1_000_000.0
+}
+
+#[derive(Debug, PartialEq)]
+enum Item {
+ Counters(Vec<usize>),
+ Graph(usize),
+ ChangeIndicator(usize),
+ Fps,
+ GpuTimeQueries,
+ GpuCacheBars,
+ Text(String),
+ Space,
+ Column,
+ Row,
+}
+
diff --git a/gfx/wr/webrender/src/render_api.rs b/gfx/wr/webrender/src/render_api.rs
new file mode 100644
index 0000000000..7362387567
--- /dev/null
+++ b/gfx/wr/webrender/src/render_api.rs
@@ -0,0 +1,1545 @@
+/* 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/. */
+
+#![deny(missing_docs)]
+
+use std::cell::Cell;
+use std::fmt;
+use std::marker::PhantomData;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::u32;
+use time::precise_time_ns;
+//use crate::api::peek_poke::PeekPoke;
+use crate::api::channel::{Sender, single_msg_channel, unbounded_channel};
+use crate::api::{ColorF, BuiltDisplayList, IdNamespace, ExternalScrollId};
+use crate::api::{SharedFontInstanceMap, FontKey, FontInstanceKey, NativeFontHandle, ZoomFactor};
+use crate::api::{BlobImageData, BlobImageKey, ImageData, ImageDescriptor, ImageKey, Epoch, QualitySettings};
+use crate::api::{BlobImageParams, BlobImageRequest, BlobImageResult, AsyncBlobImageRasterizer, BlobImageHandler};
+use crate::api::{DocumentId, PipelineId, PropertyBindingId, PropertyBindingKey, ExternalEvent};
+use crate::api::{HitTestResult, HitTesterRequest, ApiHitTester, PropertyValue, DynamicProperties};
+use crate::api::{ScrollClamping, TileSize, NotificationRequest, DebugFlags, ScrollNodeState};
+use crate::api::{GlyphDimensionRequest, GlyphIndexRequest, GlyphIndex, GlyphDimensions};
+use crate::api::{FontInstanceOptions, FontInstancePlatformOptions, FontVariation};
+use crate::api::DEFAULT_TILE_SIZE;
+use crate::api::units::*;
+use crate::api_resources::ApiResources;
+use crate::scene_builder_thread::{SceneBuilderRequest, SceneBuilderResult};
+use crate::intern::InterningMemoryReport;
+use crate::profiler::{self, TransactionProfile};
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+struct ResourceId(pub u32);
+
+/// Update of a persistent resource in WebRender.
+///
+/// ResourceUpdate changes keep theirs effect across display list changes.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum ResourceUpdate {
+ /// See `AddImage`.
+ AddImage(AddImage),
+ /// See `UpdateImage`.
+ UpdateImage(UpdateImage),
+ /// Delete an existing image resource.
+ ///
+ /// It is invalid to continue referring to the image key in any display list
+ /// in the transaction that contains the `DeleteImage` message and subsequent
+ /// transactions.
+ DeleteImage(ImageKey),
+ /// See `AddBlobImage`.
+ AddBlobImage(AddBlobImage),
+ /// See `UpdateBlobImage`.
+ UpdateBlobImage(UpdateBlobImage),
+ /// Delete existing blob image resource.
+ DeleteBlobImage(BlobImageKey),
+ /// See `AddBlobImage::visible_area`.
+ SetBlobImageVisibleArea(BlobImageKey, DeviceIntRect),
+ /// See `AddFont`.
+ AddFont(AddFont),
+ /// Deletes an already existing font resource.
+ ///
+ /// It is invalid to continue referring to the font key in any display list
+ /// in the transaction that contains the `DeleteImage` message and subsequent
+ /// transactions.
+ DeleteFont(FontKey),
+ /// See `AddFontInstance`.
+ AddFontInstance(AddFontInstance),
+ /// Deletes an already existing font instance resource.
+ ///
+ /// It is invalid to continue referring to the font instance in any display
+ /// list in the transaction that contains the `DeleteImage` message and
+ /// subsequent transactions.
+ DeleteFontInstance(FontInstanceKey),
+}
+
+impl fmt::Debug for ResourceUpdate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ResourceUpdate::AddImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUpdate::AddImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::UpdateImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUpdate::UpdateImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::AddBlobImage(ref i) => f.write_fmt(format_args!(
+ "ResourceUFpdate::AddBlobImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::UpdateBlobImage(i) => f.write_fmt(format_args!(
+ "ResourceUpdate::UpdateBlobImage size({:?})",
+ &i.descriptor.size
+ )),
+ ResourceUpdate::DeleteImage(..) => f.write_str("ResourceUpdate::DeleteImage"),
+ ResourceUpdate::DeleteBlobImage(..) => f.write_str("ResourceUpdate::DeleteBlobImage"),
+ ResourceUpdate::SetBlobImageVisibleArea(..) => f.write_str("ResourceUpdate::SetBlobImageVisibleArea"),
+ ResourceUpdate::AddFont(..) => f.write_str("ResourceUpdate::AddFont"),
+ ResourceUpdate::DeleteFont(..) => f.write_str("ResourceUpdate::DeleteFont"),
+ ResourceUpdate::AddFontInstance(..) => f.write_str("ResourceUpdate::AddFontInstance"),
+ ResourceUpdate::DeleteFontInstance(..) => f.write_str("ResourceUpdate::DeleteFontInstance"),
+ }
+ }
+}
+
+/// Whether to generate a frame, and if so, an id that allows tracking this
+/// transaction through the various frame stages.
+#[derive(Clone, Debug)]
+pub enum GenerateFrame {
+ /// Generate a frame if something changed.
+ Yes {
+ /// An id that allows tracking the frame transaction through the various
+ /// frame stages. Specified by the caller of generate_frame().
+ id: u64,
+ },
+ /// Don't generate a frame even if something has changed.
+ No,
+}
+
+impl GenerateFrame {
+ ///
+ pub fn as_bool(&self) -> bool {
+ match self {
+ GenerateFrame::Yes { .. } => true,
+ GenerateFrame::No => false,
+ }
+ }
+
+ /// Return the frame ID, if a frame is generated.
+ pub fn id(&self) -> Option<u64> {
+ match self {
+ GenerateFrame::Yes { id } => Some(*id),
+ GenerateFrame::No => None,
+ }
+ }
+}
+
+/// A Transaction is a group of commands to apply atomically to a document.
+///
+/// This mechanism ensures that:
+/// - no other message can be interleaved between two commands that need to be applied together.
+/// - no redundant work is performed if two commands in the same transaction cause the scene or
+/// the frame to be rebuilt.
+pub struct Transaction {
+ /// Operations affecting the scene (applied before scene building).
+ scene_ops: Vec<SceneMsg>,
+ /// Operations affecting the generation of frames (applied after scene building).
+ frame_ops: Vec<FrameMsg>,
+
+ notifications: Vec<NotificationRequest>,
+
+ /// Persistent resource updates to apply as part of this transaction.
+ pub resource_updates: Vec<ResourceUpdate>,
+
+ /// If true the transaction is piped through the scene building thread, if false
+ /// it will be applied directly on the render backend.
+ use_scene_builder_thread: bool,
+
+ /// Whether to generate a frame, and if so, an id that allows tracking this
+ /// transaction through the various frame stages. Specified by the caller of
+ /// generate_frame().
+ generate_frame: GenerateFrame,
+
+ /// Set to true in order to force re-rendering even if WebRender can't internally
+ /// detect that something has changed.
+ pub invalidate_rendered_frame: bool,
+
+ low_priority: bool,
+}
+
+impl Transaction {
+ /// Constructor.
+ pub fn new() -> Self {
+ Transaction {
+ scene_ops: Vec::new(),
+ frame_ops: Vec::new(),
+ resource_updates: Vec::new(),
+ notifications: Vec::new(),
+ use_scene_builder_thread: true,
+ generate_frame: GenerateFrame::No,
+ invalidate_rendered_frame: false,
+ low_priority: false,
+ }
+ }
+
+ /// Marks this transaction to allow it to skip going through the scene builder
+ /// thread.
+ ///
+ /// This is useful to avoid jank in transaction associated with animated
+ /// property updates, panning and zooming.
+ ///
+ /// Note that transactions that skip the scene builder thread can race ahead of
+ /// transactions that don't skip it.
+ pub fn skip_scene_builder(&mut self) {
+ self.use_scene_builder_thread = false;
+ }
+
+ /// Marks this transaction to enforce going through the scene builder thread.
+ pub fn use_scene_builder_thread(&mut self) {
+ self.use_scene_builder_thread = true;
+ }
+
+ /// Returns true if the transaction has no effect.
+ pub fn is_empty(&self) -> bool {
+ !self.generate_frame.as_bool() &&
+ !self.invalidate_rendered_frame &&
+ self.scene_ops.is_empty() &&
+ self.frame_ops.is_empty() &&
+ self.resource_updates.is_empty() &&
+ self.notifications.is_empty()
+ }
+
+ /// Update a pipeline's epoch.
+ pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
+ // We track epochs before and after scene building.
+ // This one will be applied to the pending scene right away:
+ self.scene_ops.push(SceneMsg::UpdateEpoch(pipeline_id, epoch));
+ // And this one will be applied to the currently built scene at the end
+ // of the transaction (potentially long after the scene_ops one).
+ self.frame_ops.push(FrameMsg::UpdateEpoch(pipeline_id, epoch));
+ // We could avoid the duplication here by storing the epoch updates in a
+ // separate array and let the render backend schedule the updates at the
+ // proper times, but it wouldn't make things simpler.
+ }
+
+ /// Sets the root pipeline.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use webrender::api::{PipelineId};
+ /// # use webrender::api::units::{DeviceIntSize};
+ /// # use webrender::render_api::{RenderApiSender, Transaction};
+ /// # fn example() {
+ /// let pipeline_id = PipelineId(0, 0);
+ /// let mut txn = Transaction::new();
+ /// txn.set_root_pipeline(pipeline_id);
+ /// # }
+ /// ```
+ pub fn set_root_pipeline(&mut self, pipeline_id: PipelineId) {
+ self.scene_ops.push(SceneMsg::SetRootPipeline(pipeline_id));
+ }
+
+ /// Removes data associated with a pipeline from the internal data structures.
+ /// If the specified `pipeline_id` is for the root pipeline, the root pipeline
+ /// is reset back to `None`.
+ pub fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
+ self.scene_ops.push(SceneMsg::RemovePipeline(pipeline_id));
+ }
+
+ /// Supplies a new frame to WebRender.
+ ///
+ /// Non-blocking, it notifies a worker process which processes the display list.
+ ///
+ /// Note: Scrolling doesn't require an own Frame.
+ ///
+ /// Arguments:
+ ///
+ /// * `epoch`: The unique Frame ID, monotonically increasing.
+ /// * `background`: The background color of this pipeline.
+ /// * `viewport_size`: The size of the viewport for this frame.
+ /// * `pipeline_id`: The ID of the pipeline that is supplying this display list.
+ /// * `display_list`: The root Display list used in this frame.
+ /// * `preserve_frame_state`: If a previous frame exists which matches this pipeline
+ /// id, this setting determines if frame state (such as scrolling
+ /// position) should be preserved for this new display list.
+ pub fn set_display_list(
+ &mut self,
+ epoch: Epoch,
+ background: Option<ColorF>,
+ viewport_size: LayoutSize,
+ (pipeline_id, mut display_list): (PipelineId, BuiltDisplayList),
+ preserve_frame_state: bool,
+ ) {
+ display_list.set_send_time_ns(precise_time_ns());
+ self.scene_ops.push(
+ SceneMsg::SetDisplayList {
+ display_list,
+ epoch,
+ pipeline_id,
+ background,
+ viewport_size,
+ preserve_frame_state,
+ }
+ );
+ }
+
+ /// Add a set of persistent resource updates to apply as part of this transaction.
+ pub fn update_resources(&mut self, mut resources: Vec<ResourceUpdate>) {
+ self.resource_updates.append(&mut resources);
+ }
+
+ // Note: Gecko uses this to get notified when a transaction that contains
+ // potentially long blob rasterization or scene build is ready to be rendered.
+ // so that the tab-switching integration can react adequately when tab
+ // switching takes too long. For this use case when matters is that the
+ // notification doesn't fire before scene building and blob rasterization.
+
+ /// Trigger a notification at a certain stage of the rendering pipeline.
+ ///
+ /// Not that notification requests are skipped during serialization, so is is
+ /// best to use them for synchronization purposes and not for things that could
+ /// affect the WebRender's state.
+ pub fn notify(&mut self, event: NotificationRequest) {
+ self.notifications.push(event);
+ }
+
+ /// Setup the output region in the framebuffer for a given document.
+ pub fn set_document_view(
+ &mut self,
+ device_rect: DeviceIntRect,
+ device_pixel_ratio: f32,
+ ) {
+ assert!(device_pixel_ratio > 0.0);
+ window_size_sanity_check(device_rect.size);
+ self.scene_ops.push(
+ SceneMsg::SetDocumentView {
+ device_rect,
+ device_pixel_ratio,
+ },
+ );
+ }
+
+ /// Scrolls the node identified by the given external scroll id to the
+ /// given scroll position, relative to the pre-scrolled offset for the
+ /// scrolling layer. That is, providing an origin of (0,0) will reset
+ /// any WR-side scrolling and just render the display items at the
+ /// pre-scrolled offsets as provided in the display list. Larger `origin`
+ /// values will cause the layer to be scrolled further towards the end of
+ /// the scroll range.
+ /// If the ScrollClamping argument is set to clamp, the scroll position
+ /// is clamped to what WebRender understands to be the bounds of the
+ /// scroll range, based on the sizes of the scrollable content and the
+ /// scroll port.
+ pub fn scroll_node_with_id(
+ &mut self,
+ origin: LayoutPoint,
+ id: ExternalScrollId,
+ clamp: ScrollClamping,
+ ) {
+ self.frame_ops.push(FrameMsg::ScrollNodeWithId(origin, id, clamp));
+ }
+
+ /// Set the current quality / performance settings for this document.
+ pub fn set_quality_settings(&mut self, settings: QualitySettings) {
+ self.scene_ops.push(SceneMsg::SetQualitySettings { settings });
+ }
+
+ ///
+ pub fn set_page_zoom(&mut self, page_zoom: ZoomFactor) {
+ self.scene_ops.push(SceneMsg::SetPageZoom(page_zoom));
+ }
+
+ ///
+ pub fn set_pinch_zoom(&mut self, pinch_zoom: ZoomFactor) {
+ self.frame_ops.push(FrameMsg::SetPinchZoom(pinch_zoom));
+ }
+
+ ///
+ pub fn set_is_transform_async_zooming(&mut self, is_zooming: bool, animation_id: PropertyBindingId) {
+ self.frame_ops.push(FrameMsg::SetIsTransformAsyncZooming(is_zooming, animation_id));
+ }
+
+ ///
+ pub fn set_pan(&mut self, pan: DeviceIntPoint) {
+ self.frame_ops.push(FrameMsg::SetPan(pan));
+ }
+
+ /// Generate a new frame. When it's done and a RenderNotifier has been set
+ /// in `webrender::Renderer`, [new_frame_ready()][notifier] gets called.
+ /// Note that the notifier is called even if the frame generation was a
+ /// no-op; the arguments passed to `new_frame_ready` will provide information
+ /// as to when happened.
+ ///
+ /// [notifier]: trait.RenderNotifier.html#tymethod.new_frame_ready
+ pub fn generate_frame(&mut self, id: u64) {
+ self.generate_frame = GenerateFrame::Yes{ id };
+ }
+
+ /// Invalidate rendered frame. It ensure that frame will be rendered during
+ /// next frame generation. WebRender could skip frame rendering if there
+ /// is no update.
+ /// But there are cases that needs to force rendering.
+ /// - Content of image is updated by reusing same ExternalImageId.
+ /// - Platform requests it if pixels become stale (like wakeup from standby).
+ pub fn invalidate_rendered_frame(&mut self) {
+ self.invalidate_rendered_frame = true;
+ }
+
+ /// Supply a list of animated property bindings that should be used to resolve
+ /// bindings in the current display list.
+ pub fn update_dynamic_properties(&mut self, properties: DynamicProperties) {
+ self.frame_ops.push(FrameMsg::UpdateDynamicProperties(properties));
+ }
+
+ /// Add to the list of animated property bindings that should be used to
+ /// resolve bindings in the current display list. This is a convenience method
+ /// so the caller doesn't have to figure out all the dynamic properties before
+ /// setting them on the transaction but can do them incrementally.
+ pub fn append_dynamic_transform_properties(&mut self, transforms: Vec<PropertyValue<LayoutTransform>>) {
+ self.frame_ops.push(FrameMsg::AppendDynamicTransformProperties(transforms));
+ }
+
+ /// Consumes this object and just returns the frame ops.
+ pub fn get_frame_ops(self) -> Vec<FrameMsg> {
+ self.frame_ops
+ }
+
+ fn finalize(self, document_id: DocumentId) -> Box<TransactionMsg> {
+ Box::new(TransactionMsg {
+ document_id,
+ scene_ops: self.scene_ops,
+ frame_ops: self.frame_ops,
+ resource_updates: self.resource_updates,
+ notifications: self.notifications,
+ use_scene_builder_thread: self.use_scene_builder_thread,
+ generate_frame: self.generate_frame,
+ invalidate_rendered_frame: self.invalidate_rendered_frame,
+ low_priority: self.low_priority,
+ blob_rasterizer: None,
+ blob_requests: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ profile: TransactionProfile::new(),
+ })
+ }
+
+ /// See `ResourceUpdate::AddImage`.
+ pub fn add_image(
+ &mut self,
+ key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: ImageData,
+ tiling: Option<TileSize>,
+ ) {
+ self.resource_updates.push(ResourceUpdate::AddImage(AddImage {
+ key,
+ descriptor,
+ data,
+ tiling,
+ }));
+ }
+
+ /// See `ResourceUpdate::UpdateImage`.
+ pub fn update_image(
+ &mut self,
+ key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: ImageData,
+ dirty_rect: &ImageDirtyRect,
+ ) {
+ self.resource_updates.push(ResourceUpdate::UpdateImage(UpdateImage {
+ key,
+ descriptor,
+ data,
+ dirty_rect: *dirty_rect,
+ }));
+ }
+
+ /// See `ResourceUpdate::DeleteImage`.
+ pub fn delete_image(&mut self, key: ImageKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteImage(key));
+ }
+
+ /// See `ResourceUpdate::AddBlobImage`.
+ pub fn add_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: ImageDescriptor,
+ data: Arc<BlobImageData>,
+ visible_rect: DeviceIntRect,
+ tile_size: Option<TileSize>,
+ ) {
+ self.resource_updates.push(
+ ResourceUpdate::AddBlobImage(AddBlobImage {
+ key,
+ descriptor,
+ data,
+ visible_rect,
+ tile_size: tile_size.unwrap_or(DEFAULT_TILE_SIZE),
+ })
+ );
+ }
+
+ /// See `ResourceUpdate::UpdateBlobImage`.
+ pub fn update_blob_image(
+ &mut self,
+ key: BlobImageKey,
+ descriptor: ImageDescriptor,
+ data: Arc<BlobImageData>,
+ visible_rect: DeviceIntRect,
+ dirty_rect: &BlobDirtyRect,
+ ) {
+ self.resource_updates.push(
+ ResourceUpdate::UpdateBlobImage(UpdateBlobImage {
+ key,
+ descriptor,
+ data,
+ visible_rect,
+ dirty_rect: *dirty_rect,
+ })
+ );
+ }
+
+ /// See `ResourceUpdate::DeleteBlobImage`.
+ pub fn delete_blob_image(&mut self, key: BlobImageKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteBlobImage(key));
+ }
+
+ /// See `ResourceUpdate::SetBlobImageVisibleArea`.
+ pub fn set_blob_image_visible_area(&mut self, key: BlobImageKey, area: DeviceIntRect) {
+ self.resource_updates.push(ResourceUpdate::SetBlobImageVisibleArea(key, area));
+ }
+
+ /// See `ResourceUpdate::AddFont`.
+ pub fn add_raw_font(&mut self, key: FontKey, bytes: Vec<u8>, index: u32) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFont(AddFont::Raw(key, Arc::new(bytes), index)));
+ }
+
+ /// See `ResourceUpdate::AddFont`.
+ pub fn add_native_font(&mut self, key: FontKey, native_handle: NativeFontHandle) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFont(AddFont::Native(key, native_handle)));
+ }
+
+ /// See `ResourceUpdate::DeleteFont`.
+ pub fn delete_font(&mut self, key: FontKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteFont(key));
+ }
+
+ /// See `ResourceUpdate::AddFontInstance`.
+ pub fn add_font_instance(
+ &mut self,
+ key: FontInstanceKey,
+ font_key: FontKey,
+ glyph_size: f32,
+ options: Option<FontInstanceOptions>,
+ platform_options: Option<FontInstancePlatformOptions>,
+ variations: Vec<FontVariation>,
+ ) {
+ self.resource_updates
+ .push(ResourceUpdate::AddFontInstance(AddFontInstance {
+ key,
+ font_key,
+ glyph_size,
+ options,
+ platform_options,
+ variations,
+ }));
+ }
+
+ /// See `ResourceUpdate::DeleteFontInstance`.
+ pub fn delete_font_instance(&mut self, key: FontInstanceKey) {
+ self.resource_updates.push(ResourceUpdate::DeleteFontInstance(key));
+ }
+
+ /// A hint that this transaction can be processed at a lower priority. High-
+ /// priority transactions can jump ahead of regular-priority transactions,
+ /// but both high- and regular-priority transactions are processed in order
+ /// relative to other transactions of the same priority.
+ pub fn set_low_priority(&mut self, low_priority: bool) {
+ self.low_priority = low_priority;
+ }
+
+ /// Returns whether this transaction is marked as low priority.
+ pub fn is_low_priority(&self) -> bool {
+ self.low_priority
+ }
+}
+
+///
+pub struct DocumentTransaction {
+ ///
+ pub document_id: DocumentId,
+ ///
+ pub transaction: Transaction,
+}
+
+/// Represents a transaction in the format sent through the channel.
+pub struct TransactionMsg {
+ ///
+ pub document_id: DocumentId,
+ /// Changes that require re-building the scene.
+ pub scene_ops: Vec<SceneMsg>,
+ /// Changes to animated properties that do not require re-building the scene.
+ pub frame_ops: Vec<FrameMsg>,
+ /// Updates to resources that persist across display lists.
+ pub resource_updates: Vec<ResourceUpdate>,
+ /// Whether to trigger frame building and rendering if something has changed.
+ pub generate_frame: GenerateFrame,
+ /// Whether to force frame building and rendering even if no changes are internally
+ /// observed.
+ pub invalidate_rendered_frame: bool,
+ /// Whether to enforce that this transaction go through the scene builder.
+ pub use_scene_builder_thread: bool,
+ ///
+ pub low_priority: bool,
+
+ /// Handlers to notify at certain points of the pipeline.
+ pub notifications: Vec<NotificationRequest>,
+ ///
+ pub blob_rasterizer: Option<Box<dyn AsyncBlobImageRasterizer>>,
+ ///
+ pub blob_requests: Vec<BlobImageParams>,
+ ///
+ pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+ /// Collect various data along the rendering pipeline to display it in the embedded profiler.
+ pub profile: TransactionProfile,
+}
+
+impl fmt::Debug for TransactionMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ writeln!(f, "threaded={}, genframe={:?}, invalidate={}, low_priority={}",
+ self.use_scene_builder_thread,
+ self.generate_frame,
+ self.invalidate_rendered_frame,
+ self.low_priority,
+ ).unwrap();
+ for scene_op in &self.scene_ops {
+ writeln!(f, "\t\t{:?}", scene_op).unwrap();
+ }
+
+ for frame_op in &self.frame_ops {
+ writeln!(f, "\t\t{:?}", frame_op).unwrap();
+ }
+
+ for resource_update in &self.resource_updates {
+ writeln!(f, "\t\t{:?}", resource_update).unwrap();
+ }
+ Ok(())
+ }
+}
+
+impl TransactionMsg {
+ /// Returns true if this transaction has no effect.
+ pub fn is_empty(&self) -> bool {
+ !self.generate_frame.as_bool() &&
+ !self.invalidate_rendered_frame &&
+ self.scene_ops.is_empty() &&
+ self.frame_ops.is_empty() &&
+ self.resource_updates.is_empty() &&
+ self.notifications.is_empty()
+ }
+}
+
+/// Creates an image resource with provided parameters.
+///
+/// Must be matched with a `DeleteImage` at some point to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddImage {
+ /// A key to identify the image resource.
+ pub key: ImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The pixels of the image.
+ pub data: ImageData,
+ /// An optional tiling scheme to apply when storing the image's data
+ /// on the GPU. Applies to both width and heights of the tiles.
+ ///
+ /// Note that WebRender may internally chose to tile large images
+ /// even if this member is set to `None`.
+ pub tiling: Option<TileSize>,
+}
+
+/// Updates an already existing image resource.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct UpdateImage {
+ /// The key identfying the image resource to update.
+ pub key: ImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The pixels of the image.
+ pub data: ImageData,
+ /// An optional dirty rect that lets WebRender optimize the amount of
+ /// data to transfer to the GPU.
+ ///
+ /// The data provided must still represent the entire image.
+ pub dirty_rect: ImageDirtyRect,
+}
+
+/// Creates a blob-image resource with provided parameters.
+///
+/// Must be matched with a `DeleteImage` at some point to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddBlobImage {
+ /// A key to identify the blob-image resource.
+ pub key: BlobImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The blob-image's serialized commands.
+ pub data: Arc<BlobImageData>,
+ /// The portion of the plane in the blob-image's internal coordinate
+ /// system that is stretched to fill the image display item.
+ ///
+ /// Unlike regular images, blob images are not limited in size. The
+ /// top-left corner of their internal coordinate system is also not
+ /// necessary at (0, 0).
+ /// This means that blob images can be updated to insert/remove content
+ /// in any direction to support panning and zooming.
+ pub visible_rect: DeviceIntRect,
+ /// The blob image's tile size to apply when rasterizing the blob-image
+ /// and when storing its rasterized data on the GPU.
+ /// Applies to both width and heights of the tiles.
+ ///
+ /// All blob images are tiled.
+ pub tile_size: TileSize,
+}
+
+/// Updates an already existing blob-image resource.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct UpdateBlobImage {
+ /// The key identfying the blob-image resource to update.
+ pub key: BlobImageKey,
+ /// Properties of the image.
+ pub descriptor: ImageDescriptor,
+ /// The blob-image's serialized commands.
+ pub data: Arc<BlobImageData>,
+ /// See `AddBlobImage::visible_rect`.
+ pub visible_rect: DeviceIntRect,
+ /// An optional dirty rect that lets WebRender optimize the amount of
+ /// data to to rasterize and transfer to the GPU.
+ pub dirty_rect: BlobDirtyRect,
+}
+
+/// Creates a font resource.
+///
+/// Must be matched with a corresponding `ResourceUpdate::DeleteFont` at some point to prevent
+/// memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub enum AddFont {
+ ///
+ Raw(FontKey, Arc<Vec<u8>>, u32),
+ ///
+ Native(FontKey, NativeFontHandle),
+}
+
+/// Creates a font instance resource.
+///
+/// Must be matched with a corresponding `DeleteFontInstance` at some point
+/// to prevent memory leaks.
+#[derive(Clone)]
+#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))]
+pub struct AddFontInstance {
+ /// A key to identify the font instance.
+ pub key: FontInstanceKey,
+ /// The font resource's key.
+ pub font_key: FontKey,
+ /// Glyph size in app units.
+ pub glyph_size: f32,
+ ///
+ pub options: Option<FontInstanceOptions>,
+ ///
+ pub platform_options: Option<FontInstancePlatformOptions>,
+ ///
+ pub variations: Vec<FontVariation>,
+}
+
+/// Frame messages affect building the scene.
+pub enum SceneMsg {
+ ///
+ UpdateEpoch(PipelineId, Epoch),
+ ///
+ SetPageZoom(ZoomFactor),
+ ///
+ SetRootPipeline(PipelineId),
+ ///
+ RemovePipeline(PipelineId),
+ ///
+ SetDisplayList {
+ ///
+ display_list: BuiltDisplayList,
+ ///
+ epoch: Epoch,
+ ///
+ pipeline_id: PipelineId,
+ ///
+ background: Option<ColorF>,
+ ///
+ viewport_size: LayoutSize,
+ ///
+ preserve_frame_state: bool,
+ },
+ ///
+ SetDocumentView {
+ ///
+ device_rect: DeviceIntRect,
+ ///
+ device_pixel_ratio: f32,
+ },
+ /// Set the current quality / performance configuration for this document.
+ SetQualitySettings {
+ /// The set of available quality / performance config values.
+ settings: QualitySettings,
+ },
+}
+
+/// Frame messages affect frame generation (applied after building the scene).
+pub enum FrameMsg {
+ ///
+ UpdateEpoch(PipelineId, Epoch),
+ ///
+ HitTest(Option<PipelineId>, WorldPoint, Sender<HitTestResult>),
+ ///
+ RequestHitTester(Sender<Arc<dyn ApiHitTester>>),
+ ///
+ SetPan(DeviceIntPoint),
+ ///
+ ScrollNodeWithId(LayoutPoint, ExternalScrollId, ScrollClamping),
+ ///
+ GetScrollNodeState(Sender<Vec<ScrollNodeState>>),
+ ///
+ UpdateDynamicProperties(DynamicProperties),
+ ///
+ AppendDynamicTransformProperties(Vec<PropertyValue<LayoutTransform>>),
+ ///
+ SetPinchZoom(ZoomFactor),
+ ///
+ SetIsTransformAsyncZooming(bool, PropertyBindingId),
+}
+
+impl fmt::Debug for SceneMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ SceneMsg::UpdateEpoch(..) => "SceneMsg::UpdateEpoch",
+ SceneMsg::SetDisplayList { .. } => "SceneMsg::SetDisplayList",
+ SceneMsg::SetPageZoom(..) => "SceneMsg::SetPageZoom",
+ SceneMsg::RemovePipeline(..) => "SceneMsg::RemovePipeline",
+ SceneMsg::SetDocumentView { .. } => "SceneMsg::SetDocumentView",
+ SceneMsg::SetRootPipeline(..) => "SceneMsg::SetRootPipeline",
+ SceneMsg::SetQualitySettings { .. } => "SceneMsg::SetQualitySettings",
+ })
+ }
+}
+
+impl fmt::Debug for FrameMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ FrameMsg::UpdateEpoch(..) => "FrameMsg::UpdateEpoch",
+ FrameMsg::HitTest(..) => "FrameMsg::HitTest",
+ FrameMsg::RequestHitTester(..) => "FrameMsg::RequestHitTester",
+ FrameMsg::SetPan(..) => "FrameMsg::SetPan",
+ FrameMsg::ScrollNodeWithId(..) => "FrameMsg::ScrollNodeWithId",
+ FrameMsg::GetScrollNodeState(..) => "FrameMsg::GetScrollNodeState",
+ FrameMsg::UpdateDynamicProperties(..) => "FrameMsg::UpdateDynamicProperties",
+ FrameMsg::AppendDynamicTransformProperties(..) => "FrameMsg::AppendDynamicTransformProperties",
+ FrameMsg::SetPinchZoom(..) => "FrameMsg::SetPinchZoom",
+ FrameMsg::SetIsTransformAsyncZooming(..) => "FrameMsg::SetIsTransformAsyncZooming",
+ })
+ }
+}
+
+bitflags!{
+ /// Bit flags for WR stages to store in a capture.
+ // Note: capturing `FRAME` without `SCENE` is not currently supported.
+ pub struct CaptureBits: u8 {
+ ///
+ const SCENE = 0x1;
+ ///
+ const FRAME = 0x2;
+ ///
+ const TILE_CACHE = 0x4;
+ ///
+ const EXTERNAL_RESOURCES = 0x8;
+ }
+}
+
+bitflags!{
+ /// Mask for clearing caches in debug commands.
+ pub struct ClearCache: u8 {
+ ///
+ const IMAGES = 0b1;
+ ///
+ const GLYPHS = 0b10;
+ ///
+ const GLYPH_DIMENSIONS = 0b100;
+ ///
+ const RENDER_TASKS = 0b1000;
+ ///
+ const TEXTURE_CACHE = 0b10000;
+ /// Clear render target pool
+ const RENDER_TARGETS = 0b100000;
+ }
+}
+
+/// Information about a loaded capture of each document
+/// that is returned by `RenderBackend`.
+#[derive(Clone, Debug)]
+pub struct CapturedDocument {
+ ///
+ pub document_id: DocumentId,
+ ///
+ pub root_pipeline_id: Option<PipelineId>,
+}
+
+/// Update of the state of built-in debugging facilities.
+#[derive(Clone)]
+pub enum DebugCommand {
+ /// Sets the provided debug flags.
+ SetFlags(DebugFlags),
+ /// Configure if dual-source blending is used, if available.
+ EnableDualSourceBlending(bool),
+ /// Fetch current documents and display lists.
+ FetchDocuments,
+ /// Fetch current passes and batches.
+ FetchPasses,
+ // TODO: This should be called FetchClipScrollTree. However, that requires making
+ // changes to webrender's web debugger ui, touching a 4Mb minified file that
+ // is too big to submit through the conventional means.
+ /// Fetch the spatial tree.
+ FetchClipScrollTree,
+ /// Fetch render tasks.
+ FetchRenderTasks,
+ /// Fetch screenshot.
+ FetchScreenshot,
+ /// Save a capture of all the documents state.
+ SaveCapture(PathBuf, CaptureBits),
+ /// Load a capture of all the documents state.
+ LoadCapture(PathBuf, Option<(u32, u32)>, Sender<CapturedDocument>),
+ /// Start capturing a sequence of scene/frame changes.
+ StartCaptureSequence(PathBuf, CaptureBits),
+ /// Stop capturing a sequence of scene/frame changes.
+ StopCaptureSequence,
+ /// Clear cached resources, forcing them to be re-uploaded from templates.
+ ClearCaches(ClearCache),
+ /// Enable/disable native compositor usage
+ EnableNativeCompositor(bool),
+ /// Enable/disable parallel job execution with rayon.
+ EnableMultithreading(bool),
+ /// Sets the maximum amount of existing batches to visit before creating a new one.
+ SetBatchingLookback(u32),
+ /// Invalidate GPU cache, forcing the update from the CPU mirror.
+ InvalidateGpuCache,
+ /// Causes the scene builder to pause for a given amount of milliseconds each time it
+ /// processes a transaction.
+ SimulateLongSceneBuild(u32),
+ /// Causes the low priority scene builder to pause for a given amount of milliseconds
+ /// each time it processes a transaction.
+ SimulateLongLowPrioritySceneBuild(u32),
+ /// Set an override tile size to use for picture caches
+ SetPictureTileSize(Option<DeviceIntSize>),
+}
+
+/// Message sent by the `RenderApi` to the render backend thread.
+pub enum ApiMsg {
+ /// Adds a new document namespace.
+ CloneApi(Sender<IdNamespace>),
+ /// Adds a new document namespace.
+ CloneApiByClient(IdNamespace),
+ /// Adds a new document with given initial size.
+ AddDocument(DocumentId, DeviceIntSize),
+ /// A message targeted at a particular document.
+ UpdateDocuments(Vec<Box<TransactionMsg>>),
+ /// Flush from the caches anything that isn't necessary, to free some memory.
+ MemoryPressure,
+ /// Collects a memory report.
+ ReportMemory(Sender<Box<MemoryReport>>),
+ /// Change debugging options.
+ DebugCommand(DebugCommand),
+ /// Wakes the render backend's event loop up. Needed when an event is communicated
+ /// through another channel.
+ WakeUp,
+ /// Message from the scene builder thread.
+ SceneBuilderResult(SceneBuilderResult),
+}
+
+impl fmt::Debug for ApiMsg {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(match *self {
+ ApiMsg::CloneApi(..) => "ApiMsg::CloneApi",
+ ApiMsg::CloneApiByClient(..) => "ApiMsg::CloneApiByClient",
+ ApiMsg::AddDocument(..) => "ApiMsg::AddDocument",
+ ApiMsg::UpdateDocuments(..) => "ApiMsg::UpdateDocuments",
+ ApiMsg::MemoryPressure => "ApiMsg::MemoryPressure",
+ ApiMsg::ReportMemory(..) => "ApiMsg::ReportMemory",
+ ApiMsg::DebugCommand(..) => "ApiMsg::DebugCommand",
+ ApiMsg::WakeUp => "ApiMsg::WakeUp",
+ ApiMsg::SceneBuilderResult(..) => "ApiMsg::SceneBuilderResult",
+ })
+ }
+}
+
+/// Allows the API to communicate with WebRender.
+///
+/// This object is created along with the `Renderer` and it's main use from a
+/// user perspective is to create one or several `RenderApi` objects.
+pub struct RenderApiSender {
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ shared_font_instances: SharedFontInstanceMap,
+}
+
+impl RenderApiSender {
+ /// Used internally by the `Renderer`.
+ pub fn new(
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ shared_font_instances: SharedFontInstanceMap,
+ ) -> Self {
+ RenderApiSender {
+ api_sender,
+ scene_sender,
+ low_priority_scene_sender,
+ blob_image_handler,
+ shared_font_instances,
+ }
+ }
+
+ /// Creates a new resource API object with a dedicated namespace.
+ pub fn create_api(&self) -> RenderApi {
+ let (sync_tx, sync_rx) = single_msg_channel();
+ let msg = ApiMsg::CloneApi(sync_tx);
+ self.api_sender.send(msg).expect("Failed to send CloneApi message");
+ let namespace_id = match sync_rx.recv() {
+ Ok(id) => id,
+ Err(e) => {
+ // This is used to discover the underlying cause of https://github.com/servo/servo/issues/13480.
+ let webrender_is_alive = self.api_sender.send(ApiMsg::WakeUp);
+ if webrender_is_alive.is_err() {
+ panic!("WebRender was shut down before processing CloneApi: {}", e);
+ } else {
+ panic!("CloneApi message response was dropped while WebRender was still alive: {}", e);
+ }
+ }
+ };
+ RenderApi {
+ api_sender: self.api_sender.clone(),
+ scene_sender: self.scene_sender.clone(),
+ low_priority_scene_sender: self.low_priority_scene_sender.clone(),
+ namespace_id,
+ next_id: Cell::new(ResourceId(0)),
+ resources: ApiResources::new(
+ self.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.shared_font_instances.clone(),
+ ),
+ }
+ }
+
+ /// Creates a new resource API object with a dedicated namespace.
+ /// Namespace id is allocated by client.
+ ///
+ /// The function could be used only when RendererOptions::namespace_alloc_by_client is true.
+ /// When the option is true, create_api() could not be used to prevent namespace id conflict.
+ pub fn create_api_by_client(&self, namespace_id: IdNamespace) -> RenderApi {
+ let msg = ApiMsg::CloneApiByClient(namespace_id);
+ self.api_sender.send(msg).expect("Failed to send CloneApiByClient message");
+ RenderApi {
+ api_sender: self.api_sender.clone(),
+ scene_sender: self.scene_sender.clone(),
+ low_priority_scene_sender: self.low_priority_scene_sender.clone(),
+ namespace_id,
+ next_id: Cell::new(ResourceId(0)),
+ resources: ApiResources::new(
+ self.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.shared_font_instances.clone(),
+ ),
+ }
+ }
+}
+
+/// The main entry point to interact with WebRender.
+pub struct RenderApi {
+ api_sender: Sender<ApiMsg>,
+ scene_sender: Sender<SceneBuilderRequest>,
+ low_priority_scene_sender: Sender<SceneBuilderRequest>,
+ namespace_id: IdNamespace,
+ next_id: Cell<ResourceId>,
+ resources: ApiResources,
+}
+
+impl RenderApi {
+ /// Returns the namespace ID used by this API object.
+ pub fn get_namespace_id(&self) -> IdNamespace {
+ self.namespace_id
+ }
+
+ ///
+ pub fn create_sender(&self) -> RenderApiSender {
+ RenderApiSender::new(
+ self.api_sender.clone(),
+ self.scene_sender.clone(),
+ self.low_priority_scene_sender.clone(),
+ self.resources.blob_image_handler.as_ref().map(|handler| handler.create_similar()),
+ self.resources.get_shared_font_instances(),
+ )
+ }
+
+ /// Add a document to the WebRender instance.
+ ///
+ /// Instances can manage one or several documents (using the same render backend thread).
+ /// Each document will internally correspond to a single scene, and scenes are made of
+ /// one or several pipelines.
+ pub fn add_document(&self, initial_size: DeviceIntSize) -> DocumentId {
+ let new_id = self.next_unique_id();
+ self.add_document_with_id(initial_size, new_id)
+ }
+
+ /// See `add_document`
+ pub fn add_document_with_id(&self,
+ initial_size: DeviceIntSize,
+ id: u32) -> DocumentId {
+ window_size_sanity_check(initial_size);
+
+ let document_id = DocumentId::new(self.namespace_id, id);
+
+ // We send this message to both the render backend and the scene builder instead of having
+ // the scene builder thread forward it to the render backend as we do elswhere. This is because
+ // some transactions can skip the scene builder thread and we want to avoid them arriving before
+ // the render backend knows about the existence of the corresponding document id.
+ // It may not be necessary, though.
+ self.api_sender.send(
+ ApiMsg::AddDocument(document_id, initial_size)
+ ).unwrap();
+ self.scene_sender.send(
+ SceneBuilderRequest::AddDocument(document_id, initial_size)
+ ).unwrap();
+
+ document_id
+ }
+
+ /// Delete a document.
+ pub fn delete_document(&self, document_id: DocumentId) {
+ self.low_priority_scene_sender.send(
+ SceneBuilderRequest::DeleteDocument(document_id)
+ ).unwrap();
+ }
+
+ /// Generate a new font key
+ pub fn generate_font_key(&self) -> FontKey {
+ let new_id = self.next_unique_id();
+ FontKey::new(self.namespace_id, new_id)
+ }
+
+ /// Generate a new font instance key
+ pub fn generate_font_instance_key(&self) -> FontInstanceKey {
+ let new_id = self.next_unique_id();
+ FontInstanceKey::new(self.namespace_id, new_id)
+ }
+
+ /// Gets the dimensions for the supplied glyph keys
+ ///
+ /// Note: Internally, the internal texture cache doesn't store
+ /// 'empty' textures (height or width = 0)
+ /// This means that glyph dimensions e.g. for spaces (' ') will mostly be None.
+ pub fn get_glyph_dimensions(
+ &self,
+ key: FontInstanceKey,
+ glyph_indices: Vec<GlyphIndex>,
+ ) -> Vec<Option<GlyphDimensions>> {
+ let (sender, rx) = single_msg_channel();
+ let msg = SceneBuilderRequest::GetGlyphDimensions(GlyphDimensionRequest {
+ key,
+ glyph_indices,
+ sender
+ });
+ self.low_priority_scene_sender.send(msg).unwrap();
+ rx.recv().unwrap()
+ }
+
+ /// Gets the glyph indices for the supplied string. These
+ /// can be used to construct GlyphKeys.
+ pub fn get_glyph_indices(&self, key: FontKey, text: &str) -> Vec<Option<u32>> {
+ let (sender, rx) = single_msg_channel();
+ let msg = SceneBuilderRequest::GetGlyphIndices(GlyphIndexRequest {
+ key,
+ text: text.to_string(),
+ sender,
+ });
+ self.low_priority_scene_sender.send(msg).unwrap();
+ rx.recv().unwrap()
+ }
+
+ /// Creates an `ImageKey`.
+ pub fn generate_image_key(&self) -> ImageKey {
+ let new_id = self.next_unique_id();
+ ImageKey::new(self.namespace_id, new_id)
+ }
+
+ /// Creates a `BlobImageKey`.
+ pub fn generate_blob_image_key(&self) -> BlobImageKey {
+ BlobImageKey(self.generate_image_key())
+ }
+
+ /// A Gecko-specific notification mechanism to get some code executed on the
+ /// `Renderer`'s thread, mostly replaced by `NotificationHandler`. You should
+ /// probably use the latter instead.
+ pub fn send_external_event(&self, evt: ExternalEvent) {
+ let msg = SceneBuilderRequest::ExternalEvent(evt);
+ self.low_priority_scene_sender.send(msg).unwrap();
+ }
+
+ /// Notify WebRender that now is a good time to flush caches and release
+ /// as much memory as possible.
+ pub fn notify_memory_pressure(&self) {
+ self.api_sender.send(ApiMsg::MemoryPressure).unwrap();
+ }
+
+ /// Synchronously requests memory report.
+ pub fn report_memory(&self, _ops: malloc_size_of::MallocSizeOfOps) -> MemoryReport {
+ let (tx, rx) = single_msg_channel();
+ self.api_sender.send(ApiMsg::ReportMemory(tx)).unwrap();
+ *rx.recv().unwrap()
+ }
+
+ /// Update debugging flags.
+ pub fn set_debug_flags(&self, flags: DebugFlags) {
+ let cmd = DebugCommand::SetFlags(flags);
+ self.api_sender.send(ApiMsg::DebugCommand(cmd)).unwrap();
+ }
+
+ /// Shut the WebRender instance down.
+ pub fn shut_down(&self, synchronously: bool) {
+ if synchronously {
+ let (tx, rx) = single_msg_channel();
+ self.low_priority_scene_sender.send(SceneBuilderRequest::ShutDown(Some(tx))).unwrap();
+ rx.recv().unwrap();
+ } else {
+ self.low_priority_scene_sender.send(SceneBuilderRequest::ShutDown(None)).unwrap();
+ }
+ }
+
+ /// Create a new unique key that can be used for
+ /// animated property bindings.
+ pub fn generate_property_binding_key<T: Copy>(&self) -> PropertyBindingKey<T> {
+ let new_id = self.next_unique_id();
+ PropertyBindingKey {
+ id: PropertyBindingId {
+ namespace: self.namespace_id,
+ uid: new_id,
+ },
+ _phantom: PhantomData,
+ }
+ }
+
+ #[inline]
+ fn next_unique_id(&self) -> u32 {
+ let ResourceId(id) = self.next_id.get();
+ self.next_id.set(ResourceId(id + 1));
+ id
+ }
+
+ // For use in Wrench only
+ #[doc(hidden)]
+ pub fn send_message(&self, msg: ApiMsg) {
+ self.api_sender.send(msg).unwrap();
+ }
+
+ /// Creates a transaction message from a single frame message.
+ fn frame_message(&self, msg: FrameMsg, document_id: DocumentId) -> Box<TransactionMsg> {
+ Box::new(TransactionMsg {
+ document_id,
+ scene_ops: Vec::new(),
+ frame_ops: vec![msg],
+ resource_updates: Vec::new(),
+ notifications: Vec::new(),
+ generate_frame: GenerateFrame::No,
+ invalidate_rendered_frame: false,
+ use_scene_builder_thread: false,
+ low_priority: false,
+ blob_rasterizer: None,
+ blob_requests: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ profile: TransactionProfile::new(),
+ })
+ }
+
+ /// Creates a transaction message from a single scene message.
+ fn scene_message(&self, msg: SceneMsg, document_id: DocumentId) -> Box<TransactionMsg> {
+ Box::new(TransactionMsg {
+ document_id,
+ scene_ops: vec![msg],
+ frame_ops: Vec::new(),
+ resource_updates: Vec::new(),
+ notifications: Vec::new(),
+ generate_frame: GenerateFrame::No,
+ invalidate_rendered_frame: false,
+ use_scene_builder_thread: false,
+ low_priority: false,
+ blob_rasterizer: None,
+ blob_requests: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ profile: TransactionProfile::new(),
+ })
+ }
+
+ /// A helper method to send document messages.
+ fn send_scene_msg(&self, document_id: DocumentId, msg: SceneMsg) {
+ // This assertion fails on Servo use-cases, because it creates different
+ // `RenderApi` instances for layout and compositor.
+ //assert_eq!(document_id.0, self.namespace_id);
+ self.api_sender
+ .send(ApiMsg::UpdateDocuments(vec![self.scene_message(msg, document_id)]))
+ .unwrap()
+ }
+
+ /// A helper method to send document messages.
+ fn send_frame_msg(&self, document_id: DocumentId, msg: FrameMsg) {
+ // This assertion fails on Servo use-cases, because it creates different
+ // `RenderApi` instances for layout and compositor.
+ //assert_eq!(document_id.0, self.namespace_id);
+ self.api_sender
+ .send(ApiMsg::UpdateDocuments(vec![self.frame_message(msg, document_id)]))
+ .unwrap()
+ }
+
+ /// Send a transaction to WebRender.
+ pub fn send_transaction(&mut self, document_id: DocumentId, transaction: Transaction) {
+ let mut transaction = transaction.finalize(document_id);
+
+ self.resources.update(&mut transaction);
+
+ transaction.use_scene_builder_thread |= !transaction.scene_ops.is_empty();
+ if transaction.generate_frame.as_bool() {
+ transaction.profile.start_time(profiler::API_SEND_TIME);
+ transaction.profile.start_time(profiler::TOTAL_FRAME_CPU_TIME);
+ }
+
+ if transaction.use_scene_builder_thread {
+ let sender = if transaction.low_priority {
+ &mut self.low_priority_scene_sender
+ } else {
+ &mut self.scene_sender
+ };
+
+ sender.send(SceneBuilderRequest::Transactions(vec![transaction])).unwrap();
+ } else {
+ self.api_sender.send(ApiMsg::UpdateDocuments(vec![transaction])).unwrap();
+ }
+ }
+
+ /// Send multiple transactions.
+ pub fn send_transactions(&mut self, document_ids: Vec<DocumentId>, mut transactions: Vec<Transaction>) {
+ debug_assert!(document_ids.len() == transactions.len());
+ let msgs: Vec<Box<TransactionMsg>> = transactions.drain(..).zip(document_ids)
+ .map(|(txn, id)| {
+ let mut txn = txn.finalize(id);
+ self.resources.update(&mut txn);
+ if txn.generate_frame.as_bool() {
+ txn.profile.start_time(profiler::API_SEND_TIME);
+ txn.profile.start_time(profiler::TOTAL_FRAME_CPU_TIME);
+ }
+
+ txn
+ })
+ .collect();
+
+ let use_scene_builder = msgs.iter().any(
+ |msg| msg.use_scene_builder_thread
+ );
+
+ let high_priority = msgs.iter().any(
+ |msg| !msg.low_priority
+ );
+
+ if use_scene_builder {
+ let sender = if high_priority {
+ &mut self.scene_sender
+ } else {
+ &mut self.low_priority_scene_sender
+ };
+
+ sender.send(SceneBuilderRequest::Transactions(msgs)).unwrap();
+ } else {
+ self.api_sender.send(ApiMsg::UpdateDocuments(msgs)).unwrap();
+ }
+ }
+
+ /// Does a hit test on display items in the specified document, at the given
+ /// point. If a pipeline_id is specified, it is used to further restrict the
+ /// hit results so that only items inside that pipeline are matched. The vector
+ /// of hit results will contain all display items that match, ordered from
+ /// front to back.
+ pub fn hit_test(&self,
+ document_id: DocumentId,
+ pipeline_id: Option<PipelineId>,
+ point: WorldPoint,
+ ) -> HitTestResult {
+ let (tx, rx) = single_msg_channel();
+
+ self.send_frame_msg(
+ document_id,
+ FrameMsg::HitTest(pipeline_id, point, tx)
+ );
+ rx.recv().unwrap()
+ }
+
+ /// Synchronously request an object that can perform fast hit testing queries.
+ pub fn request_hit_tester(&self, document_id: DocumentId) -> HitTesterRequest {
+ let (tx, rx) = single_msg_channel();
+ self.send_frame_msg(
+ document_id,
+ FrameMsg::RequestHitTester(tx)
+ );
+
+ HitTesterRequest { rx }
+ }
+
+ /// Setup the output region in the framebuffer for a given document.
+ pub fn set_document_view(
+ &self,
+ document_id: DocumentId,
+ device_rect: DeviceIntRect,
+ device_pixel_ratio: f32,
+ ) {
+ assert!(device_pixel_ratio > 0.0);
+ window_size_sanity_check(device_rect.size);
+ self.send_scene_msg(
+ document_id,
+ SceneMsg::SetDocumentView { device_rect, device_pixel_ratio },
+ );
+ }
+
+ ///
+ pub fn get_scroll_node_state(&self, document_id: DocumentId) -> Vec<ScrollNodeState> {
+ let (tx, rx) = single_msg_channel();
+ self.send_frame_msg(document_id, FrameMsg::GetScrollNodeState(tx));
+ rx.recv().unwrap()
+ }
+
+ // Some internal scheduling magic that leaked into the API.
+ // Buckle up and see APZUpdater.cpp for more info about what this is about.
+ #[doc(hidden)]
+ pub fn wake_scene_builder(&self) {
+ self.scene_sender.send(SceneBuilderRequest::WakeUp).unwrap();
+ }
+
+ /// Block until a round-trip to the scene builder thread has completed. This
+ /// ensures that any transactions (including ones deferred to the scene
+ /// builder thread) have been processed.
+ pub fn flush_scene_builder(&self) {
+ let (tx, rx) = single_msg_channel();
+ self.low_priority_scene_sender.send(SceneBuilderRequest::Flush(tx)).unwrap();
+ rx.recv().unwrap(); // Block until done.
+ }
+
+ /// Save a capture of the current frame state for debugging.
+ pub fn save_capture(&self, path: PathBuf, bits: CaptureBits) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::SaveCapture(path, bits));
+ self.send_message(msg);
+ }
+
+ /// Load a capture of the current frame state for debugging.
+ pub fn load_capture(&self, path: PathBuf, ids: Option<(u32, u32)>) -> Vec<CapturedDocument> {
+ // First flush the scene builder otherwise async scenes might clobber
+ // the capture we are about to load.
+ self.flush_scene_builder();
+
+ let (tx, rx) = unbounded_channel();
+ let msg = ApiMsg::DebugCommand(DebugCommand::LoadCapture(path, ids, tx));
+ self.send_message(msg);
+
+ let mut documents = Vec::new();
+ while let Ok(captured_doc) = rx.recv() {
+ documents.push(captured_doc);
+ }
+ documents
+ }
+
+ /// Start capturing a sequence of frames.
+ pub fn start_capture_sequence(&self, path: PathBuf, bits: CaptureBits) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::StartCaptureSequence(path, bits));
+ self.send_message(msg);
+ }
+
+ /// Stop capturing sequences of frames.
+ pub fn stop_capture_sequence(&self) {
+ let msg = ApiMsg::DebugCommand(DebugCommand::StopCaptureSequence);
+ self.send_message(msg);
+ }
+
+ /// Update the state of builtin debugging facilities.
+ pub fn send_debug_cmd(&mut self, cmd: DebugCommand) {
+ if let DebugCommand::EnableMultithreading(enable) = cmd {
+ // TODO(nical) we should enable it for all RenderApis.
+ self.resources.enable_multithreading(enable);
+ }
+ let msg = ApiMsg::DebugCommand(cmd);
+ self.send_message(msg);
+ }
+}
+
+impl Drop for RenderApi {
+ fn drop(&mut self) {
+ let msg = SceneBuilderRequest::ClearNamespace(self.namespace_id);
+ let _ = self.low_priority_scene_sender.send(msg);
+ }
+}
+
+
+fn window_size_sanity_check(size: DeviceIntSize) {
+ // Anything bigger than this will crash later when attempting to create
+ // a render task.
+ use crate::render_task::MAX_RENDER_TASK_SIZE;
+ if size.width > MAX_RENDER_TASK_SIZE || size.height > MAX_RENDER_TASK_SIZE {
+ panic!("Attempting to create a {}x{} window/document", size.width, size.height);
+ }
+}
+
+/// Collection of heap sizes, in bytes.
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-ostream=false
+#[repr(C)]
+#[allow(missing_docs)]
+#[derive(AddAssign, Clone, Debug, Default)]
+pub struct MemoryReport {
+ //
+ // CPU Memory.
+ //
+ pub clip_stores: usize,
+ pub gpu_cache_metadata: usize,
+ pub gpu_cache_cpu_mirror: usize,
+ pub render_tasks: usize,
+ pub hit_testers: usize,
+ pub fonts: usize,
+ pub weak_fonts: usize,
+ pub images: usize,
+ pub rasterized_blobs: usize,
+ pub shader_cache: usize,
+ pub interning: InterningMemoryReport,
+ pub display_list: usize,
+
+ //
+ // GPU memory.
+ //
+ pub gpu_cache_textures: usize,
+ pub vertex_data_textures: usize,
+ pub render_target_textures: usize,
+ pub texture_cache_textures: usize,
+ pub texture_cache_structures: usize,
+ pub depth_target_textures: usize,
+ pub texture_upload_pbos: usize,
+ pub swap_chain: usize,
+ pub render_texture_hosts: usize,
+}
diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs
new file mode 100644
index 0000000000..1e063ef862
--- /dev/null
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -0,0 +1,2008 @@
+/* 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/. */
+
+//! The high-level module responsible for managing the pipeline and preparing
+//! commands to be issued by the `Renderer`.
+//!
+//! See the comment at the top of the `renderer` module for a description of
+//! how these two pieces interact.
+
+use api::{DebugFlags, BlobImageHandler};
+use api::{DocumentId, ExternalScrollId, HitTestResult};
+use api::{IdNamespace, PipelineId, RenderNotifier, ScrollClamping};
+use api::{NotificationRequest, Checkpoint, QualitySettings};
+use api::{PrimitiveKeyKind};
+use api::units::*;
+use api::channel::{single_msg_channel, Sender, Receiver};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::render_api::CaptureBits;
+#[cfg(feature = "replay")]
+use crate::render_api::CapturedDocument;
+use crate::render_api::{MemoryReport, TransactionMsg, ResourceUpdate, ApiMsg, FrameMsg, ClearCache, DebugCommand};
+use crate::clip::ClipIntern;
+use crate::filterdata::FilterDataIntern;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::CaptureConfig;
+use crate::composite::{CompositorKind, CompositeDescriptor};
+#[cfg(feature = "debugger")]
+use crate::debug_server;
+use crate::frame_builder::{FrameBuilder, FrameBuilderConfig, FrameScratchBuffer};
+use crate::glyph_rasterizer::{FontInstance};
+use crate::gpu_cache::GpuCache;
+use crate::hit_test::{HitTest, HitTester, SharedHitTester};
+use crate::intern::DataStore;
+use crate::internal_types::{DebugOutput, FastHashMap, RenderedDocument, ResultMsg};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::picture::{TileCacheLogger, PictureScratchBuffer, SliceId, TileCacheInstance, TileCacheParams};
+use crate::prim_store::{PrimitiveScratchBuffer, PrimitiveInstance};
+use crate::prim_store::{PrimitiveInstanceKind, PrimTemplateCommonData, PrimitiveStore};
+use crate::prim_store::interned::*;
+use crate::profiler::{self, TransactionProfile};
+use crate::render_task_graph::RenderTaskGraphBuilder;
+use crate::renderer::{AsyncPropertySampler, PipelineInfo};
+use crate::resource_cache::ResourceCache;
+#[cfg(feature = "replay")]
+use crate::resource_cache::PlainCacheOwn;
+#[cfg(feature = "replay")]
+use crate::resource_cache::PlainResources;
+#[cfg(feature = "replay")]
+use crate::scene::Scene;
+use crate::scene::{BuiltScene, SceneProperties};
+use crate::scene_builder_thread::*;
+#[cfg(feature = "serialize")]
+use serde::{Serialize, Deserialize};
+#[cfg(feature = "debugger")]
+use serde_json;
+#[cfg(feature = "replay")]
+use std::collections::hash_map::Entry::{Occupied, Vacant};
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::time::{UNIX_EPOCH, SystemTime};
+use std::{mem, u32};
+#[cfg(feature = "capture")]
+use std::path::PathBuf;
+#[cfg(feature = "replay")]
+use crate::frame_builder::Frame;
+use time::precise_time_ns;
+use crate::util::{Recycler, VecHelper, drain_filter};
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone)]
+pub struct DocumentView {
+ scene: SceneView,
+ frame: FrameView,
+}
+
+/// Some rendering parameters applying at the scene level.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone)]
+pub struct SceneView {
+ pub device_rect: DeviceIntRect,
+ pub device_pixel_ratio: f32,
+ pub page_zoom_factor: f32,
+ pub quality_settings: QualitySettings,
+}
+
+impl SceneView {
+ pub fn accumulated_scale_factor_for_snapping(&self) -> DevicePixelScale {
+ DevicePixelScale::new(
+ self.device_pixel_ratio *
+ self.page_zoom_factor
+ )
+ }
+}
+
+/// Some rendering parameters applying at the frame level.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone)]
+pub struct FrameView {
+ pan: DeviceIntPoint,
+ pinch_zoom_factor: f32,
+}
+
+impl DocumentView {
+ pub fn accumulated_scale_factor(&self) -> DevicePixelScale {
+ DevicePixelScale::new(
+ self.scene.device_pixel_ratio *
+ self.scene.page_zoom_factor *
+ self.frame.pinch_zoom_factor
+ )
+ }
+}
+
+#[derive(Copy, Clone, Hash, MallocSizeOf, PartialEq, PartialOrd, Debug, Eq, Ord)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameId(usize);
+
+impl FrameId {
+ /// Returns a FrameId corresponding to the first frame.
+ ///
+ /// Note that we use 0 as the internal id here because the current code
+ /// increments the frame id at the beginning of the frame, rather than
+ /// at the end, and we want the first frame to be 1. It would probably
+ /// be sensible to move the advance() call to after frame-building, and
+ /// then make this method return FrameId(1).
+ pub fn first() -> Self {
+ FrameId(0)
+ }
+
+ /// Returns the backing usize for this FrameId.
+ pub fn as_usize(&self) -> usize {
+ self.0
+ }
+
+ /// Advances this FrameId to the next frame.
+ pub fn advance(&mut self) {
+ self.0 += 1;
+ }
+
+ /// An invalid sentinel FrameId, which will always compare less than
+ /// any valid FrameId.
+ pub const INVALID: FrameId = FrameId(0);
+}
+
+impl Default for FrameId {
+ fn default() -> Self {
+ FrameId::INVALID
+ }
+}
+
+impl ::std::ops::Add<usize> for FrameId {
+ type Output = Self;
+ fn add(self, other: usize) -> FrameId {
+ FrameId(self.0 + other)
+ }
+}
+
+impl ::std::ops::Sub<usize> for FrameId {
+ type Output = Self;
+ fn sub(self, other: usize) -> FrameId {
+ assert!(self.0 >= other, "Underflow subtracting FrameIds");
+ FrameId(self.0 - other)
+ }
+}
+enum RenderBackendStatus {
+ Continue,
+ ShutDown(Option<Sender<()>>),
+}
+
+/// Identifier to track a sequence of frames.
+///
+/// This is effectively a `FrameId` with a ridealong timestamp corresponding
+/// to when advance() was called, which allows for more nuanced cache eviction
+/// decisions. As such, we use the `FrameId` for equality and comparison, since
+/// we should never have two `FrameStamps` with the same id but different
+/// timestamps.
+#[derive(Copy, Clone, Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FrameStamp {
+ id: FrameId,
+ time: SystemTime,
+ document_id: DocumentId,
+}
+
+impl Eq for FrameStamp {}
+
+impl PartialEq for FrameStamp {
+ fn eq(&self, other: &Self) -> bool {
+ // We should not be checking equality unless the documents are the same
+ debug_assert!(self.document_id == other.document_id);
+ self.id == other.id
+ }
+}
+
+impl PartialOrd for FrameStamp {
+ fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> {
+ self.id.partial_cmp(&other.id)
+ }
+}
+
+impl FrameStamp {
+ /// Gets the FrameId in this stamp.
+ pub fn frame_id(&self) -> FrameId {
+ self.id
+ }
+
+ /// Gets the time associated with this FrameStamp.
+ pub fn time(&self) -> SystemTime {
+ self.time
+ }
+
+ /// Gets the DocumentId in this stamp.
+ pub fn document_id(&self) -> DocumentId {
+ self.document_id
+ }
+
+ pub fn is_valid(&self) -> bool {
+ // If any fields are their default values, the whole struct should equal INVALID
+ debug_assert!((self.time != UNIX_EPOCH && self.id != FrameId(0) && self.document_id != DocumentId::INVALID) ||
+ *self == Self::INVALID);
+ self.document_id != DocumentId::INVALID
+ }
+
+ /// Returns a FrameStamp corresponding to the first frame.
+ pub fn first(document_id: DocumentId) -> Self {
+ FrameStamp {
+ id: FrameId::first(),
+ time: SystemTime::now(),
+ document_id,
+ }
+ }
+
+ /// Advances to a new frame.
+ pub fn advance(&mut self) {
+ self.id.advance();
+ self.time = SystemTime::now();
+ }
+
+ /// An invalid sentinel FrameStamp.
+ pub const INVALID: FrameStamp = FrameStamp {
+ id: FrameId(0),
+ time: UNIX_EPOCH,
+ document_id: DocumentId::INVALID,
+ };
+}
+
+macro_rules! declare_data_stores {
+ ( $( $name:ident : $ty:ty, )+ ) => {
+ /// A collection of resources that are shared by clips, primitives
+ /// between display lists.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(Default)]
+ pub struct DataStores {
+ $(
+ pub $name: DataStore<$ty>,
+ )+
+ }
+
+ impl DataStores {
+ /// Reports CPU heap usage.
+ fn report_memory(&self, ops: &mut MallocSizeOfOps, r: &mut MemoryReport) {
+ $(
+ r.interning.data_stores.$name += self.$name.size_of(ops);
+ )+
+ }
+
+ fn apply_updates(
+ &mut self,
+ updates: InternerUpdates,
+ profile: &mut TransactionProfile,
+ ) {
+ $(
+ self.$name.apply_updates(
+ updates.$name,
+ profile,
+ );
+ )+
+ }
+ }
+ }
+}
+
+crate::enumerate_interners!(declare_data_stores);
+
+impl DataStores {
+ /// Returns the local rect for a primitive. For most primitives, this is
+ /// stored in the template. For pictures, this is stored inside the picture
+ /// primitive instance itself, since this is determined during frame building.
+ pub fn get_local_prim_rect(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ prim_store: &PrimitiveStore,
+ ) -> LayoutRect {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ let pic = &prim_store.pictures[pic_index.0];
+ pic.precise_local_rect
+ }
+ _ => {
+ self.as_common_data(prim_instance).prim_rect
+ }
+ }
+ }
+
+ /// Returns true if this primitive might need repition.
+ // TODO(gw): This seems like the wrong place for this - maybe this flag should
+ // not be in the common prim template data?
+ pub fn prim_may_need_repetition(
+ &self,
+ prim_instance: &PrimitiveInstance,
+ ) -> bool {
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { .. } => {
+ false
+ }
+ _ => {
+ self.as_common_data(prim_instance).may_need_repetition
+ }
+ }
+ }
+
+ pub fn as_common_data(
+ &self,
+ prim_inst: &PrimitiveInstance
+ ) -> &PrimTemplateCommonData {
+ match prim_inst.kind {
+ PrimitiveInstanceKind::Rectangle { data_handle, .. } |
+ PrimitiveInstanceKind::Clear { data_handle, .. } => {
+ let prim_data = &self.prim[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::Image { data_handle, .. } => {
+ let prim_data = &self.image[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let prim_data = &self.image_border[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::LineDecoration { data_handle, .. } => {
+ let prim_data = &self.line_decoration[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::LinearGradient { data_handle, .. } => {
+ let prim_data = &self.linear_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::NormalBorder { data_handle, .. } => {
+ let prim_data = &self.normal_border[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::Picture { .. } => {
+ panic!("BUG: picture prims don't have common data!");
+ }
+ PrimitiveInstanceKind::RadialGradient { data_handle, .. } => {
+ let prim_data = &self.radial_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::ConicGradient { data_handle, .. } => {
+ let prim_data = &self.conic_grad[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::TextRun { data_handle, .. } => {
+ let prim_data = &self.text_run[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+ let prim_data = &self.yuv_image[data_handle];
+ &prim_data.common
+ }
+ PrimitiveInstanceKind::Backdrop { data_handle, .. } => {
+ let prim_data = &self.backdrop[data_handle];
+ &prim_data.common
+ }
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct ScratchBuffer {
+ pub primitive: PrimitiveScratchBuffer,
+ pub picture: PictureScratchBuffer,
+ pub frame: FrameScratchBuffer,
+}
+
+impl ScratchBuffer {
+ pub fn begin_frame(&mut self) {
+ self.primitive.begin_frame();
+ self.picture.begin_frame();
+ self.frame.begin_frame();
+ }
+
+ pub fn recycle(&mut self, recycler: &mut Recycler) {
+ self.primitive.recycle(recycler);
+ self.picture.recycle(recycler);
+ self.frame.recycle(recycler);
+ }
+
+ pub fn memory_pressure(&mut self) {
+ // TODO: causes browser chrome test crahes on windows.
+ //self.primitive = Default::default();
+ self.picture = Default::default();
+ self.frame = Default::default();
+ }
+}
+
+struct Document {
+ /// The id of this document
+ id: DocumentId,
+
+ /// Temporary list of removed pipelines received from the scene builder
+ /// thread and forwarded to the renderer.
+ removed_pipelines: Vec<(PipelineId, DocumentId)>,
+
+ view: DocumentView,
+
+ /// The id and time of the current frame.
+ stamp: FrameStamp,
+
+ /// The latest built scene, usable to build frames.
+ /// received from the scene builder thread.
+ scene: BuiltScene,
+
+ /// The builder object that prodces frames, kept around to preserve some retained state.
+ frame_builder: FrameBuilder,
+
+ /// Allows graphs of render tasks to be created, and then built into an immutable graph output.
+ rg_builder: RenderTaskGraphBuilder,
+
+ /// A data structure to allow hit testing against rendered frames. This is updated
+ /// every time we produce a fully rendered frame.
+ hit_tester: Option<Arc<HitTester>>,
+ /// To avoid synchronous messaging we update a shared hit-tester that other threads
+ /// can query.
+ shared_hit_tester: Arc<SharedHitTester>,
+
+ /// Properties that are resolved during frame building and can be changed at any time
+ /// without requiring the scene to be re-built.
+ dynamic_properties: SceneProperties,
+
+ /// Track whether the last built frame is up to date or if it will need to be re-built
+ /// before rendering again.
+ frame_is_valid: bool,
+ hit_tester_is_valid: bool,
+ rendered_frame_is_valid: bool,
+ /// We track this information to be able to display debugging information from the
+ /// renderer.
+ has_built_scene: bool,
+
+ data_stores: DataStores,
+
+ /// Contains various vecs of data that is used only during frame building,
+ /// where we want to recycle the memory each new display list, to avoid constantly
+ /// re-allocating and moving memory around.
+ scratch: ScratchBuffer,
+
+ #[cfg(feature = "replay")]
+ loaded_scene: Scene,
+
+ /// Tracks the state of the picture cache tiles that were composited on the previous frame.
+ prev_composite_descriptor: CompositeDescriptor,
+
+ /// Tracks if we need to invalidate dirty rects for this document, due to the picture
+ /// cache slice configuration having changed when a new scene is swapped in.
+ dirty_rects_are_valid: bool,
+
+ profile: TransactionProfile,
+}
+
+impl Document {
+ pub fn new(
+ id: DocumentId,
+ size: DeviceIntSize,
+ default_device_pixel_ratio: f32,
+ ) -> Self {
+ Document {
+ id,
+ removed_pipelines: Vec::new(),
+ view: DocumentView {
+ scene: SceneView {
+ device_rect: size.into(),
+ page_zoom_factor: 1.0,
+ device_pixel_ratio: default_device_pixel_ratio,
+ quality_settings: QualitySettings::default(),
+ },
+ frame: FrameView {
+ pan: DeviceIntPoint::new(0, 0),
+ pinch_zoom_factor: 1.0,
+ },
+ },
+ stamp: FrameStamp::first(id),
+ scene: BuiltScene::empty(),
+ frame_builder: FrameBuilder::new(),
+ hit_tester: None,
+ shared_hit_tester: Arc::new(SharedHitTester::new()),
+ dynamic_properties: SceneProperties::new(),
+ frame_is_valid: false,
+ hit_tester_is_valid: false,
+ rendered_frame_is_valid: false,
+ has_built_scene: false,
+ data_stores: DataStores::default(),
+ scratch: ScratchBuffer::default(),
+ #[cfg(feature = "replay")]
+ loaded_scene: Scene::new(),
+ prev_composite_descriptor: CompositeDescriptor::empty(),
+ dirty_rects_are_valid: true,
+ profile: TransactionProfile::new(),
+ rg_builder: RenderTaskGraphBuilder::new(),
+ }
+ }
+
+ fn can_render(&self) -> bool {
+ self.scene.has_root_pipeline
+ }
+
+ fn has_pixels(&self) -> bool {
+ !self.view.scene.device_rect.size.is_empty()
+ }
+
+ fn process_frame_msg(
+ &mut self,
+ message: FrameMsg,
+ ) -> DocumentOps {
+ match message {
+ FrameMsg::UpdateEpoch(pipeline_id, epoch) => {
+ self.scene.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+ FrameMsg::HitTest(pipeline_id, point, tx) => {
+ if !self.hit_tester_is_valid {
+ self.rebuild_hit_tester();
+ }
+
+ let result = match self.hit_tester {
+ Some(ref hit_tester) => {
+ hit_tester.hit_test(HitTest::new(pipeline_id, point))
+ }
+ None => HitTestResult { items: Vec::new() },
+ };
+
+ tx.send(result).unwrap();
+ }
+ FrameMsg::RequestHitTester(tx) => {
+ tx.send(self.shared_hit_tester.clone()).unwrap();
+ }
+ FrameMsg::SetPan(pan) => {
+ if self.view.frame.pan != pan {
+ self.view.frame.pan = pan;
+ self.hit_tester_is_valid = false;
+ self.frame_is_valid = false;
+ }
+ }
+ FrameMsg::ScrollNodeWithId(origin, id, clamp) => {
+ profile_scope!("ScrollNodeWithScrollId");
+
+ if self.scroll_node(origin, id, clamp) {
+ self.hit_tester_is_valid = false;
+ self.frame_is_valid = false;
+ }
+
+ return DocumentOps {
+ scroll: true,
+ ..DocumentOps::nop()
+ };
+ }
+ FrameMsg::GetScrollNodeState(tx) => {
+ profile_scope!("GetScrollNodeState");
+ tx.send(self.scene.spatial_tree.get_scroll_node_state()).unwrap();
+ }
+ FrameMsg::UpdateDynamicProperties(property_bindings) => {
+ self.dynamic_properties.set_properties(property_bindings);
+ }
+ FrameMsg::AppendDynamicTransformProperties(property_bindings) => {
+ self.dynamic_properties.add_transforms(property_bindings);
+ }
+ FrameMsg::SetPinchZoom(factor) => {
+ if self.view.frame.pinch_zoom_factor != factor.get() {
+ self.view.frame.pinch_zoom_factor = factor.get();
+ self.frame_is_valid = false;
+ }
+ }
+ FrameMsg::SetIsTransformAsyncZooming(is_zooming, animation_id) => {
+ let node = self.scene.spatial_tree.spatial_nodes.iter_mut()
+ .find(|node| node.is_transform_bound_to_property(animation_id));
+ if let Some(node) = node {
+ if node.is_async_zooming != is_zooming {
+ node.is_async_zooming = is_zooming;
+ self.frame_is_valid = false;
+ }
+ }
+ }
+ }
+
+ DocumentOps::nop()
+ }
+
+ fn build_frame(
+ &mut self,
+ resource_cache: &mut ResourceCache,
+ gpu_cache: &mut GpuCache,
+ debug_flags: DebugFlags,
+ tile_cache_logger: &mut TileCacheLogger,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ ) -> RenderedDocument {
+ self.profile.start_time(profiler::FRAME_BUILDING_TIME);
+
+ let accumulated_scale_factor = self.view.accumulated_scale_factor();
+ let pan = self.view.frame.pan.to_f32() / accumulated_scale_factor;
+
+ // Advance to the next frame.
+ self.stamp.advance();
+
+ assert!(self.stamp.frame_id() != FrameId::INVALID,
+ "First frame increment must happen before build_frame()");
+
+ let frame = {
+ let frame = self.frame_builder.build(
+ &mut self.scene,
+ resource_cache,
+ gpu_cache,
+ &mut self.rg_builder,
+ self.stamp,
+ accumulated_scale_factor,
+ self.view.scene.device_rect.origin,
+ pan,
+ &self.dynamic_properties,
+ &mut self.data_stores,
+ &mut self.scratch,
+ debug_flags,
+ tile_cache_logger,
+ tile_caches,
+ self.dirty_rects_are_valid,
+ &mut self.profile,
+ );
+
+ frame
+ };
+
+ self.frame_is_valid = true;
+ self.dirty_rects_are_valid = true;
+
+ let is_new_scene = self.has_built_scene;
+ self.has_built_scene = false;
+
+ self.profile.end_time(profiler::FRAME_BUILDING_TIME);
+
+ RenderedDocument {
+ frame,
+ is_new_scene,
+ profile: self.profile.take_and_reset(),
+ }
+ }
+
+ fn rebuild_hit_tester(&mut self) {
+ let accumulated_scale_factor = self.view.accumulated_scale_factor();
+ let pan = self.view.frame.pan.to_f32() / accumulated_scale_factor;
+
+ self.scene.spatial_tree.update_tree(
+ pan,
+ accumulated_scale_factor,
+ &self.dynamic_properties,
+ );
+
+ let hit_tester = Arc::new(self.scene.create_hit_tester());
+ self.hit_tester = Some(Arc::clone(&hit_tester));
+ self.shared_hit_tester.update(hit_tester);
+ self.hit_tester_is_valid = true;
+ }
+
+ pub fn updated_pipeline_info(&mut self) -> PipelineInfo {
+ let removed_pipelines = self.removed_pipelines.take_and_preallocate();
+ PipelineInfo {
+ epochs: self.scene.pipeline_epochs.iter()
+ .map(|(&pipeline_id, &epoch)| ((pipeline_id, self.id), epoch)).collect(),
+ removed_pipelines,
+ }
+ }
+
+ /// Returns true if the node actually changed position or false otherwise.
+ pub fn scroll_node(
+ &mut self,
+ origin: LayoutPoint,
+ id: ExternalScrollId,
+ clamp: ScrollClamping
+ ) -> bool {
+ self.scene.spatial_tree.scroll_node(origin, id, clamp)
+ }
+
+ /// Update the state of tile caches when a new scene is being swapped in to
+ /// the render backend. Retain / reuse existing caches if possible, and
+ /// destroy any now unused caches.
+ fn update_tile_caches_for_new_scene(
+ &mut self,
+ mut requested_tile_caches: FastHashMap<SliceId, TileCacheParams>,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ resource_cache: &mut ResourceCache,
+ ) {
+ let mut new_tile_caches = FastHashMap::default();
+ new_tile_caches.reserve(requested_tile_caches.len());
+
+ // Step through the tile caches that are needed for the new scene, and see
+ // if we have an existing cache that can be reused.
+ for (slice_id, params) in requested_tile_caches.drain() {
+ let tile_cache = match tile_caches.remove(&slice_id) {
+ Some(mut existing_tile_cache) => {
+ // Found an existing cache - update the cache params and reuse it
+ existing_tile_cache.prepare_for_new_scene(params);
+ existing_tile_cache
+ }
+ None => {
+ // No cache exists so create a new one
+ Box::new(TileCacheInstance::new(params))
+ }
+ };
+
+ new_tile_caches.insert(slice_id, tile_cache);
+ }
+
+ // Replace current tile cache map, and return what was left over,
+ // which are now unused.
+ let unused_tile_caches = mem::replace(
+ tile_caches,
+ new_tile_caches,
+ );
+
+ if !unused_tile_caches.is_empty() {
+ // If the slice configuration changed, assume we can't rely on the
+ // current dirty rects for next composite
+ self.dirty_rects_are_valid = false;
+
+ // Destroy any native surfaces allocated by these unused caches
+ for (_, tile_cache) in unused_tile_caches {
+ tile_cache.destroy(resource_cache);
+ }
+ }
+ }
+
+ pub fn new_async_scene_ready(
+ &mut self,
+ mut built_scene: BuiltScene,
+ recycler: &mut Recycler,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+ resource_cache: &mut ResourceCache,
+ ) {
+ self.frame_is_valid = false;
+ self.hit_tester_is_valid = false;
+
+ self.update_tile_caches_for_new_scene(
+ mem::replace(&mut built_scene.tile_cache_config.tile_caches, FastHashMap::default()),
+ tile_caches,
+ resource_cache,
+ );
+
+ let old_scrolling_states = self.scene.spatial_tree.drain();
+ self.scene = built_scene;
+ self.scratch.recycle(recycler);
+ self.scene.spatial_tree.finalize_and_apply_pending_scroll_offsets(old_scrolling_states);
+ }
+}
+
+struct DocumentOps {
+ scroll: bool,
+}
+
+impl DocumentOps {
+ fn nop() -> Self {
+ DocumentOps {
+ scroll: false,
+ }
+ }
+}
+
+/// The unique id for WR resource identification.
+/// The namespace_id should start from 1.
+static NEXT_NAMESPACE_ID: AtomicUsize = AtomicUsize::new(1);
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainRenderBackend {
+ default_device_pixel_ratio: f32,
+ frame_config: FrameBuilderConfig,
+ documents: FastHashMap<DocumentId, DocumentView>,
+ resource_sequence_id: u32,
+}
+
+/// The render backend is responsible for transforming high level display lists into
+/// GPU-friendly work which is then submitted to the renderer in the form of a frame::Frame.
+///
+/// The render backend operates on its own thread.
+pub struct RenderBackend {
+ api_rx: Receiver<ApiMsg>,
+ result_tx: Sender<ResultMsg>,
+ scene_tx: Sender<SceneBuilderRequest>,
+ low_priority_scene_tx: Sender<SceneBuilderRequest>,
+
+ default_device_pixel_ratio: f32,
+
+ gpu_cache: GpuCache,
+ resource_cache: ResourceCache,
+
+ frame_config: FrameBuilderConfig,
+ default_compositor_kind: CompositorKind,
+ documents: FastHashMap<DocumentId, Document>,
+
+ notifier: Box<dyn RenderNotifier>,
+ tile_cache_logger: TileCacheLogger,
+ sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ size_of_ops: Option<MallocSizeOfOps>,
+ debug_flags: DebugFlags,
+ namespace_alloc_by_client: bool,
+
+ // We keep one around to be able to call clear_namespace
+ // after the api object is deleted. For most purposes the
+ // api object's blob handler should be used instead.
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+
+ recycler: Recycler,
+ #[cfg(feature = "capture")]
+ capture_config: Option<CaptureConfig>,
+ #[cfg(feature = "replay")]
+ loaded_resource_sequence_id: u32,
+
+ /// A map of tile caches. These are stored in the backend as they are
+ /// persisted between both frame and scenes.
+ tile_caches: FastHashMap<SliceId, Box<TileCacheInstance>>,
+}
+
+impl RenderBackend {
+ pub fn new(
+ api_rx: Receiver<ApiMsg>,
+ result_tx: Sender<ResultMsg>,
+ scene_tx: Sender<SceneBuilderRequest>,
+ low_priority_scene_tx: Sender<SceneBuilderRequest>,
+ default_device_pixel_ratio: f32,
+ resource_cache: ResourceCache,
+ notifier: Box<dyn RenderNotifier>,
+ blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ frame_config: FrameBuilderConfig,
+ sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ size_of_ops: Option<MallocSizeOfOps>,
+ debug_flags: DebugFlags,
+ namespace_alloc_by_client: bool,
+ ) -> RenderBackend {
+ RenderBackend {
+ api_rx,
+ result_tx,
+ scene_tx,
+ low_priority_scene_tx,
+ default_device_pixel_ratio,
+ resource_cache,
+ gpu_cache: GpuCache::new(),
+ frame_config,
+ default_compositor_kind : frame_config.compositor_kind,
+ documents: FastHashMap::default(),
+ notifier,
+ tile_cache_logger: TileCacheLogger::new(500usize),
+ sampler,
+ size_of_ops,
+ debug_flags,
+ namespace_alloc_by_client,
+ recycler: Recycler::new(),
+ blob_image_handler,
+ #[cfg(feature = "capture")]
+ capture_config: None,
+ #[cfg(feature = "replay")]
+ loaded_resource_sequence_id: 0,
+ tile_caches: FastHashMap::default(),
+ }
+ }
+
+ fn next_namespace_id(&self) -> IdNamespace {
+ IdNamespace(NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed) as u32)
+ }
+
+ pub fn run(&mut self) {
+ let mut frame_counter: u32 = 0;
+ let mut status = RenderBackendStatus::Continue;
+
+ if let Some(ref sampler) = self.sampler {
+ sampler.register();
+ }
+
+ while let RenderBackendStatus::Continue = status {
+ status = match self.api_rx.recv() {
+ Ok(msg) => {
+ self.process_api_msg(msg, &mut frame_counter)
+ }
+ Err(..) => { RenderBackendStatus::ShutDown(None) }
+ };
+ }
+
+ // Ensure we read everything the scene builder is sending us from
+ // inflight messages, otherwise the scene builder might panic.
+ while let Ok(msg) = self.api_rx.try_recv() {
+ match msg {
+ ApiMsg::SceneBuilderResult(SceneBuilderResult::FlushComplete(tx)) => {
+ // If somebody's blocked waiting for a flush, how did they
+ // trigger the RB thread to shut down? This shouldn't happen
+ // but handle it gracefully anyway.
+ debug_assert!(false);
+ tx.send(()).ok();
+ }
+ _ => {},
+ }
+ }
+
+ self.documents.clear();
+
+ self.notifier.shut_down();
+
+ if let Some(ref sampler) = self.sampler {
+ sampler.deregister();
+ }
+
+
+ if let RenderBackendStatus::ShutDown(Some(sender)) = status {
+ let _ = sender.send(());
+ }
+ }
+
+ fn process_transaction(
+ &mut self,
+ mut txns: Vec<Box<BuiltTransaction>>,
+ result_tx: Option<Sender<SceneSwapResult>>,
+ frame_counter: &mut u32,
+ ) -> bool {
+ self.prepare_for_frames();
+ self.maybe_force_nop_documents(
+ frame_counter,
+ |document_id| txns.iter().any(|txn| txn.document_id == document_id));
+
+ let mut built_frame = false;
+ for mut txn in txns.drain(..) {
+ let has_built_scene = txn.built_scene.is_some();
+
+ if let Some(doc) = self.documents.get_mut(&txn.document_id) {
+
+ doc.removed_pipelines.append(&mut txn.removed_pipelines);
+ doc.view.scene = txn.view;
+ doc.profile.merge(&mut txn.profile);
+
+ if let Some(built_scene) = txn.built_scene.take() {
+ doc.new_async_scene_ready(
+ built_scene,
+ &mut self.recycler,
+ &mut self.tile_caches,
+ &mut self.resource_cache,
+ );
+ }
+
+ // If there are any additions or removals of clip modes
+ // during the scene build, apply them to the data store now.
+ // This needs to happen before we build the hit tester.
+ if let Some(updates) = txn.interner_updates.take() {
+ #[cfg(feature = "capture")]
+ {
+ if self.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
+ self.tile_cache_logger.serialize_updates(&updates);
+ }
+ }
+ doc.data_stores.apply_updates(updates, &mut doc.profile);
+ }
+
+ // Build the hit tester while the APZ lock is held so that its content
+ // is in sync with the gecko APZ tree.
+ if !doc.hit_tester_is_valid {
+ doc.rebuild_hit_tester();
+ }
+
+ if let Some(ref tx) = result_tx {
+ let (resume_tx, resume_rx) = single_msg_channel();
+ tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
+ // Block until the post-swap hook has completed on
+ // the scene builder thread. We need to do this before
+ // we can sample from the sampler hook which might happen
+ // in the update_document call below.
+ resume_rx.recv().ok();
+ }
+
+ for pipeline_id in &txn.discard_frame_state_for_pipelines {
+ doc.scene
+ .spatial_tree
+ .discard_frame_state_for_pipeline(*pipeline_id);
+ }
+
+ self.resource_cache.add_rasterized_blob_images(
+ txn.rasterized_blobs.take(),
+ &mut doc.profile,
+ );
+
+ } else {
+ // The document was removed while we were building it, skip it.
+ // TODO: we might want to just ensure that removed documents are
+ // always forwarded to the scene builder thread to avoid this case.
+ if let Some(ref tx) = result_tx {
+ tx.send(SceneSwapResult::Aborted).unwrap();
+ }
+ continue;
+ }
+
+ built_frame |= self.update_document(
+ txn.document_id,
+ txn.resource_updates.take(),
+ txn.frame_ops.take(),
+ txn.notifications.take(),
+ txn.render_frame,
+ None,
+ txn.invalidate_rendered_frame,
+ frame_counter,
+ has_built_scene,
+ );
+ }
+
+ built_frame
+ }
+
+ fn process_api_msg(
+ &mut self,
+ msg: ApiMsg,
+ frame_counter: &mut u32,
+ ) -> RenderBackendStatus {
+ match msg {
+ ApiMsg::WakeUp => {}
+ ApiMsg::CloneApi(sender) => {
+ assert!(!self.namespace_alloc_by_client);
+ sender.send(self.next_namespace_id()).unwrap();
+ }
+ ApiMsg::CloneApiByClient(namespace_id) => {
+ assert!(self.namespace_alloc_by_client);
+ debug_assert!(!self.documents.iter().any(|(did, _doc)| did.namespace_id == namespace_id));
+ }
+ ApiMsg::AddDocument(document_id, initial_size) => {
+ let document = Document::new(
+ document_id,
+ initial_size,
+ self.default_device_pixel_ratio,
+ );
+ let old = self.documents.insert(document_id, document);
+ debug_assert!(old.is_none());
+ }
+ ApiMsg::MemoryPressure => {
+ // This is drastic. It will basically flush everything out of the cache,
+ // and the next frame will have to rebuild all of its resources.
+ // We may want to look into something less extreme, but on the other hand this
+ // should only be used in situations where are running low enough on memory
+ // that we risk crashing if we don't do something about it.
+ // The advantage of clearing the cache completely is that it gets rid of any
+ // remaining fragmentation that could have persisted if we kept around the most
+ // recently used resources.
+ self.resource_cache.clear(ClearCache::all());
+
+ self.gpu_cache.clear();
+
+ for (_, doc) in &mut self.documents {
+ doc.scratch.memory_pressure();
+ }
+
+ let resource_updates = self.resource_cache.pending_updates();
+ let msg = ResultMsg::UpdateResources {
+ resource_updates,
+ memory_pressure: true,
+ };
+ self.result_tx.send(msg).unwrap();
+ self.notifier.wake_up(false);
+ }
+ ApiMsg::ReportMemory(tx) => {
+ self.report_memory(tx);
+ }
+ ApiMsg::DebugCommand(option) => {
+ let msg = match option {
+ DebugCommand::EnableDualSourceBlending(enable) => {
+ // Set in the config used for any future documents
+ // that are created.
+ self.frame_config
+ .dual_source_blending_is_enabled = enable;
+ self.update_frame_builder_config();
+
+ // We don't want to forward this message to the renderer.
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetPictureTileSize(tile_size) => {
+ self.frame_config.tile_size_override = tile_size;
+ self.update_frame_builder_config();
+
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::FetchDocuments => {
+ // Ask SceneBuilderThread to send JSON presentation of the documents,
+ // that will be forwarded to Renderer.
+ self.send_backend_message(SceneBuilderRequest::DocumentsForDebugger);
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::FetchClipScrollTree => {
+ let json = self.get_spatial_tree_for_debugger();
+ ResultMsg::DebugOutput(DebugOutput::FetchClipScrollTree(json))
+ }
+ #[cfg(feature = "capture")]
+ DebugCommand::SaveCapture(root, bits) => {
+ let output = self.save_capture(root, bits);
+ ResultMsg::DebugOutput(output)
+ },
+ #[cfg(feature = "capture")]
+ DebugCommand::StartCaptureSequence(root, bits) => {
+ self.start_capture_sequence(root, bits);
+ return RenderBackendStatus::Continue;
+ },
+ #[cfg(feature = "capture")]
+ DebugCommand::StopCaptureSequence => {
+ self.stop_capture_sequence();
+ return RenderBackendStatus::Continue;
+ },
+ #[cfg(feature = "replay")]
+ DebugCommand::LoadCapture(path, ids, tx) => {
+ NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed);
+ *frame_counter += 1;
+
+ let mut config = CaptureConfig::new(path, CaptureBits::all());
+ if let Some((scene_id, frame_id)) = ids {
+ config.scene_id = scene_id;
+ config.frame_id = frame_id;
+ }
+
+ self.load_capture(config);
+
+ for (id, doc) in &self.documents {
+ let captured = CapturedDocument {
+ document_id: *id,
+ root_pipeline_id: doc.loaded_scene.root_pipeline_id,
+ };
+ tx.send(captured).unwrap();
+ }
+
+ // Note: we can't pass `LoadCapture` here since it needs to arrive
+ // before the `PublishDocument` messages sent by `load_capture`.
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::ClearCaches(mask) => {
+ self.resource_cache.clear(mask);
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::EnableNativeCompositor(enable) => {
+ // Default CompositorKind should be Native
+ if let CompositorKind::Draw { .. } = self.default_compositor_kind {
+ unreachable!();
+ }
+
+ let compositor_kind = if enable {
+ self.default_compositor_kind
+ } else {
+ CompositorKind::default()
+ };
+
+ for (_, doc) in &mut self.documents {
+ doc.scene.config.compositor_kind = compositor_kind;
+ doc.frame_is_valid = false;
+ }
+
+ self.frame_config.compositor_kind = compositor_kind;
+ self.update_frame_builder_config();
+
+ // We don't want to forward this message to the renderer.
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::EnableMultithreading(enable) => {
+ self.resource_cache.enable_multithreading(enable);
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetBatchingLookback(count) => {
+ self.frame_config.batch_lookback_count = count as usize;
+ self.update_frame_builder_config();
+
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SimulateLongSceneBuild(time_ms) => {
+ let _ = self.scene_tx.send(SceneBuilderRequest::SimulateLongSceneBuild(time_ms));
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SimulateLongLowPrioritySceneBuild(time_ms) => {
+ let _ = self.low_priority_scene_tx.send(
+ SceneBuilderRequest::SimulateLongLowPrioritySceneBuild(time_ms)
+ );
+ return RenderBackendStatus::Continue;
+ }
+ DebugCommand::SetFlags(flags) => {
+ self.resource_cache.set_debug_flags(flags);
+ self.gpu_cache.set_debug_flags(flags);
+
+ let force_invalidation = flags.contains(DebugFlags::FORCE_PICTURE_INVALIDATION);
+ if self.frame_config.force_invalidation != force_invalidation {
+ self.frame_config.force_invalidation = force_invalidation;
+ self.update_frame_builder_config();
+ }
+
+ // If we're toggling on the GPU cache debug display, we
+ // need to blow away the cache. This is because we only
+ // send allocation/free notifications to the renderer
+ // thread when the debug display is enabled, and thus
+ // enabling it when the cache is partially populated will
+ // give the renderer an incomplete view of the world.
+ // And since we might as well drop all the debugging state
+ // from the renderer when we disable the debug display,
+ // we just clear the cache on toggle.
+ let changed = self.debug_flags ^ flags;
+ if changed.contains(DebugFlags::GPU_CACHE_DBG) {
+ self.gpu_cache.clear();
+ }
+ self.debug_flags = flags;
+
+ ResultMsg::DebugCommand(option)
+ }
+ _ => ResultMsg::DebugCommand(option),
+ };
+ self.result_tx.send(msg).unwrap();
+ self.notifier.wake_up(true);
+ }
+ ApiMsg::UpdateDocuments(transaction_msgs) => {
+ self.prepare_transactions(
+ transaction_msgs,
+ frame_counter,
+ );
+ }
+ ApiMsg::SceneBuilderResult(msg) => {
+ return self.process_scene_builder_result(msg, frame_counter);
+ }
+ }
+
+ RenderBackendStatus::Continue
+ }
+
+ fn process_scene_builder_result(
+ &mut self,
+ msg: SceneBuilderResult,
+ frame_counter: &mut u32,
+ ) -> RenderBackendStatus {
+ profile_scope!("sb_msg");
+
+ match msg {
+ SceneBuilderResult::Transactions(txns, result_tx) => {
+ self.process_transaction(
+ txns,
+ result_tx,
+ frame_counter,
+ );
+ self.bookkeep_after_frames();
+ },
+ #[cfg(feature = "capture")]
+ SceneBuilderResult::CapturedTransactions(txns, capture_config, result_tx) => {
+ if let Some(ref mut old_config) = self.capture_config {
+ assert!(old_config.scene_id <= capture_config.scene_id);
+ if old_config.scene_id < capture_config.scene_id {
+ old_config.scene_id = capture_config.scene_id;
+ old_config.frame_id = 0;
+ }
+ } else {
+ self.capture_config = Some(capture_config);
+ }
+
+ let built_frame = self.process_transaction(
+ txns,
+ result_tx,
+ frame_counter,
+ );
+
+ if built_frame {
+ self.save_capture_sequence();
+ }
+
+ self.bookkeep_after_frames();
+ },
+ SceneBuilderResult::GetGlyphDimensions(request) => {
+ let mut glyph_dimensions = Vec::with_capacity(request.glyph_indices.len());
+ if let Some(base) = self.resource_cache.get_font_instance(request.key) {
+ let font = FontInstance::from_base(Arc::clone(&base));
+ for glyph_index in &request.glyph_indices {
+ let glyph_dim = self.resource_cache.get_glyph_dimensions(&font, *glyph_index);
+ glyph_dimensions.push(glyph_dim);
+ }
+ }
+ request.sender.send(glyph_dimensions).unwrap();
+ }
+ SceneBuilderResult::GetGlyphIndices(request) => {
+ let mut glyph_indices = Vec::with_capacity(request.text.len());
+ for ch in request.text.chars() {
+ let index = self.resource_cache.get_glyph_index(request.key, ch);
+ glyph_indices.push(index);
+ }
+ request.sender.send(glyph_indices).unwrap();
+ }
+ SceneBuilderResult::FlushComplete(tx) => {
+ tx.send(()).ok();
+ }
+ SceneBuilderResult::ExternalEvent(evt) => {
+ self.notifier.external_event(evt);
+ }
+ SceneBuilderResult::ClearNamespace(id) => {
+ self.resource_cache.clear_namespace(id);
+ self.documents.retain(|doc_id, _doc| doc_id.namespace_id != id);
+ if let Some(handler) = &mut self.blob_image_handler {
+ handler.clear_namespace(id);
+ }
+ }
+ SceneBuilderResult::DeleteDocument(document_id) => {
+ self.documents.remove(&document_id);
+ }
+ SceneBuilderResult::ShutDown(sender) => {
+ info!("Recycling stats: {:?}", self.recycler);
+ return RenderBackendStatus::ShutDown(sender);
+ }
+ SceneBuilderResult::DocumentsForDebugger(json) => {
+ let msg = ResultMsg::DebugOutput(DebugOutput::FetchDocuments(json));
+ self.result_tx.send(msg).unwrap();
+ self.notifier.wake_up(false);
+ }
+ }
+
+ RenderBackendStatus::Continue
+ }
+
+ fn update_frame_builder_config(&self) {
+ self.send_backend_message(
+ SceneBuilderRequest::SetFrameBuilderConfig(
+ self.frame_config.clone()
+ )
+ );
+ }
+
+ fn prepare_for_frames(&mut self) {
+ self.gpu_cache.prepare_for_frames();
+ }
+
+ fn bookkeep_after_frames(&mut self) {
+ self.gpu_cache.bookkeep_after_frames();
+ }
+
+ fn requires_frame_build(&mut self) -> bool {
+ self.gpu_cache.requires_frame_build()
+ }
+
+ fn prepare_transactions(
+ &mut self,
+ txns: Vec<Box<TransactionMsg>>,
+ frame_counter: &mut u32,
+ ) {
+ self.prepare_for_frames();
+ self.maybe_force_nop_documents(
+ frame_counter,
+ |document_id| txns.iter().any(|txn| txn.document_id == document_id));
+
+ let mut built_frame = false;
+ for mut txn in txns {
+ if txn.generate_frame.as_bool() {
+ txn.profile.end_time(profiler::API_SEND_TIME);
+ }
+
+ self.documents.get_mut(&txn.document_id).unwrap().profile.merge(&mut txn.profile);
+
+ built_frame |= self.update_document(
+ txn.document_id,
+ txn.resource_updates.take(),
+ txn.frame_ops.take(),
+ txn.notifications.take(),
+ txn.generate_frame.as_bool(),
+ txn.generate_frame.id(),
+ txn.invalidate_rendered_frame,
+ frame_counter,
+ false
+ );
+ }
+ if built_frame {
+ #[cfg(feature = "capture")]
+ self.save_capture_sequence();
+ }
+ self.bookkeep_after_frames();
+ }
+
+ /// In certain cases, resources shared by multiple documents have to run
+ /// maintenance operations, like cleaning up unused cache items. In those
+ /// cases, we are forced to build frames for all documents, however we
+ /// may not have a transaction ready for every document - this method
+ /// calls update_document with the details of a fake, nop transaction just
+ /// to force a frame build.
+ fn maybe_force_nop_documents<F>(&mut self,
+ frame_counter: &mut u32,
+ document_already_present: F) where
+ F: Fn(DocumentId) -> bool {
+ if self.requires_frame_build() {
+ let nop_documents : Vec<DocumentId> = self.documents.keys()
+ .cloned()
+ .filter(|key| !document_already_present(*key))
+ .collect();
+ #[allow(unused_variables)]
+ let mut built_frame = false;
+ for &document_id in &nop_documents {
+ built_frame |= self.update_document(
+ document_id,
+ Vec::default(),
+ Vec::default(),
+ Vec::default(),
+ false,
+ None,
+ false,
+ frame_counter,
+ false);
+ }
+ #[cfg(feature = "capture")]
+ match built_frame {
+ true => self.save_capture_sequence(),
+ _ => {},
+ }
+ }
+ }
+
+ fn update_document(
+ &mut self,
+ document_id: DocumentId,
+ resource_updates: Vec<ResourceUpdate>,
+ mut frame_ops: Vec<FrameMsg>,
+ mut notifications: Vec<NotificationRequest>,
+ mut render_frame: bool,
+ generated_frame_id: Option<u64>,
+ invalidate_rendered_frame: bool,
+ frame_counter: &mut u32,
+ has_built_scene: bool,
+ ) -> bool {
+ let requested_frame = render_frame;
+
+ let requires_frame_build = self.requires_frame_build();
+ let doc = self.documents.get_mut(&document_id).unwrap();
+
+ // If we have a sampler, get more frame ops from it and add them
+ // to the transaction. This is a hook to allow the WR user code to
+ // fiddle with things after a potentially long scene build, but just
+ // before rendering. This is useful for rendering with the latest
+ // async transforms.
+ if requested_frame || has_built_scene {
+ if let Some(ref sampler) = self.sampler {
+ frame_ops.append(&mut sampler.sample(document_id, generated_frame_id));
+ }
+ }
+
+ doc.has_built_scene |= has_built_scene;
+
+ // TODO: this scroll variable doesn't necessarily mean we scrolled. It is only used
+ // for something wrench specific and we should remove it.
+ let mut scroll = false;
+ for frame_msg in frame_ops {
+ let op = doc.process_frame_msg(frame_msg);
+ scroll |= op.scroll;
+ }
+
+ for update in &resource_updates {
+ if let ResourceUpdate::UpdateImage(..) = update {
+ doc.frame_is_valid = false;
+ }
+ }
+
+ self.resource_cache.post_scene_building_update(
+ resource_updates,
+ &mut doc.profile,
+ );
+
+ if doc.dynamic_properties.flush_pending_updates() {
+ doc.frame_is_valid = false;
+ doc.hit_tester_is_valid = false;
+ }
+
+ if !doc.can_render() {
+ // TODO: this happens if we are building the first scene asynchronously and
+ // scroll at the same time. we should keep track of the fact that we skipped
+ // composition here and do it as soon as we receive the scene.
+ render_frame = false;
+ }
+
+ // Avoid re-building the frame if the current built frame is still valid.
+ // However, if the resource_cache requires a frame build, _always_ do that, unless
+ // doc.can_render() is false, as in that case a frame build can't happen anyway.
+ // We want to ensure we do this because even if the doc doesn't have pixels it
+ // can still try to access stale texture cache items.
+ let build_frame = (render_frame && !doc.frame_is_valid && doc.has_pixels()) ||
+ (requires_frame_build && doc.can_render());
+
+ // Request composite is true when we want to composite frame even when
+ // there is no frame update. This happens when video frame is updated under
+ // external image with NativeTexture or when platform requested to composite frame.
+ if invalidate_rendered_frame {
+ doc.rendered_frame_is_valid = false;
+ if let CompositorKind::Draw { max_partial_present_rects, .. } = doc.scene.config.compositor_kind {
+
+ // When partial present is enabled, we need to force redraw.
+ if max_partial_present_rects > 0 {
+ let msg = ResultMsg::ForceRedraw;
+ self.result_tx.send(msg).unwrap();
+ }
+ }
+ }
+
+ let mut frame_build_time = None;
+ if build_frame {
+ profile_scope!("generate frame");
+
+ *frame_counter += 1;
+
+ // borrow ck hack for profile_counters
+ let (pending_update, rendered_document) = {
+ let frame_build_start_time = precise_time_ns();
+
+ let rendered_document = doc.build_frame(
+ &mut self.resource_cache,
+ &mut self.gpu_cache,
+ self.debug_flags,
+ &mut self.tile_cache_logger,
+ &mut self.tile_caches,
+ );
+
+ debug!("generated frame for document {:?} with {} passes",
+ document_id, rendered_document.frame.passes.len());
+
+ let msg = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg).unwrap();
+
+ frame_build_time = Some(precise_time_ns() - frame_build_start_time);
+
+ let pending_update = self.resource_cache.pending_updates();
+ (pending_update, rendered_document)
+ };
+
+ // Build a small struct that represents the state of the tiles to be composited.
+ let composite_descriptor = rendered_document
+ .frame
+ .composite_state
+ .descriptor
+ .clone();
+
+ // If there are texture cache updates to apply, or if the produced
+ // frame is not a no-op, or the compositor state has changed,
+ // then we cannot skip compositing this frame.
+ if !pending_update.is_nop() ||
+ !rendered_document.frame.is_nop() ||
+ composite_descriptor != doc.prev_composite_descriptor {
+ doc.rendered_frame_is_valid = false;
+ }
+ doc.prev_composite_descriptor = composite_descriptor;
+
+ #[cfg(feature = "capture")]
+ match self.capture_config {
+ Some(ref mut config) => {
+ // FIXME(aosmond): document splitting causes multiple prepare frames
+ config.prepare_frame();
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let file_name = format!("frame-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&rendered_document.frame, file_name);
+ }
+
+ let data_stores_name = format!("data-stores-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&doc.data_stores, data_stores_name);
+
+ let properties_name = format!("properties-{}-{}", document_id.namespace_id.0, document_id.id);
+ config.serialize_for_frame(&doc.dynamic_properties, properties_name);
+ },
+ None => {},
+ }
+
+ let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
+ self.result_tx.send(msg).unwrap();
+
+ // Publish the frame
+ let msg = ResultMsg::PublishDocument(
+ document_id,
+ rendered_document,
+ pending_update,
+ );
+ self.result_tx.send(msg).unwrap();
+ } else if requested_frame {
+ // WR-internal optimization to avoid doing a bunch of render work if
+ // there's no pixels. We still want to pretend to render and request
+ // a render to make sure that the callbacks (particularly the
+ // new_frame_ready callback below) has the right flags.
+ let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
+ self.result_tx.send(msg).unwrap();
+ }
+
+ drain_filter(
+ &mut notifications,
+ |n| { n.when() == Checkpoint::FrameBuilt },
+ |n| { n.notify(); },
+ );
+
+ if !notifications.is_empty() {
+ self.result_tx.send(ResultMsg::AppendNotificationRequests(notifications)).unwrap();
+ }
+
+ // Always forward the transaction to the renderer if a frame was requested,
+ // otherwise gecko can get into a state where it waits (forever) for the
+ // transaction to complete before sending new work.
+ if requested_frame {
+ // If rendered frame is already valid, there is no need to render frame.
+ if doc.rendered_frame_is_valid {
+ render_frame = false;
+ } else if render_frame {
+ doc.rendered_frame_is_valid = true;
+ }
+ self.notifier.new_frame_ready(document_id, scroll, render_frame, frame_build_time);
+ }
+
+ if !doc.hit_tester_is_valid {
+ doc.rebuild_hit_tester();
+ }
+
+ build_frame
+ }
+
+ fn send_backend_message(&self, msg: SceneBuilderRequest) {
+ self.scene_tx.send(msg).unwrap();
+ }
+
+ #[cfg(not(feature = "debugger"))]
+ fn get_spatial_tree_for_debugger(&self) -> String {
+ String::new()
+ }
+
+ #[cfg(feature = "debugger")]
+ fn get_spatial_tree_for_debugger(&self) -> String {
+ use crate::print_tree::PrintableTree;
+
+ let mut debug_root = debug_server::SpatialTreeList::new();
+
+ for (_, doc) in &self.documents {
+ let debug_node = debug_server::TreeNode::new("document spatial tree");
+ let mut builder = debug_server::TreeNodeBuilder::new(debug_node);
+
+ doc.scene.spatial_tree.print_with(&mut builder);
+
+ debug_root.add(builder.build());
+ }
+
+ serde_json::to_string(&debug_root).unwrap()
+ }
+
+ fn report_memory(&mut self, tx: Sender<Box<MemoryReport>>) {
+ let mut report = Box::new(MemoryReport::default());
+ let ops = self.size_of_ops.as_mut().unwrap();
+ let op = ops.size_of_op;
+ report.gpu_cache_metadata = self.gpu_cache.size_of(ops);
+ for doc in self.documents.values() {
+ report.clip_stores += doc.scene.clip_store.size_of(ops);
+ report.hit_testers += match &doc.hit_tester {
+ Some(hit_tester) => hit_tester.size_of(ops),
+ None => 0,
+ };
+
+ doc.data_stores.report_memory(ops, &mut report)
+ }
+
+ (*report) += self.resource_cache.report_memory(op);
+ report.texture_cache_structures = self.resource_cache
+ .texture_cache
+ .report_memory(ops);
+
+ // Send a message to report memory on the scene-builder thread, which
+ // will add its report to this one and send the result back to the original
+ // thread waiting on the request.
+ self.send_backend_message(
+ SceneBuilderRequest::ReportMemory(report, tx)
+ );
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture_sequence(&mut self) {
+ if let Some(ref mut config) = self.capture_config {
+ let deferred = self.resource_cache.save_capture_sequence(config);
+
+ let backend = PlainRenderBackend {
+ default_device_pixel_ratio: self.default_device_pixel_ratio,
+ frame_config: self.frame_config.clone(),
+ resource_sequence_id: config.resource_id,
+ documents: self.documents
+ .iter()
+ .map(|(id, doc)| (*id, doc.view))
+ .collect(),
+ };
+ config.serialize_for_frame(&backend, "backend");
+
+ if !deferred.is_empty() {
+ let msg = ResultMsg::DebugOutput(DebugOutput::SaveCapture(config.clone(), deferred));
+ self.result_tx.send(msg).unwrap();
+ }
+ }
+ }
+}
+
+impl RenderBackend {
+ #[cfg(feature = "capture")]
+ // Note: the mutable `self` is only needed here for resolving blob images
+ fn save_capture(
+ &mut self,
+ root: PathBuf,
+ bits: CaptureBits,
+ ) -> DebugOutput {
+ use std::fs;
+ use crate::render_task_graph::dump_render_tasks_as_svg;
+
+ debug!("capture: saving {:?}", root);
+ if !root.is_dir() {
+ if let Err(e) = fs::create_dir_all(&root) {
+ panic!("Unable to create capture dir: {:?}", e);
+ }
+ }
+ let config = CaptureConfig::new(root, bits);
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ self.prepare_for_frames();
+ }
+
+ for (&id, doc) in &mut self.documents {
+ debug!("\tdocument {:?}", id);
+ if config.bits.contains(CaptureBits::FRAME) {
+ let rendered_document = doc.build_frame(
+ &mut self.resource_cache,
+ &mut self.gpu_cache,
+ self.debug_flags,
+ &mut self.tile_cache_logger,
+ &mut self.tile_caches,
+ );
+ // After we rendered the frames, there are pending updates to both
+ // GPU cache and resources. Instead of serializing them, we are going to make sure
+ // they are applied on the `Renderer` side.
+ let msg_update_gpu_cache = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg_update_gpu_cache).unwrap();
+ //TODO: write down doc's pipeline info?
+ // it has `pipeline_epoch_map`,
+ // which may capture necessary details for some cases.
+ let file_name = format!("frame-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&rendered_document.frame, file_name);
+ let file_name = format!("spatial-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_tree_for_frame(&doc.scene.spatial_tree, file_name);
+ let file_name = format!("built-primitives-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scene.prim_store, file_name);
+ let file_name = format!("built-clips-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scene.clip_store, file_name);
+ let file_name = format!("scratch-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.scratch.primitive, file_name);
+ let file_name = format!("render-tasks-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut render_tasks_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ dump_render_tasks_as_svg(
+ &rendered_document.frame.render_tasks,
+ &rendered_document.frame.passes,
+ &mut render_tasks_file
+ ).unwrap();
+ let file_name = format!("texture-cache-color-linear-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_color8_linear_as_svg(&mut texture_file).unwrap();
+ let file_name = format!("texture-cache-glyphs-{}-{}.svg", id.namespace_id.0, id.id);
+ let mut texture_file = fs::File::create(&config.file_path_for_frame(file_name, "svg"))
+ .expect("Failed to open the SVG file.");
+ self.resource_cache.texture_cache.dump_glyphs_as_svg(&mut texture_file).unwrap();
+ }
+
+ let data_stores_name = format!("data-stores-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.data_stores, data_stores_name);
+
+ let properties_name = format!("properties-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_frame(&doc.dynamic_properties, properties_name);
+ }
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ // TODO: there is no guarantee that we won't hit this case, but we want to
+ // report it here if we do. If we don't, it will simply crash in
+ // Renderer::render_impl and give us less information about the source.
+ assert!(!self.requires_frame_build(), "Caches were cleared during a capture.");
+ self.bookkeep_after_frames();
+ }
+
+ debug!("\tscene builder");
+ self.send_backend_message(
+ SceneBuilderRequest::SaveScene(config.clone())
+ );
+
+ debug!("\tresource cache");
+ let (resources, deferred) = self.resource_cache.save_capture(&config.root);
+
+ if config.bits.contains(CaptureBits::TILE_CACHE) {
+ debug!("\ttile cache");
+ self.tile_cache_logger.save_capture(&config.root);
+ }
+
+ info!("\tbackend");
+ let backend = PlainRenderBackend {
+ default_device_pixel_ratio: self.default_device_pixel_ratio,
+ frame_config: self.frame_config.clone(),
+ resource_sequence_id: 0,
+ documents: self.documents
+ .iter()
+ .map(|(id, doc)| (*id, doc.view))
+ .collect(),
+ };
+
+ config.serialize_for_frame(&backend, "backend");
+ config.serialize_for_frame(&resources, "plain-resources");
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let msg_update_resources = ResultMsg::UpdateResources {
+ resource_updates: self.resource_cache.pending_updates(),
+ memory_pressure: false,
+ };
+ self.result_tx.send(msg_update_resources).unwrap();
+ // Save the texture/glyph/image caches.
+ info!("\tresource cache");
+ let caches = self.resource_cache.save_caches(&config.root);
+ config.serialize_for_resource(&caches, "resource_cache");
+ info!("\tgpu cache");
+ config.serialize_for_resource(&self.gpu_cache, "gpu_cache");
+ }
+
+ DebugOutput::SaveCapture(config, deferred)
+ }
+
+ #[cfg(feature = "capture")]
+ fn start_capture_sequence(
+ &mut self,
+ root: PathBuf,
+ bits: CaptureBits,
+ ) {
+ self.send_backend_message(
+ SceneBuilderRequest::StartCaptureSequence(CaptureConfig::new(root, bits))
+ );
+ }
+
+ #[cfg(feature = "capture")]
+ fn stop_capture_sequence(
+ &mut self,
+ ) {
+ self.send_backend_message(
+ SceneBuilderRequest::StopCaptureSequence
+ );
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_capture(
+ &mut self,
+ mut config: CaptureConfig,
+ ) {
+ debug!("capture: loading {:?}", config.frame_root());
+ let backend = config.deserialize_for_frame::<PlainRenderBackend, _>("backend")
+ .expect("Unable to open backend.ron");
+
+ // If this is a capture sequence, then the ID will be non-zero, and won't
+ // match what is loaded, but for still captures, the ID will be zero.
+ let first_load = backend.resource_sequence_id == 0;
+ if self.loaded_resource_sequence_id != backend.resource_sequence_id || first_load {
+ // FIXME(aosmond): We clear the documents because when we update the
+ // resource cache, we actually wipe and reload, because we don't
+ // know what is the same and what has changed. If we were to keep as
+ // much of the resource cache state as possible, we could avoid
+ // flushing the document state (which has its own dependecies on the
+ // cache).
+ //
+ // FIXME(aosmond): If we try to load the next capture in the
+ // sequence too quickly, we may lose resources we depend on in the
+ // current frame. This can cause panics. Ideally we would not
+ // advance to the next frame until the FrameRendered event for all
+ // of the pipelines.
+ self.documents.clear();
+
+ config.resource_id = backend.resource_sequence_id;
+ self.loaded_resource_sequence_id = backend.resource_sequence_id;
+
+ let plain_resources = config.deserialize_for_resource::<PlainResources, _>("plain-resources")
+ .expect("Unable to open plain-resources.ron");
+ let caches_maybe = config.deserialize_for_resource::<PlainCacheOwn, _>("resource_cache");
+
+ // Note: it would be great to have `RenderBackend` to be split
+ // rather explicitly on what's used before and after scene building
+ // so that, for example, we never miss anything in the code below:
+
+ let plain_externals = self.resource_cache.load_capture(
+ plain_resources,
+ caches_maybe,
+ &config,
+ );
+
+ let msg_load = ResultMsg::DebugOutput(
+ DebugOutput::LoadCapture(config.clone(), plain_externals)
+ );
+ self.result_tx.send(msg_load).unwrap();
+
+ self.gpu_cache = match config.deserialize_for_resource::<GpuCache, _>("gpu_cache") {
+ Some(gpu_cache) => gpu_cache,
+ None => GpuCache::new(),
+ };
+ }
+
+ self.default_device_pixel_ratio = backend.default_device_pixel_ratio;
+ self.frame_config = backend.frame_config;
+
+ let mut scenes_to_build = Vec::new();
+
+ for (id, view) in backend.documents {
+ debug!("\tdocument {:?}", id);
+ let scene_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ let scene = config.deserialize_for_scene::<Scene, _>(&scene_name)
+ .expect(&format!("Unable to open {}.ron", scene_name));
+
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ let interners = config.deserialize_for_scene::<Interners, _>(&interners_name)
+ .expect(&format!("Unable to open {}.ron", interners_name));
+
+ let data_stores_name = format!("data-stores-{}-{}", id.namespace_id.0, id.id);
+ let data_stores = config.deserialize_for_frame::<DataStores, _>(&data_stores_name)
+ .expect(&format!("Unable to open {}.ron", data_stores_name));
+
+ let properties_name = format!("properties-{}-{}", id.namespace_id.0, id.id);
+ let properties = config.deserialize_for_frame::<SceneProperties, _>(&properties_name)
+ .expect(&format!("Unable to open {}.ron", properties_name));
+
+ // Update the document if it still exists, rather than replace it entirely.
+ // This allows us to preserve state information such as the frame stamp,
+ // which is necessary for cache sanity.
+ match self.documents.entry(id) {
+ Occupied(entry) => {
+ let doc = entry.into_mut();
+ doc.view = view;
+ doc.loaded_scene = scene.clone();
+ doc.data_stores = data_stores;
+ doc.dynamic_properties = properties;
+ doc.frame_is_valid = false;
+ doc.rendered_frame_is_valid = false;
+ doc.has_built_scene = false;
+ doc.hit_tester_is_valid = false;
+ }
+ Vacant(entry) => {
+ let doc = Document {
+ id,
+ scene: BuiltScene::empty(),
+ removed_pipelines: Vec::new(),
+ view,
+ stamp: FrameStamp::first(id),
+ frame_builder: FrameBuilder::new(),
+ dynamic_properties: properties,
+ hit_tester: None,
+ shared_hit_tester: Arc::new(SharedHitTester::new()),
+ frame_is_valid: false,
+ hit_tester_is_valid: false,
+ rendered_frame_is_valid: false,
+ has_built_scene: false,
+ data_stores,
+ scratch: ScratchBuffer::default(),
+ loaded_scene: scene.clone(),
+ prev_composite_descriptor: CompositeDescriptor::empty(),
+ dirty_rects_are_valid: false,
+ profile: TransactionProfile::new(),
+ rg_builder: RenderTaskGraphBuilder::new(),
+ };
+ entry.insert(doc);
+ }
+ };
+
+ let frame_name = format!("frame-{}-{}", id.namespace_id.0, id.id);
+ let frame = config.deserialize_for_frame::<Frame, _>(frame_name);
+ let build_frame = match frame {
+ Some(frame) => {
+ info!("\tloaded a built frame with {} passes", frame.passes.len());
+
+ let msg_update = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+ self.result_tx.send(msg_update).unwrap();
+
+ let msg_publish = ResultMsg::PublishDocument(
+ id,
+ RenderedDocument { frame, is_new_scene: true, profile: TransactionProfile::new() },
+ self.resource_cache.pending_updates(),
+ );
+ self.result_tx.send(msg_publish).unwrap();
+
+ self.notifier.new_frame_ready(id, false, true, None);
+
+ // We deserialized the state of the frame so we don't want to build
+ // it (but we do want to update the scene builder's state)
+ false
+ }
+ None => true,
+ };
+
+ scenes_to_build.push(LoadScene {
+ document_id: id,
+ scene,
+ view: view.scene.clone(),
+ config: self.frame_config.clone(),
+ font_instances: self.resource_cache.get_font_instances(),
+ build_frame,
+ interners,
+ });
+ }
+
+ if !scenes_to_build.is_empty() {
+ self.send_backend_message(
+ SceneBuilderRequest::LoadScenes(scenes_to_build)
+ );
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs
new file mode 100644
index 0000000000..9e67179d5a
--- /dev/null
+++ b/gfx/wr/webrender/src/render_target.rs
@@ -0,0 +1,993 @@
+/* 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 api::units::*;
+use api::{ColorF, PremultipliedColorF, ImageFormat, LineOrientation, BorderStyle};
+use crate::batch::{AlphaBatchBuilder, AlphaBatchContainer, BatchTextures, resolve_image};
+use crate::batch::{ClipBatcher, BatchBuilder};
+use crate::spatial_tree::{SpatialTree, ROOT_SPATIAL_NODE_INDEX};
+use crate::clip::ClipStore;
+use crate::composite::CompositeState;
+use crate::frame_builder::{FrameGlobalResources};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress};
+use crate::gpu_types::{BorderInstance, SvgFilterInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance};
+use crate::gpu_types::{TransformPalette, ZBufferIdGenerator};
+use crate::internal_types::{FastHashMap, TextureSource, LayerIndex, Swizzle, CacheTextureId};
+use crate::picture::{SliceId, SurfaceInfo, ResolvedSurfaceTexture, TileCacheInstance};
+use crate::prim_store::{PrimitiveStore, DeferredResolve, PrimitiveScratchBuffer};
+use crate::prim_store::gradient::GRADIENT_FP_STOPS;
+use crate::render_backend::DataStores;
+use crate::render_task::{RenderTaskKind, RenderTaskAddress, BlitSource};
+use crate::render_task::{RenderTask, ScalingTask, SvgFilterInfo};
+use crate::render_task_graph::{RenderTaskGraph, RenderTaskId};
+use crate::resource_cache::ResourceCache;
+use crate::visibility::PrimitiveVisibilityMask;
+
+
+const STYLE_SOLID: i32 = ((BorderStyle::Solid as i32) << 8) | ((BorderStyle::Solid as i32) << 16);
+const STYLE_MASK: i32 = 0x00FF_FF00;
+
+/// A tag used to identify the output format of a `RenderTarget`.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTargetKind {
+ Color, // RGBA8
+ Alpha, // R8
+}
+
+/// Identifies a given `RenderTarget` in a `RenderTargetList`.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetIndex(pub usize);
+
+pub struct RenderTargetContext<'a, 'rc> {
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub prim_store: &'a PrimitiveStore,
+ pub resource_cache: &'rc mut ResourceCache,
+ pub use_dual_source_blending: bool,
+ pub use_advanced_blending: bool,
+ pub break_advanced_blend_batches: bool,
+ pub batch_lookback_count: usize,
+ pub spatial_tree: &'a SpatialTree,
+ pub data_stores: &'a DataStores,
+ pub surfaces: &'a [SurfaceInfo],
+ pub scratch: &'a PrimitiveScratchBuffer,
+ pub screen_world_rect: WorldRect,
+ pub globals: &'a FrameGlobalResources,
+ pub tile_caches: &'a FastHashMap<SliceId, Box<TileCacheInstance>>,
+}
+
+/// Represents a number of rendering operations on a surface.
+///
+/// In graphics parlance, a "render target" usually means "a surface (texture or
+/// framebuffer) bound to the output of a shader". This trait has a slightly
+/// different meaning, in that it represents the operations on that surface
+/// _before_ it's actually bound and rendered. So a `RenderTarget` is built by
+/// the `RenderBackend` by inserting tasks, and then shipped over to the
+/// `Renderer` where a device surface is resolved and the tasks are transformed
+/// into draw commands on that surface.
+///
+/// We express this as a trait to generalize over color and alpha surfaces.
+/// a given `RenderTask` will draw to one or the other, depending on its type
+/// and sometimes on its parameters. See `RenderTask::target_kind`.
+pub trait RenderTarget {
+ /// Creates a new RenderTarget of the given type.
+ fn new(
+ texture_id: CacheTextureId,
+ screen_size: DeviceIntSize,
+ gpu_supports_fast_clears: bool,
+ used_rect: DeviceIntRect,
+ ) -> Self;
+
+ /// Optional hook to provide additional processing for the target at the
+ /// end of the build phase.
+ fn build(
+ &mut self,
+ _ctx: &mut RenderTargetContext,
+ _gpu_cache: &mut GpuCache,
+ _render_tasks: &RenderTaskGraph,
+ _deferred_resolves: &mut Vec<DeferredResolve>,
+ _prim_headers: &mut PrimitiveHeaders,
+ _transforms: &mut TransformPalette,
+ _z_generator: &mut ZBufferIdGenerator,
+ _composite_state: &mut CompositeState,
+ ) {
+ }
+
+ /// Associates a `RenderTask` with this target. That task must be assigned
+ /// to a region returned by invoking `allocate()` on this target.
+ ///
+ /// TODO(gw): It's a bit odd that we need the deferred resolves and mutable
+ /// GPU cache here. They are typically used by the build step above. They
+ /// are used for the blit jobs to allow resolve_image to be called. It's a
+ /// bit of extra overhead to store the image key here and the resolve them
+ /// in the build step separately. BUT: if/when we add more texture cache
+ /// target jobs, we might want to tidy this up.
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ );
+
+ fn needs_depth(&self) -> bool;
+ fn texture_id(&self) -> CacheTextureId;
+}
+
+/// A series of `RenderTarget` instances, serving as the high-level container
+/// into which `RenderTasks` are assigned.
+///
+/// During the build phase, we iterate over the tasks in each `RenderPass`. For
+/// each task, we invoke `allocate()` on the `RenderTargetList`, which in turn
+/// attempts to allocate an output region in the last `RenderTarget` in the
+/// list. If allocation fails (or if the list is empty), a new `RenderTarget` is
+/// created and appended to the list. The build phase then assign the task into
+/// the target associated with the final allocation.
+///
+/// The result is that each `RenderPass` is associated with one or two
+/// `RenderTargetLists`, depending on whether we have all our tasks have the
+/// same `RenderTargetKind`. The lists are then shipped to the `Renderer`, which
+/// allocates a device texture array, with one slice per render target in the
+/// list.
+///
+/// The upshot of this scheme is that it maximizes batching. In a given pass,
+/// we need to do a separate batch for each individual render target. But with
+/// the texture array, we can expose the entirety of the previous pass to each
+/// task in the current pass in a single batch, which generally allows each
+/// task to be drawn in a single batch regardless of how many results from the
+/// previous pass it depends on.
+///
+/// Note that in some cases (like drop-shadows), we can depend on the output of
+/// a pass earlier than the immediately-preceding pass.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTargetList<T> {
+ pub format: ImageFormat,
+ pub targets: Vec<T>,
+}
+
+impl<T: RenderTarget> RenderTargetList<T> {
+ pub fn new(
+ format: ImageFormat,
+ ) -> Self {
+ RenderTargetList {
+ format,
+ targets: Vec::new(),
+ }
+ }
+
+ pub fn build(
+ &mut self,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ z_generator: &mut ZBufferIdGenerator,
+ composite_state: &mut CompositeState,
+ ) {
+ if self.targets.is_empty() {
+ return;
+ }
+
+ for target in &mut self.targets {
+ target.build(
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ z_generator,
+ composite_state,
+ );
+ }
+ }
+
+ pub fn needs_depth(&self) -> bool {
+ self.targets.iter().any(|target| target.needs_depth())
+ }
+}
+
+
+/// Contains the work (in the form of instance arrays) needed to fill a color
+/// color output surface (RGBA8).
+///
+/// See `RenderTarget`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ColorRenderTarget {
+ pub alpha_batch_containers: Vec<AlphaBatchContainer>,
+ // List of blur operations to apply for this render target.
+ pub vertical_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub scalings: FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ pub svg_filters: Vec<(BatchTextures, Vec<SvgFilterInstance>)>,
+ pub blits: Vec<BlitJob>,
+ alpha_tasks: Vec<RenderTaskId>,
+ screen_size: DeviceIntSize,
+ pub texture_id: CacheTextureId,
+ // Track the used rect of the render target, so that
+ // we can set a scissor rect and only clear to the
+ // used portion of the target as an optimization.
+ pub used_rect: DeviceIntRect,
+}
+
+impl RenderTarget for ColorRenderTarget {
+ fn new(
+ texture_id: CacheTextureId,
+ screen_size: DeviceIntSize,
+ _: bool,
+ used_rect: DeviceIntRect,
+ ) -> Self {
+ ColorRenderTarget {
+ alpha_batch_containers: Vec::new(),
+ vertical_blurs: FastHashMap::default(),
+ horizontal_blurs: FastHashMap::default(),
+ scalings: FastHashMap::default(),
+ svg_filters: Vec::new(),
+ blits: Vec::new(),
+ alpha_tasks: Vec::new(),
+ screen_size,
+ texture_id,
+ used_rect,
+ }
+ }
+
+ fn build(
+ &mut self,
+ ctx: &mut RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ prim_headers: &mut PrimitiveHeaders,
+ transforms: &mut TransformPalette,
+ z_generator: &mut ZBufferIdGenerator,
+ composite_state: &mut CompositeState,
+ ) {
+ profile_scope!("build");
+ let mut merged_batches = AlphaBatchContainer::new(None);
+
+ for task_id in &self.alpha_tasks {
+ profile_scope!("alpha_task");
+ let task = &render_tasks[*task_id];
+
+ match task.kind {
+ RenderTaskKind::Picture(ref pic_task) => {
+ let pic = &ctx.prim_store.pictures[pic_task.pic_index.0];
+
+ let raster_spatial_node_index = match pic.raster_config {
+ Some(ref raster_config) => {
+ let surface = &ctx.surfaces[raster_config.surface_index.0];
+ surface.raster_spatial_node_index
+ }
+ None => {
+ // This must be the main framebuffer
+ ROOT_SPATIAL_NODE_INDEX
+ }
+ };
+
+ let (target_rect, _) = task.get_target_rect();
+
+ let scissor_rect = if pic_task.can_merge {
+ None
+ } else {
+ Some(target_rect)
+ };
+
+ // Typical workloads have a single or a few batch builders with a
+ // large number of batches (regular pictres) and a higher number
+ // of batch builders with only a single or two batches (for example
+ // rendering isolated primitives to compute their shadows).
+ // We can easily guess which category we are in for each picture
+ // by checking whether it has multiple clusters.
+ let prealloc_batch_count = if pic.prim_list.clusters.len() > 1 {
+ 128
+ } else {
+ 0
+ };
+
+ // TODO(gw): The type names of AlphaBatchBuilder and BatchBuilder
+ // are still confusing. Once more of the picture caching
+ // improvement code lands, the AlphaBatchBuilder and
+ // AlphaBatchList types will be collapsed into one, which
+ // should simplify coming up with better type names.
+ let alpha_batch_builder = AlphaBatchBuilder::new(
+ self.screen_size,
+ ctx.break_advanced_blend_batches,
+ ctx.batch_lookback_count,
+ *task_id,
+ (*task_id).into(),
+ PrimitiveVisibilityMask::all(),
+ prealloc_batch_count,
+ );
+
+ let mut batch_builder = BatchBuilder::new(
+ vec![alpha_batch_builder],
+ );
+
+ batch_builder.add_pic_to_batch(
+ pic,
+ ctx,
+ gpu_cache,
+ render_tasks,
+ deferred_resolves,
+ prim_headers,
+ transforms,
+ raster_spatial_node_index,
+ pic_task.surface_spatial_node_index,
+ z_generator,
+ composite_state,
+ );
+
+ let alpha_batch_builders = batch_builder.finalize();
+
+ for batcher in alpha_batch_builders {
+ batcher.build(
+ &mut self.alpha_batch_containers,
+ &mut merged_batches,
+ target_rect,
+ scissor_rect,
+ );
+ }
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ }
+
+ if !merged_batches.is_empty() {
+ self.alpha_batch_containers.push(merged_batches);
+ }
+ }
+
+ fn texture_id(&self) -> CacheTextureId {
+ self.texture_id
+ }
+
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ _: &ClipStore,
+ _: &mut TransformPalette,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) {
+ profile_scope!("add_task");
+ let task = &render_tasks[task_id];
+
+ match task.kind {
+ RenderTaskKind::VerticalBlur(..) => {
+ add_blur_instances(
+ &mut self.vertical_blurs,
+ BlurDirection::Vertical,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::Picture(..) => {
+ self.alpha_tasks.push(task_id);
+ }
+ RenderTaskKind::SvgFilter(ref task_info) => {
+ add_svg_filter_instances(
+ &mut self.svg_filters,
+ render_tasks,
+ &task_info.info,
+ task_id,
+ task.children.get(0).cloned(),
+ task.children.get(1).cloned(),
+ task_info.extra_gpu_cache_handle.map(|handle| gpu_cache.get_address(&handle)),
+ )
+ }
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::LineDecoration(..) => {
+ panic!("Should not be added to color target!");
+ }
+ RenderTaskKind::Readback => {}
+ RenderTaskKind::Scaling(ref info) => {
+ add_scaling_instances(
+ info,
+ &mut self.scalings,
+ task,
+ task.children.first().map(|&child| &render_tasks[child]),
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ }
+ RenderTaskKind::Blit(ref task_info) => {
+ let source = match task_info.source {
+ BlitSource::Image { key } => {
+ // Get the cache item for the source texture.
+ let cache_item = resolve_image(
+ key.request,
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ // Work out a source rect to copy from the texture, depending on whether
+ // a sub-rect is present or not.
+ let source_rect = key.texel_rect.map_or(cache_item.uv_rect.to_i32(), |sub_rect| {
+ DeviceIntRect::new(
+ DeviceIntPoint::new(
+ cache_item.uv_rect.origin.x as i32 + sub_rect.origin.x,
+ cache_item.uv_rect.origin.y as i32 + sub_rect.origin.y,
+ ),
+ sub_rect.size,
+ )
+ });
+
+ // Store the blit job for the renderer to execute, including
+ // the allocated destination rect within this target.
+ BlitJobSource::Texture(
+ cache_item.texture_id,
+ cache_item.texture_layer,
+ source_rect,
+ )
+ }
+ BlitSource::RenderTask { task_id } => {
+ BlitJobSource::RenderTask(task_id)
+ }
+ };
+
+ let target_rect = task
+ .get_target_rect()
+ .0;
+ self.blits.push(BlitJob {
+ source,
+ target_rect,
+ });
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+
+ fn needs_depth(&self) -> bool {
+ self.alpha_batch_containers.iter().any(|ab| {
+ !ab.opaque_batches.is_empty()
+ })
+ }
+}
+
+/// Contains the work (in the form of instance arrays) needed to fill an alpha
+/// output surface (R8).
+///
+/// See `RenderTarget`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AlphaRenderTarget {
+ pub clip_batcher: ClipBatcher,
+ // List of blur operations to apply for this render target.
+ pub vertical_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub scalings: FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ pub zero_clears: Vec<RenderTaskId>,
+ pub one_clears: Vec<RenderTaskId>,
+ pub texture_id: CacheTextureId,
+}
+
+impl RenderTarget for AlphaRenderTarget {
+ fn new(
+ texture_id: CacheTextureId,
+ _: DeviceIntSize,
+ gpu_supports_fast_clears: bool,
+ _: DeviceIntRect,
+ ) -> Self {
+ AlphaRenderTarget {
+ clip_batcher: ClipBatcher::new(gpu_supports_fast_clears),
+ vertical_blurs: FastHashMap::default(),
+ horizontal_blurs: FastHashMap::default(),
+ scalings: FastHashMap::default(),
+ zero_clears: Vec::new(),
+ one_clears: Vec::new(),
+ texture_id,
+ }
+ }
+
+ fn texture_id(&self) -> CacheTextureId {
+ self.texture_id
+ }
+
+ fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ ctx: &RenderTargetContext,
+ gpu_cache: &mut GpuCache,
+ render_tasks: &RenderTaskGraph,
+ clip_store: &ClipStore,
+ transforms: &mut TransformPalette,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+ ) {
+ profile_scope!("add_task");
+ let task = &render_tasks[task_id];
+ let (target_rect, _) = task.get_target_rect();
+
+ match task.kind {
+ RenderTaskKind::Readback |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ panic!("BUG: should not be added to alpha target!");
+ }
+ RenderTaskKind::VerticalBlur(..) => {
+ self.zero_clears.push(task_id);
+ add_blur_instances(
+ &mut self.vertical_blurs,
+ BlurDirection::Vertical,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ self.zero_clears.push(task_id);
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_id.into(),
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::CacheMask(ref task_info) => {
+ if task_info.clear_to_one {
+ self.one_clears.push(task_id);
+ }
+ self.clip_batcher.add(
+ task_info.clip_node_range,
+ task_info.root_spatial_node_index,
+ ctx.resource_cache,
+ gpu_cache,
+ clip_store,
+ ctx.spatial_tree,
+ transforms,
+ &ctx.data_stores.clip,
+ task_info.actual_rect,
+ &ctx.screen_world_rect,
+ task_info.device_pixel_scale,
+ target_rect.origin.to_f32(),
+ task_info.actual_rect.origin,
+ );
+ }
+ RenderTaskKind::ClipRegion(ref region_task) => {
+ if region_task.clear_to_one {
+ self.one_clears.push(task_id);
+ }
+ let device_rect = DeviceRect::new(
+ DevicePoint::zero(),
+ target_rect.size.to_f32(),
+ );
+ self.clip_batcher.add_clip_region(
+ region_task.local_pos,
+ device_rect,
+ region_task.clip_data.clone(),
+ target_rect.origin.to_f32(),
+ DevicePoint::zero(),
+ region_task.device_pixel_scale.0,
+ );
+ }
+ RenderTaskKind::Scaling(ref info) => {
+ add_scaling_instances(
+ info,
+ &mut self.scalings,
+ task,
+ task.children.first().map(|&child| &render_tasks[child]),
+ ctx.resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+
+ fn needs_depth(&self) -> bool {
+ false
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheTarget {
+ pub surface: ResolvedSurfaceTexture,
+ pub alpha_batch_container: AlphaBatchContainer,
+ pub clear_color: Option<ColorF>,
+ pub dirty_rect: DeviceIntRect,
+ pub valid_rect: DeviceIntRect,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureCacheRenderTarget {
+ pub target_kind: RenderTargetKind,
+ pub horizontal_blurs: FastHashMap<TextureSource, Vec<BlurInstance>>,
+ pub blits: Vec<BlitJob>,
+ pub border_segments_complex: Vec<BorderInstance>,
+ pub border_segments_solid: Vec<BorderInstance>,
+ pub clears: Vec<DeviceIntRect>,
+ pub line_decorations: Vec<LineDecorationJob>,
+ pub gradients: Vec<GradientJob>,
+}
+
+impl TextureCacheRenderTarget {
+ pub fn new(target_kind: RenderTargetKind) -> Self {
+ TextureCacheRenderTarget {
+ target_kind,
+ horizontal_blurs: FastHashMap::default(),
+ blits: vec![],
+ border_segments_complex: vec![],
+ border_segments_solid: vec![],
+ clears: vec![],
+ line_decorations: vec![],
+ gradients: vec![],
+ }
+ }
+
+ pub fn add_task(
+ &mut self,
+ task_id: RenderTaskId,
+ render_tasks: &RenderTaskGraph,
+ ) {
+ profile_scope!("add_task");
+ let task_address = task_id.into();
+
+ let task = &render_tasks[task_id];
+ let target_rect = task.get_target_rect();
+
+ match task.kind {
+ RenderTaskKind::LineDecoration(ref info) => {
+ self.clears.push(target_rect.0);
+
+ self.line_decorations.push(LineDecorationJob {
+ task_rect: target_rect.0.to_f32(),
+ local_size: info.local_size,
+ style: info.style as i32,
+ axis_select: match info.orientation {
+ LineOrientation::Horizontal => 0.0,
+ LineOrientation::Vertical => 1.0,
+ },
+ wavy_line_thickness: info.wavy_line_thickness,
+ });
+ }
+ RenderTaskKind::HorizontalBlur(..) => {
+ add_blur_instances(
+ &mut self.horizontal_blurs,
+ BlurDirection::Horizontal,
+ task_address,
+ task.children[0],
+ render_tasks,
+ );
+ }
+ RenderTaskKind::Blit(ref task_info) => {
+ match task_info.source {
+ BlitSource::Image { .. } => {
+ // reading/writing from the texture cache at the same time
+ // is undefined behavior.
+ panic!("bug: a single blit cannot be to/from texture cache");
+ }
+ BlitSource::RenderTask { task_id } => {
+ // Add a blit job to copy from an existing render
+ // task to this target.
+ self.blits.push(BlitJob {
+ source: BlitJobSource::RenderTask(task_id),
+ target_rect: target_rect.0,
+ });
+ }
+ }
+ }
+ RenderTaskKind::Border(ref task_info) => {
+ self.clears.push(target_rect.0);
+
+ let task_origin = target_rect.0.origin.to_f32();
+ // TODO(gw): Clone here instead of a move of this vec, since the frame
+ // graph is immutable by this point. It's rare that borders
+ // are drawn since they are persisted in the texture cache,
+ // but perhaps this could be improved in future.
+ let instances = task_info.instances.clone();
+ for mut instance in instances {
+ // TODO(gw): It may be better to store the task origin in
+ // the render task data instead of per instance.
+ instance.task_origin = task_origin;
+ if instance.flags & STYLE_MASK == STYLE_SOLID {
+ self.border_segments_solid.push(instance);
+ } else {
+ self.border_segments_complex.push(instance);
+ }
+ }
+ }
+ RenderTaskKind::Gradient(ref task_info) => {
+ let mut stops = [0.0; 4];
+ let mut colors = [PremultipliedColorF::BLACK; 4];
+
+ let axis_select = match task_info.orientation {
+ LineOrientation::Horizontal => 0.0,
+ LineOrientation::Vertical => 1.0,
+ };
+
+ for (stop, (offset, color)) in task_info.stops.iter().zip(stops.iter_mut().zip(colors.iter_mut())) {
+ *offset = stop.offset;
+ *color = ColorF::from(stop.color).premultiplied();
+ }
+
+ self.gradients.push(GradientJob {
+ task_rect: target_rect.0.to_f32(),
+ axis_select,
+ stops,
+ colors,
+ start_stop: [task_info.start_point, task_info.end_point],
+ });
+ }
+ RenderTaskKind::VerticalBlur(..) |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Readback |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ panic!("BUG: unexpected task kind for texture cache target");
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {}
+ }
+ }
+}
+
+fn add_blur_instances(
+ instances: &mut FastHashMap<TextureSource, Vec<BlurInstance>>,
+ blur_direction: BlurDirection,
+ task_address: RenderTaskAddress,
+ src_task_id: RenderTaskId,
+ render_tasks: &RenderTaskGraph,
+) {
+ let source = TextureSource::TextureCache(
+ render_tasks[src_task_id].get_target_texture(),
+ Swizzle::default(),
+ );
+
+ let instance = BlurInstance {
+ task_address,
+ src_task_address: src_task_id.into(),
+ blur_direction,
+ };
+
+ instances
+ .entry(source)
+ .or_insert(Vec::new())
+ .push(instance);
+}
+
+fn add_scaling_instances(
+ task: &ScalingTask,
+ instances: &mut FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ target_task: &RenderTask,
+ source_task: Option<&RenderTask>,
+ resource_cache: &ResourceCache,
+ gpu_cache: &mut GpuCache,
+ deferred_resolves: &mut Vec<DeferredResolve>,
+) {
+ let target_rect = target_task
+ .get_target_rect()
+ .0
+ .inner_rect(task.padding)
+ .to_f32();
+
+ let (source, (source_rect, source_layer)) = match task.image {
+ Some(key) => {
+ assert!(source_task.is_none());
+
+ // Get the cache item for the source texture.
+ let cache_item = resolve_image(
+ key.request,
+ resource_cache,
+ gpu_cache,
+ deferred_resolves,
+ );
+
+ // Work out a source rect to copy from the texture, depending on whether
+ // a sub-rect is present or not.
+ let source_rect = key.texel_rect.map_or(cache_item.uv_rect, |sub_rect| {
+ DeviceIntRect::new(
+ DeviceIntPoint::new(
+ cache_item.uv_rect.origin.x + sub_rect.origin.x,
+ cache_item.uv_rect.origin.y + sub_rect.origin.y,
+ ),
+ sub_rect.size,
+ )
+ });
+
+ (
+ cache_item.texture_id,
+ (source_rect, cache_item.texture_layer as LayerIndex),
+ )
+ }
+ None => {
+ (
+ TextureSource::TextureCache(
+ source_task.unwrap().get_target_texture(),
+ Swizzle::default(),
+ ),
+ source_task.unwrap().location.to_source_rect(),
+ )
+ }
+ };
+
+ instances
+ .entry(source)
+ .or_insert(Vec::new())
+ .push(ScalingInstance {
+ target_rect,
+ source_rect,
+ source_layer: source_layer as i32,
+ });
+}
+
+fn add_svg_filter_instances(
+ instances: &mut Vec<(BatchTextures, Vec<SvgFilterInstance>)>,
+ render_tasks: &RenderTaskGraph,
+ filter: &SvgFilterInfo,
+ task_id: RenderTaskId,
+ input_1_task: Option<RenderTaskId>,
+ input_2_task: Option<RenderTaskId>,
+ extra_data_address: Option<GpuCacheAddress>,
+) {
+ let mut textures = BatchTextures::empty();
+
+ if let Some(id) = input_1_task {
+ let task = &render_tasks[id];
+
+ textures.input.colors[0] = TextureSource::TextureCache(
+ task.get_target_texture(),
+ Swizzle::default(),
+ );
+ }
+
+ if let Some(id) = input_2_task {
+ let task = &render_tasks[id];
+
+ textures.input.colors[1] = TextureSource::TextureCache(
+ task.get_target_texture(),
+ Swizzle::default(),
+ );
+ }
+
+ let kind = match filter {
+ SvgFilterInfo::Blend(..) => 0,
+ SvgFilterInfo::Flood(..) => 1,
+ SvgFilterInfo::LinearToSrgb => 2,
+ SvgFilterInfo::SrgbToLinear => 3,
+ SvgFilterInfo::Opacity(..) => 4,
+ SvgFilterInfo::ColorMatrix(..) => 5,
+ SvgFilterInfo::DropShadow(..) => 6,
+ SvgFilterInfo::Offset(..) => 7,
+ SvgFilterInfo::ComponentTransfer(..) => 8,
+ SvgFilterInfo::Identity => 9,
+ SvgFilterInfo::Composite(..) => 10,
+ };
+
+ let input_count = match filter {
+ SvgFilterInfo::Flood(..) => 0,
+
+ SvgFilterInfo::LinearToSrgb |
+ SvgFilterInfo::SrgbToLinear |
+ SvgFilterInfo::Opacity(..) |
+ SvgFilterInfo::ColorMatrix(..) |
+ SvgFilterInfo::Offset(..) |
+ SvgFilterInfo::ComponentTransfer(..) |
+ SvgFilterInfo::Identity => 1,
+
+ // Not techincally a 2 input filter, but we have 2 inputs here: original content & blurred content.
+ SvgFilterInfo::DropShadow(..) |
+ SvgFilterInfo::Blend(..) |
+ SvgFilterInfo::Composite(..) => 2,
+ };
+
+ let generic_int = match filter {
+ SvgFilterInfo::Blend(mode) => *mode as u16,
+ SvgFilterInfo::ComponentTransfer(data) =>
+ ((data.r_func.to_int() << 12 |
+ data.g_func.to_int() << 8 |
+ data.b_func.to_int() << 4 |
+ data.a_func.to_int()) as u16),
+ SvgFilterInfo::Composite(operator) =>
+ operator.as_int() as u16,
+ SvgFilterInfo::LinearToSrgb |
+ SvgFilterInfo::SrgbToLinear |
+ SvgFilterInfo::Flood(..) |
+ SvgFilterInfo::Opacity(..) |
+ SvgFilterInfo::ColorMatrix(..) |
+ SvgFilterInfo::DropShadow(..) |
+ SvgFilterInfo::Offset(..) |
+ SvgFilterInfo::Identity => 0,
+ };
+
+ let instance = SvgFilterInstance {
+ task_address: task_id.into(),
+ input_1_task_address: input_1_task.map(|id| id.into()).unwrap_or(RenderTaskAddress(0)),
+ input_2_task_address: input_2_task.map(|id| id.into()).unwrap_or(RenderTaskAddress(0)),
+ kind,
+ input_count,
+ generic_int,
+ extra_data_address: extra_data_address.unwrap_or(GpuCacheAddress::INVALID),
+ };
+
+ for (ref mut batch_textures, ref mut batch) in instances.iter_mut() {
+ if let Some(combined_textures) = batch_textures.combine_textures(textures) {
+ batch.push(instance);
+ // Update the batch textures to the newly combined batch textures
+ *batch_textures = combined_textures;
+ return;
+ }
+ }
+
+ instances.push((textures, vec![instance]));
+}
+
+// Defines where the source data for a blit job can be found.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlitJobSource {
+ Texture(TextureSource, i32, DeviceIntRect),
+ RenderTask(RenderTaskId),
+}
+
+// Information required to do a blit from a source to a target.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlitJob {
+ pub source: BlitJobSource,
+ pub target_rect: DeviceIntRect,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug)]
+pub struct LineDecorationJob {
+ pub task_rect: DeviceRect,
+ pub local_size: LayoutSize,
+ pub wavy_line_thickness: f32,
+ pub style: i32,
+ pub axis_select: f32,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[repr(C)]
+#[derive(Clone, Debug)]
+pub struct GradientJob {
+ pub task_rect: DeviceRect,
+ pub stops: [f32; GRADIENT_FP_STOPS],
+ pub colors: [PremultipliedColorF; GRADIENT_FP_STOPS],
+ pub axis_select: f32,
+ pub start_stop: [f32; 2],
+}
diff --git a/gfx/wr/webrender/src/render_task.rs b/gfx/wr/webrender/src/render_task.rs
new file mode 100644
index 0000000000..445f045aca
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -0,0 +1,1549 @@
+/* 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 api::{CompositeOperator, FilterPrimitive, FilterPrimitiveInput, FilterPrimitiveKind};
+use api::{LineStyle, LineOrientation, ClipMode, MixBlendMode, ColorF, ColorSpace};
+use api::units::*;
+use crate::clip::{ClipDataStore, ClipItemKind, ClipStore, ClipNodeRange, ClipNodeFlags};
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::filterdata::SFilterData;
+use crate::frame_builder::FrameBuilderConfig;
+use crate::frame_graph::PassId;
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::{BorderInstance, ImageSource, UvRectKind};
+use crate::internal_types::{CacheTextureId, FastHashMap, LayerIndex};
+use crate::picture::{ResolvedSurfaceTexture, SurfaceInfo};
+use crate::prim_store::{ClipData, PictureIndex};
+use crate::prim_store::image::ImageCacheKey;
+use crate::prim_store::gradient::{GRADIENT_FP_STOPS, GradientStopKey};
+#[cfg(feature = "debugger")]
+use crate::print_tree::{PrintTreePrinter};
+use crate::resource_cache::ResourceCache;
+use std::{usize, f32, i32, u32};
+use crate::render_target::{RenderTargetIndex, RenderTargetKind};
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder};
+#[cfg(feature = "debugger")]
+use crate::render_task_graph::RenderTaskGraph;
+use crate::render_task_cache::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskParent};
+use crate::visibility::PrimitiveVisibilityMask;
+use smallvec::SmallVec;
+
+const FLOATS_PER_RENDER_TASK_INFO: usize = 8;
+pub const MAX_RENDER_TASK_SIZE: i32 = 16384;
+pub const MAX_BLUR_STD_DEVIATION: f32 = 4.0;
+pub const MIN_DOWNSCALING_RT_SIZE: i32 = 8;
+
+fn render_task_sanity_check(size: &DeviceIntSize) {
+ if size.width > MAX_RENDER_TASK_SIZE ||
+ size.height > MAX_RENDER_TASK_SIZE {
+ error!("Attempting to create a render task of size {}x{}", size.width, size.height);
+ panic!();
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[repr(C)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskAddress(pub u16);
+
+impl Into<RenderTaskAddress> for RenderTaskId {
+ fn into(self) -> RenderTaskAddress {
+ RenderTaskAddress(self.index as u16)
+ }
+}
+
+/// A render task location that targets a persistent output buffer which
+/// will be retained over multiple frames.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum StaticRenderTaskSurface {
+ /// The output of the `RenderTask` will be persisted beyond this frame, and
+ /// thus should be drawn into the `TextureCache`.
+ TextureCache {
+ /// Which texture in the texture cache should be drawn into.
+ texture: CacheTextureId,
+ /// The target layer in the above texture.
+ layer: LayerIndex,
+ /// What format this texture cache surface is
+ target_kind: RenderTargetKind,
+ },
+ /// This render task will be drawn to a picture cache texture that is
+ /// persisted between both frames and scenes, if the content remains valid.
+ PictureCache {
+ /// Describes either a WR texture or a native OS compositor target
+ surface: ResolvedSurfaceTexture,
+ },
+}
+
+/// Identifies the output buffer location for a given `RenderTask`.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskLocation {
+ /// A dynamic task that has not yet been allocated a texture and rect.
+ Unallocated {
+ /// Requested size of this render task
+ size: DeviceIntSize,
+ },
+ /// The `RenderTask` should be drawn to a target provided by the atlas
+ /// allocator. This is the most common case.
+ Dynamic {
+ /// Texture that this task was allocated to render on
+ texture_id: CacheTextureId,
+ /// Rectangle in the texture this task occupies
+ rect: DeviceIntRect,
+ },
+ /// A task that is output to a persistent / retained target.
+ Static {
+ /// Target to draw to
+ surface: StaticRenderTaskSurface,
+ /// Rectangle in the texture this task occupies
+ rect: DeviceIntRect,
+ },
+}
+
+impl RenderTaskLocation {
+ /// Returns true if this is a dynamic location.
+ pub fn is_dynamic(&self) -> bool {
+ match *self {
+ RenderTaskLocation::Dynamic { .. } => true,
+ _ => false,
+ }
+ }
+
+ pub fn size(&self) -> DeviceIntSize {
+ match self {
+ RenderTaskLocation::Unallocated { size } => *size,
+ RenderTaskLocation::Dynamic { rect, .. } => rect.size,
+ RenderTaskLocation::Static { rect, .. } => rect.size,
+ }
+ }
+
+ pub fn to_source_rect(&self) -> (DeviceIntRect, LayerIndex) {
+ match *self {
+ RenderTaskLocation::Unallocated { .. } => panic!("Expected position to be set for the task!"),
+ RenderTaskLocation::Dynamic { rect, .. } => (rect, 0),
+ RenderTaskLocation::Static { surface: StaticRenderTaskSurface::PictureCache { .. }, .. } => {
+ panic!("bug: picture cache tasks should never be a source!");
+ }
+ RenderTaskLocation::Static { rect, surface: StaticRenderTaskSurface::TextureCache { layer, .. } } => {
+ (rect, layer)
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheMaskTask {
+ pub actual_rect: DeviceRect,
+ pub root_spatial_node_index: SpatialNodeIndex,
+ pub clip_node_range: ClipNodeRange,
+ pub device_pixel_scale: DevicePixelScale,
+ pub clear_to_one: bool,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ClipRegionTask {
+ pub local_pos: LayoutPoint,
+ pub device_pixel_scale: DevicePixelScale,
+ pub clip_data: ClipData,
+ pub clear_to_one: bool,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureTask {
+ pub pic_index: PictureIndex,
+ pub can_merge: bool,
+ pub content_origin: DevicePoint,
+ pub uv_rect_handle: GpuCacheHandle,
+ pub surface_spatial_node_index: SpatialNodeIndex,
+ pub uv_rect_kind: UvRectKind,
+ pub device_pixel_scale: DevicePixelScale,
+ /// A bitfield that describes which dirty regions should be included
+ /// in batches built for this picture task.
+ pub vis_mask: PrimitiveVisibilityMask,
+ pub scissor_rect: Option<DeviceIntRect>,
+ pub valid_rect: Option<DeviceIntRect>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlurTask {
+ pub blur_std_deviation: f32,
+ pub target_kind: RenderTargetKind,
+ pub uv_rect_handle: GpuCacheHandle,
+ pub blur_region: DeviceIntSize,
+ pub uv_rect_kind: UvRectKind,
+}
+
+impl BlurTask {
+ #[cfg(feature = "debugger")]
+ fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
+ pt.add_item(format!("std deviation: {}", self.blur_std_deviation));
+ pt.add_item(format!("target: {:?}", self.target_kind));
+ }
+
+ // In order to do the blur down-scaling passes without introducing errors, we need the
+ // source of each down-scale pass to be a multuple of two. If need be, this inflates
+ // the source size so that each down-scale pass will sample correctly.
+ pub fn adjusted_blur_source_size(original_size: DeviceSize, mut std_dev: DeviceSize) -> DeviceSize {
+ let mut adjusted_size = original_size;
+ let mut scale_factor = 1.0;
+ while std_dev.width > MAX_BLUR_STD_DEVIATION && std_dev.height > MAX_BLUR_STD_DEVIATION {
+ if adjusted_size.width < MIN_DOWNSCALING_RT_SIZE as f32 ||
+ adjusted_size.height < MIN_DOWNSCALING_RT_SIZE as f32 {
+ break;
+ }
+ std_dev = std_dev * 0.5;
+ scale_factor *= 2.0;
+ adjusted_size = (original_size.to_f32() / scale_factor).ceil();
+ }
+
+ adjusted_size * scale_factor
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ScalingTask {
+ pub target_kind: RenderTargetKind,
+ pub image: Option<ImageCacheKey>,
+ pub uv_rect_kind: UvRectKind,
+ pub padding: DeviceIntSideOffsets,
+}
+
+// Where the source data for a blit task can be found.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlitSource {
+ Image {
+ key: ImageCacheKey,
+ },
+ RenderTask {
+ task_id: RenderTaskId,
+ },
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BorderTask {
+ pub instances: Vec<BorderInstance>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct BlitTask {
+ pub source: BlitSource,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GradientTask {
+ pub stops: [GradientStopKey; GRADIENT_FP_STOPS],
+ pub orientation: LineOrientation,
+ pub start_point: f32,
+ pub end_point: f32,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct LineDecorationTask {
+ pub wavy_line_thickness: f32,
+ pub style: LineStyle,
+ pub orientation: LineOrientation,
+ pub local_size: LayoutSize,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum SvgFilterInfo {
+ Blend(MixBlendMode),
+ Flood(ColorF),
+ LinearToSrgb,
+ SrgbToLinear,
+ Opacity(f32),
+ ColorMatrix(Box<[f32; 20]>),
+ DropShadow(ColorF),
+ Offset(DeviceVector2D),
+ ComponentTransfer(SFilterData),
+ Composite(CompositeOperator),
+ // TODO: This is used as a hack to ensure that a blur task's input is always in the blur's previous pass.
+ Identity,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SvgFilterTask {
+ pub info: SvgFilterInfo,
+ pub extra_gpu_cache_handle: Option<GpuCacheHandle>,
+ pub uv_rect_handle: GpuCacheHandle,
+ pub uv_rect_kind: UvRectKind,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskData {
+ pub data: [f32; FLOATS_PER_RENDER_TASK_INFO],
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskKind {
+ Picture(PictureTask),
+ CacheMask(CacheMaskTask),
+ ClipRegion(ClipRegionTask),
+ VerticalBlur(BlurTask),
+ HorizontalBlur(BlurTask),
+ Readback,
+ Scaling(ScalingTask),
+ Blit(BlitTask),
+ Border(BorderTask),
+ LineDecoration(LineDecorationTask),
+ Gradient(GradientTask),
+ SvgFilter(SvgFilterTask),
+ #[cfg(test)]
+ Test(RenderTargetKind),
+}
+
+impl RenderTaskKind {
+ pub fn as_str(&self) -> &'static str {
+ match *self {
+ RenderTaskKind::Picture(..) => "Picture",
+ RenderTaskKind::CacheMask(..) => "CacheMask",
+ RenderTaskKind::ClipRegion(..) => "ClipRegion",
+ RenderTaskKind::VerticalBlur(..) => "VerticalBlur",
+ RenderTaskKind::HorizontalBlur(..) => "HorizontalBlur",
+ RenderTaskKind::Readback => "Readback",
+ RenderTaskKind::Scaling(..) => "Scaling",
+ RenderTaskKind::Blit(..) => "Blit",
+ RenderTaskKind::Border(..) => "Border",
+ RenderTaskKind::LineDecoration(..) => "LineDecoration",
+ RenderTaskKind::Gradient(..) => "Gradient",
+ RenderTaskKind::SvgFilter(..) => "SvgFilter",
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => "Test",
+ }
+ }
+
+ pub fn target_kind(&self) -> RenderTargetKind {
+ match *self {
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::Readback |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::Picture(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::SvgFilter(..) => {
+ RenderTargetKind::Color
+ }
+
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::CacheMask(..) => {
+ RenderTargetKind::Alpha
+ }
+
+ RenderTaskKind::VerticalBlur(ref task_info) |
+ RenderTaskKind::HorizontalBlur(ref task_info) => {
+ task_info.target_kind
+ }
+
+ RenderTaskKind::Scaling(ref task_info) => {
+ task_info.target_kind
+ }
+
+ #[cfg(test)]
+ RenderTaskKind::Test(kind) => kind,
+ }
+ }
+
+ pub fn new_picture(
+ size: DeviceIntSize,
+ unclipped_size: DeviceSize,
+ pic_index: PictureIndex,
+ content_origin: DevicePoint,
+ uv_rect_kind: UvRectKind,
+ surface_spatial_node_index: SpatialNodeIndex,
+ device_pixel_scale: DevicePixelScale,
+ vis_mask: PrimitiveVisibilityMask,
+ scissor_rect: Option<DeviceIntRect>,
+ valid_rect: Option<DeviceIntRect>,
+ ) -> Self {
+ render_task_sanity_check(&size);
+
+ let can_merge = size.width as f32 >= unclipped_size.width &&
+ size.height as f32 >= unclipped_size.height;
+
+ RenderTaskKind::Picture(PictureTask {
+ pic_index,
+ content_origin,
+ can_merge,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect_kind,
+ surface_spatial_node_index,
+ device_pixel_scale,
+ vis_mask,
+ scissor_rect,
+ valid_rect,
+ })
+ }
+
+ pub fn new_gradient(
+ stops: [GradientStopKey; GRADIENT_FP_STOPS],
+ orientation: LineOrientation,
+ start_point: f32,
+ end_point: f32,
+ ) -> Self {
+ RenderTaskKind::Gradient(GradientTask {
+ stops,
+ orientation,
+ start_point,
+ end_point,
+ })
+ }
+
+ pub fn new_readback() -> Self {
+ RenderTaskKind::Readback
+ }
+
+ pub fn new_line_decoration(
+ style: LineStyle,
+ orientation: LineOrientation,
+ wavy_line_thickness: f32,
+ local_size: LayoutSize,
+ ) -> Self {
+ RenderTaskKind::LineDecoration(LineDecorationTask {
+ style,
+ orientation,
+ wavy_line_thickness,
+ local_size,
+ })
+ }
+
+ pub fn new_border_segment(
+ instances: Vec<BorderInstance>,
+ ) -> Self {
+ RenderTaskKind::Border(BorderTask {
+ instances,
+ })
+ }
+
+ pub fn new_rounded_rect_mask(
+ local_pos: LayoutPoint,
+ clip_data: ClipData,
+ device_pixel_scale: DevicePixelScale,
+ fb_config: &FrameBuilderConfig,
+ ) -> Self {
+ RenderTaskKind::ClipRegion(ClipRegionTask {
+ local_pos,
+ device_pixel_scale,
+ clip_data,
+ clear_to_one: fb_config.gpu_supports_fast_clears,
+ })
+ }
+
+ pub fn new_mask(
+ outer_rect: DeviceRect,
+ clip_node_range: ClipNodeRange,
+ root_spatial_node_index: SpatialNodeIndex,
+ clip_store: &mut ClipStore,
+ gpu_cache: &mut GpuCache,
+ resource_cache: &mut ResourceCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ clip_data_store: &mut ClipDataStore,
+ device_pixel_scale: DevicePixelScale,
+ fb_config: &FrameBuilderConfig,
+ surfaces: &[SurfaceInfo],
+ ) -> RenderTaskId {
+ // Step through the clip sources that make up this mask. If we find
+ // any box-shadow clip sources, request that image from the render
+ // task cache. This allows the blurred box-shadow rect to be cached
+ // in the texture cache across frames.
+ // TODO(gw): Consider moving this logic outside this function, especially
+ // as we add more clip sources that depend on render tasks.
+ // TODO(gw): If this ever shows up in a profile, we could pre-calculate
+ // whether a ClipSources contains any box-shadows and skip
+ // this iteration for the majority of cases.
+ let task_size = outer_rect.size.to_i32();
+
+ // If we have a potentially tiled clip mask, clear the mask area first. Otherwise,
+ // the first (primary) clip mask will overwrite all the clip mask pixels with
+ // blending disabled to set to the initial value.
+
+ let clip_task_id = rg_builder.add().init(
+ RenderTask::new_dynamic(
+ task_size,
+ RenderTaskKind::CacheMask(CacheMaskTask {
+ actual_rect: outer_rect,
+ clip_node_range,
+ root_spatial_node_index,
+ device_pixel_scale,
+ clear_to_one: fb_config.gpu_supports_fast_clears,
+ }),
+ )
+ );
+
+ for i in 0 .. clip_node_range.count {
+ let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+ let clip_node = &mut clip_data_store[clip_instance.handle];
+ match clip_node.item.kind {
+ ClipItemKind::BoxShadow { ref mut source } => {
+ let (cache_size, cache_key) = source.cache_key
+ .as_ref()
+ .expect("bug: no cache key set")
+ .clone();
+ let blur_radius_dp = cache_key.blur_radius_dp as f32;
+ let device_pixel_scale = DevicePixelScale::new(cache_key.device_pixel_scale.to_f32_px());
+
+ // Request a cacheable render task with a blurred, minimal
+ // sized box-shadow rect.
+ source.cache_handle = Some(resource_cache.request_render_task(
+ RenderTaskCacheKey {
+ size: cache_size,
+ kind: RenderTaskCacheKeyKind::BoxShadow(cache_key),
+ },
+ gpu_cache,
+ rg_builder,
+ None,
+ false,
+ RenderTaskParent::RenderTask(clip_task_id),
+ surfaces,
+ |rg_builder| {
+ let clip_data = ClipData::rounded_rect(
+ source.minimal_shadow_rect.size,
+ &source.shadow_radius,
+ ClipMode::Clip,
+ );
+
+ // Draw the rounded rect.
+ let mask_task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ cache_size,
+ RenderTaskKind::new_rounded_rect_mask(
+ source.minimal_shadow_rect.origin,
+ clip_data,
+ device_pixel_scale,
+ fb_config,
+ ),
+ ));
+
+ // Blur it
+ RenderTask::new_blur(
+ DeviceSize::new(blur_radius_dp, blur_radius_dp),
+ mask_task_id,
+ rg_builder,
+ RenderTargetKind::Alpha,
+ None,
+ cache_size,
+ )
+ }
+ ));
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {
+ if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) {
+ // This is conservative - it's only the case that we actually need
+ // a clear here if we end up adding this mask via add_tiled_clip_mask,
+ // but for simplicity we will just clear if any of these are encountered,
+ // since they are rare.
+ let clip_task = rg_builder.get_task_mut(clip_task_id);
+ match clip_task.kind {
+ RenderTaskKind::CacheMask(ref mut task) => {
+ task.clear_to_one = true;
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ }
+ }
+ ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } |
+ ClipItemKind::RoundedRectangle { .. } |
+ ClipItemKind::Image { .. } => {}
+ }
+ }
+
+ clip_task_id
+ }
+
+ // Write (up to) 8 floats of data specific to the type
+ // of render task that is provided to the GPU shaders
+ // via a vertex texture.
+ pub fn write_task_data(
+ &self,
+ target_rect: DeviceIntRect,
+ target_index: RenderTargetIndex,
+ ) -> RenderTaskData {
+ // NOTE: The ordering and layout of these structures are
+ // required to match both the GPU structures declared
+ // in prim_shared.glsl, and also the uses in submit_batch()
+ // in renderer.rs.
+ // TODO(gw): Maybe there's a way to make this stuff a bit
+ // more type-safe. Although, it will always need
+ // to be kept in sync with the GLSL code anyway.
+
+ let data = match self {
+ RenderTaskKind::Picture(ref task) => {
+ // Note: has to match `PICTURE_TYPE_*` in shaders
+ [
+ task.device_pixel_scale.0,
+ task.content_origin.x,
+ task.content_origin.y,
+ ]
+ }
+ RenderTaskKind::CacheMask(ref task) => {
+ [
+ task.device_pixel_scale.0,
+ task.actual_rect.origin.x,
+ task.actual_rect.origin.y,
+ ]
+ }
+ RenderTaskKind::ClipRegion(ref task) => {
+ [
+ task.device_pixel_scale.0,
+ 0.0,
+ 0.0,
+ ]
+ }
+ RenderTaskKind::VerticalBlur(ref task) |
+ RenderTaskKind::HorizontalBlur(ref task) => {
+ [
+ task.blur_std_deviation,
+ task.blur_region.width as f32,
+ task.blur_region.height as f32,
+ ]
+ }
+ RenderTaskKind::Readback |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::Blit(..) => {
+ [0.0; 3]
+ }
+
+
+ RenderTaskKind::SvgFilter(ref task) => {
+ match task.info {
+ SvgFilterInfo::Opacity(opacity) => [opacity, 0.0, 0.0],
+ SvgFilterInfo::Offset(offset) => [offset.x, offset.y, 0.0],
+ _ => [0.0; 3]
+ }
+ }
+
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ [0.0; 3]
+ }
+ };
+
+ RenderTaskData {
+ data: [
+ target_rect.origin.x as f32,
+ target_rect.origin.y as f32,
+ target_rect.size.width as f32,
+ target_rect.size.height as f32,
+ target_index.0 as f32,
+ data[0],
+ data[1],
+ data[2],
+ ]
+ }
+ }
+
+ pub fn write_gpu_blocks(
+ &mut self,
+ target_rect: DeviceIntRect,
+ target_index: RenderTargetIndex,
+ gpu_cache: &mut GpuCache,
+ ) {
+ profile_scope!("write_gpu_blocks");
+
+ let (cache_handle, uv_rect_kind) = match self {
+ RenderTaskKind::HorizontalBlur(ref mut info) |
+ RenderTaskKind::VerticalBlur(ref mut info) => {
+ (&mut info.uv_rect_handle, info.uv_rect_kind)
+ }
+ RenderTaskKind::Picture(ref mut info) => {
+ (&mut info.uv_rect_handle, info.uv_rect_kind)
+ }
+ RenderTaskKind::SvgFilter(ref mut info) => {
+ (&mut info.uv_rect_handle, info.uv_rect_kind)
+ }
+ RenderTaskKind::Readback |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::LineDecoration(..) => {
+ return;
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ return;
+ }
+ };
+
+ if let Some(mut request) = gpu_cache.request(cache_handle) {
+ let p0 = target_rect.min().to_f32();
+ let p1 = target_rect.max().to_f32();
+ let image_source = ImageSource {
+ p0,
+ p1,
+ texture_layer: target_index.0 as f32,
+ user_data: [0.0; 3],
+ uv_rect_kind,
+ };
+ image_source.write_gpu_blocks(&mut request);
+ }
+
+ if let RenderTaskKind::SvgFilter(ref mut filter_task) = self {
+ match filter_task.info {
+ SvgFilterInfo::ColorMatrix(ref matrix) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ for i in 0..5 {
+ request.push([matrix[i*4], matrix[i*4+1], matrix[i*4+2], matrix[i*4+3]]);
+ }
+ }
+ }
+ SvgFilterInfo::DropShadow(color) |
+ SvgFilterInfo::Flood(color) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ request.push(color.to_array());
+ }
+ }
+ SvgFilterInfo::ComponentTransfer(ref data) => {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(request) = gpu_cache.request(handle) {
+ data.update(request);
+ }
+ }
+ SvgFilterInfo::Composite(ref operator) => {
+ if let CompositeOperator::Arithmetic(k_vals) = operator {
+ let handle = filter_task.extra_gpu_cache_handle.get_or_insert_with(GpuCacheHandle::new);
+ if let Some(mut request) = gpu_cache.request(handle) {
+ request.push(*k_vals);
+ }
+ }
+ }
+ _ => {},
+ }
+ }
+ }
+}
+
+/// In order to avoid duplicating the down-scaling and blur passes when a picture has several blurs,
+/// we use a local (primitive-level) cache of the render tasks generated for a single shadowed primitive
+/// in a single frame.
+pub type BlurTaskCache = FastHashMap<BlurTaskKey, RenderTaskId>;
+
+/// Since we only use it within a single primitive, the key only needs to contain the down-scaling level
+/// and the blur std deviation.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub enum BlurTaskKey {
+ DownScale(u32),
+ Blur { downscale_level: u32, stddev_x: u32, stddev_y: u32 },
+}
+
+impl BlurTaskKey {
+ fn downscale_and_blur(downscale_level: u32, blur_stddev: DeviceSize) -> Self {
+ // Quantise the std deviations and store it as integers to work around
+ // Eq and Hash's f32 allergy.
+ // The blur radius is rounded before RenderTask::new_blur so we don't need
+ // a lot of precision.
+ const QUANTIZATION_FACTOR: f32 = 1024.0;
+ let stddev_x = (blur_stddev.width * QUANTIZATION_FACTOR) as u32;
+ let stddev_y = (blur_stddev.height * QUANTIZATION_FACTOR) as u32;
+ BlurTaskKey::Blur { downscale_level, stddev_x, stddev_y }
+ }
+}
+
+// The majority of render tasks have 0, 1 or 2 dependencies, except for pictures that
+// typically have dozens to hundreds of dependencies. SmallVec with 2 inline elements
+// avoids many tiny heap allocations in pages with a lot of text shadows and other
+// types of render tasks.
+pub type TaskDependencies = SmallVec<[RenderTaskId;2]>;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTask {
+ pub location: RenderTaskLocation,
+ pub children: TaskDependencies,
+ pub kind: RenderTaskKind,
+
+ // TODO(gw): These fields and perhaps others can become private once the
+ // frame_graph / render_task source files are unified / cleaned up.
+ pub free_after: PassId,
+ pub render_on: PassId,
+}
+
+impl RenderTask {
+ pub fn new(
+ location: RenderTaskLocation,
+ kind: RenderTaskKind,
+ ) -> Self {
+ render_task_sanity_check(&location.size());
+
+ RenderTask {
+ location,
+ children: TaskDependencies::new(),
+ kind,
+ free_after: PassId::MAX,
+ render_on: PassId::MIN,
+ }
+ }
+
+ pub fn new_dynamic(
+ size: DeviceIntSize,
+ kind: RenderTaskKind,
+ ) -> Self {
+ RenderTask::new(
+ RenderTaskLocation::Unallocated { size },
+ kind,
+ )
+ }
+
+ #[cfg(test)]
+ pub fn new_test(
+ location: RenderTaskLocation,
+ target: RenderTargetKind,
+ ) -> Self {
+ RenderTask {
+ location,
+ children: TaskDependencies::new(),
+ kind: RenderTaskKind::Test(target),
+ free_after: PassId::MAX,
+ render_on: PassId::MIN,
+ }
+ }
+
+ pub fn new_blit(
+ size: DeviceIntSize,
+ source: BlitSource,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ ) -> RenderTaskId {
+ // If this blit uses a render task as a source,
+ // ensure it's added as a child task. This will
+ // ensure it gets allocated in the correct pass
+ // and made available as an input when this task
+ // executes.
+ let child_id = match source {
+ BlitSource::RenderTask { task_id } => Some(task_id),
+ BlitSource::Image { .. } => None,
+ };
+
+ let blit_task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ size,
+ RenderTaskKind::Blit(BlitTask {
+ source,
+ }),
+ ));
+
+ if let Some(child_id) = child_id {
+ rg_builder.add_dependency(blit_task_id, child_id);
+ }
+
+ blit_task_id
+ }
+
+ // Construct a render task to apply a blur to a primitive.
+ // The render task chain that is constructed looks like:
+ //
+ // PrimitiveCacheTask: Draw the primitives.
+ // ^
+ // |
+ // DownscalingTask(s): Each downscaling task reduces the size of render target to
+ // ^ half. Also reduce the std deviation to half until the std
+ // | deviation less than 4.0.
+ // |
+ // |
+ // VerticalBlurTask: Apply the separable vertical blur to the primitive.
+ // ^
+ // |
+ // HorizontalBlurTask: Apply the separable horizontal blur to the vertical blur.
+ // |
+ // +---- This is stored as the input task to the primitive shader.
+ //
+ pub fn new_blur(
+ blur_std_deviation: DeviceSize,
+ src_task_id: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ mut blur_cache: Option<&mut BlurTaskCache>,
+ blur_region: DeviceIntSize,
+ ) -> RenderTaskId {
+ // Adjust large std deviation value.
+ let mut adjusted_blur_std_deviation = blur_std_deviation;
+ let (blur_target_size, uv_rect_kind) = {
+ let src_task = rg_builder.get_task(src_task_id);
+ (src_task.location.size(), src_task.uv_rect_kind())
+ };
+ let mut adjusted_blur_target_size = blur_target_size;
+ let mut downscaling_src_task_id = src_task_id;
+ let mut scale_factor = 1.0;
+ let mut n_downscales = 1;
+ while adjusted_blur_std_deviation.width > MAX_BLUR_STD_DEVIATION &&
+ adjusted_blur_std_deviation.height > MAX_BLUR_STD_DEVIATION {
+ if adjusted_blur_target_size.width < MIN_DOWNSCALING_RT_SIZE ||
+ adjusted_blur_target_size.height < MIN_DOWNSCALING_RT_SIZE {
+ break;
+ }
+ adjusted_blur_std_deviation = adjusted_blur_std_deviation * 0.5;
+ scale_factor *= 2.0;
+ adjusted_blur_target_size = (blur_target_size.to_f32() / scale_factor).to_i32();
+
+ let cached_task = match blur_cache {
+ Some(ref mut cache) => cache.get(&BlurTaskKey::DownScale(n_downscales)).cloned(),
+ None => None,
+ };
+
+ downscaling_src_task_id = cached_task.unwrap_or_else(|| {
+ RenderTask::new_scaling(
+ downscaling_src_task_id,
+ rg_builder,
+ target_kind,
+ adjusted_blur_target_size,
+ )
+ });
+
+ if let Some(ref mut cache) = blur_cache {
+ cache.insert(BlurTaskKey::DownScale(n_downscales), downscaling_src_task_id);
+ }
+
+ n_downscales += 1;
+ }
+
+
+ let blur_key = BlurTaskKey::downscale_and_blur(n_downscales, adjusted_blur_std_deviation);
+
+ let cached_task = match blur_cache {
+ Some(ref mut cache) => cache.get(&blur_key).cloned(),
+ None => None,
+ };
+
+ let blur_region = blur_region / (scale_factor as i32);
+
+ let blur_task_id = cached_task.unwrap_or_else(|| {
+ let blur_task_v = rg_builder.add().init(RenderTask::new_dynamic(
+ adjusted_blur_target_size,
+ RenderTaskKind::VerticalBlur(BlurTask {
+ blur_std_deviation: adjusted_blur_std_deviation.height,
+ target_kind,
+ uv_rect_handle: GpuCacheHandle::new(),
+ blur_region,
+ uv_rect_kind,
+ }),
+ ));
+ rg_builder.add_dependency(blur_task_v, downscaling_src_task_id);
+
+ let task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ adjusted_blur_target_size,
+ RenderTaskKind::HorizontalBlur(BlurTask {
+ blur_std_deviation: adjusted_blur_std_deviation.width,
+ target_kind,
+ uv_rect_handle: GpuCacheHandle::new(),
+ blur_region,
+ uv_rect_kind,
+ }),
+ ));
+ rg_builder.add_dependency(task_id, blur_task_v);
+
+ task_id
+ });
+
+ if let Some(ref mut cache) = blur_cache {
+ cache.insert(blur_key, blur_task_id);
+ }
+
+ blur_task_id
+ }
+
+ pub fn new_scaling(
+ src_task_id: RenderTaskId,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ size: DeviceIntSize,
+ ) -> RenderTaskId {
+ Self::new_scaling_with_padding(
+ BlitSource::RenderTask { task_id: src_task_id },
+ rg_builder,
+ target_kind,
+ size,
+ DeviceIntSideOffsets::zero(),
+ )
+ }
+
+ pub fn new_scaling_with_padding(
+ source: BlitSource,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ target_kind: RenderTargetKind,
+ padded_size: DeviceIntSize,
+ padding: DeviceIntSideOffsets,
+ ) -> RenderTaskId {
+ let (uv_rect_kind, child, image) = match source {
+ BlitSource::RenderTask { task_id } => (rg_builder.get_task(task_id).uv_rect_kind(), Some(task_id), None),
+ BlitSource::Image { key } => (UvRectKind::Rect, None, Some(key)),
+ };
+
+ let task_id = rg_builder.add().init(
+ RenderTask::new_dynamic(
+ padded_size,
+ RenderTaskKind::Scaling(ScalingTask {
+ target_kind,
+ image,
+ uv_rect_kind,
+ padding,
+ }),
+ )
+ );
+
+ if let Some(child_id) = child {
+ rg_builder.add_dependency(task_id, child_id);
+ }
+
+ task_id
+ }
+
+ pub fn new_svg_filter(
+ filter_primitives: &[FilterPrimitive],
+ filter_datas: &[SFilterData],
+ rg_builder: &mut RenderTaskGraphBuilder,
+ content_size: DeviceIntSize,
+ uv_rect_kind: UvRectKind,
+ original_task_id: RenderTaskId,
+ device_pixel_scale: DevicePixelScale,
+ ) -> RenderTaskId {
+
+ if filter_primitives.is_empty() {
+ return original_task_id;
+ }
+
+ // Resolves the input to a filter primitive
+ let get_task_input = |
+ input: &FilterPrimitiveInput,
+ filter_primitives: &[FilterPrimitive],
+ rg_builder: &mut RenderTaskGraphBuilder,
+ cur_index: usize,
+ outputs: &[RenderTaskId],
+ original: RenderTaskId,
+ color_space: ColorSpace,
+ | {
+ // TODO(cbrewster): Not sure we can assume that the original input is sRGB.
+ let (mut task_id, input_color_space) = match input.to_index(cur_index) {
+ Some(index) => (outputs[index], filter_primitives[index].color_space),
+ None => (original, ColorSpace::Srgb),
+ };
+
+ match (input_color_space, color_space) {
+ (ColorSpace::Srgb, ColorSpace::LinearRgb) => {
+ task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::SrgbToLinear,
+ rg_builder,
+ );
+ },
+ (ColorSpace::LinearRgb, ColorSpace::Srgb) => {
+ task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::LinearToSrgb,
+ rg_builder,
+ );
+ },
+ _ => {},
+ }
+
+ task_id
+ };
+
+ let mut outputs = vec![];
+ let mut cur_filter_data = 0;
+ for (cur_index, primitive) in filter_primitives.iter().enumerate() {
+ let render_task_id = match primitive.kind {
+ FilterPrimitiveKind::Identity(ref identity) => {
+ // Identity does not create a task, it provides its input's render task
+ get_task_input(
+ &identity.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ )
+ }
+ FilterPrimitiveKind::Blend(ref blend) => {
+ let input_1_task_id = get_task_input(
+ &blend.input1,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+ let input_2_task_id = get_task_input(
+ &blend.input2,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_1_task_id, input_2_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Blend(blend.mode),
+ rg_builder,
+ )
+ },
+ FilterPrimitiveKind::Flood(ref flood) => {
+ RenderTask::new_svg_filter_primitive(
+ smallvec![],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Flood(flood.color),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::Blur(ref blur) => {
+ let width_std_deviation = blur.width * device_pixel_scale.0;
+ let height_std_deviation = blur.height * device_pixel_scale.0;
+ let input_task_id = get_task_input(
+ &blur.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_blur(
+ DeviceSize::new(width_std_deviation, height_std_deviation),
+ // TODO: This is a hack to ensure that a blur task's input is always
+ // in the blur's previous pass.
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Identity,
+ rg_builder,
+ ),
+ rg_builder,
+ RenderTargetKind::Color,
+ None,
+ content_size,
+ )
+ }
+ FilterPrimitiveKind::Opacity(ref opacity) => {
+ let input_task_id = get_task_input(
+ &opacity.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Opacity(opacity.opacity),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::ColorMatrix(ref color_matrix) => {
+ let input_task_id = get_task_input(
+ &color_matrix.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::ColorMatrix(Box::new(color_matrix.matrix)),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::DropShadow(ref drop_shadow) => {
+ let input_task_id = get_task_input(
+ &drop_shadow.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let blur_std_deviation = drop_shadow.shadow.blur_radius * device_pixel_scale.0;
+ let offset = drop_shadow.shadow.offset * LayoutToWorldScale::new(1.0) * device_pixel_scale;
+
+ let offset_task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Offset(offset),
+ rg_builder,
+ );
+
+ let blur_task_id = RenderTask::new_blur(
+ DeviceSize::new(blur_std_deviation, blur_std_deviation),
+ offset_task_id,
+ rg_builder,
+ RenderTargetKind::Color,
+ None,
+ content_size,
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id, blur_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::DropShadow(drop_shadow.shadow.color),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::ComponentTransfer(ref component_transfer) => {
+ let input_task_id = get_task_input(
+ &component_transfer.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let filter_data = &filter_datas[cur_filter_data];
+ cur_filter_data += 1;
+ if filter_data.is_identity() {
+ input_task_id
+ } else {
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::ComponentTransfer(filter_data.clone()),
+ rg_builder,
+ )
+ }
+ }
+ FilterPrimitiveKind::Offset(ref info) => {
+ let input_task_id = get_task_input(
+ &info.input,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ let offset = info.offset * LayoutToWorldScale::new(1.0) * device_pixel_scale;
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Offset(offset),
+ rg_builder,
+ )
+ }
+ FilterPrimitiveKind::Composite(info) => {
+ let input_1_task_id = get_task_input(
+ &info.input1,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+ let input_2_task_id = get_task_input(
+ &info.input2,
+ filter_primitives,
+ rg_builder,
+ cur_index,
+ &outputs,
+ original_task_id,
+ primitive.color_space
+ );
+
+ RenderTask::new_svg_filter_primitive(
+ smallvec![input_1_task_id, input_2_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::Composite(info.operator),
+ rg_builder,
+ )
+ }
+ };
+ outputs.push(render_task_id);
+ }
+
+ // The output of a filter is the output of the last primitive in the chain.
+ let mut render_task_id = *outputs.last().unwrap();
+
+ // Convert to sRGB if needed
+ if filter_primitives.last().unwrap().color_space == ColorSpace::LinearRgb {
+ render_task_id = RenderTask::new_svg_filter_primitive(
+ smallvec![render_task_id],
+ content_size,
+ uv_rect_kind,
+ SvgFilterInfo::LinearToSrgb,
+ rg_builder,
+ );
+ }
+
+ render_task_id
+ }
+
+ pub fn new_svg_filter_primitive(
+ tasks: TaskDependencies,
+ target_size: DeviceIntSize,
+ uv_rect_kind: UvRectKind,
+ info: SvgFilterInfo,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ ) -> RenderTaskId {
+ let task_id = rg_builder.add().init(RenderTask::new_dynamic(
+ target_size,
+ RenderTaskKind::SvgFilter(SvgFilterTask {
+ extra_gpu_cache_handle: None,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect_kind,
+ info,
+ }),
+ ));
+
+ for child_id in tasks {
+ rg_builder.add_dependency(task_id, child_id);
+ }
+
+ task_id
+ }
+
+ pub fn uv_rect_kind(&self) -> UvRectKind {
+ match self.kind {
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Readback => {
+ unreachable!("bug: unexpected render task");
+ }
+
+ RenderTaskKind::Picture(ref task) => {
+ task.uv_rect_kind
+ }
+
+ RenderTaskKind::VerticalBlur(ref task) |
+ RenderTaskKind::HorizontalBlur(ref task) => {
+ task.uv_rect_kind
+ }
+
+ RenderTaskKind::Scaling(ref task) => {
+ task.uv_rect_kind
+ }
+
+ RenderTaskKind::SvgFilter(ref task) => {
+ task.uv_rect_kind
+ }
+
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::LineDecoration(..) |
+ RenderTaskKind::Blit(..) => {
+ UvRectKind::Rect
+ }
+
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ unreachable!("Unexpected render task");
+ }
+ }
+ }
+
+ pub fn get_texture_address(&self, gpu_cache: &GpuCache) -> GpuCacheAddress {
+ match self.kind {
+ RenderTaskKind::Picture(ref info) => {
+ gpu_cache.get_address(&info.uv_rect_handle)
+ }
+ RenderTaskKind::VerticalBlur(ref info) |
+ RenderTaskKind::HorizontalBlur(ref info) => {
+ gpu_cache.get_address(&info.uv_rect_handle)
+ }
+ RenderTaskKind::SvgFilter(ref info) => {
+ gpu_cache.get_address(&info.uv_rect_handle)
+ }
+ RenderTaskKind::ClipRegion(..) |
+ RenderTaskKind::Readback |
+ RenderTaskKind::Scaling(..) |
+ RenderTaskKind::Blit(..) |
+ RenderTaskKind::Border(..) |
+ RenderTaskKind::CacheMask(..) |
+ RenderTaskKind::Gradient(..) |
+ RenderTaskKind::LineDecoration(..) => {
+ panic!("texture handle not supported for this task kind");
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ panic!("RenderTask tests aren't expected to exercise this code");
+ }
+ }
+ }
+
+ pub fn get_dynamic_size(&self) -> DeviceIntSize {
+ self.location.size()
+ }
+
+ pub fn get_target_texture(&self) -> CacheTextureId {
+ match self.location {
+ RenderTaskLocation::Dynamic { texture_id, .. } => {
+ assert_ne!(texture_id, CacheTextureId::INVALID);
+ texture_id
+ }
+ RenderTaskLocation::Unallocated { .. } |
+ RenderTaskLocation::Static { .. } => {
+ unreachable!();
+ }
+ }
+ }
+
+ pub fn get_target_rect(&self) -> (DeviceIntRect, RenderTargetIndex) {
+ match self.location {
+ // Previously, we only added render tasks after the entire
+ // primitive chain was determined visible. This meant that
+ // we could assert any render task in the list was also
+ // allocated (assigned to passes). Now, we add render
+ // tasks earlier, and the picture they belong to may be
+ // culled out later, so we can't assert that the task
+ // has been allocated.
+ // Render tasks that are created but not assigned to
+ // passes consume a row in the render task texture, but
+ // don't allocate any space in render targets nor
+ // draw any pixels.
+ // TODO(gw): Consider some kind of tag or other method
+ // to mark a task as unused explicitly. This
+ // would allow us to restore this debug check.
+ RenderTaskLocation::Dynamic { rect, .. } => {
+ (rect, RenderTargetIndex(0))
+ }
+ RenderTaskLocation::Unallocated { .. } => {
+ panic!("bug: get_target_rect called before allocating");
+ }
+ RenderTaskLocation::Static { rect, surface: StaticRenderTaskSurface::PictureCache { ref surface, .. } } => {
+ let layer = match surface {
+ ResolvedSurfaceTexture::TextureCache { layer, .. } => *layer,
+ ResolvedSurfaceTexture::Native { .. } => 0,
+ };
+
+ (
+ rect,
+ RenderTargetIndex(layer as usize),
+ )
+ }
+ RenderTaskLocation::Static { rect, surface: StaticRenderTaskSurface::TextureCache { layer, .. } } => {
+ (rect, RenderTargetIndex(layer as usize))
+ }
+ }
+ }
+
+ pub fn target_kind(&self) -> RenderTargetKind {
+ self.kind.target_kind()
+ }
+
+ #[cfg(feature = "debugger")]
+ pub fn print_with<T: PrintTreePrinter>(&self, pt: &mut T, tree: &RenderTaskGraph) -> bool {
+ match self.kind {
+ RenderTaskKind::Picture(ref task) => {
+ pt.new_level(format!("Picture of {:?}", task.pic_index));
+ }
+ RenderTaskKind::CacheMask(ref task) => {
+ pt.new_level(format!("CacheMask with {} clips", task.clip_node_range.count));
+ pt.add_item(format!("rect: {:?}", task.actual_rect));
+ }
+ RenderTaskKind::LineDecoration(..) => {
+ pt.new_level("LineDecoration".to_owned());
+ }
+ RenderTaskKind::ClipRegion(..) => {
+ pt.new_level("ClipRegion".to_owned());
+ }
+ RenderTaskKind::VerticalBlur(ref task) => {
+ pt.new_level("VerticalBlur".to_owned());
+ task.print_with(pt);
+ }
+ RenderTaskKind::HorizontalBlur(ref task) => {
+ pt.new_level("HorizontalBlur".to_owned());
+ task.print_with(pt);
+ }
+ RenderTaskKind::Readback => {
+ pt.new_level("Readback".to_owned());
+ }
+ RenderTaskKind::Scaling(ref kind) => {
+ pt.new_level("Scaling".to_owned());
+ pt.add_item(format!("kind: {:?}", kind));
+ }
+ RenderTaskKind::Border(..) => {
+ pt.new_level("Border".to_owned());
+ }
+ RenderTaskKind::Blit(ref task) => {
+ pt.new_level("Blit".to_owned());
+ pt.add_item(format!("source: {:?}", task.source));
+ }
+ RenderTaskKind::Gradient(..) => {
+ pt.new_level("Gradient".to_owned());
+ }
+ RenderTaskKind::SvgFilter(ref task) => {
+ pt.new_level("SvgFilter".to_owned());
+ pt.add_item(format!("primitive: {:?}", task.info));
+ }
+ #[cfg(test)]
+ RenderTaskKind::Test(..) => {
+ pt.new_level("Test".to_owned());
+ }
+ }
+
+ pt.add_item(format!("dimensions: {:?}", self.location.size()));
+
+ for &child_id in &self.children {
+ if tree[child_id].print_with(pt, tree) {
+ pt.add_item(format!("self: {:?}", child_id))
+ }
+ }
+
+ pt.end_level();
+ true
+ }
+}
diff --git a/gfx/wr/webrender/src/render_task_cache.rs b/gfx/wr/webrender/src/render_task_cache.rs
new file mode 100644
index 0000000000..ddd61dec68
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task_cache.rs
@@ -0,0 +1,336 @@
+/* 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 api::{ImageDescriptor, ImageDescriptorFlags, DirtyRect};
+use api::units::*;
+use crate::border::BorderSegmentCacheKey;
+use crate::box_shadow::{BoxShadowCacheKey};
+use crate::device::TextureFilter;
+use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use crate::gpu_cache::GpuCache;
+use crate::internal_types::FastHashMap;
+use crate::picture::{SurfaceIndex, SurfaceInfo};
+use crate::prim_store::image::ImageCacheKey;
+use crate::prim_store::gradient::GradientCacheKey;
+use crate::prim_store::line_dec::LineDecorationCacheKey;
+use crate::resource_cache::CacheItem;
+use std::{mem, usize, f32, i32};
+use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+use crate::render_target::RenderTargetKind;
+use crate::render_task::{RenderTask, StaticRenderTaskSurface, RenderTaskLocation};
+use crate::render_task_graph::{RenderTaskGraphBuilder, RenderTaskId};
+use crate::frame_builder::add_child_render_task;
+use euclid::Scale;
+
+const MAX_CACHE_TASK_SIZE: f32 = 4096.0;
+
+/// Describes a parent dependency for a render task. Render tasks
+/// may depend on a surface (e.g. when a surface uses a cached border)
+/// or an arbitrary render task (e.g. when a clip mask uses a blurred
+/// box-shadow input).
+pub enum RenderTaskParent {
+ /// Parent is a surface
+ Surface(SurfaceIndex),
+ /// Parent is a render task
+ RenderTask(RenderTaskId),
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum RenderTaskCacheKeyKind {
+ BoxShadow(BoxShadowCacheKey),
+ Image(ImageCacheKey),
+ BorderSegment(BorderSegmentCacheKey),
+ LineDecoration(LineDecorationCacheKey),
+ Gradient(GradientCacheKey),
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCacheKey {
+ pub size: DeviceIntSize,
+ pub kind: RenderTaskCacheKeyKind,
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCacheEntry {
+ user_data: Option<[f32; 3]>,
+ is_opaque: bool,
+ pub handle: TextureCacheHandle,
+ /// If a render task was generated for this cache entry on _this_ frame,
+ /// we need to track the task id here. This allows us to hook it up as
+ /// a dependency of any parent tasks that make a reqiest from the render
+ /// task cache.
+ pub render_task_id: Option<RenderTaskId>,
+}
+
+#[derive(Debug, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum RenderTaskCacheMarker {}
+
+// A cache of render tasks that are stored in the texture
+// cache for usage across frames.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskCache {
+ map: FastHashMap<RenderTaskCacheKey, FreeListHandle<RenderTaskCacheMarker>>,
+ cache_entries: FreeList<RenderTaskCacheEntry, RenderTaskCacheMarker>,
+}
+
+pub type RenderTaskCacheEntryHandle = WeakFreeListHandle<RenderTaskCacheMarker>;
+
+impl RenderTaskCache {
+ pub fn new() -> Self {
+ RenderTaskCache {
+ map: FastHashMap::default(),
+ cache_entries: FreeList::new(),
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.map.clear();
+ self.cache_entries.clear();
+ }
+
+ pub fn begin_frame(
+ &mut self,
+ texture_cache: &mut TextureCache,
+ ) {
+ profile_scope!("begin_frame");
+ // Drop any items from the cache that have been
+ // evicted from the texture cache.
+ //
+ // This isn't actually necessary for the texture
+ // cache to be able to evict old render tasks.
+ // It will evict render tasks as required, since
+ // the access time in the texture cache entry will
+ // be stale if this task hasn't been requested
+ // for a while.
+ //
+ // Nonetheless, we should remove stale entries
+ // from here so that this hash map doesn't
+ // grow indefinitely!
+ let cache_entries = &mut self.cache_entries;
+
+ self.map.retain(|_, handle| {
+ let retain = texture_cache.is_allocated(
+ &cache_entries.get(handle).handle,
+ );
+ if !retain {
+ let handle = mem::replace(handle, FreeListHandle::invalid());
+ cache_entries.free(handle);
+ }
+ retain
+ });
+
+ // Clear out the render task ID of any remaining cache entries that were drawn
+ // on the previous frame, so we don't accidentally hook up stale dependencies
+ // when building the frame graph.
+ for (_, handle) in &self.map {
+ let entry = self.cache_entries.get_mut(handle);
+ entry.render_task_id = None;
+ }
+ }
+
+ fn alloc_render_task(
+ render_task: &mut RenderTask,
+ entry: &mut RenderTaskCacheEntry,
+ gpu_cache: &mut GpuCache,
+ texture_cache: &mut TextureCache,
+ ) {
+ // Find out what size to alloc in the texture cache.
+ let size = render_task.location.size();
+ let target_kind = render_task.target_kind();
+
+ // Select the right texture page to allocate from.
+ let image_format = match target_kind {
+ RenderTargetKind::Color => texture_cache.shared_color_expected_format(),
+ RenderTargetKind::Alpha => texture_cache.shared_alpha_expected_format(),
+ };
+
+ let flags = if entry.is_opaque {
+ ImageDescriptorFlags::IS_OPAQUE
+ } else {
+ ImageDescriptorFlags::empty()
+ };
+
+ let descriptor = ImageDescriptor::new(
+ size.width,
+ size.height,
+ image_format,
+ flags,
+ );
+
+ // Allocate space in the texture cache, but don't supply
+ // and CPU-side data to be uploaded.
+ texture_cache.update(
+ &mut entry.handle,
+ descriptor,
+ TextureFilter::Linear,
+ None,
+ entry.user_data.unwrap_or([0.0; 3]),
+ DirtyRect::All,
+ gpu_cache,
+ None,
+ render_task.uv_rect_kind(),
+ Eviction::Auto,
+ TargetShader::Default,
+ );
+
+ // Get the allocation details in the texture cache, and store
+ // this in the render task. The renderer will draw this
+ // task into the appropriate layer and rect of the texture
+ // cache on this frame.
+ let (texture_id, texture_layer, uv_rect, _, _, _) =
+ texture_cache.get_cache_location(&entry.handle);
+
+ let surface = StaticRenderTaskSurface::TextureCache {
+ texture: texture_id,
+ layer: texture_layer,
+ target_kind,
+ };
+
+ render_task.location = RenderTaskLocation::Static {
+ surface,
+ rect: uv_rect.to_i32(),
+ };
+ }
+
+ pub fn request_render_task<F>(
+ &mut self,
+ key: RenderTaskCacheKey,
+ texture_cache: &mut TextureCache,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ user_data: Option<[f32; 3]>,
+ is_opaque: bool,
+ parent: RenderTaskParent,
+ surfaces: &[SurfaceInfo],
+ f: F,
+ ) -> Result<RenderTaskCacheEntryHandle, ()>
+ where
+ F: FnOnce(&mut RenderTaskGraphBuilder) -> Result<RenderTaskId, ()>,
+ {
+ // Get the texture cache handle for this cache key,
+ // or create one.
+ let cache_entries = &mut self.cache_entries;
+ let entry_handle = self.map.entry(key).or_insert_with(|| {
+ let entry = RenderTaskCacheEntry {
+ handle: TextureCacheHandle::invalid(),
+ user_data,
+ is_opaque,
+ render_task_id: None,
+ };
+ cache_entries.insert(entry)
+ });
+ let cache_entry = cache_entries.get_mut(entry_handle);
+
+ // Check if this texture cache handle is valid.
+ if texture_cache.request(&cache_entry.handle, gpu_cache) {
+ // Invoke user closure to get render task chain
+ // to draw this into the texture cache.
+ let render_task_id = f(rg_builder)?;
+
+ cache_entry.user_data = user_data;
+ cache_entry.is_opaque = is_opaque;
+ cache_entry.render_task_id = Some(render_task_id);
+
+ let render_task = rg_builder.get_task_mut(render_task_id);
+
+ RenderTaskCache::alloc_render_task(
+ render_task,
+ cache_entry,
+ gpu_cache,
+ texture_cache,
+ );
+ }
+
+ // If this render task cache is being drawn this frame, ensure we hook up the
+ // render task for it as a dependency of any render task that uses this as
+ // an input source.
+ if let Some(render_task_id) = cache_entry.render_task_id {
+ match parent {
+ RenderTaskParent::Surface(surface_index) => {
+ // If parent is a surface, use helper fn to add this dependency,
+ // which correctly takes account of the render task configuration
+ // of the surface.
+ add_child_render_task(
+ surface_index,
+ render_task_id,
+ surfaces,
+ rg_builder
+ );
+ }
+ RenderTaskParent::RenderTask(parent_render_task_id) => {
+ // For render tasks, just add it as a direct dependency on the
+ // task graph builder.
+ rg_builder.add_dependency(
+ parent_render_task_id,
+ render_task_id,
+ );
+ }
+ }
+ }
+
+ Ok(entry_handle.weak())
+ }
+
+ pub fn get_cache_entry(
+ &self,
+ handle: &RenderTaskCacheEntryHandle,
+ ) -> &RenderTaskCacheEntry {
+ self.cache_entries
+ .get_opt(handle)
+ .expect("bug: invalid render task cache handle")
+ }
+
+ #[allow(dead_code)]
+ pub fn get_cache_item_for_render_task(&self,
+ texture_cache: &TextureCache,
+ key: &RenderTaskCacheKey)
+ -> CacheItem {
+ // Get the texture cache handle for this cache key.
+ let handle = self.map.get(key).unwrap();
+ let cache_entry = self.cache_entries.get(handle);
+ texture_cache.get(&cache_entry.handle)
+ }
+
+ #[allow(dead_code)]
+ pub fn get_allocated_size_for_render_task(&self,
+ texture_cache: &TextureCache,
+ key: &RenderTaskCacheKey)
+ -> Option<usize> {
+ let handle = self.map.get(key).unwrap();
+ let cache_entry = self.cache_entries.get(handle);
+ texture_cache.get_allocated_size(&cache_entry.handle)
+ }
+}
+
+// TODO(gw): Rounding the content rect here to device pixels is not
+// technically correct. Ideally we should ceil() here, and ensure that
+// the extra part pixel in the case of fractional sizes is correctly
+// handled. For now, just use rounding which passes the existing
+// Gecko tests.
+// Note: zero-square tasks are prohibited in WR task graph, so
+// we ensure each dimension to be at least the length of 1 after rounding.
+pub fn to_cache_size(size: LayoutSize, device_pixel_scale: &mut Scale<f32, LayoutPixel, DevicePixel>) -> DeviceIntSize {
+ let mut device_size = (size * *device_pixel_scale).round();
+
+ if device_size.width > MAX_CACHE_TASK_SIZE || device_size.height > MAX_CACHE_TASK_SIZE {
+ let scale = MAX_CACHE_TASK_SIZE / f32::max(device_size.width, device_size.height);
+ *device_pixel_scale = *device_pixel_scale * Scale::new(scale);
+ device_size = (size * *device_pixel_scale).round();
+ }
+
+ DeviceIntSize::new(
+ 1.max(device_size.width as i32),
+ 1.max(device_size.height as i32),
+ )
+}
diff --git a/gfx/wr/webrender/src/render_task_graph.rs b/gfx/wr/webrender/src/render_task_graph.rs
new file mode 100644
index 0000000000..ece1f2e39a
--- /dev/null
+++ b/gfx/wr/webrender/src/render_task_graph.rs
@@ -0,0 +1,270 @@
+//! This module contains the render task graph.
+//!
+//! Code associated with creating specific render tasks is in the render_task
+//! module.
+
+use api::ImageFormat;
+use crate::frame_graph::{FrameGraph, FrameGraphBuilder, Pass};
+use crate::internal_types::{CacheTextureId, FastHashMap};
+use crate::render_target::{RenderTargetList, ColorRenderTarget};
+use crate::render_target::{PictureCacheTarget, TextureCacheRenderTarget, AlphaRenderTarget};
+use crate::render_task::RenderTask;
+use crate::util::Allocation;
+use std::{usize, f32, u32};
+
+// TODO(gw): To reduce the size of the initial inegration patch, typedef the
+// old task graph and builder to the new frame graph / builder, which
+// are API compatible. In future, use the new types directly and adjust
+// the API.
+pub type RenderTaskGraphBuilder = FrameGraphBuilder;
+pub type RenderTaskGraph = FrameGraph;
+
+/// Allows initializing a render task directly into the render task buffer.
+///
+/// See utils::VecHelpers. RenderTask is fairly large so avoiding the move when
+/// pushing into the vector can save a lot of exensive memcpys on pages with many
+/// render tasks.
+pub struct RenderTaskAllocation<'a> {
+ pub alloc: Allocation<'a, RenderTask>,
+}
+
+impl<'l> RenderTaskAllocation<'l> {
+ #[inline(always)]
+ pub fn init(self, value: RenderTask) -> RenderTaskId {
+ RenderTaskId {
+ index: self.alloc.init(value) as u32,
+ }
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderTaskId {
+ pub index: u32,
+}
+
+impl RenderTaskId {
+ pub const INVALID: RenderTaskId = RenderTaskId {
+ index: u32::MAX,
+ };
+}
+
+/// A render pass represents a set of rendering operations that don't depend on one
+/// another.
+///
+/// A render pass can have several render targets if there wasn't enough space in one
+/// target to do all of the rendering for that pass. See `RenderTargetList`.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct RenderPass {
+ /// The subpasses that describe targets being rendered to in this pass
+ pub alpha: RenderTargetList<AlphaRenderTarget>,
+ pub color: RenderTargetList<ColorRenderTarget>,
+ pub texture_cache: FastHashMap<(CacheTextureId, usize), TextureCacheRenderTarget>,
+ pub picture_cache: Vec<PictureCacheTarget>,
+ /// The set of tasks to be performed in this pass, as indices into the
+ /// `RenderTaskGraph`.
+ pub tasks: Vec<RenderTaskId>,
+ pub textures_to_invalidate: Vec<CacheTextureId>,
+}
+
+impl RenderPass {
+ /// Creates an intermediate off-screen pass.
+ pub fn new(src: &Pass) -> Self {
+ RenderPass {
+ color: RenderTargetList::new(
+ ImageFormat::RGBA8,
+ ),
+ alpha: RenderTargetList::new(
+ ImageFormat::R8,
+ ),
+ texture_cache: FastHashMap::default(),
+ picture_cache: Vec::new(),
+ tasks: vec![],
+ textures_to_invalidate: src.textures_to_invalidate.clone(),
+ }
+ }
+}
+
+// Dump an SVG visualization of the render graph for debugging purposes
+#[allow(dead_code)]
+pub fn dump_render_tasks_as_svg(
+ render_tasks: &RenderTaskGraph,
+ passes: &[RenderPass],
+ output: &mut dyn std::io::Write,
+) -> std::io::Result<()> {
+ use svg_fmt::*;
+
+ let node_width = 80.0;
+ let node_height = 30.0;
+ let vertical_spacing = 8.0;
+ let horizontal_spacing = 20.0;
+ let margin = 10.0;
+ let text_size = 10.0;
+
+ let mut pass_rects = Vec::new();
+ let mut nodes = vec![None; render_tasks.tasks.len()];
+
+ let mut x = margin;
+ let mut max_y: f32 = 0.0;
+
+ #[derive(Clone)]
+ struct Node {
+ rect: Rectangle,
+ label: Text,
+ size: Text,
+ }
+
+ for pass in passes {
+ let mut layout = VerticalLayout::new(x, margin, node_width);
+
+ for task_id in &pass.tasks {
+ let task_index = task_id.index as usize;
+ let task = &render_tasks.tasks[task_index];
+
+ let rect = layout.push_rectangle(node_height);
+
+ let tx = rect.x + rect.w / 2.0;
+ let ty = rect.y + 10.0;
+
+ let label = text(tx, ty, format!("{}", task.kind.as_str()));
+ let size = text(tx, ty + 12.0, format!("{:?}", task.location.size()));
+
+ nodes[task_index] = Some(Node { rect, label, size });
+
+ layout.advance(vertical_spacing);
+ }
+
+ pass_rects.push(layout.total_rectangle());
+
+ x += node_width + horizontal_spacing;
+ max_y = max_y.max(layout.y + margin);
+ }
+
+ let mut links = Vec::new();
+ for node_index in 0..nodes.len() {
+ if nodes[node_index].is_none() {
+ continue;
+ }
+
+ let task = &render_tasks.tasks[node_index];
+ for dep in &task.children {
+ let dep_index = dep.index as usize;
+
+ if let (&Some(ref node), &Some(ref dep_node)) = (&nodes[node_index], &nodes[dep_index]) {
+ links.push((
+ dep_node.rect.x + dep_node.rect.w,
+ dep_node.rect.y + dep_node.rect.h / 2.0,
+ node.rect.x,
+ node.rect.y + node.rect.h / 2.0,
+ ));
+ }
+ }
+ }
+
+ let svg_w = x + margin;
+ let svg_h = max_y + margin;
+ writeln!(output, "{}", BeginSvg { w: svg_w, h: svg_h })?;
+
+ // Background.
+ writeln!(output,
+ " {}",
+ rectangle(0.0, 0.0, svg_w, svg_h)
+ .inflate(1.0, 1.0)
+ .fill(rgb(50, 50, 50))
+ )?;
+
+ // Passes.
+ for rect in pass_rects {
+ writeln!(output,
+ " {}",
+ rect.inflate(3.0, 3.0)
+ .border_radius(4.0)
+ .opacity(0.4)
+ .fill(black())
+ )?;
+ }
+
+ // Links.
+ for (x1, y1, x2, y2) in links {
+ dump_task_dependency_link(output, x1, y1, x2, y2);
+ }
+
+ // Tasks.
+ for node in &nodes {
+ if let Some(node) = node {
+ writeln!(output,
+ " {}",
+ node.rect
+ .clone()
+ .fill(black())
+ .border_radius(3.0)
+ .opacity(0.5)
+ .offset(0.0, 2.0)
+ )?;
+ writeln!(output,
+ " {}",
+ node.rect
+ .clone()
+ .fill(rgb(200, 200, 200))
+ .border_radius(3.0)
+ .opacity(0.8)
+ )?;
+
+ writeln!(output,
+ " {}",
+ node.label
+ .clone()
+ .size(text_size)
+ .align(Align::Center)
+ .color(rgb(50, 50, 50))
+ )?;
+ writeln!(output,
+ " {}",
+ node.size
+ .clone()
+ .size(text_size * 0.7)
+ .align(Align::Center)
+ .color(rgb(50, 50, 50))
+ )?;
+ }
+ }
+
+ writeln!(output, "{}", EndSvg)
+}
+
+#[allow(dead_code)]
+fn dump_task_dependency_link(
+ output: &mut dyn std::io::Write,
+ x1: f32, y1: f32,
+ x2: f32, y2: f32,
+) {
+ use svg_fmt::*;
+
+ // If the link is a straight horizontal line and spans over multiple passes, it
+ // is likely to go straight though unrelated nodes in a way that makes it look like
+ // they are connected, so we bend the line upward a bit to avoid that.
+ let simple_path = (y1 - y2).abs() > 1.0 || (x2 - x1) < 45.0;
+
+ let mid_x = (x1 + x2) / 2.0;
+ if simple_path {
+ write!(output, " {}",
+ path().move_to(x1, y1)
+ .cubic_bezier_to(mid_x, y1, mid_x, y2, x2, y2)
+ .fill(Fill::None)
+ .stroke(Stroke::Color(rgb(100, 100, 100), 3.0))
+ ).unwrap();
+ } else {
+ let ctrl1_x = (mid_x + x1) / 2.0;
+ let ctrl2_x = (mid_x + x2) / 2.0;
+ let ctrl_y = y1 - 25.0;
+ write!(output, " {}",
+ path().move_to(x1, y1)
+ .cubic_bezier_to(ctrl1_x, y1, ctrl1_x, ctrl_y, mid_x, ctrl_y)
+ .cubic_bezier_to(ctrl2_x, ctrl_y, ctrl2_x, y2, x2, y2)
+ .fill(Fill::None)
+ .stroke(Stroke::Color(rgb(100, 100, 100), 3.0))
+ ).unwrap();
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/debug.rs b/gfx/wr/webrender/src/renderer/debug.rs
new file mode 100644
index 0000000000..c6070ce9c4
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/debug.rs
@@ -0,0 +1,416 @@
+/* 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 api::{ColorU, ImageFormat, ImageBufferKind};
+use api::units::*;
+use crate::debug_font_data;
+use crate::device::{Device, Program, Texture, TextureSlot, VertexDescriptor, ShaderError, VAO};
+use crate::device::{TextureFilter, VertexAttribute, VertexAttributeKind, VertexUsageHint};
+use euclid::{Point2D, Rect, Size2D, Transform3D, default};
+use crate::internal_types::Swizzle;
+use std::f32;
+
+#[derive(Debug, Copy, Clone)]
+enum DebugSampler {
+ Font,
+}
+
+impl Into<TextureSlot> for DebugSampler {
+ fn into(self) -> TextureSlot {
+ match self {
+ DebugSampler::Font => TextureSlot(0),
+ }
+ }
+}
+
+const DESC_FONT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::U8Norm,
+ },
+ VertexAttribute {
+ name: "aColorTexCoord",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ instance_attributes: &[],
+};
+
+const DESC_COLOR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::U8Norm,
+ },
+ ],
+ instance_attributes: &[],
+};
+
+#[repr(C)]
+pub struct DebugFontVertex {
+ pub x: f32,
+ pub y: f32,
+ pub color: ColorU,
+ pub u: f32,
+ pub v: f32,
+}
+
+impl DebugFontVertex {
+ pub fn new(x: f32, y: f32, u: f32, v: f32, color: ColorU) -> DebugFontVertex {
+ DebugFontVertex { x, y, color, u, v }
+ }
+}
+
+#[repr(C)]
+pub struct DebugColorVertex {
+ pub x: f32,
+ pub y: f32,
+ pub color: ColorU,
+}
+
+impl DebugColorVertex {
+ pub fn new(x: f32, y: f32, color: ColorU) -> DebugColorVertex {
+ DebugColorVertex { x, y, color }
+ }
+}
+
+pub struct DebugRenderer {
+ font_vertices: Vec<DebugFontVertex>,
+ font_indices: Vec<u32>,
+ font_program: Program,
+ font_vao: VAO,
+ font_texture: Texture,
+
+ tri_vertices: Vec<DebugColorVertex>,
+ tri_indices: Vec<u32>,
+ tri_vao: VAO,
+ line_vertices: Vec<DebugColorVertex>,
+ line_vao: VAO,
+ color_program: Program,
+}
+
+impl DebugRenderer {
+ pub fn new(device: &mut Device) -> Result<Self, ShaderError> {
+ let font_program = device.create_program_linked(
+ "debug_font",
+ &[],
+ &DESC_FONT,
+ )?;
+ device.bind_program(&font_program);
+ device.bind_shader_samplers(&font_program, &[("sColor0", DebugSampler::Font)]);
+
+ let color_program = device.create_program_linked(
+ "debug_color",
+ &[],
+ &DESC_COLOR,
+ )?;
+
+ let font_vao = device.create_vao(&DESC_FONT, 1);
+ let line_vao = device.create_vao(&DESC_COLOR, 1);
+ let tri_vao = device.create_vao(&DESC_COLOR, 1);
+
+ let font_texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::R8,
+ debug_font_data::BMP_WIDTH,
+ debug_font_data::BMP_HEIGHT,
+ TextureFilter::Linear,
+ None,
+ 1,
+ );
+ device.upload_texture_immediate(
+ &font_texture,
+ &debug_font_data::FONT_BITMAP
+ );
+
+ Ok(DebugRenderer {
+ font_vertices: Vec::new(),
+ font_indices: Vec::new(),
+ line_vertices: Vec::new(),
+ tri_vao,
+ tri_vertices: Vec::new(),
+ tri_indices: Vec::new(),
+ font_program,
+ color_program,
+ font_vao,
+ line_vao,
+ font_texture,
+ })
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ device.delete_texture(self.font_texture);
+ device.delete_program(self.font_program);
+ device.delete_program(self.color_program);
+ device.delete_vao(self.tri_vao);
+ device.delete_vao(self.line_vao);
+ device.delete_vao(self.font_vao);
+ }
+
+ pub fn line_height(&self) -> f32 {
+ debug_font_data::FONT_SIZE as f32 * 1.1
+ }
+
+ /// Draws a line of text at the provided starting coordinates.
+ ///
+ /// If |bounds| is specified, glyphs outside the bounds are discarded.
+ ///
+ /// Y-coordinates is relative to screen top, along with everything else in
+ /// this file.
+ pub fn add_text(
+ &mut self,
+ x: f32,
+ y: f32,
+ text: &str,
+ color: ColorU,
+ bounds: Option<DeviceRect>,
+ ) -> default::Rect<f32> {
+ let mut x_start = x;
+ let ipw = 1.0 / debug_font_data::BMP_WIDTH as f32;
+ let iph = 1.0 / debug_font_data::BMP_HEIGHT as f32;
+
+ let mut min_x = f32::MAX;
+ let mut max_x = -f32::MAX;
+ let mut min_y = f32::MAX;
+ let mut max_y = -f32::MAX;
+
+ for c in text.chars() {
+ let c = c as usize - debug_font_data::FIRST_GLYPH_INDEX as usize;
+ if c < debug_font_data::GLYPHS.len() {
+ let glyph = &debug_font_data::GLYPHS[c];
+
+ let x0 = (x_start + glyph.xo + 0.5).floor();
+ let y0 = (y + glyph.yo + 0.5).floor();
+
+ let x1 = x0 + glyph.x1 as f32 - glyph.x0 as f32;
+ let y1 = y0 + glyph.y1 as f32 - glyph.y0 as f32;
+
+ // If either corner of the glyph will end up out of bounds, drop it.
+ if let Some(b) = bounds {
+ let rect = DeviceRect::new(
+ DevicePoint::new(x0, y0),
+ DeviceSize::new(x1 - x0, y1 - y0),
+ );
+ if !b.contains_rect(&rect) {
+ continue;
+ }
+ }
+
+ let s0 = glyph.x0 as f32 * ipw;
+ let t0 = glyph.y0 as f32 * iph;
+ let s1 = glyph.x1 as f32 * ipw;
+ let t1 = glyph.y1 as f32 * iph;
+
+ x_start += glyph.xa;
+
+ let vertex_count = self.font_vertices.len() as u32;
+
+ self.font_vertices
+ .push(DebugFontVertex::new(x0, y0, s0, t0, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x1, y0, s1, t0, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x0, y1, s0, t1, color));
+ self.font_vertices
+ .push(DebugFontVertex::new(x1, y1, s1, t1, color));
+
+ self.font_indices.push(vertex_count + 0);
+ self.font_indices.push(vertex_count + 1);
+ self.font_indices.push(vertex_count + 2);
+ self.font_indices.push(vertex_count + 2);
+ self.font_indices.push(vertex_count + 1);
+ self.font_indices.push(vertex_count + 3);
+
+ min_x = min_x.min(x0);
+ max_x = max_x.max(x1);
+ min_y = min_y.min(y0);
+ max_y = max_y.max(y1);
+ }
+ }
+
+ Rect::new(
+ Point2D::new(min_x, min_y),
+ Size2D::new(max_x - min_x, max_y - min_y),
+ )
+ }
+
+ pub fn add_quad(
+ &mut self,
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ color_top: ColorU,
+ color_bottom: ColorU,
+ ) {
+ let vertex_count = self.tri_vertices.len() as u32;
+
+ self.tri_vertices
+ .push(DebugColorVertex::new(x0, y0, color_top));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x1, y0, color_top));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x0, y1, color_bottom));
+ self.tri_vertices
+ .push(DebugColorVertex::new(x1, y1, color_bottom));
+
+ self.tri_indices.push(vertex_count + 0);
+ self.tri_indices.push(vertex_count + 1);
+ self.tri_indices.push(vertex_count + 2);
+ self.tri_indices.push(vertex_count + 2);
+ self.tri_indices.push(vertex_count + 1);
+ self.tri_indices.push(vertex_count + 3);
+ }
+
+ #[allow(dead_code)]
+ pub fn add_line(&mut self, x0: i32, y0: i32, color0: ColorU, x1: i32, y1: i32, color1: ColorU) {
+ self.line_vertices
+ .push(DebugColorVertex::new(x0 as f32, y0 as f32, color0));
+ self.line_vertices
+ .push(DebugColorVertex::new(x1 as f32, y1 as f32, color1));
+ }
+
+
+ pub fn add_rect(&mut self, rect: &DeviceIntRect, color: ColorU) {
+ let p0 = rect.origin;
+ let p1 = p0 + rect.size;
+ self.add_line(p0.x, p0.y, color, p1.x, p0.y, color);
+ self.add_line(p1.x, p0.y, color, p1.x, p1.y, color);
+ self.add_line(p1.x, p1.y, color, p0.x, p1.y, color);
+ self.add_line(p0.x, p1.y, color, p0.x, p0.y, color);
+ }
+
+ pub fn render(
+ &mut self,
+ device: &mut Device,
+ viewport_size: Option<DeviceIntSize>,
+ scale: f32,
+ surface_origin_is_top_left: bool,
+ ) {
+ if let Some(viewport_size) = viewport_size {
+ device.disable_depth();
+ device.set_blend(true);
+ device.set_blend_mode_premultiplied_alpha();
+
+ let (bottom, top) = if surface_origin_is_top_left {
+ (0.0, viewport_size.height as f32 * scale)
+ } else {
+ (viewport_size.height as f32 * scale, 0.0)
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ viewport_size.width as f32 * scale,
+ bottom,
+ top,
+ device.ortho_near_plane(),
+ device.ortho_far_plane(),
+ );
+
+ // Triangles
+ if !self.tri_vertices.is_empty() {
+ device.bind_program(&self.color_program);
+ device.set_uniforms(&self.color_program, &projection);
+ device.bind_vao(&self.tri_vao);
+ device.update_vao_indices(&self.tri_vao, &self.tri_indices, VertexUsageHint::Dynamic);
+ device.update_vao_main_vertices(
+ &self.tri_vao,
+ &self.tri_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_triangles_u32(0, self.tri_indices.len() as i32);
+ }
+
+ // Lines
+ if !self.line_vertices.is_empty() {
+ device.bind_program(&self.color_program);
+ device.set_uniforms(&self.color_program, &projection);
+ device.bind_vao(&self.line_vao);
+ device.update_vao_main_vertices(
+ &self.line_vao,
+ &self.line_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_nonindexed_lines(0, self.line_vertices.len() as i32);
+ }
+
+ // Glyph
+ if !self.font_indices.is_empty() {
+ device.bind_program(&self.font_program);
+ device.set_uniforms(&self.font_program, &projection);
+ device.bind_texture(DebugSampler::Font, &self.font_texture, Swizzle::default());
+ device.bind_vao(&self.font_vao);
+ device.update_vao_indices(&self.font_vao, &self.font_indices, VertexUsageHint::Dynamic);
+ device.update_vao_main_vertices(
+ &self.font_vao,
+ &self.font_vertices,
+ VertexUsageHint::Dynamic,
+ );
+ device.draw_triangles_u32(0, self.font_indices.len() as i32);
+ }
+ }
+
+ self.font_indices.clear();
+ self.font_vertices.clear();
+ self.line_vertices.clear();
+ self.tri_vertices.clear();
+ self.tri_indices.clear();
+ }
+}
+
+pub struct LazyInitializedDebugRenderer {
+ debug_renderer: Option<DebugRenderer>,
+ failed: bool,
+}
+
+impl LazyInitializedDebugRenderer {
+ pub fn new() -> Self {
+ Self {
+ debug_renderer: None,
+ failed: false,
+ }
+ }
+
+ pub fn get_mut<'a>(&'a mut self, device: &mut Device) -> Option<&'a mut DebugRenderer> {
+ if self.failed {
+ return None;
+ }
+ if self.debug_renderer.is_none() {
+ match DebugRenderer::new(device) {
+ Ok(renderer) => { self.debug_renderer = Some(renderer); }
+ Err(_) => {
+ // The shader compilation code already logs errors.
+ self.failed = true;
+ }
+ }
+ }
+
+ self.debug_renderer.as_mut()
+ }
+
+ /// Returns mut ref to `debug::DebugRenderer` if one already exists, otherwise returns `None`.
+ pub fn try_get_mut<'a>(&'a mut self) -> Option<&'a mut DebugRenderer> {
+ self.debug_renderer.as_mut()
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ if let Some(debug_renderer) = self.debug_renderer {
+ debug_renderer.deinit(device);
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/gpu_cache.rs b/gfx/wr/webrender/src/renderer/gpu_cache.rs
new file mode 100644
index 0000000000..f9975675ca
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/gpu_cache.rs
@@ -0,0 +1,507 @@
+/* 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 std::{cmp, mem};
+use api::units::*;
+use malloc_size_of::MallocSizeOfOps;
+use crate::{
+ device::{CustomVAO, Device, DrawTarget, Program, ReadTarget, Texture, TextureFilter, UploadPBOPool, VBO},
+ gpu_cache::{GpuBlockData, GpuCacheUpdate, GpuCacheUpdateList},
+ internal_types::{RenderTargetInfo, Swizzle},
+ prim_store::DeferredResolve,
+ profiler,
+ render_api::MemoryReport,
+ render_backend::FrameId,
+};
+
+/// Enabling this toggle would force the GPU cache scattered texture to
+/// be resized every frame, which enables GPU debuggers to see if this
+/// is performed correctly.
+const GPU_CACHE_RESIZE_TEST: bool = false;
+
+/// Tracks the state of each row in the GPU cache texture.
+struct CacheRow {
+ /// Mirrored block data on CPU for this row. We store a copy of
+ /// the data on the CPU side to improve upload batching.
+ cpu_blocks: Box<[GpuBlockData; super::MAX_VERTEX_TEXTURE_WIDTH]>,
+ /// The first offset in this row that is dirty.
+ min_dirty: u16,
+ /// The last offset in this row that is dirty.
+ max_dirty: u16,
+}
+
+impl CacheRow {
+ fn new() -> Self {
+ CacheRow {
+ cpu_blocks: Box::new([GpuBlockData::EMPTY; super::MAX_VERTEX_TEXTURE_WIDTH]),
+ min_dirty: super::MAX_VERTEX_TEXTURE_WIDTH as _,
+ max_dirty: 0,
+ }
+ }
+
+ fn is_dirty(&self) -> bool {
+ return self.min_dirty < self.max_dirty;
+ }
+
+ fn clear_dirty(&mut self) {
+ self.min_dirty = super::MAX_VERTEX_TEXTURE_WIDTH as _;
+ self.max_dirty = 0;
+ }
+
+ fn add_dirty(&mut self, block_offset: usize, block_count: usize) {
+ self.min_dirty = self.min_dirty.min(block_offset as _);
+ self.max_dirty = self.max_dirty.max((block_offset + block_count) as _);
+ }
+
+ fn dirty_blocks(&self) -> &[GpuBlockData] {
+ return &self.cpu_blocks[self.min_dirty as usize .. self.max_dirty as usize];
+ }
+}
+
+/// The bus over which CPU and GPU versions of the GPU cache
+/// get synchronized.
+enum GpuCacheBus {
+ /// PBO-based updates, currently operate on a row granularity.
+ /// Therefore, are subject to fragmentation issues.
+ PixelBuffer {
+ /// Per-row data.
+ rows: Vec<CacheRow>,
+ },
+ /// Shader-based scattering updates. Currently rendered by a set
+ /// of points into the GPU texture, each carrying a `GpuBlockData`.
+ Scatter {
+ /// Special program to run the scattered update.
+ program: Program,
+ /// VAO containing the source vertex buffers.
+ vao: CustomVAO,
+ /// VBO for positional data, supplied as normalized `u16`.
+ buf_position: VBO<[u16; 2]>,
+ /// VBO for gpu block data.
+ buf_value: VBO<GpuBlockData>,
+ /// Currently stored block count.
+ count: usize,
+ },
+}
+
+/// The device-specific representation of the cache texture in gpu_cache.rs
+pub struct GpuCacheTexture {
+ texture: Option<Texture>,
+ bus: GpuCacheBus,
+}
+
+impl GpuCacheTexture {
+ /// Ensures that we have an appropriately-sized texture. Returns true if a
+ /// new texture was created.
+ fn ensure_texture(&mut self, device: &mut Device, height: i32) {
+ // If we already have a texture that works, we're done.
+ if self.texture.as_ref().map_or(false, |t| t.get_dimensions().height >= height) {
+ if GPU_CACHE_RESIZE_TEST {
+ // Special debug mode - resize the texture even though it's fine.
+ } else {
+ return;
+ }
+ }
+
+ // Take the old texture, if any.
+ let blit_source = self.texture.take();
+
+ // Create the new texture.
+ assert!(height >= 2, "Height is too small for ANGLE");
+ let new_size = DeviceIntSize::new(super::MAX_VERTEX_TEXTURE_WIDTH as _, height);
+ // If glCopyImageSubData is supported, this texture doesn't need
+ // to be a render target. This prevents GL errors due to framebuffer
+ // incompleteness on devices that don't support RGBAF32 render targets.
+ // TODO(gw): We still need a proper solution for the subset of devices
+ // that don't support glCopyImageSubData *OR* rendering to a
+ // RGBAF32 render target. These devices will currently fail
+ // to resize the GPU cache texture.
+ let supports_copy_image_sub_data = device.get_capabilities().supports_copy_image_sub_data;
+ let rt_info = match self.bus {
+ GpuCacheBus::PixelBuffer { .. } if supports_copy_image_sub_data => None,
+ _ => Some(RenderTargetInfo { has_depth: false }),
+ };
+ let mut texture = device.create_texture(
+ api::ImageBufferKind::Texture2D,
+ api::ImageFormat::RGBAF32,
+ new_size.width,
+ new_size.height,
+ TextureFilter::Nearest,
+ rt_info,
+ 1,
+ );
+
+ // Copy the contents of the previous texture, if applicable.
+ if let Some(blit_source) = blit_source {
+ device.copy_entire_texture(&mut texture, &blit_source);
+ device.delete_texture(blit_source);
+ }
+
+ self.texture = Some(texture);
+ }
+
+ pub fn new(device: &mut Device, use_scatter: bool) -> Result<Self, super::RendererError> {
+ use super::desc::GPU_CACHE_UPDATE;
+
+ let bus = if use_scatter {
+ let program = device.create_program_linked(
+ "gpu_cache_update",
+ &[],
+ &GPU_CACHE_UPDATE,
+ )?;
+ let buf_position = device.create_vbo();
+ let buf_value = device.create_vbo();
+ //Note: the vertex attributes have to be supplied in the same order
+ // as for program creation, but each assigned to a different stream.
+ let vao = device.create_custom_vao(&[
+ buf_position.stream_with(&GPU_CACHE_UPDATE.vertex_attributes[0..1]),
+ buf_value .stream_with(&GPU_CACHE_UPDATE.vertex_attributes[1..2]),
+ ]);
+ GpuCacheBus::Scatter {
+ program,
+ vao,
+ buf_position,
+ buf_value,
+ count: 0,
+ }
+ } else {
+ GpuCacheBus::PixelBuffer {
+ rows: Vec::new(),
+ }
+ };
+
+ Ok(GpuCacheTexture {
+ texture: None,
+ bus,
+ })
+ }
+
+ pub fn deinit(mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ if let GpuCacheBus::Scatter { program, vao, buf_position, buf_value, .. } = self.bus {
+ device.delete_program(program);
+ device.delete_custom_vao(vao);
+ device.delete_vbo(buf_position);
+ device.delete_vbo(buf_value);
+ }
+ }
+
+ pub fn get_height(&self) -> i32 {
+ self.texture.as_ref().map_or(0, |t| t.get_dimensions().height)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn get_texture(&self) -> &Texture {
+ self.texture.as_ref().unwrap()
+ }
+
+ fn prepare_for_updates(
+ &mut self,
+ device: &mut Device,
+ total_block_count: usize,
+ max_height: i32,
+ ) {
+ self.ensure_texture(device, max_height);
+ match self.bus {
+ GpuCacheBus::PixelBuffer { .. } => {},
+ GpuCacheBus::Scatter {
+ ref mut buf_position,
+ ref mut buf_value,
+ ref mut count,
+ ..
+ } => {
+ *count = 0;
+ if total_block_count > buf_value.allocated_count() {
+ device.allocate_vbo(buf_position, total_block_count, super::ONE_TIME_USAGE_HINT);
+ device.allocate_vbo(buf_value, total_block_count, super::ONE_TIME_USAGE_HINT);
+ }
+ }
+ }
+ }
+
+ pub fn invalidate(&mut self) {
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ info!("Invalidating GPU caches");
+ for row in rows {
+ row.add_dirty(0, super::MAX_VERTEX_TEXTURE_WIDTH);
+ }
+ }
+ GpuCacheBus::Scatter { .. } => {
+ warn!("Unable to invalidate scattered GPU cache");
+ }
+ }
+ }
+
+ fn update(&mut self, device: &mut Device, updates: &GpuCacheUpdateList) {
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ for update in &updates.updates {
+ match *update {
+ GpuCacheUpdate::Copy {
+ block_index,
+ block_count,
+ address,
+ } => {
+ let row = address.v as usize;
+
+ // Ensure that the CPU-side shadow copy of the GPU cache data has enough
+ // rows to apply this patch.
+ while rows.len() <= row {
+ // Add a new row.
+ rows.push(CacheRow::new());
+ }
+
+ // Copy the blocks from the patch array in the shadow CPU copy.
+ let block_offset = address.u as usize;
+ let data = &mut rows[row].cpu_blocks;
+ for i in 0 .. block_count {
+ data[block_offset + i] = updates.blocks[block_index + i];
+ }
+
+ // This row is dirty (needs to be updated in GPU texture).
+ rows[row].add_dirty(block_offset, block_count);
+ }
+ }
+ }
+ }
+ GpuCacheBus::Scatter {
+ ref buf_position,
+ ref buf_value,
+ ref mut count,
+ ..
+ } => {
+ //TODO: re-use this heap allocation
+ // Unused positions will be left as 0xFFFF, which translates to
+ // (1.0, 1.0) in the vertex output position and gets culled out
+ let mut position_data = vec![[!0u16; 2]; updates.blocks.len()];
+ let size = self.texture.as_ref().unwrap().get_dimensions().to_usize();
+
+ for update in &updates.updates {
+ match *update {
+ GpuCacheUpdate::Copy {
+ block_index,
+ block_count,
+ address,
+ } => {
+ // Convert the absolute texel position into normalized
+ let y = ((2*address.v as usize + 1) << 15) / size.height;
+ for i in 0 .. block_count {
+ let x = ((2*address.u as usize + 2*i + 1) << 15) / size.width;
+ position_data[block_index + i] = [x as _, y as _];
+ }
+ }
+ }
+ }
+
+ device.fill_vbo(buf_value, &updates.blocks, *count);
+ device.fill_vbo(buf_position, &position_data, *count);
+ *count += position_data.len();
+ }
+ }
+ }
+
+ fn flush(&mut self, device: &mut Device, pbo_pool: &mut UploadPBOPool) -> usize {
+ let texture = self.texture.as_ref().unwrap();
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows } => {
+ let rows_dirty = rows
+ .iter()
+ .filter(|row| row.is_dirty())
+ .count();
+ if rows_dirty == 0 {
+ return 0
+ }
+
+ let mut uploader = device.upload_texture(pbo_pool);
+
+ for (row_index, row) in rows.iter_mut().enumerate() {
+ if !row.is_dirty() {
+ continue;
+ }
+
+ let blocks = row.dirty_blocks();
+ let rect = DeviceIntRect::new(
+ DeviceIntPoint::new(row.min_dirty as i32, row_index as i32),
+ DeviceIntSize::new(blocks.len() as i32, 1),
+ );
+
+ uploader.upload(device, texture, rect, 0, None, None, blocks.as_ptr(), blocks.len());
+
+ row.clear_dirty();
+ }
+
+ uploader.flush(device);
+
+ rows_dirty
+ }
+ GpuCacheBus::Scatter { ref program, ref vao, count, .. } => {
+ device.disable_depth();
+ device.set_blend(false);
+ device.bind_program(program);
+ device.bind_custom_vao(vao);
+ device.bind_draw_target(
+ DrawTarget::from_texture(
+ texture,
+ 0,
+ false,
+ ),
+ );
+ device.draw_nonindexed_points(0, count as _);
+ 0
+ }
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn remove_texture(&mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn load_from_data(&mut self, texture: Texture, data: Vec<u8>) {
+ assert!(self.texture.is_none());
+ match self.bus {
+ GpuCacheBus::PixelBuffer { ref mut rows, .. } => {
+ let dim = texture.get_dimensions();
+ let blocks = unsafe {
+ std::slice::from_raw_parts(
+ data.as_ptr() as *const GpuBlockData,
+ data.len() / mem::size_of::<GpuBlockData>(),
+ )
+ };
+ // fill up the CPU cache from the contents we just loaded
+ rows.clear();
+ rows.extend((0 .. dim.height).map(|_| CacheRow::new()));
+ let chunks = blocks.chunks(super::MAX_VERTEX_TEXTURE_WIDTH);
+ debug_assert_eq!(chunks.len(), rows.len());
+ for (row, chunk) in rows.iter_mut().zip(chunks) {
+ row.cpu_blocks.copy_from_slice(chunk);
+ }
+ }
+ GpuCacheBus::Scatter { .. } => {}
+ }
+ self.texture = Some(texture);
+ }
+
+ pub fn report_memory_to(&self, report: &mut MemoryReport, size_op_funs: &MallocSizeOfOps) {
+ if let GpuCacheBus::PixelBuffer{ref rows, ..} = self.bus {
+ for row in rows.iter() {
+ report.gpu_cache_cpu_mirror += unsafe { (size_op_funs.size_of_op)(row.cpu_blocks.as_ptr() as *const _) };
+ }
+ }
+
+ // GPU cache GPU memory.
+ report.gpu_cache_textures +=
+ self.texture.as_ref().map_or(0, |t| t.size_in_bytes());
+ }
+}
+
+impl super::Renderer {
+ pub fn update_gpu_cache(&mut self) {
+ let _gm = self.gpu_profiler.start_marker("gpu cache update");
+
+ // For an artificial stress test of GPU cache resizing,
+ // always pass an extra update list with at least one block in it.
+ let gpu_cache_height = self.gpu_cache_texture.get_height();
+ if gpu_cache_height != 0 && GPU_CACHE_RESIZE_TEST {
+ self.pending_gpu_cache_updates.push(GpuCacheUpdateList {
+ frame_id: FrameId::INVALID,
+ clear: false,
+ height: gpu_cache_height,
+ blocks: vec![[1f32; 4].into()],
+ updates: Vec::new(),
+ debug_commands: Vec::new(),
+ });
+ }
+
+ let (updated_blocks, max_requested_height) = self
+ .pending_gpu_cache_updates
+ .iter()
+ .fold((0, gpu_cache_height), |(count, height), list| {
+ (count + list.blocks.len(), cmp::max(height, list.height))
+ });
+
+ if max_requested_height > self.get_max_texture_size() && !self.gpu_cache_overflow {
+ self.gpu_cache_overflow = true;
+ self.renderer_errors.push(super::RendererError::MaxTextureSize);
+ }
+
+ // Note: if we decide to switch to scatter-style GPU cache update
+ // permanently, we can have this code nicer with `BufferUploader` kind
+ // of helper, similarly to how `TextureUploader` API is used.
+ self.gpu_cache_texture.prepare_for_updates(
+ &mut self.device,
+ updated_blocks,
+ max_requested_height,
+ );
+
+ for update_list in self.pending_gpu_cache_updates.drain(..) {
+ assert!(update_list.height <= max_requested_height);
+ if update_list.frame_id > self.gpu_cache_frame_id {
+ self.gpu_cache_frame_id = update_list.frame_id
+ }
+ self.gpu_cache_texture
+ .update(&mut self.device, &update_list);
+ }
+
+ self.profile.start_time(profiler::GPU_CACHE_UPLOAD_TIME);
+ let updated_rows = self.gpu_cache_texture.flush(
+ &mut self.device,
+ &mut self.texture_upload_pbo_pool
+ );
+ self.gpu_cache_upload_time += self.profile.end_time(profiler::GPU_CACHE_UPLOAD_TIME);
+
+ self.profile.set(profiler::GPU_CACHE_ROWS_UPDATED, updated_rows);
+ self.profile.set(profiler::GPU_CACHE_BLOCKS_UPDATED, updated_blocks);
+ }
+
+ pub fn prepare_gpu_cache(
+ &mut self,
+ deferred_resolves: &[DeferredResolve],
+ ) -> Result<(), super::RendererError> {
+ if self.pending_gpu_cache_clear {
+ let use_scatter =
+ matches!(self.gpu_cache_texture.bus, GpuCacheBus::Scatter { .. });
+ let new_cache = GpuCacheTexture::new(&mut self.device, use_scatter)?;
+ let old_cache = mem::replace(&mut self.gpu_cache_texture, new_cache);
+ old_cache.deinit(&mut self.device);
+ self.pending_gpu_cache_clear = false;
+ }
+
+ let deferred_update_list = self.update_deferred_resolves(deferred_resolves);
+ self.pending_gpu_cache_updates.extend(deferred_update_list);
+
+ self.update_gpu_cache();
+
+ // Note: the texture might have changed during the `update`,
+ // so we need to bind it here.
+ self.device.bind_texture(
+ super::TextureSampler::GpuCache,
+ self.gpu_cache_texture.texture.as_ref().unwrap(),
+ Swizzle::default(),
+ );
+
+ Ok(())
+ }
+
+ pub fn read_gpu_cache(&mut self) -> (DeviceIntSize, Vec<u8>) {
+ let texture = self.gpu_cache_texture.texture.as_ref().unwrap();
+ let size = device_size_as_framebuffer_size(texture.get_dimensions());
+ let mut texels = vec![0; (size.width * size.height * 16) as usize];
+ self.device.begin_frame();
+ self.device.bind_read_target(ReadTarget::from_texture(texture, 0));
+ self.device.read_pixels_into(
+ size.into(),
+ api::ImageFormat::RGBAF32,
+ &mut texels,
+ );
+ self.device.reset_read_target();
+ self.device.end_frame();
+ (texture.get_dimensions(), texels)
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
new file mode 100644
index 0000000000..7086e8805e
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -0,0 +1,6165 @@
+/* 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/. */
+
+//! The high-level module responsible for interfacing with the GPU.
+//!
+//! Much of WebRender's design is driven by separating work into different
+//! threads. To avoid the complexities of multi-threaded GPU access, we restrict
+//! all communication with the GPU to one thread, the render thread. But since
+//! issuing GPU commands is often a bottleneck, we move everything else (i.e.
+//! the computation of what commands to issue) to another thread, the
+//! RenderBackend thread. The RenderBackend, in turn, may delegate work to other
+//! thread (like the SceneBuilder threads or Rayon workers), but the
+//! Render-vs-RenderBackend distinction is the most important.
+//!
+//! The consumer is responsible for initializing the render thread before
+//! calling into WebRender, which means that this module also serves as the
+//! initial entry point into WebRender, and is responsible for spawning the
+//! various other threads discussed above. That said, WebRender initialization
+//! returns both the `Renderer` instance as well as a channel for communicating
+//! directly with the `RenderBackend`. Aside from a few high-level operations
+//! like 'render now', most of interesting commands from the consumer go over
+//! that channel and operate on the `RenderBackend`.
+//!
+//! ## Space conversion guidelines
+//! At this stage, we shuld be operating with `DevicePixel` and `FramebufferPixel` only.
+//! "Framebuffer" space represents the final destination of our rendeing,
+//! and it happens to be Y-flipped on OpenGL. The conversion is done as follows:
+//! - for rasterized primitives, the orthographics projection transforms
+//! the content rectangle to -1 to 1
+//! - the viewport transformation is setup to map the whole range to
+//! the framebuffer rectangle provided by the document view, stored in `DrawTarget`
+//! - all the direct framebuffer operations, like blitting, reading pixels, and setting
+//! up the scissor, are accepting already transformed coordinates, which we can get by
+//! calling `DrawTarget::to_framebuffer_rect`
+
+use api::{BlobImageHandler, ColorF, ColorU, MixBlendMode};
+use api::{DocumentId, Epoch, ExternalImageHandler};
+use api::CrashAnnotator;
+#[cfg(feature = "replay")]
+use api::ExternalImageId;
+use api::{ExternalImageSource, ExternalImageType, FontRenderMode, ImageFormat};
+use api::{PipelineId, ImageRendering, Checkpoint, NotificationRequest};
+use api::{VoidPtrToSizeFn, PremultipliedColorF};
+use api::{RenderNotifier, ImageBufferKind, SharedFontInstanceMap};
+#[cfg(feature = "replay")]
+use api::ExternalImage;
+use api::units::*;
+use api::channel::{unbounded_channel, Sender, Receiver};
+pub use api::DebugFlags;
+use core::time::Duration;
+
+use crate::render_api::{RenderApiSender, DebugCommand, ApiMsg, FrameMsg, MemoryReport};
+use crate::batch::{AlphaBatchContainer, BatchKind, BatchFeatures, BatchTextures, BrushBatchKind, ClipBatchList};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use crate::capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
+use crate::composite::{CompositeState, CompositeTileSurface, CompositeTile, ResolvedExternalSurface, CompositorSurfaceTransform};
+use crate::composite::{CompositorKind, Compositor, NativeTileId, CompositeSurfaceFormat, ResolvedExternalSurfaceColorData};
+use crate::composite::{CompositorConfig, NativeSurfaceOperationDetails, NativeSurfaceId, NativeSurfaceOperation};
+use crate::c_str;
+use crate::debug_colors;
+use crate::device::{DepthFunction, Device, DrawTarget, ExternalTexture, GpuFrameId};
+use crate::device::{ProgramCache, ReadTarget, ShaderError, Texture, TextureFilter, TextureFlags, TextureSlot};
+use crate::device::{UploadMethod, UploadPBOPool, UploadStagingBuffer, VertexUsageHint};
+use crate::device::query::{GpuSampler, GpuTimer};
+#[cfg(feature = "capture")]
+use crate::device::FBOId;
+use crate::debug_item::DebugItem;
+use crate::frame_builder::{Frame, ChasePrimitive, FrameBuilderConfig};
+use crate::glyph_cache::GlyphCache;
+use crate::glyph_rasterizer::{GlyphFormat, GlyphRasterizer};
+use crate::gpu_cache::{GpuCacheUpdate, GpuCacheUpdateList};
+use crate::gpu_cache::{GpuCacheDebugChunk, GpuCacheDebugCmd};
+use crate::gpu_types::{PrimitiveInstanceData, ScalingInstance, SvgFilterInstance};
+use crate::gpu_types::{BlurInstance, ClearInstance, CompositeInstance, ZBufferId};
+use crate::internal_types::{TextureSource, ResourceCacheError};
+use crate::internal_types::{CacheTextureId, DebugOutput, FastHashMap, FastHashSet, LayerIndex, RenderedDocument, ResultMsg};
+use crate::internal_types::{TextureCacheAllocationKind, TextureCacheUpdate, TextureUpdateList, TextureUpdateSource};
+use crate::internal_types::{RenderTargetInfo, Swizzle, DeferredResolveIndex};
+use crate::picture::{self, ResolvedSurfaceTexture};
+use crate::prim_store::DeferredResolve;
+use crate::profiler::{self, GpuProfileTag, TransactionProfile};
+use crate::profiler::{Profiler, add_event_marker, add_text_marker, thread_is_being_profiled};
+use crate::device::query::{GpuProfiler, GpuDebugMethod};
+use crate::render_backend::{FrameId, RenderBackend};
+use crate::render_task_graph::RenderTaskGraph;
+use crate::render_task::{RenderTask, RenderTaskKind};
+use crate::resource_cache::ResourceCache;
+use crate::scene_builder_thread::{SceneBuilderThread, SceneBuilderThreadChannels, LowPrioritySceneBuilderThread};
+use crate::screen_capture::AsyncScreenshotGrabber;
+use crate::render_target::{AlphaRenderTarget, ColorRenderTarget, PictureCacheTarget};
+use crate::render_target::{RenderTarget, TextureCacheRenderTarget};
+use crate::render_target::{RenderTargetKind, BlitJob, BlitJobSource};
+use crate::texture_cache::{TextureCache, TextureCacheConfig};
+use crate::texture_pack::{GuillotineAllocator, FreeRectSlice};
+use crate::tile_cache::PictureCacheDebugInfo;
+use crate::util::drain_filter;
+
+use euclid::{rect, Transform3D, Scale, default};
+use gleam::gl;
+use malloc_size_of::MallocSizeOfOps;
+use rayon::{ThreadPool, ThreadPoolBuilder};
+
+use std::{
+ cell::RefCell,
+ collections::VecDeque,
+ f32,
+ mem,
+ num::NonZeroUsize,
+ path::PathBuf,
+ rc::Rc,
+ sync::Arc,
+ sync::atomic::{AtomicBool, Ordering},
+ thread,
+};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use std::collections::hash_map::Entry;
+use tracy_rs::register_thread_with_profiler;
+use time::precise_time_ns;
+
+cfg_if! {
+ if #[cfg(feature = "debugger")] {
+ use serde_json;
+ use crate::debug_server;
+ }
+}
+
+mod debug;
+mod gpu_cache;
+mod shade;
+mod vertex;
+
+pub use debug::DebugRenderer;
+pub use shade::{Shaders, SharedShaders};
+pub use vertex::{desc, VertexArrayKind, MAX_VERTEX_TEXTURE_WIDTH};
+
+/// Use this hint for all vertex data re-initialization. This allows
+/// the driver to better re-use RBOs internally.
+pub const ONE_TIME_USAGE_HINT: VertexUsageHint = VertexUsageHint::Stream;
+
+/// Is only false if no WR instances have ever been created.
+static HAS_BEEN_INITIALIZED: AtomicBool = AtomicBool::new(false);
+
+/// Returns true if a WR instance has ever been initialized in this process.
+pub fn wr_has_been_initialized() -> bool {
+ HAS_BEEN_INITIALIZED.load(Ordering::SeqCst)
+}
+
+/// The size of the array of each type of vertex data texture that
+/// is round-robin-ed each frame during bind_frame_data. Doing this
+/// helps avoid driver stalls while updating the texture in some
+/// drivers. The size of these textures are typically very small
+/// (e.g. < 16 kB) so it's not a huge waste of memory. Despite that,
+/// this is a short-term solution - we want to find a better way
+/// to provide this frame data, which will likely involve some
+/// combination of UBO/SSBO usage. Although this only affects some
+/// platforms, it's enabled on all platforms to reduce testing
+/// differences between platforms.
+pub const VERTEX_DATA_TEXTURE_COUNT: usize = 3;
+
+/// Number of GPU blocks per UV rectangle provided for an image.
+pub const BLOCKS_PER_UV_RECT: usize = 2;
+
+const GPU_TAG_BRUSH_OPACITY: GpuProfileTag = GpuProfileTag {
+ label: "B_Opacity",
+ color: debug_colors::DARKMAGENTA,
+};
+const GPU_TAG_BRUSH_LINEAR_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "B_LinearGradient",
+ color: debug_colors::POWDERBLUE,
+};
+const GPU_TAG_BRUSH_RADIAL_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "B_RadialGradient",
+ color: debug_colors::LIGHTPINK,
+};
+const GPU_TAG_BRUSH_CONIC_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "B_ConicGradient",
+ color: debug_colors::GREEN,
+};
+const GPU_TAG_BRUSH_YUV_IMAGE: GpuProfileTag = GpuProfileTag {
+ label: "B_YuvImage",
+ color: debug_colors::DARKGREEN,
+};
+const GPU_TAG_BRUSH_MIXBLEND: GpuProfileTag = GpuProfileTag {
+ label: "B_MixBlend",
+ color: debug_colors::MAGENTA,
+};
+const GPU_TAG_BRUSH_BLEND: GpuProfileTag = GpuProfileTag {
+ label: "B_Blend",
+ color: debug_colors::ORANGE,
+};
+const GPU_TAG_BRUSH_IMAGE: GpuProfileTag = GpuProfileTag {
+ label: "B_Image",
+ color: debug_colors::SPRINGGREEN,
+};
+const GPU_TAG_BRUSH_SOLID: GpuProfileTag = GpuProfileTag {
+ label: "B_Solid",
+ color: debug_colors::RED,
+};
+const GPU_TAG_CACHE_CLIP: GpuProfileTag = GpuProfileTag {
+ label: "C_Clip",
+ color: debug_colors::PURPLE,
+};
+const GPU_TAG_CACHE_BORDER: GpuProfileTag = GpuProfileTag {
+ label: "C_Border",
+ color: debug_colors::CORNSILK,
+};
+const GPU_TAG_CACHE_LINE_DECORATION: GpuProfileTag = GpuProfileTag {
+ label: "C_LineDecoration",
+ color: debug_colors::YELLOWGREEN,
+};
+const GPU_TAG_CACHE_GRADIENT: GpuProfileTag = GpuProfileTag {
+ label: "C_Gradient",
+ color: debug_colors::BROWN,
+};
+const GPU_TAG_SETUP_TARGET: GpuProfileTag = GpuProfileTag {
+ label: "target init",
+ color: debug_colors::SLATEGREY,
+};
+const GPU_TAG_SETUP_DATA: GpuProfileTag = GpuProfileTag {
+ label: "data init",
+ color: debug_colors::LIGHTGREY,
+};
+const GPU_TAG_PRIM_SPLIT_COMPOSITE: GpuProfileTag = GpuProfileTag {
+ label: "SplitComposite",
+ color: debug_colors::DARKBLUE,
+};
+const GPU_TAG_PRIM_TEXT_RUN: GpuProfileTag = GpuProfileTag {
+ label: "TextRun",
+ color: debug_colors::BLUE,
+};
+const GPU_TAG_BLUR: GpuProfileTag = GpuProfileTag {
+ label: "Blur",
+ color: debug_colors::VIOLET,
+};
+const GPU_TAG_BLIT: GpuProfileTag = GpuProfileTag {
+ label: "Blit",
+ color: debug_colors::LIME,
+};
+const GPU_TAG_SCALE: GpuProfileTag = GpuProfileTag {
+ label: "Scale",
+ color: debug_colors::GHOSTWHITE,
+};
+const GPU_SAMPLER_TAG_ALPHA: GpuProfileTag = GpuProfileTag {
+ label: "Alpha targets",
+ color: debug_colors::BLACK,
+};
+const GPU_SAMPLER_TAG_OPAQUE: GpuProfileTag = GpuProfileTag {
+ label: "Opaque pass",
+ color: debug_colors::BLACK,
+};
+const GPU_SAMPLER_TAG_TRANSPARENT: GpuProfileTag = GpuProfileTag {
+ label: "Transparent pass",
+ color: debug_colors::BLACK,
+};
+const GPU_TAG_SVG_FILTER: GpuProfileTag = GpuProfileTag {
+ label: "SvgFilter",
+ color: debug_colors::LEMONCHIFFON,
+};
+const GPU_TAG_COMPOSITE: GpuProfileTag = GpuProfileTag {
+ label: "Composite",
+ color: debug_colors::TOMATO,
+};
+const GPU_TAG_CLEAR: GpuProfileTag = GpuProfileTag {
+ label: "Clear",
+ color: debug_colors::CHOCOLATE,
+};
+
+/// The clear color used for the texture cache when the debug display is enabled.
+/// We use a shade of blue so that we can still identify completely blue items in
+/// the texture cache.
+const TEXTURE_CACHE_DBG_CLEAR_COLOR: [f32; 4] = [0.0, 0.0, 0.8, 1.0];
+
+impl BatchKind {
+ #[cfg(feature = "debugger")]
+ fn debug_name(&self) -> &'static str {
+ match *self {
+ BatchKind::SplitComposite => "SplitComposite",
+ BatchKind::Brush(kind) => {
+ match kind {
+ BrushBatchKind::Solid => "Brush (Solid)",
+ BrushBatchKind::Image(..) => "Brush (Image)",
+ BrushBatchKind::Blend => "Brush (Blend)",
+ BrushBatchKind::MixBlend { .. } => "Brush (Composite)",
+ BrushBatchKind::YuvImage(..) => "Brush (YuvImage)",
+ BrushBatchKind::ConicGradient => "Brush (ConicGradient)",
+ BrushBatchKind::RadialGradient => "Brush (RadialGradient)",
+ BrushBatchKind::LinearGradient => "Brush (LinearGradient)",
+ BrushBatchKind::Opacity => "Brush (Opacity)",
+ }
+ }
+ BatchKind::TextRun(_) => "TextRun",
+ }
+ }
+
+ fn sampler_tag(&self) -> GpuProfileTag {
+ match *self {
+ BatchKind::SplitComposite => GPU_TAG_PRIM_SPLIT_COMPOSITE,
+ BatchKind::Brush(kind) => {
+ match kind {
+ BrushBatchKind::Solid => GPU_TAG_BRUSH_SOLID,
+ BrushBatchKind::Image(..) => GPU_TAG_BRUSH_IMAGE,
+ BrushBatchKind::Blend => GPU_TAG_BRUSH_BLEND,
+ BrushBatchKind::MixBlend { .. } => GPU_TAG_BRUSH_MIXBLEND,
+ BrushBatchKind::YuvImage(..) => GPU_TAG_BRUSH_YUV_IMAGE,
+ BrushBatchKind::ConicGradient => GPU_TAG_BRUSH_CONIC_GRADIENT,
+ BrushBatchKind::RadialGradient => GPU_TAG_BRUSH_RADIAL_GRADIENT,
+ BrushBatchKind::LinearGradient => GPU_TAG_BRUSH_LINEAR_GRADIENT,
+ BrushBatchKind::Opacity => GPU_TAG_BRUSH_OPACITY,
+ }
+ }
+ BatchKind::TextRun(_) => GPU_TAG_PRIM_TEXT_RUN,
+ }
+ }
+}
+
+fn flag_changed(before: DebugFlags, after: DebugFlags, select: DebugFlags) -> Option<bool> {
+ if before & select != after & select {
+ Some(after.contains(select))
+ } else {
+ None
+ }
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub enum ShaderColorMode {
+ FromRenderPassMode = 0,
+ Alpha = 1,
+ SubpixelConstantTextColor = 2,
+ SubpixelWithBgColorPass0 = 3,
+ SubpixelWithBgColorPass1 = 4,
+ SubpixelWithBgColorPass2 = 5,
+ SubpixelDualSource = 6,
+ Bitmap = 7,
+ ColorBitmap = 8,
+ Image = 9,
+}
+
+impl From<GlyphFormat> for ShaderColorMode {
+ fn from(format: GlyphFormat) -> ShaderColorMode {
+ match format {
+ GlyphFormat::Alpha | GlyphFormat::TransformedAlpha => ShaderColorMode::Alpha,
+ GlyphFormat::Subpixel | GlyphFormat::TransformedSubpixel => {
+ panic!("Subpixel glyph formats must be handled separately.");
+ }
+ GlyphFormat::Bitmap => ShaderColorMode::Bitmap,
+ GlyphFormat::ColorBitmap => ShaderColorMode::ColorBitmap,
+ }
+ }
+}
+
+/// Enumeration of the texture samplers used across the various WebRender shaders.
+///
+/// Each variant corresponds to a uniform declared in shader source. We only bind
+/// the variants we need for a given shader, so not every variant is bound for every
+/// batch.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub(crate) enum TextureSampler {
+ Color0,
+ Color1,
+ Color2,
+ GpuCache,
+ TransformPalette,
+ RenderTasks,
+ Dither,
+ PrimitiveHeadersF,
+ PrimitiveHeadersI,
+ ClipMask,
+}
+
+impl TextureSampler {
+ pub(crate) fn color(n: usize) -> TextureSampler {
+ match n {
+ 0 => TextureSampler::Color0,
+ 1 => TextureSampler::Color1,
+ 2 => TextureSampler::Color2,
+ _ => {
+ panic!("There are only 3 color samplers.");
+ }
+ }
+ }
+}
+
+impl Into<TextureSlot> for TextureSampler {
+ fn into(self) -> TextureSlot {
+ match self {
+ TextureSampler::Color0 => TextureSlot(0),
+ TextureSampler::Color1 => TextureSlot(1),
+ TextureSampler::Color2 => TextureSlot(2),
+ TextureSampler::GpuCache => TextureSlot(3),
+ TextureSampler::TransformPalette => TextureSlot(4),
+ TextureSampler::RenderTasks => TextureSlot(5),
+ TextureSampler::Dither => TextureSlot(6),
+ TextureSampler::PrimitiveHeadersF => TextureSlot(7),
+ TextureSampler::PrimitiveHeadersI => TextureSlot(8),
+ TextureSampler::ClipMask => TextureSlot(9),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum GraphicsApi {
+ OpenGL,
+}
+
+#[derive(Clone, Debug)]
+pub struct GraphicsApiInfo {
+ pub kind: GraphicsApi,
+ pub renderer: String,
+ pub version: String,
+}
+
+#[derive(Debug)]
+pub struct GpuProfile {
+ pub frame_id: GpuFrameId,
+ pub paint_time_ns: u64,
+}
+
+impl GpuProfile {
+ fn new(frame_id: GpuFrameId, timers: &[GpuTimer]) -> GpuProfile {
+ let mut paint_time_ns = 0;
+ for timer in timers {
+ paint_time_ns += timer.time_ns;
+ }
+ GpuProfile {
+ frame_id,
+ paint_time_ns,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct CpuProfile {
+ pub frame_id: GpuFrameId,
+ pub backend_time_ns: u64,
+ pub composite_time_ns: u64,
+ pub draw_calls: usize,
+}
+
+impl CpuProfile {
+ fn new(
+ frame_id: GpuFrameId,
+ backend_time_ns: u64,
+ composite_time_ns: u64,
+ draw_calls: usize,
+ ) -> CpuProfile {
+ CpuProfile {
+ frame_id,
+ backend_time_ns,
+ composite_time_ns,
+ draw_calls,
+ }
+ }
+}
+
+/// The selected partial present mode for a given frame.
+#[derive(Debug, Copy, Clone)]
+enum PartialPresentMode {
+ /// The device supports fewer dirty rects than the number of dirty rects
+ /// that WR produced. In this case, the WR dirty rects are union'ed into
+ /// a single dirty rect, that is provided to the caller.
+ Single {
+ dirty_rect: DeviceRect,
+ },
+}
+
+/// Helper struct for resolving device Textures for use during rendering passes.
+///
+/// Manages the mapping between the at-a-distance texture handles used by the
+/// `RenderBackend` (which does not directly interface with the GPU) and actual
+/// device texture handles.
+struct TextureResolver {
+ /// A map to resolve texture cache IDs to native textures.
+ texture_cache_map: FastHashMap<CacheTextureId, Texture>,
+
+ /// Map of external image IDs to native textures.
+ external_images: FastHashMap<DeferredResolveIndex, ExternalTexture>,
+
+ /// A special 1x1 dummy texture used for shaders that expect to work with
+ /// the output of the previous pass but are actually running in the first
+ /// pass.
+ dummy_cache_texture: Texture,
+}
+
+impl TextureResolver {
+ fn new(device: &mut Device) -> TextureResolver {
+ let dummy_cache_texture = device
+ .create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::RGBA8,
+ 1,
+ 1,
+ TextureFilter::Linear,
+ None,
+ 1,
+ );
+ device.upload_texture_immediate(
+ &dummy_cache_texture,
+ &[0xff, 0xff, 0xff, 0xff],
+ );
+
+ TextureResolver {
+ texture_cache_map: FastHashMap::default(),
+ external_images: FastHashMap::default(),
+ dummy_cache_texture,
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ device.delete_texture(self.dummy_cache_texture);
+
+ for (_id, texture) in self.texture_cache_map {
+ device.delete_texture(texture);
+ }
+ }
+
+ fn begin_frame(&mut self) {
+ }
+
+ fn end_pass(
+ &mut self,
+ device: &mut Device,
+ textures_to_invalidate: &[CacheTextureId],
+ ) {
+ // For any texture that is no longer needed, immediately
+ // invalidate it so that tiled GPUs don't need to resolve it
+ // back to memory.
+ for texture_id in textures_to_invalidate {
+ let render_target = &self.texture_cache_map[texture_id];
+ device.invalidate_render_target(render_target);
+ }
+ }
+
+ // Bind a source texture to the device.
+ fn bind(&self, texture_id: &TextureSource, sampler: TextureSampler, device: &mut Device) -> Swizzle {
+ match *texture_id {
+ TextureSource::Invalid => {
+ Swizzle::default()
+ }
+ TextureSource::Dummy => {
+ let swizzle = Swizzle::default();
+ device.bind_texture(sampler, &self.dummy_cache_texture, swizzle);
+ swizzle
+ }
+ TextureSource::External(ref index, _) => {
+ let texture = self.external_images
+ .get(index)
+ .expect("BUG: External image should be resolved by now");
+ device.bind_external_texture(sampler, texture);
+ Swizzle::default()
+ }
+ TextureSource::TextureCache(index, swizzle) => {
+ let texture = &self.texture_cache_map[&index];
+ device.bind_texture(sampler, texture, swizzle);
+ swizzle
+ }
+ }
+ }
+
+ // Get the real (OpenGL) texture ID for a given source texture.
+ // For a texture cache texture, the IDs are stored in a vector
+ // map for fast access.
+ fn resolve(&self, texture_id: &TextureSource) -> Option<(&Texture, Swizzle)> {
+ match *texture_id {
+ TextureSource::Invalid => None,
+ TextureSource::Dummy => {
+ Some((&self.dummy_cache_texture, Swizzle::default()))
+ }
+ TextureSource::External(..) => {
+ panic!("BUG: External textures cannot be resolved, they can only be bound.");
+ }
+ TextureSource::TextureCache(index, swizzle) => {
+ Some((&self.texture_cache_map[&index], swizzle))
+ }
+ }
+ }
+
+ // Retrieve the deferred / resolved UV rect if an external texture, otherwise
+ // return the default supplied UV rect.
+ fn get_uv_rect(
+ &self,
+ source: &TextureSource,
+ default_value: TexelRect,
+ ) -> TexelRect {
+ match source {
+ TextureSource::External(ref index, _) => {
+ let texture = self.external_images
+ .get(index)
+ .expect("BUG: External image should be resolved by now");
+ texture.get_uv_rect()
+ }
+ _ => {
+ default_value
+ }
+ }
+ }
+
+ fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ // We're reporting GPU memory rather than heap-allocations, so we don't
+ // use size_of_op.
+ for t in self.texture_cache_map.values() {
+ report.texture_cache_textures += t.size_in_bytes();
+ }
+
+ report
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum BlendMode {
+ None,
+ Alpha,
+ PremultipliedAlpha,
+ PremultipliedDestOut,
+ SubpixelDualSource,
+ SubpixelConstantTextColor(ColorF),
+ SubpixelWithBgColor,
+ Advanced(MixBlendMode),
+}
+
+#[derive(PartialEq)]
+struct TargetSelector {
+ size: DeviceIntSize,
+ num_layers: usize,
+ format: ImageFormat,
+}
+
+/// Information about the state of the debugging / profiler overlay in native compositing mode.
+struct DebugOverlayState {
+ /// True if any of the current debug flags will result in drawing a debug overlay.
+ is_enabled: bool,
+
+ /// The current size of the debug overlay surface. None implies that the
+ /// debug surface isn't currently allocated.
+ current_size: Option<DeviceIntSize>,
+}
+
+impl DebugOverlayState {
+ fn new() -> Self {
+ DebugOverlayState {
+ is_enabled: false,
+ current_size: None,
+ }
+ }
+}
+
+/// Tracks buffer damage rects over a series of frames.
+#[derive(Debug, Default)]
+struct BufferDamageTracker {
+ damage_rects: [DeviceRect; 2],
+ current_offset: usize,
+}
+
+impl BufferDamageTracker {
+ /// Sets the damage rect for the current frame. Should only be called *after*
+ /// get_damage_rect() has been called to get the current backbuffer's damage rect.
+ fn push_dirty_rect(&mut self, rect: &DeviceRect) {
+ self.damage_rects[self.current_offset] = rect.clone();
+ self.current_offset = match self.current_offset {
+ 0 => self.damage_rects.len() - 1,
+ n => n - 1,
+ }
+ }
+
+ /// Gets the damage rect for the current backbuffer, given the backbuffer's age.
+ /// (The number of frames since it was previously the backbuffer.)
+ /// Returns an empty rect if the buffer is valid, and None if the entire buffer is invalid.
+ fn get_damage_rect(&self, buffer_age: usize) -> Option<DeviceRect> {
+ match buffer_age {
+ // 0 means this is a new buffer, so is completely invalid.
+ 0 => None,
+ // 1 means this backbuffer was also the previous frame's backbuffer
+ // (so must have been copied to the frontbuffer). It is therefore entirely valid.
+ 1 => Some(DeviceRect::zero()),
+ // We must calculate the union of the damage rects since this buffer was previously
+ // the backbuffer.
+ n if n <= self.damage_rects.len() + 1 => {
+ Some(
+ self.damage_rects.iter()
+ .cycle()
+ .skip(self.current_offset + 1)
+ .take(n - 1)
+ .fold(DeviceRect::zero(), |acc, r| acc.union(r))
+ )
+ }
+ // The backbuffer is older than the number of frames for which we track,
+ // so we treat it as entirely invalid.
+ _ => None,
+ }
+ }
+}
+
+/// The renderer is responsible for submitting to the GPU the work prepared by the
+/// RenderBackend.
+///
+/// We have a separate `Renderer` instance for each instance of WebRender (generally
+/// one per OS window), and all instances share the same thread.
+pub struct Renderer {
+ result_rx: Receiver<ResultMsg>,
+ debug_server: Box<dyn DebugServer>,
+ pub device: Device,
+ pending_texture_updates: Vec<TextureUpdateList>,
+ /// True if there are any TextureCacheUpdate pending.
+ pending_texture_cache_updates: bool,
+ pending_native_surface_updates: Vec<NativeSurfaceOperation>,
+ pending_gpu_cache_updates: Vec<GpuCacheUpdateList>,
+ pending_gpu_cache_clear: bool,
+ pending_shader_updates: Vec<PathBuf>,
+ active_documents: FastHashMap<DocumentId, RenderedDocument>,
+
+ shaders: Rc<RefCell<Shaders>>,
+
+ max_recorded_profiles: usize,
+
+ clear_color: Option<ColorF>,
+ enable_clear_scissor: bool,
+ enable_advanced_blend_barriers: bool,
+ clear_caches_with_quads: bool,
+
+ debug: debug::LazyInitializedDebugRenderer,
+ debug_flags: DebugFlags,
+ profile: TransactionProfile,
+ frame_counter: u64,
+ resource_upload_time: f64,
+ gpu_cache_upload_time: f64,
+ profiler: Profiler,
+
+ last_time: u64,
+
+ pub gpu_profiler: GpuProfiler,
+ vaos: vertex::RendererVAOs,
+
+ gpu_cache_texture: gpu_cache::GpuCacheTexture,
+ vertex_data_textures: Vec<vertex::VertexDataTextures>,
+ current_vertex_data_textures: usize,
+
+ /// When the GPU cache debugger is enabled, we keep track of the live blocks
+ /// in the GPU cache so that we can use them for the debug display. This
+ /// member stores those live blocks, indexed by row.
+ gpu_cache_debug_chunks: Vec<Vec<GpuCacheDebugChunk>>,
+
+ gpu_cache_frame_id: FrameId,
+ gpu_cache_overflow: bool,
+
+ pipeline_info: PipelineInfo,
+
+ // Manages and resolves source textures IDs to real texture IDs.
+ texture_resolver: TextureResolver,
+
+ texture_upload_pbo_pool: UploadPBOPool,
+
+ dither_matrix_texture: Option<Texture>,
+
+ /// Optional trait object that allows the client
+ /// application to provide external buffers for image data.
+ external_image_handler: Option<Box<dyn ExternalImageHandler>>,
+
+ /// Optional function pointers for measuring memory used by a given
+ /// heap-allocated pointer.
+ size_of_ops: Option<MallocSizeOfOps>,
+
+ pub renderer_errors: Vec<RendererError>,
+
+ pub(in crate) async_frame_recorder: Option<AsyncScreenshotGrabber>,
+ pub(in crate) async_screenshots: Option<AsyncScreenshotGrabber>,
+
+ /// List of profile results from previous frames. Can be retrieved
+ /// via get_frame_profiles().
+ cpu_profiles: VecDeque<CpuProfile>,
+ gpu_profiles: VecDeque<GpuProfile>,
+
+ /// Notification requests to be fulfilled after rendering.
+ notifications: Vec<NotificationRequest>,
+
+ device_size: Option<DeviceIntSize>,
+
+ /// A lazily created texture for the zoom debugging widget.
+ zoom_debug_texture: Option<Texture>,
+
+ /// The current mouse position. This is used for debugging
+ /// functionality only, such as the debug zoom widget.
+ cursor_position: DeviceIntPoint,
+
+ /// Guards to check if we might be rendering a frame with expired texture
+ /// cache entries.
+ shared_texture_cache_cleared: bool,
+
+ /// The set of documents which we've seen a publish for since last render.
+ documents_seen: FastHashSet<DocumentId>,
+
+ #[cfg(feature = "capture")]
+ read_fbo: FBOId,
+ #[cfg(feature = "replay")]
+ owned_external_images: FastHashMap<(ExternalImageId, u8), ExternalTexture>,
+
+ /// The compositing config, affecting how WR composites into the final scene.
+ compositor_config: CompositorConfig,
+
+ current_compositor_kind: CompositorKind,
+
+ /// Maintains a set of allocated native composite surfaces. This allows any
+ /// currently allocated surfaces to be cleaned up as soon as deinit() is
+ /// called (the normal bookkeeping for native surfaces exists in the
+ /// render backend thread).
+ allocated_native_surfaces: FastHashSet<NativeSurfaceId>,
+
+ /// If true, partial present state has been reset and everything needs to
+ /// be drawn on the next render.
+ force_redraw: bool,
+
+ /// State related to the debug / profiling overlays
+ debug_overlay_state: DebugOverlayState,
+
+ /// Tracks the dirty rectangles from previous frames. Used on platforms
+ /// that require keeping the front buffer fully correct when doing
+ /// partial present (e.g. unix desktop with EGL_EXT_buffer_age).
+ buffer_damage_tracker: BufferDamageTracker,
+
+ max_primitive_instance_count: usize,
+ enable_instancing: bool,
+}
+
+#[derive(Debug)]
+pub enum RendererError {
+ Shader(ShaderError),
+ Thread(std::io::Error),
+ Resource(ResourceCacheError),
+ MaxTextureSize,
+}
+
+impl From<ShaderError> for RendererError {
+ fn from(err: ShaderError) -> Self {
+ RendererError::Shader(err)
+ }
+}
+
+impl From<std::io::Error> for RendererError {
+ fn from(err: std::io::Error) -> Self {
+ RendererError::Thread(err)
+ }
+}
+
+impl From<ResourceCacheError> for RendererError {
+ fn from(err: ResourceCacheError) -> Self {
+ RendererError::Resource(err)
+ }
+}
+
+impl Renderer {
+ /// Initializes WebRender and creates a `Renderer` and `RenderApiSender`.
+ ///
+ /// # Examples
+ /// Initializes a `Renderer` with some reasonable values. For more information see
+ /// [`RendererOptions`][rendereroptions].
+ ///
+ /// ```rust,ignore
+ /// # use webrender::renderer::Renderer;
+ /// # use std::path::PathBuf;
+ /// let opts = webrender::RendererOptions {
+ /// device_pixel_ratio: 1.0,
+ /// resource_override_path: None,
+ /// enable_aa: false,
+ /// };
+ /// let (renderer, sender) = Renderer::new(opts);
+ /// ```
+ /// [rendereroptions]: struct.RendererOptions.html
+ pub fn new(
+ gl: Rc<dyn gl::Gl>,
+ notifier: Box<dyn RenderNotifier>,
+ mut options: RendererOptions,
+ shaders: Option<&SharedShaders>,
+ ) -> Result<(Self, RenderApiSender), RendererError> {
+ if !wr_has_been_initialized() {
+ // If the profiler feature is enabled, try to load the profiler shared library
+ // if the path was provided.
+ #[cfg(feature = "profiler")]
+ unsafe {
+ if let Ok(ref tracy_path) = std::env::var("WR_TRACY_PATH") {
+ let ok = tracy_rs::load(tracy_path);
+ println!("Load tracy from {} -> {}", tracy_path, ok);
+ }
+ }
+
+ register_thread_with_profiler("Compositor".to_owned());
+ }
+
+ HAS_BEEN_INITIALIZED.store(true, Ordering::SeqCst);
+
+ let (api_tx, api_rx) = unbounded_channel();
+ let (result_tx, result_rx) = unbounded_channel();
+ let gl_type = gl.get_type();
+
+ let debug_server = new_debug_server(options.start_debug_server, api_tx.clone());
+
+ let mut device = Device::new(
+ gl,
+ options.crash_annotator.clone(),
+ options.resource_override_path.clone(),
+ options.use_optimized_shaders,
+ options.upload_method.clone(),
+ options.cached_programs.take(),
+ options.allow_texture_storage_support,
+ options.allow_texture_swizzling,
+ options.dump_shader_source.take(),
+ options.surface_origin_is_top_left,
+ options.panic_on_gl_error,
+ );
+
+ let color_cache_formats = device.preferred_color_formats();
+ let swizzle_settings = device.swizzle_settings();
+ let use_dual_source_blending =
+ device.get_capabilities().supports_dual_source_blending &&
+ options.allow_dual_source_blending;
+ let ext_blend_equation_advanced =
+ options.allow_advanced_blend_equation &&
+ device.get_capabilities().supports_advanced_blend_equation;
+ let ext_blend_equation_advanced_coherent =
+ device.supports_extension("GL_KHR_blend_equation_advanced_coherent");
+
+ // 512 is the minimum that the texture cache can work with.
+ const MIN_TEXTURE_SIZE: i32 = 512;
+ if let Some(user_limit) = options.max_texture_size {
+ assert!(user_limit >= MIN_TEXTURE_SIZE);
+ device.clamp_max_texture_size(user_limit);
+ }
+ if device.max_texture_size() < MIN_TEXTURE_SIZE {
+ // Broken GL contexts can return a max texture size of zero (See #1260).
+ // Better to gracefully fail now than panic as soon as a texture is allocated.
+ error!(
+ "Device reporting insufficient max texture size ({})",
+ device.max_texture_size()
+ );
+ return Err(RendererError::MaxTextureSize);
+ }
+ let max_texture_size = device.max_texture_size();
+
+ device.begin_frame();
+
+ let shaders = match shaders {
+ Some(shaders) => Rc::clone(shaders),
+ None => Rc::new(RefCell::new(Shaders::new(&mut device, gl_type, &options)?)),
+ };
+
+ let dither_matrix_texture = if options.enable_dithering {
+ let dither_matrix: [u8; 64] = [
+ 0,
+ 48,
+ 12,
+ 60,
+ 3,
+ 51,
+ 15,
+ 63,
+ 32,
+ 16,
+ 44,
+ 28,
+ 35,
+ 19,
+ 47,
+ 31,
+ 8,
+ 56,
+ 4,
+ 52,
+ 11,
+ 59,
+ 7,
+ 55,
+ 40,
+ 24,
+ 36,
+ 20,
+ 43,
+ 27,
+ 39,
+ 23,
+ 2,
+ 50,
+ 14,
+ 62,
+ 1,
+ 49,
+ 13,
+ 61,
+ 34,
+ 18,
+ 46,
+ 30,
+ 33,
+ 17,
+ 45,
+ 29,
+ 10,
+ 58,
+ 6,
+ 54,
+ 9,
+ 57,
+ 5,
+ 53,
+ 42,
+ 26,
+ 38,
+ 22,
+ 41,
+ 25,
+ 37,
+ 21,
+ ];
+
+ let texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::R8,
+ 8,
+ 8,
+ TextureFilter::Nearest,
+ None,
+ 1,
+ );
+ device.upload_texture_immediate(&texture, &dither_matrix);
+
+ Some(texture)
+ } else {
+ None
+ };
+
+ let max_primitive_instance_count =
+ RendererOptions::MAX_INSTANCE_BUFFER_SIZE / mem::size_of::<PrimitiveInstanceData>();
+ let vaos = vertex::RendererVAOs::new(
+ &mut device,
+ if options.enable_instancing { None } else { NonZeroUsize::new(max_primitive_instance_count) },
+ );
+
+ let texture_upload_pbo_pool = UploadPBOPool::new(&mut device, options.upload_pbo_default_size);
+ let texture_resolver = TextureResolver::new(&mut device);
+
+ let mut vertex_data_textures = Vec::new();
+ for _ in 0 .. VERTEX_DATA_TEXTURE_COUNT {
+ vertex_data_textures.push(vertex::VertexDataTextures::new());
+ }
+
+ // On some (mostly older, integrated) GPUs, the normal GPU texture cache update path
+ // doesn't work well when running on ANGLE, causing CPU stalls inside D3D and/or the
+ // GPU driver. See https://bugzilla.mozilla.org/show_bug.cgi?id=1576637 for much
+ // more detail. To reduce the number of code paths we have active that require testing,
+ // we will enable the GPU cache scatter update path on all devices running with ANGLE.
+ // We want a better solution long-term, but for now this is a significant performance
+ // improvement on HD4600 era GPUs, and shouldn't hurt performance in a noticeable
+ // way on other systems running under ANGLE.
+ let is_software = device.get_capabilities().renderer_name.starts_with("Software");
+
+ // On other GL platforms, like macOS or Android, creating many PBOs is very inefficient.
+ // This is what happens in GPU cache updates in PBO path. Instead, we switch everything
+ // except software GL to use the GPU scattered updates.
+ let supports_scatter = match gl_type {
+ gl::GlType::Gl => true,
+ gl::GlType::Gles => device.supports_extension("GL_EXT_color_buffer_float"),
+ };
+
+ let gpu_cache_texture = gpu_cache::GpuCacheTexture::new(
+ &mut device,
+ supports_scatter && !is_software,
+ )?;
+
+ device.end_frame();
+
+ let backend_notifier = notifier.clone();
+
+ let prefer_subpixel_aa = options.force_subpixel_aa || (options.enable_subpixel_aa && use_dual_source_blending);
+ let default_font_render_mode = match (options.enable_aa, prefer_subpixel_aa) {
+ (true, true) => FontRenderMode::Subpixel,
+ (true, false) => FontRenderMode::Alpha,
+ (false, _) => FontRenderMode::Mono,
+ };
+
+ let compositor_kind = match options.compositor_config {
+ CompositorConfig::Draw { max_partial_present_rects, draw_previous_partial_present_regions, .. } => {
+ CompositorKind::Draw { max_partial_present_rects, draw_previous_partial_present_regions }
+ }
+ CompositorConfig::Native { ref compositor, max_update_rects, .. } => {
+ let capabilities = compositor.get_capabilities();
+
+ CompositorKind::Native {
+ max_update_rects,
+ virtual_surface_size: capabilities.virtual_surface_size,
+ }
+ }
+ };
+
+ let config = FrameBuilderConfig {
+ default_font_render_mode,
+ dual_source_blending_is_enabled: true,
+ dual_source_blending_is_supported: use_dual_source_blending,
+ chase_primitive: options.chase_primitive,
+ testing: options.testing,
+ gpu_supports_fast_clears: options.gpu_supports_fast_clears,
+ gpu_supports_advanced_blend: ext_blend_equation_advanced,
+ advanced_blend_is_coherent: ext_blend_equation_advanced_coherent,
+ gpu_supports_render_target_partial_update: device.get_capabilities().supports_render_target_partial_update,
+ batch_lookback_count: RendererOptions::BATCH_LOOKBACK_COUNT,
+ background_color: options.clear_color,
+ compositor_kind,
+ tile_size_override: None,
+ max_depth_ids: device.max_depth_ids(),
+ max_target_size: max_texture_size,
+ force_invalidation: false,
+ };
+ info!("WR {:?}", config);
+
+ let device_pixel_ratio = options.device_pixel_ratio;
+ let debug_flags = options.debug_flags;
+ let size_of_op = options.size_of_op;
+ let enclosing_size_of_op = options.enclosing_size_of_op;
+ let make_size_of_ops =
+ move || size_of_op.map(|o| MallocSizeOfOps::new(o, enclosing_size_of_op));
+ let thread_listener = Arc::new(options.thread_listener);
+ let thread_listener_for_rayon_start = thread_listener.clone();
+ let thread_listener_for_rayon_end = thread_listener.clone();
+ let workers = options
+ .workers
+ .take()
+ .unwrap_or_else(|| {
+ let worker = ThreadPoolBuilder::new()
+ .thread_name(|idx|{ format!("WRWorker#{}", idx) })
+ .start_handler(move |idx| {
+ register_thread_with_profiler(format!("WRWorker#{}", idx));
+ if let Some(ref thread_listener) = *thread_listener_for_rayon_start {
+ thread_listener.thread_started(&format!("WRWorker#{}", idx));
+ }
+ })
+ .exit_handler(move |idx| {
+ if let Some(ref thread_listener) = *thread_listener_for_rayon_end {
+ thread_listener.thread_stopped(&format!("WRWorker#{}", idx));
+ }
+ })
+ .build();
+ Arc::new(worker.unwrap())
+ });
+ let sampler = options.sampler;
+ let namespace_alloc_by_client = options.namespace_alloc_by_client;
+
+ let font_instances = SharedFontInstanceMap::new();
+
+ 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 thread_listener_for_lp_scene_builder = thread_listener.clone();
+ let scene_builder_hooks = options.scene_builder_hooks;
+ let rb_thread_name = format!("WRRenderBackend#{}", options.renderer_id.unwrap_or(0));
+ let scene_thread_name = format!("WRSceneBuilder#{}", options.renderer_id.unwrap_or(0));
+ let lp_scene_thread_name = format!("WRSceneBuilderLP#{}", options.renderer_id.unwrap_or(0));
+ let glyph_rasterizer = GlyphRasterizer::new(workers)?;
+
+ let (scene_builder_channels, scene_tx) =
+ SceneBuilderThreadChannels::new(api_tx.clone());
+
+ let sb_font_instances = font_instances.clone();
+
+ thread::Builder::new().name(scene_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(scene_thread_name.clone());
+ if let Some(ref thread_listener) = *thread_listener_for_scene_builder {
+ thread_listener.thread_started(&scene_thread_name);
+ }
+
+ let mut scene_builder = SceneBuilderThread::new(
+ config,
+ device_pixel_ratio,
+ sb_font_instances,
+ make_size_of_ops(),
+ scene_builder_hooks,
+ scene_builder_channels,
+ );
+ scene_builder.run();
+
+ if let Some(ref thread_listener) = *thread_listener_for_scene_builder {
+ thread_listener.thread_stopped(&scene_thread_name);
+ }
+ })?;
+
+ let low_priority_scene_tx = if options.support_low_priority_transactions {
+ let (low_priority_scene_tx, low_priority_scene_rx) = unbounded_channel();
+ let lp_builder = LowPrioritySceneBuilderThread {
+ rx: low_priority_scene_rx,
+ tx: scene_tx.clone(),
+ simulate_slow_ms: 0,
+ };
+
+ thread::Builder::new().name(lp_scene_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(lp_scene_thread_name.clone());
+ if let Some(ref thread_listener) = *thread_listener_for_lp_scene_builder {
+ thread_listener.thread_started(&lp_scene_thread_name);
+ }
+
+ let mut scene_builder = lp_builder;
+ scene_builder.run();
+
+ if let Some(ref thread_listener) = *thread_listener_for_lp_scene_builder {
+ thread_listener.thread_stopped(&lp_scene_thread_name);
+ }
+ })?;
+
+ low_priority_scene_tx
+ } else {
+ scene_tx.clone()
+ };
+
+ let backend_blob_handler = blob_image_handler
+ .as_ref()
+ .map(|handler| handler.create_similar());
+
+ let texture_cache_config = options.texture_cache_config.clone();
+ let mut picture_tile_size = options.picture_tile_size.unwrap_or(picture::TILE_SIZE_DEFAULT);
+ // Clamp the picture tile size to reasonable values.
+ picture_tile_size.width = picture_tile_size.width.max(128).min(4096);
+ picture_tile_size.height = picture_tile_size.height.max(128).min(4096);
+
+ let rb_scene_tx = scene_tx.clone();
+ let rb_low_priority_scene_tx = scene_tx.clone();
+ let rb_font_instances = font_instances.clone();
+ let enable_multithreading = options.enable_multithreading;
+ thread::Builder::new().name(rb_thread_name.clone()).spawn(move || {
+ register_thread_with_profiler(rb_thread_name.clone());
+ if let Some(ref thread_listener) = *thread_listener_for_render_backend {
+ thread_listener.thread_started(&rb_thread_name);
+ }
+
+ let texture_cache = TextureCache::new(
+ max_texture_size,
+ picture_tile_size,
+ color_cache_formats,
+ swizzle_settings,
+ &texture_cache_config,
+ );
+
+ let glyph_cache = GlyphCache::new();
+
+ let mut resource_cache = ResourceCache::new(
+ texture_cache,
+ glyph_rasterizer,
+ glyph_cache,
+ rb_font_instances,
+ );
+
+ resource_cache.enable_multithreading(enable_multithreading);
+
+ let mut backend = RenderBackend::new(
+ api_rx,
+ result_tx,
+ rb_scene_tx,
+ rb_low_priority_scene_tx,
+ device_pixel_ratio,
+ resource_cache,
+ backend_notifier,
+ backend_blob_handler,
+ config,
+ sampler,
+ make_size_of_ops(),
+ debug_flags,
+ namespace_alloc_by_client,
+ );
+ backend.run();
+ if let Some(ref thread_listener) = *thread_listener_for_render_backend {
+ thread_listener.thread_stopped(&rb_thread_name);
+ }
+ })?;
+
+ let debug_method = if !options.enable_gpu_markers {
+ // The GPU markers are disabled.
+ GpuDebugMethod::None
+ } else if device.supports_extension("GL_KHR_debug") {
+ GpuDebugMethod::KHR
+ } else if device.supports_extension("GL_EXT_debug_marker") {
+ GpuDebugMethod::MarkerEXT
+ } else {
+ println!("Warning: asking to enable_gpu_markers but no supporting extension was found");
+ GpuDebugMethod::None
+ };
+
+ info!("using {:?}", debug_method);
+
+ let gpu_profiler = GpuProfiler::new(Rc::clone(device.rc_gl()), debug_method);
+ #[cfg(feature = "capture")]
+ let read_fbo = device.create_fbo();
+
+ let mut renderer = Renderer {
+ result_rx,
+ debug_server,
+ device,
+ active_documents: FastHashMap::default(),
+ pending_texture_updates: Vec::new(),
+ pending_texture_cache_updates: false,
+ pending_native_surface_updates: Vec::new(),
+ pending_gpu_cache_updates: Vec::new(),
+ pending_gpu_cache_clear: false,
+ pending_shader_updates: Vec::new(),
+ shaders,
+ debug: debug::LazyInitializedDebugRenderer::new(),
+ debug_flags: DebugFlags::empty(),
+ profile: TransactionProfile::new(),
+ frame_counter: 0,
+ resource_upload_time: 0.0,
+ gpu_cache_upload_time: 0.0,
+ profiler: Profiler::new(),
+ max_recorded_profiles: options.max_recorded_profiles,
+ clear_color: options.clear_color,
+ enable_clear_scissor: options.enable_clear_scissor,
+ enable_advanced_blend_barriers: !ext_blend_equation_advanced_coherent,
+ clear_caches_with_quads: options.clear_caches_with_quads,
+ last_time: 0,
+ gpu_profiler,
+ vaos,
+ vertex_data_textures,
+ current_vertex_data_textures: 0,
+ pipeline_info: PipelineInfo::default(),
+ dither_matrix_texture,
+ external_image_handler: None,
+ size_of_ops: make_size_of_ops(),
+ cpu_profiles: VecDeque::new(),
+ gpu_profiles: VecDeque::new(),
+ gpu_cache_texture,
+ gpu_cache_debug_chunks: Vec::new(),
+ gpu_cache_frame_id: FrameId::INVALID,
+ gpu_cache_overflow: false,
+ texture_upload_pbo_pool,
+ texture_resolver,
+ renderer_errors: Vec::new(),
+ async_frame_recorder: None,
+ async_screenshots: None,
+ #[cfg(feature = "capture")]
+ read_fbo,
+ #[cfg(feature = "replay")]
+ owned_external_images: FastHashMap::default(),
+ notifications: Vec::new(),
+ device_size: None,
+ zoom_debug_texture: None,
+ cursor_position: DeviceIntPoint::zero(),
+ shared_texture_cache_cleared: false,
+ documents_seen: FastHashSet::default(),
+ force_redraw: true,
+ compositor_config: options.compositor_config,
+ current_compositor_kind: compositor_kind,
+ allocated_native_surfaces: FastHashSet::default(),
+ debug_overlay_state: DebugOverlayState::new(),
+ buffer_damage_tracker: BufferDamageTracker::default(),
+ max_primitive_instance_count,
+ enable_instancing: options.enable_instancing,
+ };
+
+ // We initially set the flags to default and then now call set_debug_flags
+ // to ensure any potential transition when enabling a flag is run.
+ renderer.set_debug_flags(debug_flags);
+
+ let sender = RenderApiSender::new(
+ api_tx,
+ scene_tx,
+ low_priority_scene_tx,
+ blob_image_handler,
+ font_instances,
+ );
+ Ok((renderer, sender))
+ }
+
+ pub fn device_size(&self) -> Option<DeviceIntSize> {
+ self.device_size
+ }
+
+ /// Update the current position of the debug cursor.
+ pub fn set_cursor_position(
+ &mut self,
+ position: DeviceIntPoint,
+ ) {
+ self.cursor_position = position;
+ }
+
+ pub fn get_max_texture_size(&self) -> i32 {
+ self.device.max_texture_size()
+ }
+
+ pub fn get_graphics_api_info(&self) -> GraphicsApiInfo {
+ GraphicsApiInfo {
+ kind: GraphicsApi::OpenGL,
+ version: self.device.gl().get_string(gl::VERSION),
+ renderer: self.device.gl().get_string(gl::RENDERER),
+ }
+ }
+
+ pub fn preferred_color_format(&self) -> ImageFormat {
+ self.device.preferred_color_formats().external
+ }
+
+ pub fn optimal_texture_stride_alignment(&self, format: ImageFormat) -> usize {
+ self.device.optimal_pbo_stride().num_bytes(format).get()
+ }
+
+ pub fn set_clear_color(&mut self, color: Option<ColorF>) {
+ self.clear_color = color;
+ }
+
+ pub fn flush_pipeline_info(&mut self) -> PipelineInfo {
+ mem::replace(&mut self.pipeline_info, PipelineInfo::default())
+ }
+
+ /// Returns the Epoch of the current frame in a pipeline.
+ pub fn current_epoch(&self, document_id: DocumentId, pipeline_id: PipelineId) -> Option<Epoch> {
+ self.pipeline_info.epochs.get(&(pipeline_id, document_id)).cloned()
+ }
+
+ /// Processes the result queue.
+ ///
+ /// Should be called before `render()`, as texture cache updates are done here.
+ pub fn update(&mut self) {
+ profile_scope!("update");
+
+ // Pull any pending results and return the most recent.
+ while let Ok(msg) = self.result_rx.try_recv() {
+ match msg {
+ ResultMsg::PublishPipelineInfo(mut pipeline_info) => {
+ for ((pipeline_id, document_id), epoch) in pipeline_info.epochs {
+ self.pipeline_info.epochs.insert((pipeline_id, document_id), epoch);
+ }
+ self.pipeline_info.removed_pipelines.extend(pipeline_info.removed_pipelines.drain(..));
+ }
+ ResultMsg::PublishDocument(
+ document_id,
+ mut doc,
+ resource_update_list,
+ ) => {
+ // Add a new document to the active set
+
+ // If the document we are replacing must be drawn (in order to
+ // update the texture cache), issue a render just to
+ // off-screen targets, ie pass None to render_impl. We do this
+ // because a) we don't need to render to the main framebuffer
+ // so it is cheaper not to, and b) doing so without a
+ // subsequent present would break partial present.
+ if let Some(mut prev_doc) = self.active_documents.remove(&document_id) {
+ doc.profile.merge(&mut prev_doc.profile);
+
+ if prev_doc.frame.must_be_drawn() {
+ self.render_impl(
+ document_id,
+ &mut prev_doc,
+ None,
+ 0,
+ ).ok();
+ }
+ }
+
+ self.active_documents.insert(document_id, doc);
+
+ // IMPORTANT: The pending texture cache updates must be applied
+ // *after* the previous frame has been rendered above
+ // (if neceessary for a texture cache update). For
+ // an example of why this is required:
+ // 1) Previous frame contains a render task that
+ // targets Texture X.
+ // 2) New frame contains a texture cache update which
+ // frees Texture X.
+ // 3) bad stuff happens.
+
+ //TODO: associate `document_id` with target window
+ self.pending_texture_cache_updates |= !resource_update_list.texture_updates.updates.is_empty();
+ self.pending_texture_updates.push(resource_update_list.texture_updates);
+ self.pending_native_surface_updates.extend(resource_update_list.native_surface_updates);
+ self.documents_seen.insert(document_id);
+ }
+ ResultMsg::UpdateGpuCache(mut list) => {
+ if list.clear {
+ self.pending_gpu_cache_clear = true;
+ }
+ if list.clear {
+ self.gpu_cache_debug_chunks = Vec::new();
+ }
+ for cmd in mem::replace(&mut list.debug_commands, Vec::new()) {
+ match cmd {
+ GpuCacheDebugCmd::Alloc(chunk) => {
+ let row = chunk.address.v as usize;
+ if row >= self.gpu_cache_debug_chunks.len() {
+ self.gpu_cache_debug_chunks.resize(row + 1, Vec::new());
+ }
+ self.gpu_cache_debug_chunks[row].push(chunk);
+ },
+ GpuCacheDebugCmd::Free(address) => {
+ let chunks = &mut self.gpu_cache_debug_chunks[address.v as usize];
+ let pos = chunks.iter()
+ .position(|x| x.address == address).unwrap();
+ chunks.remove(pos);
+ },
+ }
+ }
+ self.pending_gpu_cache_updates.push(list);
+ }
+ ResultMsg::UpdateResources {
+ resource_updates,
+ memory_pressure,
+ } => {
+ if memory_pressure {
+ // If a memory pressure event arrives _after_ a new scene has
+ // been published that writes persistent targets (i.e. cached
+ // render tasks to the texture cache, or picture cache tiles)
+ // but _before_ the next update/render loop, those targets
+ // will not be updated due to the active_documents list being
+ // cleared at the end of this message. To work around that,
+ // if any of the existing documents have not rendered yet, and
+ // have picture/texture cache targets, force a render so that
+ // those targets are updated.
+ let active_documents = mem::replace(
+ &mut self.active_documents,
+ FastHashMap::default(),
+ );
+ for (doc_id, mut doc) in active_documents {
+ if doc.frame.must_be_drawn() {
+ // As this render will not be presented, we must pass None to
+ // render_impl. This avoids interfering with partial present
+ // logic, as well as being more efficient.
+ self.render_impl(
+ doc_id,
+ &mut doc,
+ None,
+ 0,
+ ).ok();
+ }
+ }
+ }
+
+ self.pending_texture_cache_updates |= !resource_updates.texture_updates.updates.is_empty();
+ self.pending_texture_updates.push(resource_updates.texture_updates);
+ self.pending_native_surface_updates.extend(resource_updates.native_surface_updates);
+ self.device.begin_frame();
+
+ self.update_texture_cache();
+ self.update_native_surfaces();
+
+ // Flush the render target pool on memory pressure.
+ //
+ // This needs to be separate from the block below because
+ // the device module asserts if we delete textures while
+ // not in a frame.
+ if memory_pressure {
+ self.texture_upload_pbo_pool.on_memory_pressure(&mut self.device);
+ }
+
+ self.device.end_frame();
+ }
+ ResultMsg::AppendNotificationRequests(mut notifications) => {
+ // We need to know specifically if there are any pending
+ // TextureCacheUpdate updates in any of the entries in
+ // pending_texture_updates. They may simply be nops, which do not
+ // need to prevent issuing the notification, and if so, may not
+ // cause a timely frame render to occur to wake up any listeners.
+ if !self.pending_texture_cache_updates {
+ drain_filter(
+ &mut notifications,
+ |n| { n.when() == Checkpoint::FrameTexturesUpdated },
+ |n| { n.notify(); },
+ );
+ }
+ self.notifications.append(&mut notifications);
+ }
+ ResultMsg::ForceRedraw => {
+ self.force_redraw = true;
+ }
+ ResultMsg::RefreshShader(path) => {
+ self.pending_shader_updates.push(path);
+ }
+ ResultMsg::DebugOutput(output) => match output {
+ DebugOutput::FetchDocuments(string) |
+ DebugOutput::FetchClipScrollTree(string) => {
+ self.debug_server.send(string);
+ }
+ #[cfg(feature = "capture")]
+ DebugOutput::SaveCapture(config, deferred) => {
+ self.save_capture(config, deferred);
+ }
+ #[cfg(feature = "replay")]
+ DebugOutput::LoadCapture(config, plain_externals) => {
+ self.active_documents.clear();
+ self.load_capture(config, plain_externals);
+ }
+ },
+ ResultMsg::DebugCommand(command) => {
+ self.handle_debug_command(command);
+ }
+ }
+ }
+ }
+
+ #[cfg(not(feature = "debugger"))]
+ fn get_screenshot_for_debugger(&mut self) -> String {
+ // Avoid unused param warning.
+ let _ = &self.debug_server;
+ String::new()
+ }
+
+ #[cfg(feature = "debugger")]
+ fn get_screenshot_for_debugger(&mut self) -> String {
+ use api::{ImageDescriptor, ImageDescriptorFlags};
+
+ let desc = ImageDescriptor::new(1024, 768, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE);
+ let data = self.device.read_pixels(&desc);
+ let screenshot = debug_server::Screenshot::new(desc.size, data);
+
+ serde_json::to_string(&screenshot).unwrap()
+ }
+
+ #[cfg(not(feature = "debugger"))]
+ fn get_passes_for_debugger(&self) -> String {
+ // Avoid unused param warning.
+ let _ = &self.debug_server;
+ String::new()
+ }
+
+ #[cfg(feature = "debugger")]
+ fn debug_alpha_target(target: &AlphaRenderTarget) -> debug_server::Target {
+ let mut debug_target = debug_server::Target::new("A8");
+
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Scalings",
+ target.scalings.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Zero Clears",
+ target.zero_clears.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "One Clears",
+ target.one_clears.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "BoxShadows [p]",
+ target.clip_batcher.primary_clips.box_shadows.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "BoxShadows [s]",
+ target.clip_batcher.secondary_clips.box_shadows.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Vertical Blur",
+ target.vertical_blurs.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Horizontal Blur",
+ target.horizontal_blurs.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "Slow Rectangles [p]",
+ target.clip_batcher.primary_clips.slow_rectangles.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "Fast Rectangles [p]",
+ target.clip_batcher.primary_clips.fast_rectangles.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "Slow Rectangles [s]",
+ target.clip_batcher.secondary_clips.slow_rectangles.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Clip,
+ "Fast Rectangles [s]",
+ target.clip_batcher.secondary_clips.fast_rectangles.len(),
+ );
+ for (_, items) in target.clip_batcher.primary_clips.images.iter() {
+ debug_target.add(debug_server::BatchKind::Clip, "Image mask [p]", items.len());
+ }
+ for (_, items) in target.clip_batcher.secondary_clips.images.iter() {
+ debug_target.add(debug_server::BatchKind::Clip, "Image mask [s]", items.len());
+ }
+
+ debug_target
+ }
+
+ #[cfg(feature = "debugger")]
+ fn debug_color_target(target: &ColorRenderTarget) -> debug_server::Target {
+ let mut debug_target = debug_server::Target::new("RGBA8");
+
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Scalings",
+ target.scalings.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Vertical Blur",
+ target.vertical_blurs.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Horizontal Blur",
+ target.horizontal_blurs.len(),
+ );
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "SVG Filters",
+ target.svg_filters.iter().map(|(_, batch)| batch.len()).sum(),
+ );
+
+ for alpha_batch_container in &target.alpha_batch_containers {
+ for batch in alpha_batch_container.opaque_batches.iter().rev() {
+ debug_target.add(
+ debug_server::BatchKind::Opaque,
+ batch.key.kind.debug_name(),
+ batch.instances.len(),
+ );
+ }
+
+ for batch in &alpha_batch_container.alpha_batches {
+ debug_target.add(
+ debug_server::BatchKind::Alpha,
+ batch.key.kind.debug_name(),
+ batch.instances.len(),
+ );
+ }
+ }
+
+ debug_target
+ }
+
+ #[cfg(feature = "debugger")]
+ fn debug_texture_cache_target(target: &TextureCacheRenderTarget) -> debug_server::Target {
+ let mut debug_target = debug_server::Target::new("Texture Cache");
+
+ debug_target.add(
+ debug_server::BatchKind::Cache,
+ "Horizontal Blur",
+ target.horizontal_blurs.len(),
+ );
+
+ debug_target
+ }
+
+ #[cfg(feature = "debugger")]
+ fn get_passes_for_debugger(&self) -> String {
+ let mut debug_passes = debug_server::PassList::new();
+
+ for (_, render_doc) in &self.active_documents {
+ for pass in &render_doc.frame.passes {
+ let mut debug_targets = Vec::new();
+ debug_targets.extend(pass.alpha.targets.iter().map(Self::debug_alpha_target));
+ debug_targets.extend(pass.color.targets.iter().map(Self::debug_color_target));
+ debug_targets.extend(pass.texture_cache.iter().map(|(_, target)| Self::debug_texture_cache_target(target)));
+
+ debug_passes.add(debug_server::Pass { targets: debug_targets });
+ }
+ }
+
+ serde_json::to_string(&debug_passes).unwrap()
+ }
+
+ #[cfg(not(feature = "debugger"))]
+ fn get_render_tasks_for_debugger(&self) -> String {
+ String::new()
+ }
+
+ #[cfg(feature = "debugger")]
+ fn get_render_tasks_for_debugger(&self) -> String {
+ let mut debug_root = debug_server::RenderTaskList::new();
+
+ for (_, render_doc) in &self.active_documents {
+ let debug_node = debug_server::TreeNode::new("document render tasks");
+ let mut builder = debug_server::TreeNodeBuilder::new(debug_node);
+
+ let render_tasks = &render_doc.frame.render_tasks;
+ match render_tasks.tasks.first() {
+ Some(main_task) => main_task.print_with(&mut builder, render_tasks),
+ None => continue,
+ };
+
+ debug_root.add(builder.build());
+ }
+
+ serde_json::to_string(&debug_root).unwrap()
+ }
+
+ fn handle_debug_command(&mut self, command: DebugCommand) {
+ match command {
+ DebugCommand::EnableDualSourceBlending(_) |
+ DebugCommand::SetPictureTileSize(_) => {
+ panic!("Should be handled by render backend");
+ }
+ DebugCommand::FetchDocuments |
+ DebugCommand::FetchClipScrollTree => {}
+ DebugCommand::FetchRenderTasks => {
+ let json = self.get_render_tasks_for_debugger();
+ self.debug_server.send(json);
+ }
+ DebugCommand::FetchPasses => {
+ let json = self.get_passes_for_debugger();
+ self.debug_server.send(json);
+ }
+ DebugCommand::FetchScreenshot => {
+ let json = self.get_screenshot_for_debugger();
+ self.debug_server.send(json);
+ }
+ DebugCommand::SaveCapture(..) |
+ DebugCommand::LoadCapture(..) |
+ DebugCommand::StartCaptureSequence(..) |
+ DebugCommand::StopCaptureSequence => {
+ panic!("Capture commands are not welcome here! Did you build with 'capture' feature?")
+ }
+ DebugCommand::ClearCaches(_)
+ | DebugCommand::SimulateLongSceneBuild(_)
+ | DebugCommand::SimulateLongLowPrioritySceneBuild(_)
+ | DebugCommand::EnableNativeCompositor(_)
+ | DebugCommand::SetBatchingLookback(_)
+ | DebugCommand::EnableMultithreading(_) => {}
+ DebugCommand::InvalidateGpuCache => {
+ self.gpu_cache_texture.invalidate();
+ }
+ DebugCommand::SetFlags(flags) => {
+ self.set_debug_flags(flags);
+ }
+ }
+ }
+
+ /// Set a callback for handling external images.
+ pub fn set_external_image_handler(&mut self, handler: Box<dyn ExternalImageHandler>) {
+ self.external_image_handler = Some(handler);
+ }
+
+ /// Retrieve (and clear) the current list of recorded frame profiles.
+ pub fn get_frame_profiles(&mut self) -> (Vec<CpuProfile>, Vec<GpuProfile>) {
+ let cpu_profiles = self.cpu_profiles.drain(..).collect();
+ let gpu_profiles = self.gpu_profiles.drain(..).collect();
+ (cpu_profiles, gpu_profiles)
+ }
+
+ /// Reset the current partial present state. This forces the entire framebuffer
+ /// to be refreshed next time `render` is called.
+ pub fn force_redraw(&mut self) {
+ self.force_redraw = true;
+ }
+
+ /// Renders the current frame.
+ ///
+ /// A Frame is supplied by calling [`generate_frame()`][webrender_api::Transaction::generate_frame].
+ /// buffer_age is the age of the current backbuffer. It is only relevant if partial present
+ /// is active, otherwise 0 should be passed here.
+ pub fn render(
+ &mut self,
+ device_size: DeviceIntSize,
+ buffer_age: usize,
+ ) -> Result<RenderResults, Vec<RendererError>> {
+ self.device_size = Some(device_size);
+
+ // TODO(gw): We want to make the active document that is
+ // being rendered configurable via the public
+ // API in future. For now, just select the last
+ // added document as the active one to render
+ // (Gecko only ever creates a single document
+ // per renderer right now).
+ let doc_id = self.active_documents.keys().last().cloned();
+
+ let result = match doc_id {
+ Some(doc_id) => {
+ // Remove the doc from the map to appease the borrow checker
+ let mut doc = self.active_documents
+ .remove(&doc_id)
+ .unwrap();
+
+ let result = self.render_impl(
+ doc_id,
+ &mut doc,
+ Some(device_size),
+ buffer_age,
+ );
+
+ self.active_documents.insert(doc_id, doc);
+
+ result
+ }
+ None => {
+ self.last_time = precise_time_ns();
+ Ok(RenderResults::default())
+ }
+ };
+
+ drain_filter(
+ &mut self.notifications,
+ |n| { n.when() == Checkpoint::FrameRendered },
+ |n| { n.notify(); },
+ );
+
+ // This is the end of the rendering pipeline. If some notifications are is still there,
+ // just clear them and they will autimatically fire the Checkpoint::TransactionDropped
+ // event. Otherwise they would just pile up in this vector forever.
+ self.notifications.clear();
+
+ tracy_frame_marker!();
+
+ result
+ }
+
+ /// Update the state of any debug / profiler overlays. This is currently only needed
+ /// when running with the native compositor enabled.
+ fn update_debug_overlay(&mut self, framebuffer_size: DeviceIntSize) {
+ // If any of the following debug flags are set, something will be drawn on the debug overlay.
+ self.debug_overlay_state.is_enabled = self.debug_flags.intersects(
+ DebugFlags::PROFILER_DBG |
+ DebugFlags::RENDER_TARGET_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::EPOCHS |
+ DebugFlags::GPU_CACHE_DBG |
+ DebugFlags::PICTURE_CACHING_DBG |
+ DebugFlags::PRIMITIVE_DBG |
+ DebugFlags::ZOOM_DBG
+ );
+
+ // Update the debug overlay surface, if we are running in native compositor mode.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+
+ // If there is a current surface, destroy it if we don't need it for this frame, or if
+ // the size has changed.
+ if let Some(current_size) = self.debug_overlay_state.current_size {
+ if !self.debug_overlay_state.is_enabled || current_size != framebuffer_size {
+ compositor.destroy_surface(NativeSurfaceId::DEBUG_OVERLAY);
+ self.debug_overlay_state.current_size = None;
+ }
+ }
+
+ // Allocate a new surface, if we need it and there isn't one.
+ if self.debug_overlay_state.is_enabled && self.debug_overlay_state.current_size.is_none() {
+ compositor.create_surface(
+ NativeSurfaceId::DEBUG_OVERLAY,
+ DeviceIntPoint::zero(),
+ framebuffer_size,
+ false,
+ );
+ compositor.create_tile(
+ NativeTileId::DEBUG_OVERLAY,
+ );
+ self.debug_overlay_state.current_size = Some(framebuffer_size);
+ }
+ }
+ }
+
+ /// Bind a draw target for the debug / profiler overlays, if required.
+ fn bind_debug_overlay(&mut self, device_size: DeviceIntSize) -> Option<DrawTarget> {
+ // Debug overlay setup are only required in native compositing mode
+ if self.debug_overlay_state.is_enabled {
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ let surface_size = self.debug_overlay_state.current_size.unwrap();
+
+ // Bind the native surface
+ let surface_info = compositor.bind(
+ NativeTileId::DEBUG_OVERLAY,
+ DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ surface_size,
+ ),
+ DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ surface_size,
+ ),
+ );
+
+ // Bind the native surface to current FBO target
+ let draw_target = DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: surface_size,
+ };
+ self.device.bind_draw_target(draw_target);
+
+ // When native compositing, clear the debug overlay each frame.
+ self.device.clear_target(
+ Some([0.0, 0.0, 0.0, 0.0]),
+ None, // debug renderer does not use depth
+ None,
+ );
+
+ Some(draw_target)
+ } else {
+ // If we're not using the native compositor, then the default
+ // frame buffer is already bound. Create a DrawTarget for it and
+ // return it.
+ Some(DrawTarget::new_default(device_size, self.device.surface_origin_is_top_left()))
+ }
+ } else {
+ None
+ }
+ }
+
+ /// Unbind the draw target for debug / profiler overlays, if required.
+ fn unbind_debug_overlay(&mut self) {
+ // Debug overlay setup are only required in native compositing mode
+ if self.debug_overlay_state.is_enabled {
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ // Unbind the draw target and add it to the visual tree to be composited
+ compositor.unbind();
+
+ compositor.add_surface(
+ NativeSurfaceId::DEBUG_OVERLAY,
+ CompositorSurfaceTransform::identity(),
+ DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ self.debug_overlay_state.current_size.unwrap(),
+ ),
+ ImageRendering::Auto,
+ );
+ }
+ }
+ }
+
+ // If device_size is None, don't render to the main frame buffer. This is useful to
+ // update texture cache render tasks but avoid doing a full frame render. If the
+ // render is not going to be presented, then this must be set to None, as performing a
+ // composite without a present will confuse partial present.
+ fn render_impl(
+ &mut self,
+ doc_id: DocumentId,
+ active_doc: &mut RenderedDocument,
+ device_size: Option<DeviceIntSize>,
+ buffer_age: usize,
+ ) -> Result<RenderResults, Vec<RendererError>> {
+ profile_scope!("render");
+ let mut results = RenderResults::default();
+ self.profile.start_time(profiler::RENDERER_TIME);
+
+ let compositor_kind = active_doc.frame.composite_state.compositor_kind;
+ // CompositorKind is updated
+ if self.current_compositor_kind != compositor_kind {
+ let enable = match (self.current_compositor_kind, compositor_kind) {
+ (CompositorKind::Native { .. }, CompositorKind::Draw { .. }) => {
+ if self.debug_overlay_state.current_size.is_some() {
+ self.compositor_config
+ .compositor()
+ .unwrap()
+ .destroy_surface(NativeSurfaceId::DEBUG_OVERLAY);
+ self.debug_overlay_state.current_size = None;
+ }
+ false
+ }
+ (CompositorKind::Draw { .. }, CompositorKind::Native { .. }) => {
+ true
+ }
+ (current_compositor_kind, active_doc_compositor_kind) => {
+ dbg!(current_compositor_kind, active_doc_compositor_kind);
+ unreachable!();
+ }
+ };
+
+ self.compositor_config
+ .compositor()
+ .unwrap()
+ .enable_native_compositor(enable);
+ self.current_compositor_kind = compositor_kind;
+ }
+
+ // The texture resolver scope should be outside of any rendering, including
+ // debug rendering. This ensures that when we return render targets to the
+ // pool via glInvalidateFramebuffer, we don't do any debug rendering after
+ // that point. Otherwise, the bind / invalidate / bind logic trips up the
+ // render pass logic in tiled / mobile GPUs, resulting in an extra copy /
+ // resolve step when the debug overlay is enabled.
+ self.texture_resolver.begin_frame();
+
+ if let Some(device_size) = device_size {
+ self.update_gpu_profile(device_size);
+ }
+
+ let cpu_frame_id = {
+ let _gm = self.gpu_profiler.start_marker("begin frame");
+ let frame_id = self.device.begin_frame();
+ self.gpu_profiler.begin_frame(frame_id);
+
+ self.device.disable_scissor();
+ self.device.disable_depth();
+ self.set_blend(false, FramebufferKind::Main);
+ //self.update_shaders();
+
+ self.update_texture_cache();
+ self.update_native_surfaces();
+
+ frame_id
+ };
+
+ // Inform the client that we are starting a composition transaction if native
+ // compositing is enabled. This needs to be done early in the frame, so that
+ // we can create debug overlays after drawing the main surfaces.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.begin_frame();
+ }
+
+ if let Some(device_size) = device_size {
+ // Update the state of the debug overlay surface, ensuring that
+ // the compositor mode has a suitable surface to draw to, if required.
+ self.update_debug_overlay(device_size);
+ }
+
+ let frame = &mut active_doc.frame;
+ let profile = &mut active_doc.profile;
+ assert!(self.current_compositor_kind == frame.composite_state.compositor_kind);
+
+ if self.shared_texture_cache_cleared {
+ assert!(self.documents_seen.contains(&doc_id),
+ "Cleared texture cache without sending new document frame.");
+ }
+
+ match self.prepare_gpu_cache(&frame.deferred_resolves) {
+ Ok(..) => {
+ assert!(frame.gpu_cache_frame_id <= self.gpu_cache_frame_id,
+ "Received frame depends on a later GPU cache epoch ({:?}) than one we received last via `UpdateGpuCache` ({:?})",
+ frame.gpu_cache_frame_id, self.gpu_cache_frame_id);
+
+ {
+ profile_scope!("gl.flush");
+ self.device.gl().flush(); // early start on gpu cache updates
+ }
+
+ self.draw_frame(
+ frame,
+ device_size,
+ buffer_age,
+ &mut results,
+ );
+
+ // TODO(nical): do this automatically by selecting counters in the wr profiler
+ // Profile marker for the number of invalidated picture cache
+ if thread_is_being_profiled() {
+ let duration = Duration::new(0,0);
+ if let Some(n) = self.profiler.get(profiler::RENDERED_PICTURE_TILES) {
+ let message = (n as usize).to_string();
+ add_text_marker(cstr!("NumPictureCacheInvalidated"), &message, duration);
+ }
+ }
+
+ if device_size.is_some() {
+ self.draw_frame_debug_items(&frame.debug_items);
+ }
+
+ self.profile.merge(profile);
+ }
+ Err(e) => {
+ self.renderer_errors.push(e);
+ }
+ }
+
+ self.unlock_external_images(&frame.deferred_resolves);
+
+ let _gm = self.gpu_profiler.start_marker("end frame");
+ self.gpu_profiler.end_frame();
+
+ if let Some(device_size) = device_size {
+ // Bind a surface to draw the debug / profiler information to.
+ if let Some(draw_target) = self.bind_debug_overlay(device_size) {
+ self.draw_render_target_debug(&draw_target);
+ self.draw_texture_cache_debug(&draw_target);
+ self.draw_gpu_cache_debug(device_size);
+ self.draw_zoom_debug(device_size);
+ self.draw_epoch_debug();
+ }
+ }
+
+ self.profile.end_time(profiler::RENDERER_TIME);
+ self.profile.end_time_if_started(profiler::TOTAL_FRAME_CPU_TIME);
+
+ let current_time = precise_time_ns();
+ if device_size.is_some() {
+ let time = profiler::ns_to_ms(current_time - self.last_time);
+ self.profile.set(profiler::FRAME_TIME, time);
+ }
+
+ if self.max_recorded_profiles > 0 {
+ while self.cpu_profiles.len() >= self.max_recorded_profiles {
+ self.cpu_profiles.pop_front();
+ }
+ let cpu_profile = CpuProfile::new(
+ cpu_frame_id,
+ (self.profile.get_or(profiler::FRAME_BUILDING_TIME, 0.0) * 1000000.0) as u64,
+ (self.profile.get_or(profiler::RENDERER_TIME, 0.0) * 1000000.0) as u64,
+ self.profile.get_or(profiler::DRAW_CALLS, 0.0) as usize,
+ );
+ self.cpu_profiles.push_back(cpu_profile);
+ }
+
+ if thread_is_being_profiled() {
+ let duration = Duration::new(0,0);
+ let message = (self.profile.get_or(profiler::DRAW_CALLS, 0.0) as usize).to_string();
+ add_text_marker(cstr!("NumDrawCalls"), &message, duration);
+ }
+
+ results.stats.texture_upload_mb = self.profile.get_or(profiler::TEXTURE_UPLOADS_MEM, 0.0);
+ self.frame_counter += 1;
+ results.stats.resource_upload_time = self.resource_upload_time;
+ self.resource_upload_time = 0.0;
+ results.stats.gpu_cache_upload_time = self.gpu_cache_upload_time;
+ self.gpu_cache_upload_time = 0.0;
+
+ // Note: this clears the values in self.profile.
+ self.profiler.set_counters(&mut self.profile);
+
+ // Note: profile counters must be set before this or they will count for next frame.
+ self.profiler.update();
+
+ if self.debug_flags.intersects(DebugFlags::PROFILER_DBG | DebugFlags::PROFILER_CAPTURE) {
+ if let Some(device_size) = device_size {
+ //TODO: take device/pixel ratio into equation?
+ if let Some(debug_renderer) = self.debug.get_mut(&mut self.device) {
+ self.profiler.draw_profile(
+ self.frame_counter,
+ debug_renderer,
+ device_size,
+ );
+ }
+ }
+ }
+
+ if self.debug_flags.contains(DebugFlags::ECHO_DRIVER_MESSAGES) {
+ self.device.echo_driver_messages();
+ }
+
+ if let Some(debug_renderer) = self.debug.try_get_mut() {
+ let small_screen = self.debug_flags.contains(DebugFlags::SMALL_SCREEN);
+ let scale = if small_screen { 1.6 } else { 1.0 };
+ // TODO(gw): Tidy this up so that compositor config integrates better
+ // with the (non-compositor) surface y-flip options.
+ let surface_origin_is_top_left = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => true,
+ CompositorKind::Draw { .. } => self.device.surface_origin_is_top_left(),
+ };
+ debug_renderer.render(
+ &mut self.device,
+ device_size,
+ scale,
+ surface_origin_is_top_left,
+ );
+ }
+ self.texture_upload_pbo_pool.end_frame(&mut self.device);
+ self.device.end_frame();
+
+ if device_size.is_some() {
+ self.last_time = current_time;
+
+ // Unbind the target for the debug overlay. No debug or profiler drawing
+ // can occur afer this point.
+ self.unbind_debug_overlay();
+ }
+
+ // Inform the client that we are finished this composition transaction if native
+ // compositing is enabled. This must be called after any debug / profiling compositor
+ // surfaces have been drawn and added to the visual tree.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ profile_scope!("compositor.end_frame");
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.end_frame();
+ }
+
+ self.documents_seen.clear();
+ self.shared_texture_cache_cleared = false;
+
+ if self.renderer_errors.is_empty() {
+ Ok(results)
+ } else {
+ Err(mem::replace(&mut self.renderer_errors, Vec::new()))
+ }
+ }
+
+ fn update_gpu_profile(&mut self, device_size: DeviceIntSize) {
+ let _gm = self.gpu_profiler.start_marker("build samples");
+ // Block CPU waiting for last frame's GPU profiles to arrive.
+ // In general this shouldn't block unless heavily GPU limited.
+ let (gpu_frame_id, timers, samplers) = self.gpu_profiler.build_samples();
+
+ if self.max_recorded_profiles > 0 {
+ while self.gpu_profiles.len() >= self.max_recorded_profiles {
+ self.gpu_profiles.pop_front();
+ }
+
+ self.gpu_profiles.push_back(GpuProfile::new(gpu_frame_id, &timers));
+ }
+
+ self.profiler.set_gpu_time_queries(timers);
+
+ if !samplers.is_empty() {
+ let screen_fraction = 1.0 / device_size.to_f32().area();
+
+ fn accumulate_sampler_value(description: &str, samplers: &[GpuSampler]) -> f32 {
+ let mut accum = 0.0;
+ for sampler in samplers {
+ if sampler.tag.label != description {
+ continue;
+ }
+
+ accum += sampler.count as f32;
+ }
+
+ accum
+ }
+
+ let alpha_targets = accumulate_sampler_value(&"Alpha targets", &samplers) * screen_fraction;
+ let transparent_pass = accumulate_sampler_value(&"Transparent pass", &samplers) * screen_fraction;
+ let opaque_pass = accumulate_sampler_value(&"Opaque pass", &samplers) * screen_fraction;
+ self.profile.set(profiler::ALPHA_TARGETS_SAMPLERS, alpha_targets);
+ self.profile.set(profiler::TRANSPARENT_PASS_SAMPLERS, transparent_pass);
+ self.profile.set(profiler::OPAQUE_PASS_SAMPLERS, opaque_pass);
+ self.profile.set(profiler::TOTAL_SAMPLERS, alpha_targets + transparent_pass + opaque_pass);
+ }
+ }
+
+ fn update_texture_cache(&mut self) {
+ profile_scope!("update_texture_cache");
+
+ let _gm = self.gpu_profiler.start_marker("texture cache update");
+ let mut pending_texture_updates = mem::replace(&mut self.pending_texture_updates, vec![]);
+ self.pending_texture_cache_updates = false;
+
+ self.profile.start_time(profiler::TEXTURE_CACHE_UPLOAD_TIME);
+
+ for update_list in pending_texture_updates.drain(..) {
+ for allocation in update_list.allocations {
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(_) => add_event_marker(c_str!("TextureCacheAlloc")),
+ TextureCacheAllocationKind::Reset(_) => add_event_marker(c_str!("TextureCacheReset")),
+ TextureCacheAllocationKind::Free => add_event_marker(c_str!("TextureCacheFree")),
+ };
+ let old = match allocation.kind {
+ TextureCacheAllocationKind::Alloc(ref info) |
+ TextureCacheAllocationKind::Reset(ref info) => {
+ // Create a new native texture, as requested by the texture cache.
+ //
+ // Ensure no PBO is bound when creating the texture storage,
+ // or GL will attempt to read data from there.
+ let mut texture = self.device.create_texture(
+ info.target,
+ info.format,
+ info.width,
+ info.height,
+ info.filter,
+ // This needs to be a render target because some render
+ // tasks get rendered into the texture cache.
+ Some(RenderTargetInfo { has_depth: info.has_depth }),
+ info.layer_count,
+ );
+
+ if info.is_shared_cache {
+ texture.flags_mut()
+ .insert(TextureFlags::IS_SHARED_TEXTURE_CACHE);
+
+ // On Mali-Gxx devices we use batched texture uploads as it performs much better.
+ // However, due to another driver bug we must ensure the textures are fully cleared,
+ // otherwise we get visual artefacts when blitting to the texture cache.
+ if self.device.get_capabilities().prefers_batched_texture_uploads &&
+ !self.device.get_capabilities().supports_render_target_partial_update
+ {
+ self.clear_texture(&texture, [0.0; 4]);
+ }
+
+ // Textures in the cache generally don't need to be cleared,
+ // but we do so if the debug display is active to make it
+ // easier to identify unallocated regions.
+ if self.debug_flags.contains(DebugFlags::TEXTURE_CACHE_DBG) {
+ self.clear_texture(&texture, TEXTURE_CACHE_DBG_CLEAR_COLOR);
+ }
+ }
+
+ self.texture_resolver.texture_cache_map.insert(allocation.id, texture)
+ }
+ TextureCacheAllocationKind::Free => {
+ self.texture_resolver.texture_cache_map.remove(&allocation.id)
+ }
+ };
+
+ match allocation.kind {
+ TextureCacheAllocationKind::Alloc(_) => {
+ assert!(old.is_none(), "Renderer and backend disagree!");
+ }
+ TextureCacheAllocationKind::Reset(_) |
+ TextureCacheAllocationKind::Free => {
+ assert!(old.is_some(), "Renderer and backend disagree!");
+ }
+ }
+
+ if let Some(old) = old {
+ self.device.delete_texture(old);
+ }
+ }
+
+ let mut bytes_uploaded = 0;
+
+ // On some devices performing many small texture uploads is slow, so instead we batch
+ // updates in to a small number of uploads to temporary textures, then copy from those
+ // textures to the correct place in the texture cache.
+ // A list of temporary textures that batches of updates are uploaded to.
+ let mut batch_upload_textures = Vec::new();
+ #[derive(Debug)]
+ pub struct BatchUploadCopy<'a> {
+ // Index within batch_upload_textures
+ src_texture_index: usize,
+ src_offset: DeviceIntPoint,
+ // We store the texture ID as well as a reference to the texture so that we
+ // can easily sort the copies to group them by destination target.
+ dest_texture_id: CacheTextureId,
+ dest_texture: &'a Texture,
+ dest_layer_index: LayerIndex,
+ dest_offset: DeviceIntPoint,
+ size: DeviceIntSize,
+ }
+ // A list of copies that must be performed from the temporary textures to the texture cache.
+ let mut batch_upload_copies = Vec::new();
+ #[derive(Debug)]
+ pub struct BatchUploadBuffer<'a> {
+ staging_buffer: UploadStagingBuffer<'a>,
+ texture_index: usize,
+ }
+ // For each texture format, this stores a list of staging buffers
+ // and a texture allocator for packing the buffers.
+ let mut batch_upload_buffers = FastHashMap::default();
+
+ // For best performance we use a single TextureUploader for all uploads.
+ // This allows us to fill PBOs more efficiently and therefore allocate fewer PBOs.
+ let mut uploader = self.device.upload_texture(
+ &mut self.texture_upload_pbo_pool,
+ );
+ let device = &mut self.device;
+
+ for (texture_id, updates) in update_list.updates {
+ let texture = &self.texture_resolver.texture_cache_map[&texture_id];
+
+ for update in updates {
+ let TextureCacheUpdate { rect, stride, offset, layer_index, format_override, source } = update;
+
+ let dummy_data;
+ let data = match source {
+ TextureUpdateSource::Bytes { ref data } => {
+ &data[offset as usize ..]
+ }
+ TextureUpdateSource::External { id, channel_index } => {
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+ // The filter is only relevant for NativeTexture external images.
+ match handler.lock(id, channel_index, ImageRendering::Auto).source {
+ ExternalImageSource::RawData(data) => {
+ &data[offset as usize ..]
+ }
+ ExternalImageSource::Invalid => {
+ // Create a local buffer to fill the pbo.
+ let bpp = texture.get_format().bytes_per_pixel();
+ let width = stride.unwrap_or(rect.size.width * bpp);
+ let total_size = width * rect.size.height;
+ // WR haven't support RGBAF32 format in texture_cache, so
+ // we use u8 type here.
+ dummy_data = vec![0xFFu8; total_size as usize];
+ &dummy_data
+ }
+ ExternalImageSource::NativeTexture(eid) => {
+ panic!("Unexpected external texture {:?} for the texture cache update of {:?}", eid, id);
+ }
+ }
+ }
+ TextureUpdateSource::DebugClear => {
+ let draw_target = DrawTarget::from_texture(
+ texture,
+ layer_index as usize,
+ false,
+ );
+ device.bind_draw_target(draw_target);
+ device.clear_target(
+ Some(TEXTURE_CACHE_DBG_CLEAR_COLOR),
+ None,
+ Some(draw_target.to_framebuffer_rect(update.rect.to_i32()))
+ );
+
+ continue;
+ }
+ };
+
+ const BATCH_UPLOAD_TEXTURE_SIZE: DeviceIntSize = DeviceIntSize::new(512, 512);
+ let use_batch_upload = device.get_capabilities().prefers_batched_texture_uploads &&
+ texture.flags().contains(TextureFlags::IS_SHARED_TEXTURE_CACHE) &&
+ rect.size.width <= BATCH_UPLOAD_TEXTURE_SIZE.width &&
+ rect.size.height <= BATCH_UPLOAD_TEXTURE_SIZE.height;
+
+ if use_batch_upload {
+ let (allocator, buffers) = batch_upload_buffers.entry(texture.get_format())
+ .or_insert_with(|| (GuillotineAllocator::new(None), Vec::new()));
+
+ // Allocate a region within the staging buffer for this update. If there is
+ // no room in an existing buffer then allocate another texture and buffer.
+ let (slice, origin) = match allocator.allocate(&rect.size) {
+ Some((slice, origin)) => (slice, origin),
+ None => {
+ let new_slice = FreeRectSlice(buffers.len() as u32);
+ allocator.extend(new_slice, BATCH_UPLOAD_TEXTURE_SIZE, rect.size);
+
+ let staging_texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ texture.get_format(),
+ BATCH_UPLOAD_TEXTURE_SIZE.width,
+ BATCH_UPLOAD_TEXTURE_SIZE.height,
+ TextureFilter::Nearest,
+ // Currently we need render target support as we always use glBlitFramebuffer
+ // to copy the texture data. Instead, we should use glCopyImageSubData on some
+ // platforms, and avoid creating the FBOs in that case.
+ Some(RenderTargetInfo { has_depth: false }),
+ 1,
+ );
+
+ let staging_buffer = uploader.stage(
+ device,
+ staging_texture.get_format(),
+ staging_texture.get_dimensions(),
+ ).unwrap();
+
+ let texture_index = batch_upload_textures.len();
+ batch_upload_textures.push(staging_texture);
+
+ buffers.push(BatchUploadBuffer {
+ staging_buffer: staging_buffer,
+ texture_index,
+ });
+ (new_slice, DeviceIntPoint::zero())
+ }
+ };
+ let buffer = &mut buffers[slice.0 as usize];
+ let allocated_rect = DeviceIntRect::new(origin, rect.size);
+
+ batch_upload_copies.push(BatchUploadCopy {
+ src_texture_index: buffer.texture_index,
+ src_offset: allocated_rect.origin,
+ dest_texture_id: texture_id,
+ dest_texture: texture,
+ dest_layer_index: layer_index as LayerIndex,
+ dest_offset: rect.origin,
+ size: rect.size,
+ });
+
+ unsafe {
+ let bytes_pp = texture.get_format().bytes_per_pixel() as usize;
+ let width_bytes = rect.size.width as usize * bytes_pp;
+ let src_stride = stride.map_or(width_bytes, |stride| {
+ assert!(stride >= 0);
+ stride as usize
+ });
+ let src_size = (rect.size.height as usize - 1) * src_stride + width_bytes;
+ assert!(src_size <= data.len());
+
+ let src: &[mem::MaybeUninit<u8>] = std::slice::from_raw_parts(data.as_ptr() as *const _, src_size);
+ let dst_stride = buffer.staging_buffer.get_stride();
+ let dst = buffer.staging_buffer.get_mapping();
+
+ // copy the data line-by-line in to the buffer so that we do not overwrite
+ // any other region of the buffer.
+ for y in 0..allocated_rect.size.height as usize {
+ let src_start = y * src_stride;
+ let src_end = src_start + width_bytes;
+ let dst_start = (allocated_rect.origin.y as usize + y as usize) * dst_stride +
+ allocated_rect.origin.x as usize * bytes_pp;
+ let dst_end = dst_start + width_bytes;
+
+ dst[dst_start..dst_end].copy_from_slice(&src[src_start..src_end])
+ }
+ }
+ } else {
+ bytes_uploaded += uploader.upload(
+ device,
+ texture,
+ rect,
+ layer_index,
+ stride,
+ format_override,
+ data.as_ptr(),
+ data.len()
+ );
+ }
+
+ if let TextureUpdateSource::External { id, channel_index } = source {
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+ handler.unlock(id, channel_index);
+ }
+ }
+ }
+
+ // Upload batched texture updates to their temporary textures.
+ for batch_buffer in batch_upload_buffers.into_iter().map(|(_, (_, buffers))| buffers).flatten() {
+ let texture = &batch_upload_textures[batch_buffer.texture_index];
+ bytes_uploaded += uploader.upload_staged(
+ device,
+ texture,
+ DeviceIntRect::from_size(texture.get_dimensions()),
+ 0,
+ None,
+ batch_buffer.staging_buffer,
+ );
+ }
+
+ // Flush all uploads, batched or otherwise.
+ uploader.flush(device);
+
+ // Copy updates that were batch uploaded to their correct destination in the texture cache.
+ // Sort them by destination and source to minimize framebuffer binding changes.
+ batch_upload_copies.sort_unstable_by_key(|b| (b.dest_texture_id.0, b.dest_layer_index, b.src_texture_index));
+ for copy in batch_upload_copies {
+ device.copy_texture_sub_region(
+ &batch_upload_textures[copy.src_texture_index],
+ copy.src_offset.x as _,
+ copy.src_offset.y as _,
+ 0,
+ copy.dest_texture,
+ copy.dest_offset.x as _,
+ copy.dest_offset.y as _,
+ copy.dest_layer_index,
+ copy.size.width as _,
+ copy.size.height as _,
+ 1,
+ );
+ }
+
+ for texture in batch_upload_textures.drain(..) {
+ device.delete_texture(texture);
+ }
+
+ if update_list.clears_shared_cache {
+ self.shared_texture_cache_cleared = true;
+ }
+
+ self.profile.inc(profiler::TEXTURE_UPLOADS);
+ self.profile.add(profiler::TEXTURE_UPLOADS_MEM, profiler::bytes_to_mb(bytes_uploaded));
+ }
+
+ drain_filter(
+ &mut self.notifications,
+ |n| { n.when() == Checkpoint::FrameTexturesUpdated },
+ |n| { n.notify(); },
+ );
+
+ let t = self.profile.end_time(profiler::TEXTURE_CACHE_UPLOAD_TIME);
+ self.resource_upload_time += t;
+ }
+
+ fn bind_textures(&mut self, textures: &BatchTextures) {
+ for i in 0 .. 3 {
+ self.texture_resolver.bind(
+ &textures.input.colors[i],
+ TextureSampler::color(i),
+ &mut self.device,
+ );
+ }
+
+ self.texture_resolver.bind(
+ &textures.clip_mask,
+ TextureSampler::ClipMask,
+ &mut self.device,
+ );
+
+ // TODO: this probably isn't the best place for this.
+ if let Some(ref texture) = self.dither_matrix_texture {
+ self.device.bind_texture(TextureSampler::Dither, texture, Swizzle::default());
+ }
+ }
+
+ fn draw_instanced_batch<T: Clone>(
+ &mut self,
+ data: &[T],
+ vertex_array_kind: VertexArrayKind,
+ textures: &BatchTextures,
+ stats: &mut RendererStats,
+ ) {
+ self.bind_textures(textures);
+
+ // If we end up with an empty draw call here, that means we have
+ // probably introduced unnecessary batch breaks during frame
+ // building - so we should be catching this earlier and removing
+ // the batch.
+ debug_assert!(!data.is_empty());
+
+ let vao = &self.vaos[vertex_array_kind];
+ self.device.bind_vao(vao);
+
+ let chunk_size = if self.debug_flags.contains(DebugFlags::DISABLE_BATCHING) {
+ 1
+ } else if vertex_array_kind == VertexArrayKind::Primitive {
+ self.max_primitive_instance_count
+ } else {
+ data.len()
+ };
+
+ for chunk in data.chunks(chunk_size) {
+ if self.enable_instancing {
+ self.device
+ .update_vao_instances(vao, chunk, ONE_TIME_USAGE_HINT, None);
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, chunk.len() as i32);
+ } else {
+ self.device
+ .update_vao_instances(vao, chunk, ONE_TIME_USAGE_HINT, NonZeroUsize::new(4));
+ self.device
+ .draw_indexed_triangles(6 * chunk.len() as i32);
+ }
+ self.profile.inc(profiler::DRAW_CALLS);
+ stats.total_draw_calls += 1;
+ }
+
+ self.profile.add(profiler::VERTICES, 6 * data.len());
+ }
+
+ fn handle_readback_composite(
+ &mut self,
+ draw_target: DrawTarget,
+ uses_scissor: bool,
+ source: &RenderTask,
+ backdrop: &RenderTask,
+ readback: &RenderTask,
+ ) {
+ if uses_scissor {
+ self.device.disable_scissor();
+ }
+
+ let texture_source = TextureSource::TextureCache(
+ readback.get_target_texture(),
+ Swizzle::default(),
+ );
+ let (cache_texture, _) = self.texture_resolver
+ .resolve(&texture_source).expect("bug: no source texture");
+
+ // Before submitting the composite batch, do the
+ // framebuffer readbacks that are needed for each
+ // composite operation in this batch.
+ let (readback_rect, readback_layer) = readback.get_target_rect();
+ let (backdrop_rect, _) = backdrop.get_target_rect();
+ let (backdrop_screen_origin, backdrop_scale) = match backdrop.kind {
+ RenderTaskKind::Picture(ref task_info) => (task_info.content_origin, task_info.device_pixel_scale),
+ _ => panic!("bug: composite on non-picture?"),
+ };
+ let (source_screen_origin, source_scale) = match source.kind {
+ RenderTaskKind::Picture(ref task_info) => (task_info.content_origin, task_info.device_pixel_scale),
+ _ => panic!("bug: composite on non-picture?"),
+ };
+
+ // Bind the FBO to blit the backdrop to.
+ // Called per-instance in case the layer (and therefore FBO)
+ // changes. The device will skip the GL call if the requested
+ // target is already bound.
+ let cache_draw_target = DrawTarget::from_texture(
+ cache_texture,
+ readback_layer.0 as usize,
+ false,
+ );
+
+ let source_in_backdrop_space = source_screen_origin * (backdrop_scale.0 / source_scale.0);
+
+ let mut src = DeviceIntRect::new(
+ (source_in_backdrop_space + (backdrop_rect.origin.to_f32() - backdrop_screen_origin)).to_i32(),
+ readback_rect.size,
+ );
+ let mut dest = readback_rect.to_i32();
+ let device_to_framebuffer = Scale::new(1i32);
+
+ // Need to invert the y coordinates and flip the image vertically when
+ // reading back from the framebuffer.
+ if draw_target.is_default() {
+ src.origin.y = draw_target.dimensions().height as i32 - src.size.height - src.origin.y;
+ dest.origin.y += dest.size.height;
+ dest.size.height = -dest.size.height;
+ }
+
+ self.device.blit_render_target(
+ draw_target.into(),
+ src * device_to_framebuffer,
+ cache_draw_target,
+ dest * device_to_framebuffer,
+ TextureFilter::Linear,
+ );
+
+ // Restore draw target to current pass render target + layer, and reset
+ // the read target.
+ self.device.bind_draw_target(draw_target);
+ self.device.reset_read_target();
+
+ if uses_scissor {
+ self.device.enable_scissor();
+ }
+ }
+
+ fn handle_blits(
+ &mut self,
+ blits: &[BlitJob],
+ render_tasks: &RenderTaskGraph,
+ draw_target: DrawTarget,
+ ) {
+ if blits.is_empty() {
+ return;
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLIT);
+
+ // TODO(gw): For now, we don't bother batching these by source texture.
+ // If if ever shows up as an issue, we can easily batch them.
+ for blit in blits {
+ let (source, layer, source_rect) = match blit.source {
+ BlitJobSource::Texture(texture_id, layer, source_rect) => {
+ // A blit from a texture into this target.
+ (texture_id, layer as usize, source_rect)
+ }
+ BlitJobSource::RenderTask(task_id) => {
+ // A blit from the child render task into this target.
+ // TODO(gw): Support R8 format here once we start
+ // creating mips for alpha masks.
+ let source = &render_tasks[task_id];
+ let (source_rect, layer) = source.get_target_rect();
+ let source_texture = TextureSource::TextureCache(
+ source.get_target_texture(),
+ Swizzle::default(),
+ );
+ (source_texture, layer.0, source_rect)
+ }
+ };
+
+ debug_assert_eq!(source_rect.size, blit.target_rect.size);
+ let (texture, swizzle) = self.texture_resolver
+ .resolve(&source)
+ .expect("BUG: invalid source texture");
+
+ if swizzle != Swizzle::default() {
+ error!("Swizzle {:?} can't be handled by a blit", swizzle);
+ }
+
+ let read_target = DrawTarget::from_texture(
+ texture,
+ layer,
+ false,
+ );
+
+ self.device.blit_render_target(
+ read_target.into(),
+ read_target.to_framebuffer_rect(source_rect),
+ draw_target,
+ draw_target.to_framebuffer_rect(blit.target_rect),
+ TextureFilter::Linear,
+ );
+ }
+ }
+
+ fn handle_scaling(
+ &mut self,
+ scalings: &FastHashMap<TextureSource, Vec<ScalingInstance>>,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if scalings.is_empty() {
+ return
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SCALE);
+
+ for (source, instances) in scalings {
+ let buffer_kind = source.image_buffer_kind();
+
+ self.shaders
+ .borrow_mut()
+ .get_scale_shader(buffer_kind)
+ .bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ instances,
+ VertexArrayKind::Scale,
+ &BatchTextures::composite_rgb(*source),
+ stats,
+ );
+ }
+ }
+
+ fn handle_svg_filters(
+ &mut self,
+ textures: &BatchTextures,
+ svg_filters: &[SvgFilterInstance],
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if svg_filters.is_empty() {
+ return;
+ }
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SVG_FILTER);
+
+ self.shaders.borrow_mut().cs_svg_filter.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors
+ );
+
+ self.draw_instanced_batch(
+ &svg_filters,
+ VertexArrayKind::SvgFilter,
+ textures,
+ stats,
+ );
+ }
+
+ fn draw_picture_cache_target(
+ &mut self,
+ target: &PictureCacheTarget,
+ draw_target: DrawTarget,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_picture_cache_target");
+
+ self.profile.inc(profiler::RENDERED_PICTURE_TILES);
+ let _gm = self.gpu_profiler.start_marker("picture cache target");
+ let framebuffer_kind = FramebufferKind::Other;
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+ self.device.enable_depth_write();
+ self.set_blend(false, framebuffer_kind);
+
+ let clear_color = target.clear_color.map(|c| c.to_array());
+ let scissor_rect = if self.device.get_capabilities().supports_render_target_partial_update {
+ target.alpha_batch_container.task_scissor_rect
+ } else {
+ None
+ };
+ match scissor_rect {
+ // If updating only a dirty rect within a picture cache target, the
+ // clear must also be scissored to that dirty region.
+ Some(r) if self.clear_caches_with_quads => {
+ self.device.enable_depth(DepthFunction::Always);
+ // Save the draw call count so that our reftests don't get confused...
+ let old_draw_call_count = stats.total_draw_calls;
+ if clear_color.is_none() {
+ self.device.disable_color_write();
+ }
+ let instance = ClearInstance {
+ rect: [
+ r.origin.x as f32, r.origin.y as f32,
+ r.size.width as f32, r.size.height as f32,
+ ],
+ color: clear_color.unwrap_or([0.0; 4]),
+ };
+ self.shaders.borrow_mut().ps_clear.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+ self.draw_instanced_batch(
+ &[instance],
+ VertexArrayKind::Clear,
+ &BatchTextures::empty(),
+ stats,
+ );
+ if clear_color.is_none() {
+ self.device.enable_color_write();
+ }
+ stats.total_draw_calls = old_draw_call_count;
+ self.device.disable_depth();
+ }
+ other => {
+ let scissor_rect = other.map(|rect| {
+ draw_target.build_scissor_rect(Some(rect))
+ });
+ self.device.clear_target(clear_color, Some(1.0), scissor_rect);
+ }
+ };
+ self.device.disable_depth_write();
+ }
+
+ self.draw_alpha_batch_container(
+ &target.alpha_batch_container,
+ draw_target,
+ framebuffer_kind,
+ projection,
+ render_tasks,
+ stats,
+ );
+
+ self.device.invalidate_depth_target();
+ }
+
+ /// Draw an alpha batch container into a given draw target. This is used
+ /// by both color and picture cache target kinds.
+ fn draw_alpha_batch_container(
+ &mut self,
+ alpha_batch_container: &AlphaBatchContainer,
+ draw_target: DrawTarget,
+ framebuffer_kind: FramebufferKind,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ let uses_scissor = alpha_batch_container.task_scissor_rect.is_some();
+
+ if uses_scissor {
+ self.device.enable_scissor();
+ let scissor_rect = draw_target.build_scissor_rect(
+ alpha_batch_container.task_scissor_rect,
+ );
+ self.device.set_scissor_rect(scissor_rect)
+ }
+
+ if !alpha_batch_container.opaque_batches.is_empty()
+ && !self.debug_flags.contains(DebugFlags::DISABLE_OPAQUE_PASS) {
+ let _gl = self.gpu_profiler.start_marker("opaque batches");
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+ self.set_blend(false, framebuffer_kind);
+ //Note: depth equality is needed for split planes
+ self.device.enable_depth(DepthFunction::LessEqual);
+ self.device.enable_depth_write();
+
+ // Draw opaque batches front-to-back for maximum
+ // z-buffer efficiency!
+ for batch in alpha_batch_container
+ .opaque_batches
+ .iter()
+ .rev()
+ {
+ if should_skip_batch(&batch.key.kind, self.debug_flags) {
+ continue;
+ }
+
+ self.shaders.borrow_mut()
+ .get(&batch.key, batch.features, self.debug_flags)
+ .bind(
+ &mut self.device, projection,
+ &mut self.renderer_errors,
+ );
+
+ let _timer = self.gpu_profiler.start_timer(batch.key.kind.sampler_tag());
+ self.draw_instanced_batch(
+ &batch.instances,
+ VertexArrayKind::Primitive,
+ &batch.key.textures,
+ stats
+ );
+ }
+
+ self.device.disable_depth_write();
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ } else {
+ self.device.disable_depth();
+ }
+
+ if !alpha_batch_container.alpha_batches.is_empty()
+ && !self.debug_flags.contains(DebugFlags::DISABLE_ALPHA_PASS) {
+ let _gl = self.gpu_profiler.start_marker("alpha batches");
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.set_blend(true, framebuffer_kind);
+
+ let mut prev_blend_mode = BlendMode::None;
+ let shaders_rc = self.shaders.clone();
+
+ for batch in &alpha_batch_container.alpha_batches {
+ if should_skip_batch(&batch.key.kind, self.debug_flags) {
+ continue;
+ }
+
+ let mut shaders = shaders_rc.borrow_mut();
+ let shader = shaders.get(
+ &batch.key,
+ batch.features | BatchFeatures::ALPHA_PASS,
+ self.debug_flags,
+ );
+
+ if batch.key.blend_mode != prev_blend_mode {
+ match batch.key.blend_mode {
+ _ if self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) &&
+ framebuffer_kind == FramebufferKind::Main => {
+ self.device.set_blend_mode_show_overdraw();
+ }
+ BlendMode::None => {
+ unreachable!("bug: opaque blend in alpha pass");
+ }
+ BlendMode::Alpha => {
+ self.device.set_blend_mode_alpha();
+ }
+ BlendMode::PremultipliedAlpha => {
+ self.device.set_blend_mode_premultiplied_alpha();
+ }
+ BlendMode::PremultipliedDestOut => {
+ self.device.set_blend_mode_premultiplied_dest_out();
+ }
+ BlendMode::SubpixelDualSource => {
+ self.device.set_blend_mode_subpixel_dual_source();
+ }
+ BlendMode::SubpixelConstantTextColor(color) => {
+ self.device.set_blend_mode_subpixel_constant_text_color(color);
+ }
+ BlendMode::SubpixelWithBgColor => {
+ // Using the three pass "component alpha with font smoothing
+ // background color" rendering technique:
+ //
+ // /webrender/doc/text-rendering.md
+ //
+ self.device.set_blend_mode_subpixel_with_bg_color_pass0();
+ // need to make sure the shader is bound
+ shader.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass0 as _);
+ }
+ BlendMode::Advanced(mode) => {
+ if self.enable_advanced_blend_barriers {
+ self.device.gl().blend_barrier_khr();
+ }
+ self.device.set_blend_mode_advanced(mode);
+ }
+ }
+ prev_blend_mode = batch.key.blend_mode;
+ }
+
+ // Handle special case readback for composites.
+ if let BatchKind::Brush(BrushBatchKind::MixBlend { task_id, source_id, backdrop_id }) = batch.key.kind {
+ // composites can't be grouped together because
+ // they may overlap and affect each other.
+ debug_assert_eq!(batch.instances.len(), 1);
+ self.handle_readback_composite(
+ draw_target,
+ uses_scissor,
+ &render_tasks[source_id],
+ &render_tasks[task_id],
+ &render_tasks[backdrop_id],
+ );
+ }
+
+ let _timer = self.gpu_profiler.start_timer(batch.key.kind.sampler_tag());
+ shader.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ &batch.instances,
+ VertexArrayKind::Primitive,
+ &batch.key.textures,
+ stats
+ );
+
+ if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+ self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+ // re-binding the shader after the blend mode change
+ shader.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
+
+ // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
+ // are all set up from the previous draw_instanced_batch call,
+ // so just issue a draw call here to avoid re-uploading the
+ // instances and re-binding textures etc.
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+
+ self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+ // re-binding the shader after the blend mode change
+ shader.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+ self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
+
+ self.device
+ .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+ }
+
+ if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+ prev_blend_mode = BlendMode::None;
+ }
+ }
+
+ self.set_blend(false, framebuffer_kind);
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+
+ self.device.disable_depth();
+ if uses_scissor {
+ self.device.disable_scissor();
+ }
+ }
+
+ /// Rasterize any external compositor surfaces that require updating
+ fn update_external_native_surfaces(
+ &mut self,
+ external_surfaces: &[ResolvedExternalSurface],
+ results: &mut RenderResults,
+ ) {
+ if external_surfaces.is_empty() {
+ return;
+ }
+
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+
+ self.device.disable_depth();
+ self.set_blend(false, FramebufferKind::Main);
+
+ for surface in external_surfaces {
+ // See if this surface needs to be updated
+ let (native_surface_id, surface_size) = match surface.update_params {
+ Some(params) => params,
+ None => continue,
+ };
+
+ // When updating an external surface, the entire surface rect is used
+ // for all of the draw, dirty, valid and clip rect parameters.
+ let surface_rect = surface_size.into();
+
+ // Bind the native compositor surface to update
+ let surface_info = self.compositor_config
+ .compositor()
+ .unwrap()
+ .bind(
+ NativeTileId {
+ surface_id: native_surface_id,
+ x: 0,
+ y: 0,
+ },
+ surface_rect,
+ surface_rect,
+ );
+
+ // Bind the native surface to current FBO target
+ let draw_target = DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: surface_size,
+ };
+ self.device.bind_draw_target(draw_target);
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ surface_size.width as f32,
+ 0.0,
+ surface_size.height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let ( textures, instance ) = match surface.color_data {
+ ResolvedExternalSurfaceColorData::Yuv{
+ ref planes, color_space, format, rescale, .. } => {
+
+ // Bind an appropriate YUV shader for the texture format kind
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ CompositeSurfaceFormat::Yuv,
+ surface.image_buffer_kind,
+ ).bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors
+ );
+
+ let textures = BatchTextures::composite_yuv(
+ planes[0].texture,
+ planes[1].texture,
+ planes[2].texture,
+ );
+
+ // When the texture is an external texture, the UV rect is not known when
+ // the external surface descriptor is created, because external textures
+ // are not resolved until the lock() callback is invoked at the start of
+ // the frame render. To handle this, query the texture resolver for the
+ // UV rect if it's an external texture, otherwise use the default UV rect.
+ let uv_rects = [
+ self.texture_resolver.get_uv_rect(&textures.input.colors[0], planes[0].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[1], planes[1].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[2], planes[2].uv_rect),
+ ];
+
+ let instance = CompositeInstance::new_yuv(
+ surface_rect.to_f32(),
+ surface_rect.to_f32(),
+ // z-id is not relevant when updating a native compositor surface.
+ // TODO(gw): Support compositor surfaces without z-buffer, for memory / perf win here.
+ ZBufferId(0),
+ color_space,
+ format,
+ rescale,
+ [
+ planes[0].texture_layer as f32,
+ planes[1].texture_layer as f32,
+ planes[2].texture_layer as f32,
+ ],
+ uv_rects,
+ );
+
+ ( textures, instance )
+ },
+ ResolvedExternalSurfaceColorData::Rgb{ ref plane, flip_y, .. } => {
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ CompositeSurfaceFormat::Rgba,
+ surface.image_buffer_kind,
+ ).bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors
+ );
+
+ let textures = BatchTextures::composite_rgb(plane.texture);
+ let mut uv_rect = self.texture_resolver.get_uv_rect(&textures.input.colors[0], plane.uv_rect);
+ if flip_y {
+ let y = uv_rect.uv0.y;
+ uv_rect.uv0.y = uv_rect.uv1.y;
+ uv_rect.uv1.y = y;
+ }
+ let instance = CompositeInstance::new_rgb(
+ surface_rect.to_f32(),
+ surface_rect.to_f32(),
+ PremultipliedColorF::WHITE,
+ plane.texture_layer as f32,
+ ZBufferId(0),
+ uv_rect,
+ );
+
+ ( textures, instance )
+ },
+ };
+
+ self.draw_instanced_batch(
+ &[instance],
+ VertexArrayKind::Composite,
+ &textures,
+ &mut results.stats,
+ );
+
+ self.compositor_config
+ .compositor()
+ .unwrap()
+ .unbind();
+ }
+
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ }
+
+ /// Draw a list of tiles to the framebuffer
+ fn draw_tile_list<'a, I: Iterator<Item = &'a CompositeTile>>(
+ &mut self,
+ tiles_iter: I,
+ external_surfaces: &[ResolvedExternalSurface],
+ projection: &default::Transform3D<f32>,
+ partial_present_mode: Option<PartialPresentMode>,
+ stats: &mut RendererStats,
+ ) {
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(
+ CompositeSurfaceFormat::Rgba,
+ ImageBufferKind::Texture2D,
+ ).bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors
+ );
+
+ let mut current_shader_params = (CompositeSurfaceFormat::Rgba, ImageBufferKind::Texture2D);
+ let mut current_textures = BatchTextures::empty();
+ let mut instances = Vec::new();
+
+ for tile in tiles_iter {
+ // Determine a clip rect to apply to this tile, depending on what
+ // the partial present mode is.
+ let partial_clip_rect = match partial_present_mode {
+ Some(PartialPresentMode::Single { dirty_rect }) => dirty_rect,
+ None => tile.rect,
+ };
+
+ let clip_rect = match partial_clip_rect.intersection(&tile.clip_rect) {
+ Some(rect) => rect,
+ None => continue,
+ };
+
+ // Simple compositor needs the valid rect in device space to match clip rect
+ let valid_device_rect = tile.valid_rect.translate(
+ tile.rect.origin.to_vector()
+ );
+
+ // Only composite the part of the tile that contains valid pixels
+ let clip_rect = match clip_rect.intersection(&valid_device_rect) {
+ Some(rect) => rect,
+ None => continue,
+ };
+
+ // Work out the draw params based on the tile surface
+ let (instance, textures, shader_params) = match tile.surface {
+ CompositeTileSurface::Color { color } => {
+ let dummy = TextureSource::Dummy;
+ let image_buffer_kind = dummy.image_buffer_kind();
+ (
+ CompositeInstance::new(
+ tile.rect,
+ clip_rect,
+ color.premultiplied(),
+ 0.0,
+ tile.z_id,
+ ),
+ BatchTextures::composite_rgb(dummy),
+ (CompositeSurfaceFormat::Rgba, image_buffer_kind),
+ )
+ }
+ CompositeTileSurface::Clear => {
+ let dummy = TextureSource::Dummy;
+ let image_buffer_kind = dummy.image_buffer_kind();
+ (
+ CompositeInstance::new(
+ tile.rect,
+ clip_rect,
+ PremultipliedColorF::BLACK,
+ 0.0,
+ tile.z_id,
+ ),
+ BatchTextures::composite_rgb(dummy),
+ (CompositeSurfaceFormat::Rgba, image_buffer_kind),
+ )
+ }
+ CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::TextureCache { texture, layer } } => {
+ (
+ CompositeInstance::new(
+ tile.rect,
+ clip_rect,
+ PremultipliedColorF::WHITE,
+ layer as f32,
+ tile.z_id,
+ ),
+ BatchTextures::composite_rgb(texture),
+ (CompositeSurfaceFormat::Rgba, ImageBufferKind::Texture2D),
+ )
+ }
+ CompositeTileSurface::ExternalSurface { external_surface_index } => {
+ let surface = &external_surfaces[external_surface_index.0];
+
+ match surface.color_data {
+ ResolvedExternalSurfaceColorData::Yuv{ ref planes, color_space, format, rescale, .. } => {
+ let textures = BatchTextures::composite_yuv(
+ planes[0].texture,
+ planes[1].texture,
+ planes[2].texture,
+ );
+
+ // When the texture is an external texture, the UV rect is not known when
+ // the external surface descriptor is created, because external textures
+ // are not resolved until the lock() callback is invoked at the start of
+ // the frame render. To handle this, query the texture resolver for the
+ // UV rect if it's an external texture, otherwise use the default UV rect.
+ let uv_rects = [
+ self.texture_resolver.get_uv_rect(&textures.input.colors[0], planes[0].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[1], planes[1].uv_rect),
+ self.texture_resolver.get_uv_rect(&textures.input.colors[2], planes[2].uv_rect),
+ ];
+
+ (
+ CompositeInstance::new_yuv(
+ tile.rect,
+ clip_rect,
+ tile.z_id,
+ color_space,
+ format,
+ rescale,
+ [
+ planes[0].texture_layer as f32,
+ planes[1].texture_layer as f32,
+ planes[2].texture_layer as f32,
+ ],
+ uv_rects,
+ ),
+ textures,
+ (CompositeSurfaceFormat::Yuv, surface.image_buffer_kind),
+ )
+ },
+ ResolvedExternalSurfaceColorData::Rgb{ ref plane, flip_y, .. } => {
+
+ let mut uv_rect = self.texture_resolver.get_uv_rect(&plane.texture, plane.uv_rect);
+ if flip_y {
+ let y = uv_rect.uv0.y;
+ uv_rect.uv0.y = uv_rect.uv1.y;
+ uv_rect.uv1.y = y;
+ }
+
+ (
+ CompositeInstance::new_rgb(
+ tile.rect,
+ clip_rect,
+ PremultipliedColorF::WHITE,
+ plane.texture_layer as f32,
+ tile.z_id,
+ uv_rect,
+ ),
+ BatchTextures::composite_rgb(plane.texture),
+ (CompositeSurfaceFormat::Rgba, surface.image_buffer_kind),
+ )
+ },
+ }
+ }
+ CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::Native { .. } } => {
+ unreachable!("bug: found native surface in simple composite path");
+ }
+ };
+
+ // Flush batch if shader params or textures changed
+ let flush_batch = !current_textures.is_compatible_with(&textures) ||
+ shader_params != current_shader_params;
+
+ if flush_batch {
+ if !instances.is_empty() {
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Composite,
+ &current_textures,
+ stats,
+ );
+ instances.clear();
+ }
+ }
+
+ if shader_params != current_shader_params {
+ self.shaders
+ .borrow_mut()
+ .get_composite_shader(shader_params.0, shader_params.1)
+ .bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors
+ );
+
+ current_shader_params = shader_params;
+ }
+
+ current_textures = textures;
+
+ // Add instance to current batch
+ instances.push(instance);
+ }
+
+ // Flush the last batch
+ if !instances.is_empty() {
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Composite,
+ &current_textures,
+ stats,
+ );
+ }
+ }
+
+ /// Composite picture cache tiles into the framebuffer. This is currently
+ /// the only way that picture cache tiles get drawn. In future, the tiles
+ /// will often be handed to the OS compositor, and this method will be
+ /// rarely used.
+ fn composite_simple(
+ &mut self,
+ composite_state: &CompositeState,
+ draw_target: DrawTarget,
+ projection: &default::Transform3D<f32>,
+ results: &mut RenderResults,
+ partial_present_mode: Option<PartialPresentMode>,
+ ) {
+ let _gm = self.gpu_profiler.start_marker("framebuffer");
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_COMPOSITE);
+
+ self.device.bind_draw_target(draw_target);
+ self.device.enable_depth(DepthFunction::LessEqual);
+ self.device.enable_depth_write();
+
+ // If using KHR_partial_update, call eglSetDamageRegion.
+ // This must be called exactly once per frame, and prior to any rendering to the main
+ // framebuffer. Additionally, on Mali-G77 we encountered rendering issues when calling
+ // this earlier in the frame, during offscreen render passes. So call it now, immediately
+ // before rendering to the main framebuffer. See bug 1685276 for details.
+ if let Some(partial_present) = self.compositor_config.partial_present() {
+ if let Some(PartialPresentMode::Single { dirty_rect }) = partial_present_mode {
+ partial_present.set_buffer_damage_region(&[dirty_rect.to_i32()]);
+ }
+ }
+
+ // Clear the framebuffer
+ let clear_color = self.clear_color.map(|color| color.to_array());
+
+ match partial_present_mode {
+ Some(PartialPresentMode::Single { dirty_rect }) => {
+ // We have a single dirty rect, so clear only that
+ self.device.clear_target(clear_color,
+ Some(1.0),
+ Some(draw_target.to_framebuffer_rect(dirty_rect.to_i32())));
+ }
+ None => {
+ // Partial present is disabled, so clear the entire framebuffer
+ self.device.clear_target(clear_color,
+ Some(1.0),
+ None);
+ }
+ }
+
+ // We are only interested in tiles backed with actual cached pixels so we don't
+ // count clear tiles here.
+ let num_tiles = composite_state.opaque_tiles.len()
+ + composite_state.alpha_tiles.len();
+ self.profile.set(profiler::PICTURE_TILES, num_tiles);
+
+ // Draw opaque tiles first, front-to-back to get maxmum
+ // z-reject efficiency.
+ if !composite_state.opaque_tiles.is_empty() {
+ let opaque_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
+ self.device.enable_depth_write();
+ self.set_blend(false, FramebufferKind::Main);
+ self.draw_tile_list(
+ composite_state.opaque_tiles.iter().rev(),
+ &composite_state.external_surfaces,
+ projection,
+ partial_present_mode,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(opaque_sampler);
+ }
+
+ if !composite_state.clear_tiles.is_empty() {
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.device.disable_depth_write();
+ self.set_blend(true, FramebufferKind::Main);
+ self.device.set_blend_mode_premultiplied_dest_out();
+ self.draw_tile_list(
+ composite_state.clear_tiles.iter(),
+ &composite_state.external_surfaces,
+ projection,
+ partial_present_mode,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+
+ // Draw alpha tiles
+ if !composite_state.alpha_tiles.is_empty() {
+ let transparent_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_TRANSPARENT);
+ self.device.disable_depth_write();
+ self.set_blend(true, FramebufferKind::Main);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Main);
+ self.draw_tile_list(
+ composite_state.alpha_tiles.iter(),
+ &composite_state.external_surfaces,
+ projection,
+ partial_present_mode,
+ &mut results.stats,
+ );
+ self.gpu_profiler.finish_sampler(transparent_sampler);
+ }
+ }
+
+ fn draw_color_target(
+ &mut self,
+ draw_target: DrawTarget,
+ target: &ColorRenderTarget,
+ clear_color: Option<[f32; 4]>,
+ clear_depth: Option<f32>,
+ render_tasks: &RenderTaskGraph,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_color_target");
+
+ self.profile.inc(profiler::COLOR_PASSES);
+ let _gm = self.gpu_profiler.start_marker("color target");
+
+ // sanity check for the depth buffer
+ if let DrawTarget::Texture { with_depth, .. } = draw_target {
+ assert!(with_depth >= target.needs_depth());
+ }
+
+ let framebuffer_kind = if draw_target.is_default() {
+ FramebufferKind::Main
+ } else {
+ FramebufferKind::Other
+ };
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+ self.device.disable_depth();
+ self.set_blend(false, framebuffer_kind);
+
+ if clear_depth.is_some() {
+ self.device.enable_depth_write();
+ }
+
+ let clear_rect = match draw_target {
+ DrawTarget::NativeSurface { .. } => {
+ unreachable!("bug: native compositor surface in child target");
+ }
+ DrawTarget::Default { rect, total_size, .. } if rect.origin == FramebufferIntPoint::zero() && rect.size == total_size => {
+ // whole screen is covered, no need for scissor
+ None
+ }
+ DrawTarget::Default { rect, .. } => {
+ Some(rect)
+ }
+ DrawTarget::Texture { .. } if self.enable_clear_scissor => {
+ // TODO(gw): Applying a scissor rect and minimal clear here
+ // is a very large performance win on the Intel and nVidia
+ // GPUs that I have tested with. It's possible it may be a
+ // performance penalty on other GPU types - we should test this
+ // and consider different code paths.
+ //
+ // Note: The above measurements were taken when render
+ // target slices were minimum 2048x2048. Now that we size
+ // them adaptively, this may be less of a win (except perhaps
+ // on a mostly-unused last slice of a large texture array).
+ Some(draw_target.to_framebuffer_rect(target.used_rect))
+ }
+ DrawTarget::Texture { .. } | DrawTarget::External { .. } => {
+ None
+ }
+ };
+
+ self.device.clear_target(
+ clear_color,
+ clear_depth,
+ clear_rect,
+ );
+
+ if clear_depth.is_some() {
+ self.device.disable_depth_write();
+ }
+ }
+
+ // Handle any blits from the texture cache to this target.
+ self.handle_blits(
+ &target.blits,
+ render_tasks,
+ draw_target,
+ );
+
+ // Draw any blurs for this target.
+ // Blurs are rendered as a standard 2-pass
+ // separable implementation.
+ // TODO(gw): In the future, consider having
+ // fast path blur shaders for common
+ // blur radii with fixed weights.
+ if !target.vertical_blurs.is_empty() || !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ self.set_blend(false, framebuffer_kind);
+ self.shaders.borrow_mut().cs_blur_rgba8
+ .bind(&mut self.device, projection, &mut self.renderer_errors);
+
+ if !target.vertical_blurs.is_empty() {
+ self.draw_blurs(
+ &target.vertical_blurs,
+ stats,
+ );
+ }
+
+ if !target.horizontal_blurs.is_empty() {
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ self.handle_scaling(
+ &target.scalings,
+ projection,
+ stats,
+ );
+
+ for (ref textures, ref filters) in &target.svg_filters {
+ self.handle_svg_filters(
+ textures,
+ filters,
+ projection,
+ stats,
+ );
+ }
+
+ for alpha_batch_container in &target.alpha_batch_containers {
+ self.draw_alpha_batch_container(
+ alpha_batch_container,
+ draw_target,
+ framebuffer_kind,
+ projection,
+ render_tasks,
+ stats,
+ );
+ }
+
+ if clear_depth.is_some() {
+ self.device.invalidate_depth_target();
+ }
+ }
+
+ fn draw_blurs(
+ &mut self,
+ blurs: &FastHashMap<TextureSource, Vec<BlurInstance>>,
+ stats: &mut RendererStats,
+ ) {
+ for (texture, blurs) in blurs {
+ let textures = BatchTextures::composite_rgb(
+ *texture,
+ );
+
+ self.draw_instanced_batch(
+ blurs,
+ VertexArrayKind::Blur,
+ &textures,
+ stats,
+ );
+ }
+ }
+
+ /// Draw all the instances in a clip batcher list to the current target.
+ fn draw_clip_batch_list(
+ &mut self,
+ list: &ClipBatchList,
+ projection: &default::Transform3D<f32>,
+ stats: &mut RendererStats,
+ ) {
+ if self.debug_flags.contains(DebugFlags::DISABLE_CLIP_MASKS) {
+ return;
+ }
+
+ // draw rounded cornered rectangles
+ if !list.slow_rectangles.is_empty() {
+ let _gm2 = self.gpu_profiler.start_marker("slow clip rectangles");
+ self.shaders.borrow_mut().cs_clip_rectangle_slow.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+ self.draw_instanced_batch(
+ &list.slow_rectangles,
+ VertexArrayKind::ClipRect,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+ if !list.fast_rectangles.is_empty() {
+ let _gm2 = self.gpu_profiler.start_marker("fast clip rectangles");
+ self.shaders.borrow_mut().cs_clip_rectangle_fast.bind(
+ &mut self.device,
+ projection,
+ &mut self.renderer_errors,
+ );
+ self.draw_instanced_batch(
+ &list.fast_rectangles,
+ VertexArrayKind::ClipRect,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // draw box-shadow clips
+ for (mask_texture_id, items) in list.box_shadows.iter() {
+ let _gm2 = self.gpu_profiler.start_marker("box-shadows");
+ let textures = BatchTextures::composite_rgb(*mask_texture_id);
+ self.shaders.borrow_mut().cs_clip_box_shadow
+ .bind(&mut self.device, projection, &mut self.renderer_errors);
+ self.draw_instanced_batch(
+ items,
+ VertexArrayKind::ClipBoxShadow,
+ &textures,
+ stats,
+ );
+ }
+
+ // draw image masks
+ for (mask_texture_id, items) in list.images.iter() {
+ let _gm2 = self.gpu_profiler.start_marker("clip images");
+ let textures = BatchTextures::composite_rgb(*mask_texture_id);
+ self.shaders.borrow_mut().cs_clip_image
+ .bind(&mut self.device, projection, &mut self.renderer_errors);
+ self.draw_instanced_batch(
+ items,
+ VertexArrayKind::ClipImage,
+ &textures,
+ stats,
+ );
+ }
+ }
+
+ fn draw_alpha_target(
+ &mut self,
+ draw_target: DrawTarget,
+ target: &AlphaRenderTarget,
+ projection: &default::Transform3D<f32>,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_alpha_target");
+
+ self.profile.inc(profiler::ALPHA_PASSES);
+ let _gm = self.gpu_profiler.start_marker("alpha target");
+ let alpha_sampler = self.gpu_profiler.start_sampler(GPU_SAMPLER_TAG_ALPHA);
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_TARGET);
+ self.device.bind_draw_target(draw_target);
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+
+ // TODO(gw): Applying a scissor rect and minimal clear here
+ // is a very large performance win on the Intel and nVidia
+ // GPUs that I have tested with. It's possible it may be a
+ // performance penalty on other GPU types - we should test this
+ // and consider different code paths.
+
+ let zero_color = [0.0, 0.0, 0.0, 0.0];
+ for &task_id in &target.zero_clears {
+ let (rect, _) = render_tasks[task_id].get_target_rect();
+ self.device.clear_target(
+ Some(zero_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(rect)),
+ );
+ }
+
+ let one_color = [1.0, 1.0, 1.0, 1.0];
+ for &task_id in &target.one_clears {
+ let (rect, _) = render_tasks[task_id].get_target_rect();
+ self.device.clear_target(
+ Some(one_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(rect)),
+ );
+ }
+ }
+
+ // Draw any blurs for this target.
+ // Blurs are rendered as a standard 2-pass
+ // separable implementation.
+ // TODO(gw): In the future, consider having
+ // fast path blur shaders for common
+ // blur radii with fixed weights.
+ if !target.vertical_blurs.is_empty() || !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ self.shaders.borrow_mut().cs_blur_a8
+ .bind(&mut self.device, projection, &mut self.renderer_errors);
+
+ if !target.vertical_blurs.is_empty() {
+ self.draw_blurs(
+ &target.vertical_blurs,
+ stats,
+ );
+ }
+
+ if !target.horizontal_blurs.is_empty() {
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ self.handle_scaling(
+ &target.scalings,
+ projection,
+ stats,
+ );
+
+ // Draw the clip items into the tiled alpha mask.
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_CLIP);
+
+ // TODO(gw): Consider grouping multiple clip masks per shader
+ // invocation here to reduce memory bandwith further?
+
+ // Draw the primary clip mask - since this is the first mask
+ // for the task, we can disable blending, knowing that it will
+ // overwrite every pixel in the mask area.
+ self.set_blend(false, FramebufferKind::Other);
+ self.draw_clip_batch_list(
+ &target.clip_batcher.primary_clips,
+ projection,
+ stats,
+ );
+
+ // switch to multiplicative blending for secondary masks, using
+ // multiplicative blending to accumulate clips into the mask.
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_multiply(FramebufferKind::Other);
+ self.draw_clip_batch_list(
+ &target.clip_batcher.secondary_clips,
+ projection,
+ stats,
+ );
+ }
+
+ self.gpu_profiler.finish_sampler(alpha_sampler);
+ }
+
+ fn draw_texture_cache_target(
+ &mut self,
+ texture: &CacheTextureId,
+ layer: LayerIndex,
+ target: &TextureCacheRenderTarget,
+ render_tasks: &RenderTaskGraph,
+ stats: &mut RendererStats,
+ ) {
+ profile_scope!("draw_texture_cache_target");
+
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ let texture = &self.texture_resolver.texture_cache_map[texture];
+ let target_size = texture.get_dimensions();
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ target_size.width as f32,
+ 0.0,
+ target_size.height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let draw_target = DrawTarget::from_texture(
+ texture,
+ layer,
+ false,
+ );
+ self.device.bind_draw_target(draw_target);
+
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CLEAR);
+
+ self.device.disable_depth();
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+
+ let color = [0.0, 0.0, 0.0, 0.0];
+ if self.clear_caches_with_quads && !target.clears.is_empty() {
+ let instances = target.clears
+ .iter()
+ .map(|r| ClearInstance {
+ rect: [
+ r.origin.x as f32, r.origin.y as f32,
+ r.size.width as f32, r.size.height as f32,
+ ],
+ color,
+ })
+ .collect::<Vec<_>>();
+ self.shaders.borrow_mut().ps_clear.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+ self.draw_instanced_batch(
+ &instances,
+ VertexArrayKind::Clear,
+ &BatchTextures::empty(),
+ stats,
+ );
+ } else {
+ for rect in &target.clears {
+ self.device.clear_target(
+ Some(color),
+ None,
+ Some(draw_target.to_framebuffer_rect(*rect)),
+ );
+ }
+ }
+
+ // Handle any blits to this texture from child tasks.
+ self.handle_blits(
+ &target.blits,
+ render_tasks,
+ draw_target,
+ );
+ }
+
+ // Draw any borders for this target.
+ if !target.border_segments_solid.is_empty() ||
+ !target.border_segments_complex.is_empty()
+ {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_BORDER);
+
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
+
+ if !target.border_segments_solid.is_empty() {
+ self.shaders.borrow_mut().cs_border_solid.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_solid,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ if !target.border_segments_complex.is_empty() {
+ self.shaders.borrow_mut().cs_border_segment.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_complex,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ self.set_blend(false, FramebufferKind::Other);
+ }
+
+ // Draw any line decorations for this target.
+ if !target.line_decorations.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_LINE_DECORATION);
+
+ self.set_blend(true, FramebufferKind::Other);
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_line_decoration.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ &target.line_decorations,
+ VertexArrayKind::LineDecoration,
+ &BatchTextures::empty(),
+ stats,
+ );
+
+ self.set_blend(false, FramebufferKind::Other);
+ }
+
+ // Draw any gradients for this target.
+ if !target.gradients.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_GRADIENT);
+
+ self.set_blend(false, FramebufferKind::Other);
+
+ self.shaders.borrow_mut().cs_gradient.bind(
+ &mut self.device,
+ &projection,
+ &mut self.renderer_errors,
+ );
+
+ self.draw_instanced_batch(
+ &target.gradients,
+ VertexArrayKind::Gradient,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ // Draw any blurs for this target.
+ if !target.horizontal_blurs.is_empty() {
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_BLUR);
+
+ {
+ let mut shaders = self.shaders.borrow_mut();
+ match target.target_kind {
+ RenderTargetKind::Alpha => &mut shaders.cs_blur_a8,
+ RenderTargetKind::Color => &mut shaders.cs_blur_rgba8,
+ }.bind(&mut self.device, &projection, &mut self.renderer_errors);
+ }
+
+ self.draw_blurs(
+ &target.horizontal_blurs,
+ stats,
+ );
+ }
+ }
+
+ fn update_deferred_resolves(&mut self, deferred_resolves: &[DeferredResolve]) -> Option<GpuCacheUpdateList> {
+ // The first thing we do is run through any pending deferred
+ // resolves, and use a callback to get the UV rect for this
+ // custom item. Then we patch the resource_rects structure
+ // here before it's uploaded to the GPU.
+ if deferred_resolves.is_empty() {
+ return None;
+ }
+
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+
+ let mut list = GpuCacheUpdateList {
+ frame_id: FrameId::INVALID,
+ clear: false,
+ height: self.gpu_cache_texture.get_height(),
+ blocks: Vec::new(),
+ updates: Vec::new(),
+ debug_commands: Vec::new(),
+ };
+
+ for (i, deferred_resolve) in deferred_resolves.iter().enumerate() {
+ self.gpu_profiler.place_marker("deferred resolve");
+ let props = &deferred_resolve.image_properties;
+ let ext_image = props
+ .external_image
+ .expect("BUG: Deferred resolves must be external images!");
+ // Provide rendering information for NativeTexture external images.
+ let image = handler.lock(ext_image.id, ext_image.channel_index, deferred_resolve.rendering);
+ let texture_target = match ext_image.image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => {
+ panic!("not a suitable image type in update_deferred_resolves()");
+ }
+ };
+
+ // In order to produce the handle, the external image handler may call into
+ // the GL context and change some states.
+ self.device.reset_state();
+
+ let texture = match image.source {
+ ExternalImageSource::NativeTexture(texture_id) => {
+ ExternalTexture::new(
+ texture_id,
+ texture_target,
+ Swizzle::default(),
+ image.uv,
+ )
+ }
+ ExternalImageSource::Invalid => {
+ warn!("Invalid ext-image");
+ debug!(
+ "For ext_id:{:?}, channel:{}.",
+ ext_image.id,
+ ext_image.channel_index
+ );
+ // Just use 0 as the gl handle for this failed case.
+ ExternalTexture::new(
+ 0,
+ texture_target,
+ Swizzle::default(),
+ image.uv,
+ )
+ }
+ ExternalImageSource::RawData(_) => {
+ panic!("Raw external data is not expected for deferred resolves!");
+ }
+ };
+
+ self.texture_resolver
+ .external_images
+ .insert(DeferredResolveIndex(i as u32), texture);
+
+ list.updates.push(GpuCacheUpdate::Copy {
+ block_index: list.blocks.len(),
+ block_count: BLOCKS_PER_UV_RECT,
+ address: deferred_resolve.address,
+ });
+ list.blocks.push(image.uv.into());
+ list.blocks.push([0f32; 4].into());
+ }
+
+ Some(list)
+ }
+
+ fn unlock_external_images(
+ &mut self,
+ deferred_resolves: &[DeferredResolve],
+ ) {
+ if !self.texture_resolver.external_images.is_empty() {
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Found external image, but no handler set!");
+
+ for (index, _) in self.texture_resolver.external_images.drain() {
+ let props = &deferred_resolves[index.0 as usize].image_properties;
+ let ext_image = props
+ .external_image
+ .expect("BUG: Deferred resolves must be external images!");
+ handler.unlock(ext_image.id, ext_image.channel_index);
+ }
+ }
+ }
+
+ /// Update the dirty rects based on current compositing mode and config
+ // TODO(gw): This can be tidied up significantly once the Draw compositor
+ // is implemented in terms of the compositor trait.
+ fn calculate_dirty_rects(
+ &mut self,
+ buffer_age: usize,
+ composite_state: &CompositeState,
+ draw_target_dimensions: DeviceIntSize,
+ results: &mut RenderResults,
+ ) -> Option<PartialPresentMode> {
+ let mut partial_present_mode = None;
+
+ let (max_partial_present_rects, draw_previous_partial_present_regions) = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ // Assume that we can return a single dirty rect for native
+ // compositor for now, and that there is no buffer-age functionality.
+ // These params can be exposed by the compositor capabilities struct
+ // as the Draw compositor is ported to use it.
+ (1, false)
+ }
+ CompositorKind::Draw { draw_previous_partial_present_regions, max_partial_present_rects } => {
+ (max_partial_present_rects, draw_previous_partial_present_regions)
+ }
+ };
+
+ if max_partial_present_rects > 0 {
+ let prev_frames_damage_rect = if let Some(..) = self.compositor_config.partial_present() {
+ self.buffer_damage_tracker
+ .get_damage_rect(buffer_age)
+ .or_else(|| Some(DeviceRect::from_size(draw_target_dimensions.to_f32())))
+ } else {
+ None
+ };
+
+ let can_use_partial_present =
+ composite_state.dirty_rects_are_valid &&
+ !self.force_redraw &&
+ !(prev_frames_damage_rect.is_none() && draw_previous_partial_present_regions) &&
+ !self.debug_overlay_state.is_enabled;
+
+ if can_use_partial_present {
+ let mut combined_dirty_rect = DeviceRect::zero();
+
+ // Work out how many dirty rects WR produced, and if that's more than
+ // what the device supports.
+ for tile in composite_state.opaque_tiles.iter().chain(composite_state.alpha_tiles.iter()) {
+ let tile_dirty_rect = tile.dirty_rect.translate(tile.rect.origin.to_vector());
+ let transformed_dirty_rect = if let Some(transform) = tile.transform {
+ transform.outer_transformed_rect(&tile_dirty_rect)
+ } else {
+ Some(tile_dirty_rect)
+ };
+
+ if let Some(dirty_rect) = transformed_dirty_rect {
+ combined_dirty_rect = combined_dirty_rect.union(&dirty_rect);
+ }
+ }
+
+ let combined_dirty_rect = combined_dirty_rect.round();
+ let combined_dirty_rect_i32 = combined_dirty_rect.to_i32();
+ // Return this frame's dirty region. If nothing has changed, don't return any dirty
+ // rects at all (the client can use this as a signal to skip present completely).
+ if !combined_dirty_rect.is_empty() {
+ results.dirty_rects.push(combined_dirty_rect_i32);
+ }
+
+ // Track this frame's dirty region, for calculating subsequent frames' damage.
+ if draw_previous_partial_present_regions {
+ self.buffer_damage_tracker.push_dirty_rect(&combined_dirty_rect);
+ }
+
+ // If the implementation requires manually keeping the buffer consistent,
+ // then we must combine this frame's dirty region with that of previous frames
+ // to determine the total_dirty_rect. The is used to determine what region we
+ // render to, and is what we send to the compositor as the buffer damage region
+ // (eg for KHR_partial_update).
+ let total_dirty_rect = if draw_previous_partial_present_regions {
+ combined_dirty_rect.union(&prev_frames_damage_rect.unwrap())
+ } else {
+ combined_dirty_rect
+ };
+
+ partial_present_mode = Some(PartialPresentMode::Single {
+ dirty_rect: total_dirty_rect,
+ });
+ } else {
+ // If we don't have a valid partial present scenario, return a single
+ // dirty rect to the client that covers the entire framebuffer.
+ let fb_rect = DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ draw_target_dimensions,
+ );
+ results.dirty_rects.push(fb_rect);
+
+ if draw_previous_partial_present_regions {
+ self.buffer_damage_tracker.push_dirty_rect(&fb_rect.to_f32());
+ }
+ }
+
+ self.force_redraw = false;
+ }
+
+ partial_present_mode
+ }
+
+ fn bind_frame_data(&mut self, frame: &mut Frame) {
+ profile_scope!("bind_frame_data");
+
+ let _timer = self.gpu_profiler.start_timer(GPU_TAG_SETUP_DATA);
+
+ self.vertex_data_textures[self.current_vertex_data_textures].update(
+ &mut self.device,
+ &mut self.texture_upload_pbo_pool,
+ frame,
+ );
+ self.current_vertex_data_textures =
+ (self.current_vertex_data_textures + 1) % VERTEX_DATA_TEXTURE_COUNT;
+ }
+
+ fn update_native_surfaces(&mut self) {
+ profile_scope!("update_native_surfaces");
+
+ match self.compositor_config {
+ CompositorConfig::Native { ref mut compositor, .. } => {
+ for op in self.pending_native_surface_updates.drain(..) {
+ match op.details {
+ NativeSurfaceOperationDetails::CreateSurface { id, virtual_offset, tile_size, is_opaque } => {
+ let _inserted = self.allocated_native_surfaces.insert(id);
+ debug_assert!(_inserted, "bug: creating existing surface");
+ compositor.create_surface(
+ id,
+ virtual_offset,
+ tile_size,
+ is_opaque,
+ );
+ }
+ NativeSurfaceOperationDetails::CreateExternalSurface { id, is_opaque } => {
+ let _inserted = self.allocated_native_surfaces.insert(id);
+ debug_assert!(_inserted, "bug: creating existing surface");
+ compositor.create_external_surface(
+ id,
+ is_opaque,
+ );
+ }
+ NativeSurfaceOperationDetails::DestroySurface { id } => {
+ let _existed = self.allocated_native_surfaces.remove(&id);
+ debug_assert!(_existed, "bug: removing unknown surface");
+ compositor.destroy_surface(id);
+ }
+ NativeSurfaceOperationDetails::CreateTile { id } => {
+ compositor.create_tile(id);
+ }
+ NativeSurfaceOperationDetails::DestroyTile { id } => {
+ compositor.destroy_tile(id);
+ }
+ NativeSurfaceOperationDetails::AttachExternalImage { id, external_image } => {
+ compositor.attach_external_image(id, external_image);
+ }
+ }
+ }
+ }
+ CompositorConfig::Draw { .. } => {
+ // Ensure nothing is added in simple composite mode, since otherwise
+ // memory will leak as this doesn't get drained
+ debug_assert!(self.pending_native_surface_updates.is_empty());
+ }
+ }
+ }
+
+ fn draw_frame(
+ &mut self,
+ frame: &mut Frame,
+ device_size: Option<DeviceIntSize>,
+ buffer_age: usize,
+ results: &mut RenderResults,
+ ) {
+ profile_scope!("draw_frame");
+
+ // These markers seem to crash a lot on Android, see bug 1559834
+ #[cfg(not(target_os = "android"))]
+ let _gm = self.gpu_profiler.start_marker("draw frame");
+
+ if frame.passes.is_empty() {
+ frame.has_been_rendered = true;
+ return;
+ }
+
+ self.device.disable_depth_write();
+ self.set_blend(false, FramebufferKind::Other);
+ self.device.disable_stencil();
+
+ self.bind_frame_data(frame);
+
+ // Determine the present mode and dirty rects, if device_size
+ // is Some(..). If it's None, no composite will occur and only
+ // picture cache and texture cache targets will be updated.
+ // TODO(gw): Split Frame so that it's clearer when a composite
+ // is occurring.
+ let present_mode = device_size.and_then(|device_size| {
+ self.calculate_dirty_rects(
+ buffer_age,
+ &frame.composite_state,
+ device_size,
+ results,
+ )
+ });
+
+ // If we have a native OS compositor, then make use of that interface to
+ // specify how to composite each of the picture cache surfaces. First, we
+ // need to find each tile that may be bound and updated later in the frame
+ // and invalidate it so that the native render compositor knows that these
+ // tiles can't be composited early. Next, after all such tiles have been
+ // invalidated, then we queue surfaces for native composition by the render
+ // compositor before we actually update the tiles. This allows the render
+ // compositor to start early composition while the tiles are updating.
+ if let CompositorKind::Native { .. } = self.current_compositor_kind {
+ let compositor = self.compositor_config.compositor().unwrap();
+ // Invalidate any native surface tiles that might be updated by passes.
+ if !frame.has_been_rendered {
+ for tile in frame.composite_state.opaque_tiles.iter().chain(frame.composite_state.alpha_tiles.iter()) {
+ if !tile.dirty_rect.is_empty() {
+ if let CompositeTileSurface::Texture { surface: ResolvedSurfaceTexture::Native { id, .. } } =
+ tile.surface {
+ let valid_rect = tile.valid_rect
+ .round()
+ .to_i32();
+ compositor.invalidate_tile(id, valid_rect);
+ }
+ }
+ }
+ }
+ // Ensure any external surfaces that might be used during early composition
+ // are invalidated first so that the native compositor can properly schedule
+ // composition to happen only when the external surface is updated.
+ // See update_external_native_surfaces for more details.
+ for surface in &frame.composite_state.external_surfaces {
+ if let Some((native_surface_id, size)) = surface.update_params {
+ let surface_rect = size.into();
+ compositor.invalidate_tile(NativeTileId { surface_id: native_surface_id, x: 0, y: 0 }, surface_rect);
+ }
+ }
+ // Finally queue native surfaces for early composition, if applicable. By now,
+ // we have already invalidated any tiles that such surfaces may depend upon, so
+ // the native render compositor can keep track of when to actually schedule
+ // composition as surfaces are updated.
+ frame.composite_state.composite_native(
+ &results.dirty_rects,
+ &mut **compositor,
+ );
+ }
+
+ for (_pass_index, pass) in frame.passes.iter_mut().enumerate() {
+ #[cfg(not(target_os = "android"))]
+ let _gm = self.gpu_profiler.start_marker(&format!("pass {}", _pass_index));
+
+ profile_scope!("offscreen target");
+
+ // If this frame has already been drawn, then any texture
+ // cache targets have already been updated and can be
+ // skipped this time.
+ if !frame.has_been_rendered {
+ for (&(texture_id, target_index), target) in &pass.texture_cache {
+ self.draw_texture_cache_target(
+ &texture_id,
+ target_index,
+ target,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+ }
+
+ if !pass.picture_cache.is_empty() {
+ self.profile.inc(profiler::COLOR_PASSES);
+ }
+
+ // Draw picture caching tiles for this pass.
+ for picture_target in &pass.picture_cache {
+ results.stats.color_target_count += 1;
+
+ let draw_target = match picture_target.surface {
+ ResolvedSurfaceTexture::TextureCache { ref texture, layer } => {
+ let (texture, _) = self.texture_resolver
+ .resolve(texture)
+ .expect("bug");
+
+ DrawTarget::from_texture(
+ texture,
+ layer as usize,
+ true,
+ )
+ }
+ ResolvedSurfaceTexture::Native { id, size } => {
+ let surface_info = match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.bind(
+ id,
+ picture_target.dirty_rect,
+ picture_target.valid_rect,
+ )
+ }
+ CompositorKind::Draw { .. } => {
+ unreachable!();
+ }
+ };
+
+ DrawTarget::NativeSurface {
+ offset: surface_info.origin,
+ external_fbo_id: surface_info.fbo_id,
+ dimensions: size,
+ }
+ }
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ self.draw_picture_cache_target(
+ picture_target,
+ draw_target,
+ &projection,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+
+ // Native OS surfaces must be unbound at the end of drawing to them
+ if let ResolvedSurfaceTexture::Native { .. } = picture_target.surface {
+ match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ let compositor = self.compositor_config.compositor().unwrap();
+ compositor.unbind();
+ }
+ CompositorKind::Draw { .. } => {
+ unreachable!();
+ }
+ }
+ }
+ }
+ }
+
+ for target in &pass.alpha.targets {
+ results.stats.alpha_target_count += 1;
+
+ let texture_id = target.texture_id();
+
+ let alpha_tex = self.texture_resolver
+ .texture_cache_map
+ .get_mut(&texture_id)
+ .expect("bug: texture not allocated");
+
+ let draw_target = DrawTarget::from_texture(
+ alpha_tex,
+ 0,
+ false,
+ );
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ self.draw_alpha_target(
+ draw_target,
+ target,
+ &projection,
+ &frame.render_tasks,
+ &mut results.stats,
+ );
+ }
+
+ let color_rt_info = RenderTargetInfo { has_depth: pass.color.needs_depth() };
+
+ for target in &pass.color.targets {
+ results.stats.color_target_count += 1;
+
+ let texture_id = target.texture_id();
+
+ let color_tex = self.texture_resolver
+ .texture_cache_map
+ .get_mut(&texture_id)
+ .expect("bug: texture not allocated");
+
+ self.device.reuse_render_target::<u8>(
+ color_tex,
+ color_rt_info,
+ );
+
+ let draw_target = DrawTarget::from_texture(
+ color_tex,
+ 0,
+ target.needs_depth(),
+ );
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ draw_target.dimensions().width as f32,
+ 0.0,
+ draw_target.dimensions().height as f32,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let clear_depth = if target.needs_depth() {
+ Some(1.0)
+ } else {
+ None
+ };
+
+ self.draw_color_target(
+ draw_target,
+ target,
+ Some([0.0, 0.0, 0.0, 0.0]),
+ clear_depth,
+ &frame.render_tasks,
+ &projection,
+ &mut results.stats,
+ );
+ }
+
+ // Only end the pass here and invalidate previous textures for
+ // off-screen targets. Deferring return of the inputs to the
+ // frame buffer until the implicit end_pass in end_frame allows
+ // debug draw overlays to be added without triggering a copy
+ // resolve stage in mobile / tiled GPUs.
+ self.texture_resolver.end_pass(
+ &mut self.device,
+ &pass.textures_to_invalidate,
+ );
+ {
+ profile_scope!("gl.flush");
+ self.device.gl().flush();
+ }
+ }
+
+ self.composite_frame(
+ frame,
+ device_size,
+ results,
+ present_mode,
+ );
+
+ frame.has_been_rendered = true;
+ }
+
+ fn composite_frame(
+ &mut self,
+ frame: &mut Frame,
+ device_size: Option<DeviceIntSize>,
+ results: &mut RenderResults,
+ present_mode: Option<PartialPresentMode>,
+ ) {
+ profile_scope!("main target");
+
+ if let Some(device_size) = device_size {
+ results.stats.color_target_count += 1;
+ results.picture_cache_debug = mem::replace(
+ &mut frame.composite_state.picture_cache_debug,
+ PictureCacheDebugInfo::new(),
+ );
+
+ let size = frame.device_rect.size.to_f32();
+ let surface_origin_is_top_left = self.device.surface_origin_is_top_left();
+ let (bottom, top) = if surface_origin_is_top_left {
+ (0.0, size.height)
+ } else {
+ (size.height, 0.0)
+ };
+
+ let projection = Transform3D::ortho(
+ 0.0,
+ size.width,
+ bottom,
+ top,
+ self.device.ortho_near_plane(),
+ self.device.ortho_far_plane(),
+ );
+
+ let fb_scale = Scale::<_, _, FramebufferPixel>::new(1i32);
+ let mut fb_rect = frame.device_rect * fb_scale;
+
+ if !surface_origin_is_top_left {
+ fb_rect.origin.y = device_size.height - fb_rect.origin.y - fb_rect.size.height;
+ }
+
+ let draw_target = DrawTarget::Default {
+ rect: fb_rect,
+ total_size: device_size * fb_scale,
+ surface_origin_is_top_left,
+ };
+
+ // If we have a native OS compositor, then make use of that interface
+ // to specify how to composite each of the picture cache surfaces.
+ match self.current_compositor_kind {
+ CompositorKind::Native { .. } => {
+ // We have already queued surfaces for early native composition by this point.
+ // All that is left is to finally update any external native surfaces that were
+ // invalidated so that composition can complete.
+ self.update_external_native_surfaces(
+ &frame.composite_state.external_surfaces,
+ results,
+ );
+ }
+ CompositorKind::Draw { .. } => {
+ self.composite_simple(
+ &frame.composite_state,
+ draw_target,
+ &projection,
+ results,
+ present_mode,
+ );
+ }
+ }
+ } else {
+ // Rendering a frame without presenting it will confuse the partial
+ // present logic, so force a full present for the next frame.
+ self.force_redraw();
+ }
+ }
+
+ pub fn debug_renderer(&mut self) -> Option<&mut DebugRenderer> {
+ self.debug.get_mut(&mut self.device)
+ }
+
+ pub fn get_debug_flags(&self) -> DebugFlags {
+ self.debug_flags
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ if let Some(enabled) = flag_changed(self.debug_flags, flags, DebugFlags::GPU_TIME_QUERIES) {
+ if enabled {
+ self.gpu_profiler.enable_timers();
+ } else {
+ self.gpu_profiler.disable_timers();
+ }
+ }
+ if let Some(enabled) = flag_changed(self.debug_flags, flags, DebugFlags::GPU_SAMPLE_QUERIES) {
+ if enabled {
+ self.gpu_profiler.enable_samplers();
+ } else {
+ self.gpu_profiler.disable_samplers();
+ }
+ }
+
+ self.debug_flags = flags;
+ }
+
+ pub fn set_profiler_ui(&mut self, ui_str: &str) {
+ self.profiler.set_ui(ui_str);
+ }
+
+ fn draw_frame_debug_items(&mut self, items: &[DebugItem]) {
+ if items.is_empty() {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ for item in items {
+ match item {
+ DebugItem::Rect { rect, outer_color, inner_color } => {
+ debug_renderer.add_quad(
+ rect.origin.x,
+ rect.origin.y,
+ rect.origin.x + rect.size.width,
+ rect.origin.y + rect.size.height,
+ (*inner_color).into(),
+ (*inner_color).into(),
+ );
+
+ debug_renderer.add_rect(
+ &rect.to_i32(),
+ (*outer_color).into(),
+ );
+ }
+ DebugItem::Text { ref msg, position, color } => {
+ debug_renderer.add_text(
+ position.x,
+ position.y,
+ msg,
+ (*color).into(),
+ None,
+ );
+ }
+ }
+ }
+ }
+
+ fn draw_render_target_debug(&mut self, draw_target: &DrawTarget) {
+ if !self.debug_flags.contains(DebugFlags::RENDER_TARGET_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let textures = self.texture_resolver
+ .texture_cache_map
+ .values()
+ .filter(|texture| { texture.is_render_target() })
+ .collect::<Vec<&Texture>>();
+
+ Self::do_debug_blit(
+ &mut self.device,
+ debug_renderer,
+ textures,
+ draw_target,
+ 0,
+ &|_| [0.0, 1.0, 0.0, 1.0], // Use green for all RTs.
+ );
+ }
+
+ fn draw_zoom_debug(
+ &mut self,
+ device_size: DeviceIntSize,
+ ) {
+ if !self.debug_flags.contains(DebugFlags::ZOOM_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let source_size = DeviceIntSize::new(64, 64);
+ let target_size = DeviceIntSize::new(1024, 1024);
+
+ let source_origin = DeviceIntPoint::new(
+ (self.cursor_position.x - source_size.width / 2)
+ .min(device_size.width - source_size.width)
+ .max(0),
+ (self.cursor_position.y - source_size.height / 2)
+ .min(device_size.height - source_size.height)
+ .max(0),
+ );
+
+ let source_rect = DeviceIntRect::new(
+ source_origin,
+ source_size,
+ );
+
+ let target_rect = DeviceIntRect::new(
+ DeviceIntPoint::new(
+ device_size.width - target_size.width - 64,
+ device_size.height - target_size.height - 64,
+ ),
+ target_size,
+ );
+
+ let texture_rect = FramebufferIntRect::new(
+ FramebufferIntPoint::zero(),
+ source_rect.size.cast_unit(),
+ );
+
+ debug_renderer.add_rect(
+ &target_rect.inflate(1, 1),
+ debug_colors::RED.into(),
+ );
+
+ if self.zoom_debug_texture.is_none() {
+ let texture = self.device.create_texture(
+ ImageBufferKind::Texture2D,
+ ImageFormat::BGRA8,
+ source_rect.size.width,
+ source_rect.size.height,
+ TextureFilter::Nearest,
+ Some(RenderTargetInfo { has_depth: false }),
+ 1,
+ );
+
+ self.zoom_debug_texture = Some(texture);
+ }
+
+ // Copy frame buffer into the zoom texture
+ let read_target = DrawTarget::new_default(device_size, self.device.surface_origin_is_top_left());
+ self.device.blit_render_target(
+ read_target.into(),
+ read_target.to_framebuffer_rect(source_rect),
+ DrawTarget::from_texture(
+ self.zoom_debug_texture.as_ref().unwrap(),
+ 0,
+ false,
+ ),
+ texture_rect,
+ TextureFilter::Nearest,
+ );
+
+ // Draw the zoom texture back to the framebuffer
+ self.device.blit_render_target(
+ ReadTarget::from_texture(
+ self.zoom_debug_texture.as_ref().unwrap(),
+ 0,
+ ),
+ texture_rect,
+ read_target,
+ read_target.to_framebuffer_rect(target_rect),
+ TextureFilter::Nearest,
+ );
+ }
+
+ fn draw_texture_cache_debug(&mut self, draw_target: &DrawTarget) {
+ if !self.debug_flags.contains(DebugFlags::TEXTURE_CACHE_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let textures =
+ self.texture_resolver.texture_cache_map.values().collect::<Vec<&Texture>>();
+
+ fn select_color(texture: &Texture) -> [f32; 4] {
+ if texture.flags().contains(TextureFlags::IS_SHARED_TEXTURE_CACHE) {
+ [1.0, 0.5, 0.0, 1.0] // Orange for shared.
+ } else {
+ [1.0, 0.0, 1.0, 1.0] // Fuchsia for standalone.
+ }
+ }
+
+ Self::do_debug_blit(
+ &mut self.device,
+ debug_renderer,
+ textures,
+ draw_target,
+ if self.debug_flags.contains(DebugFlags::RENDER_TARGET_DBG) { 544 } else { 0 },
+ &select_color,
+ );
+ }
+
+ fn do_debug_blit(
+ device: &mut Device,
+ debug_renderer: &mut DebugRenderer,
+ mut textures: Vec<&Texture>,
+ draw_target: &DrawTarget,
+ bottom: i32,
+ select_color: &dyn Fn(&Texture) -> [f32; 4],
+ ) {
+ let mut spacing = 16;
+ let mut size = 512;
+
+ let device_size = draw_target.dimensions();
+ let fb_width = device_size.width;
+ let fb_height = device_size.height;
+ let surface_origin_is_top_left = draw_target.surface_origin_is_top_left();
+
+ let num_layers: i32 = textures.iter()
+ .map(|texture| texture.get_layer_count())
+ .sum();
+
+ if num_layers * (size + spacing) > fb_width {
+ let factor = fb_width as f32 / (num_layers * (size + spacing)) as f32;
+ size = (size as f32 * factor) as i32;
+ spacing = (spacing as f32 * factor) as i32;
+ }
+
+ let text_height = 14; // Visually approximated.
+ let text_margin = 1;
+ let tag_height = text_height + text_margin * 2;
+ let tag_y = fb_height - (bottom + spacing + tag_height);
+ let image_y = tag_y - size;
+
+ // Sort the display by layer size (in bytes), so that left-to-right is
+ // largest-to-smallest.
+ //
+ // Note that the vec here is in increasing order, because the elements
+ // get drawn right-to-left.
+ textures.sort_by_key(|t| t.layer_size_in_bytes());
+
+ let mut i = 0;
+ for texture in textures.iter() {
+ let dimensions = texture.get_dimensions();
+ let src_rect = FramebufferIntRect::new(
+ FramebufferIntPoint::zero(),
+ FramebufferIntSize::new(dimensions.width as i32, dimensions.height as i32),
+ );
+
+ let layer_count = texture.get_layer_count() as usize;
+ for layer in 0 .. layer_count {
+ let x = fb_width - (spacing + size) * (i as i32 + 1);
+
+ // If we have more targets than fit on one row in screen, just early exit.
+ if x > fb_width {
+ return;
+ }
+
+ // Draw the info tag.
+ let tag_rect = rect(x, tag_y, size, tag_height);
+ let tag_color = select_color(texture);
+ device.clear_target(
+ Some(tag_color),
+ None,
+ Some(draw_target.to_framebuffer_rect(tag_rect)),
+ );
+
+ // Draw the dimensions onto the tag.
+ let dim = texture.get_dimensions();
+ let text_rect = tag_rect.inflate(-text_margin, -text_margin);
+ debug_renderer.add_text(
+ text_rect.min_x() as f32,
+ text_rect.max_y() as f32, // Top-relative.
+ &format!("{}x{}", dim.width, dim.height),
+ ColorU::new(0, 0, 0, 255),
+ Some(tag_rect.to_f32())
+ );
+
+ // Blit the contents of the layer.
+ let dest_rect = draw_target.to_framebuffer_rect(rect(x, image_y, size, size));
+ let read_target = ReadTarget::from_texture(texture, layer);
+
+ if surface_origin_is_top_left {
+ device.blit_render_target(
+ read_target,
+ src_rect,
+ *draw_target,
+ dest_rect,
+ TextureFilter::Linear,
+ );
+ } else {
+ // Invert y.
+ device.blit_render_target_invert_y(
+ read_target,
+ src_rect,
+ *draw_target,
+ dest_rect,
+ );
+ }
+ i += 1;
+ }
+ }
+ }
+
+ fn draw_epoch_debug(&mut self) {
+ if !self.debug_flags.contains(DebugFlags::EPOCHS) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let dy = debug_renderer.line_height();
+ let x0: f32 = 30.0;
+ let y0: f32 = 30.0;
+ let mut y = y0;
+ let mut text_width = 0.0;
+ for ((pipeline, document_id), epoch) in &self.pipeline_info.epochs {
+ y += dy;
+ let w = debug_renderer.add_text(
+ x0, y,
+ &format!("({:?}, {:?}): {:?}", pipeline, document_id, epoch),
+ ColorU::new(255, 255, 0, 255),
+ None,
+ ).size.width;
+ text_width = f32::max(text_width, w);
+ }
+
+ let margin = 10.0;
+ debug_renderer.add_quad(
+ x0 - margin,
+ y0 - margin,
+ x0 + text_width + margin,
+ y + margin,
+ ColorU::new(25, 25, 25, 200),
+ ColorU::new(51, 51, 51, 200),
+ );
+ }
+
+ fn draw_gpu_cache_debug(&mut self, device_size: DeviceIntSize) {
+ if !self.debug_flags.contains(DebugFlags::GPU_CACHE_DBG) {
+ return;
+ }
+
+ let debug_renderer = match self.debug.get_mut(&mut self.device) {
+ Some(render) => render,
+ None => return,
+ };
+
+ let (x_off, y_off) = (30f32, 30f32);
+ let height = self.gpu_cache_texture.get_height()
+ .min(device_size.height - (y_off as i32) * 2) as usize;
+ debug_renderer.add_quad(
+ x_off,
+ y_off,
+ x_off + MAX_VERTEX_TEXTURE_WIDTH as f32,
+ y_off + height as f32,
+ ColorU::new(80, 80, 80, 80),
+ ColorU::new(80, 80, 80, 80),
+ );
+
+ let upper = self.gpu_cache_debug_chunks.len().min(height);
+ for chunk in self.gpu_cache_debug_chunks[0..upper].iter().flatten() {
+ let color = ColorU::new(250, 0, 0, 200);
+ debug_renderer.add_quad(
+ x_off + chunk.address.u as f32,
+ y_off + chunk.address.v as f32,
+ x_off + chunk.address.u as f32 + chunk.size as f32,
+ y_off + chunk.address.v as f32 + 1.0,
+ color,
+ color,
+ );
+ }
+ }
+
+ /// Pass-through to `Device::read_pixels_into`, used by Gecko's WR bindings.
+ pub fn read_pixels_into(&mut self, rect: FramebufferIntRect, format: ImageFormat, output: &mut [u8]) {
+ self.device.read_pixels_into(rect, format, output);
+ }
+
+ pub fn read_pixels_rgba8(&mut self, rect: FramebufferIntRect) -> Vec<u8> {
+ let mut pixels = vec![0; (rect.size.width * rect.size.height * 4) as usize];
+ self.device.read_pixels_into(rect, ImageFormat::RGBA8, &mut pixels);
+ pixels
+ }
+
+ // De-initialize the Renderer safely, assuming the GL is still alive and active.
+ pub fn deinit(mut self) {
+ //Note: this is a fake frame, only needed because texture deletion is require to happen inside a frame
+ self.device.begin_frame();
+ // If we are using a native compositor, ensure that any remaining native
+ // surfaces are freed.
+ if let CompositorConfig::Native { mut compositor, .. } = self.compositor_config {
+ for id in self.allocated_native_surfaces.drain() {
+ compositor.destroy_surface(id);
+ }
+ // Destroy the debug overlay surface, if currently allocated.
+ if self.debug_overlay_state.current_size.is_some() {
+ compositor.destroy_surface(NativeSurfaceId::DEBUG_OVERLAY);
+ }
+ compositor.deinit();
+ }
+ self.gpu_cache_texture.deinit(&mut self.device);
+ if let Some(dither_matrix_texture) = self.dither_matrix_texture {
+ self.device.delete_texture(dither_matrix_texture);
+ }
+ if let Some(zoom_debug_texture) = self.zoom_debug_texture {
+ self.device.delete_texture(zoom_debug_texture);
+ }
+ for textures in self.vertex_data_textures.drain(..) {
+ textures.deinit(&mut self.device);
+ }
+ self.texture_upload_pbo_pool.deinit(&mut self.device);
+ self.texture_resolver.deinit(&mut self.device);
+ self.vaos.deinit(&mut self.device);
+ self.debug.deinit(&mut self.device);
+
+ if let Ok(shaders) = Rc::try_unwrap(self.shaders) {
+ shaders.into_inner().deinit(&mut self.device);
+ }
+
+ if let Some(async_screenshots) = self.async_screenshots.take() {
+ async_screenshots.deinit(&mut self.device);
+ }
+
+ if let Some(async_frame_recorder) = self.async_frame_recorder.take() {
+ async_frame_recorder.deinit(&mut self.device);
+ }
+
+ #[cfg(feature = "capture")]
+ self.device.delete_fbo(self.read_fbo);
+ #[cfg(feature = "replay")]
+ for (_, ext) in self.owned_external_images {
+ self.device.delete_external_texture(ext);
+ }
+ self.device.end_frame();
+ }
+
+ fn size_of<T>(&self, ptr: *const T) -> usize {
+ let ops = self.size_of_ops.as_ref().unwrap();
+ unsafe { ops.malloc_size_of(ptr) }
+ }
+
+ /// Collects a memory report.
+ pub fn report_memory(&self) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ // GPU cache CPU memory.
+ self.gpu_cache_texture.report_memory_to(&mut report, self.size_of_ops.as_ref().unwrap());
+
+ // Render task CPU memory.
+ for (_id, doc) in &self.active_documents {
+ report.render_tasks += self.size_of(doc.frame.render_tasks.tasks.as_ptr());
+ report.render_tasks += self.size_of(doc.frame.render_tasks.task_data.as_ptr());
+ }
+
+ // Vertex data GPU memory.
+ for textures in &self.vertex_data_textures {
+ report.vertex_data_textures += textures.size_in_bytes();
+ }
+
+ // Texture cache and render target GPU memory.
+ report += self.texture_resolver.report_memory();
+
+ // Texture upload PBO memory.
+ report += self.texture_upload_pbo_pool.report_memory();
+
+ // Textures held internally within the device layer.
+ report += self.device.report_memory();
+
+ report
+ }
+
+ // Sets the blend mode. Blend is unconditionally set if the "show overdraw" debugging mode is
+ // enabled.
+ fn set_blend(&mut self, mut blend: bool, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ blend = true
+ }
+ self.device.set_blend(blend)
+ }
+
+ fn set_blend_mode_multiply(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_multiply();
+ }
+ }
+
+ fn set_blend_mode_premultiplied_alpha(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_premultiplied_alpha();
+ }
+ }
+
+ fn set_blend_mode_subpixel_with_bg_color_pass1(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_subpixel_with_bg_color_pass1();
+ }
+ }
+
+ fn set_blend_mode_subpixel_with_bg_color_pass2(&mut self, framebuffer_kind: FramebufferKind) {
+ if framebuffer_kind == FramebufferKind::Main &&
+ self.debug_flags.contains(DebugFlags::SHOW_OVERDRAW) {
+ self.device.set_blend_mode_show_overdraw();
+ } else {
+ self.device.set_blend_mode_subpixel_with_bg_color_pass2();
+ }
+ }
+
+ /// Clears all the layers of a texture with a given color.
+ fn clear_texture(&mut self, texture: &Texture, color: [f32; 4]) {
+ for i in 0..texture.get_layer_count() {
+ self.device.bind_draw_target(DrawTarget::from_texture(
+ &texture,
+ i as usize,
+ false,
+ ));
+ self.device.clear_target(Some(color), None, None);
+ }
+ }
+}
+
+pub trait ThreadListener {
+ fn thread_started(&self, thread_name: &str);
+ fn thread_stopped(&self, thread_name: &str);
+}
+
+/// Allows callers to hook in at certain points of the async scene build. These
+/// functions are all called from the scene builder thread.
+pub trait SceneBuilderHooks {
+ /// This is called exactly once, when the scene builder thread is started
+ /// and before it processes anything.
+ fn register(&self);
+ /// This is called before each scene build starts.
+ fn pre_scene_build(&self);
+ /// This is called before each scene swap occurs.
+ fn pre_scene_swap(&self, scenebuild_time: u64);
+ /// This is called after each scene swap occurs. The PipelineInfo contains
+ /// the updated epochs and pipelines removed in the new scene compared to
+ /// the old scene.
+ fn post_scene_swap(&self, document_id: &Vec<DocumentId>, info: PipelineInfo, sceneswap_time: u64);
+ /// This is called after a resource update operation on the scene builder
+ /// thread, in the case where resource updates were applied without a scene
+ /// build.
+ fn post_resource_update(&self, document_ids: &Vec<DocumentId>);
+ /// This is called after a scene build completes without any changes being
+ /// made. We guarantee that each pre_scene_build call will be matched with
+ /// exactly one of post_scene_swap, post_resource_update or
+ /// post_empty_scene_build.
+ fn post_empty_scene_build(&self);
+ /// This is a generic callback which provides an opportunity to run code
+ /// on the scene builder thread. This is called as part of the main message
+ /// loop of the scene builder thread, but outside of any specific message
+ /// handler.
+ fn poke(&self);
+ /// This is called exactly once, when the scene builder thread is about to
+ /// terminate.
+ fn deregister(&self);
+}
+
+/// Allows callers to hook into the main render_backend loop and provide
+/// additional frame ops for generate_frame transactions. These functions
+/// are all called from the render backend thread.
+pub trait AsyncPropertySampler {
+ /// This is called exactly once, when the render backend thread is started
+ /// and before it processes anything.
+ fn register(&self);
+ /// This is called for each transaction with the generate_frame flag set
+ /// (i.e. that will trigger a render). The list of frame messages returned
+ /// are processed as though they were part of the original transaction.
+ fn sample(&self, document_id: DocumentId, generated_frame_id: Option<u64>) -> Vec<FrameMsg>;
+ /// This is called exactly once, when the render backend thread is about to
+ /// terminate.
+ fn deregister(&self);
+}
+
+bitflags! {
+ /// Flags that control how shaders are pre-cached, if at all.
+ #[derive(Default)]
+ pub struct ShaderPrecacheFlags: u32 {
+ /// Needed for const initialization
+ const EMPTY = 0;
+
+ /// Only start async compile
+ const ASYNC_COMPILE = 1 << 2;
+
+ /// Do a full compile/link during startup
+ const FULL_COMPILE = 1 << 3;
+ }
+}
+
+pub struct RendererOptions {
+ pub device_pixel_ratio: f32,
+ pub resource_override_path: Option<PathBuf>,
+ /// Whether to use shaders that have been optimized at build time.
+ pub use_optimized_shaders: bool,
+ pub enable_aa: bool,
+ pub enable_dithering: bool,
+ pub max_recorded_profiles: usize,
+ pub precache_flags: ShaderPrecacheFlags,
+ /// Enable sub-pixel anti-aliasing if a fast implementation is available.
+ pub enable_subpixel_aa: bool,
+ /// Enable sub-pixel anti-aliasing if it requires a slow implementation.
+ pub force_subpixel_aa: bool,
+ pub clear_color: Option<ColorF>,
+ pub enable_clear_scissor: bool,
+ pub max_texture_size: Option<i32>,
+ pub upload_method: UploadMethod,
+ /// The default size in bytes for PBOs used to upload texture data.
+ pub upload_pbo_default_size: usize,
+ pub workers: Option<Arc<ThreadPool>>,
+ pub enable_multithreading: bool,
+ pub blob_image_handler: Option<Box<dyn BlobImageHandler>>,
+ pub crash_annotator: Option<Box<dyn CrashAnnotator>>,
+ pub thread_listener: Option<Box<dyn ThreadListener + Send + Sync>>,
+ pub size_of_op: Option<VoidPtrToSizeFn>,
+ pub enclosing_size_of_op: Option<VoidPtrToSizeFn>,
+ pub cached_programs: Option<Rc<ProgramCache>>,
+ pub debug_flags: DebugFlags,
+ pub renderer_id: Option<u64>,
+ pub scene_builder_hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ pub sampler: Option<Box<dyn AsyncPropertySampler + Send>>,
+ pub chase_primitive: ChasePrimitive,
+ pub support_low_priority_transactions: bool,
+ pub namespace_alloc_by_client: bool,
+ pub testing: bool,
+ /// Set to true if this GPU supports hardware fast clears as a performance
+ /// optimization. Likely requires benchmarking on various GPUs to see if
+ /// it is a performance win. The default is false, which tends to be best
+ /// performance on lower end / integrated GPUs.
+ pub gpu_supports_fast_clears: bool,
+ pub allow_dual_source_blending: bool,
+ pub allow_advanced_blend_equation: bool,
+ /// If true, allow textures to be initialized with glTexStorage.
+ /// This affects VRAM consumption and data upload paths.
+ pub allow_texture_storage_support: bool,
+ /// If true, we allow the data uploaded in a different format from the
+ /// one expected by the driver, pretending the format is matching, and
+ /// swizzling the components on all the shader sampling.
+ pub allow_texture_swizzling: bool,
+ /// Use `ps_clear` shader with batched quad rendering to clear the rects
+ /// in texture cache and picture cache tasks.
+ /// This helps to work around some Intel drivers
+ /// that incorrectly synchronize clears to following draws.
+ pub clear_caches_with_quads: bool,
+ /// Start the debug server for this renderer.
+ pub start_debug_server: bool,
+ /// Output the source of the shader with the given name.
+ pub dump_shader_source: Option<String>,
+ pub surface_origin_is_top_left: bool,
+ /// The configuration options defining how WR composites the final scene.
+ pub compositor_config: CompositorConfig,
+ pub enable_gpu_markers: bool,
+ /// If true, panic whenever a GL error occurs. This has a significant
+ /// performance impact, so only use when debugging specific problems!
+ pub panic_on_gl_error: bool,
+ pub picture_tile_size: Option<DeviceIntSize>,
+ pub texture_cache_config: TextureCacheConfig,
+ /// If true, we'll use instanced vertex attributes. Each instace is a quad.
+ /// If false, we'll duplicate the instance attributes per vertex and issue
+ /// regular indexed draws instead.
+ pub enable_instancing: bool,
+}
+
+impl RendererOptions {
+ /// Number of batches to look back in history for adding the current
+ /// transparent instance into.
+ const BATCH_LOOKBACK_COUNT: usize = 10;
+
+ /// Since we are re-initializing the instance buffers on every draw call,
+ /// the driver has to internally manage PBOs in flight.
+ /// It's typically done by bucketing up to a specific limit, and then
+ /// just individually managing the largest buffers.
+ /// Having a limit here allows the drivers to more easily manage
+ /// the PBOs for us.
+ const MAX_INSTANCE_BUFFER_SIZE: usize = 0x20000; // actual threshold in macOS GL drivers
+}
+
+impl Default for RendererOptions {
+ fn default() -> Self {
+ RendererOptions {
+ device_pixel_ratio: 1.0,
+ resource_override_path: None,
+ use_optimized_shaders: false,
+ enable_aa: true,
+ enable_dithering: false,
+ debug_flags: DebugFlags::empty(),
+ max_recorded_profiles: 0,
+ precache_flags: ShaderPrecacheFlags::empty(),
+ enable_subpixel_aa: false,
+ force_subpixel_aa: false,
+ clear_color: Some(ColorF::new(1.0, 1.0, 1.0, 1.0)),
+ enable_clear_scissor: true,
+ max_texture_size: None,
+ // This is best as `Immediate` on Angle, or `Pixelbuffer(Dynamic)` on GL,
+ // but we are unable to make this decision here, so picking the reasonable medium.
+ upload_method: UploadMethod::PixelBuffer(ONE_TIME_USAGE_HINT),
+ upload_pbo_default_size: 512 * 512 * 4,
+ workers: None,
+ enable_multithreading: true,
+ blob_image_handler: None,
+ crash_annotator: None,
+ thread_listener: None,
+ size_of_op: None,
+ enclosing_size_of_op: None,
+ renderer_id: None,
+ cached_programs: None,
+ scene_builder_hooks: None,
+ sampler: None,
+ chase_primitive: ChasePrimitive::Nothing,
+ support_low_priority_transactions: false,
+ namespace_alloc_by_client: false,
+ testing: false,
+ gpu_supports_fast_clears: false,
+ allow_dual_source_blending: true,
+ allow_advanced_blend_equation: false,
+ allow_texture_storage_support: true,
+ allow_texture_swizzling: true,
+ clear_caches_with_quads: true,
+ // For backwards compatibility we set this to true by default, so
+ // that if the debugger feature is enabled, the debug server will
+ // be started automatically. Users can explicitly disable this as
+ // needed.
+ start_debug_server: true,
+ dump_shader_source: None,
+ surface_origin_is_top_left: false,
+ compositor_config: CompositorConfig::default(),
+ enable_gpu_markers: true,
+ panic_on_gl_error: false,
+ picture_tile_size: None,
+ texture_cache_config: TextureCacheConfig::DEFAULT,
+ // Disabling instancing means more vertex data to upload and potentially
+ // process by the vertex shaders.
+ enable_instancing: true,
+ }
+ }
+}
+
+pub trait DebugServer {
+ fn send(&mut self, _message: String);
+}
+
+struct NoopDebugServer;
+
+impl NoopDebugServer {
+ fn new(_: Sender<ApiMsg>) -> Self {
+ NoopDebugServer
+ }
+}
+
+impl DebugServer for NoopDebugServer {
+ fn send(&mut self, _: String) {}
+}
+
+#[cfg(feature = "debugger")]
+fn new_debug_server(enable: bool, api_tx: Sender<ApiMsg>) -> Box<dyn DebugServer> {
+ if enable {
+ Box::new(debug_server::DebugServerImpl::new(api_tx))
+ } else {
+ Box::new(NoopDebugServer::new(api_tx))
+ }
+}
+
+#[cfg(not(feature = "debugger"))]
+fn new_debug_server(_enable: bool, api_tx: Sender<ApiMsg>) -> Box<dyn DebugServer> {
+ Box::new(NoopDebugServer::new(api_tx))
+}
+
+/// Some basic statistics about the rendered scene, used in Gecko, as
+/// well as in wrench reftests to ensure that tests are batching and/or
+/// allocating on render targets as we expect them to.
+#[repr(C)]
+#[derive(Debug, Default)]
+pub struct RendererStats {
+ pub total_draw_calls: usize,
+ pub alpha_target_count: usize,
+ pub color_target_count: usize,
+ pub texture_upload_mb: f64,
+ pub resource_upload_time: f64,
+ pub gpu_cache_upload_time: f64,
+}
+
+/// Return type from render(), which contains some repr(C) statistics as well as
+/// some non-repr(C) data.
+#[derive(Debug, Default)]
+pub struct RenderResults {
+ /// Statistics about the frame that was rendered.
+ pub stats: RendererStats,
+
+ /// A list of the device dirty rects that were updated
+ /// this frame.
+ /// TODO(gw): This is an initial interface, likely to change in future.
+ /// TODO(gw): The dirty rects here are currently only useful when scrolling
+ /// is not occurring. They are still correct in the case of
+ /// scrolling, but will be very large (until we expose proper
+ /// OS compositor support where the dirty rects apply to a
+ /// specific picture cache slice / OS compositor surface).
+ pub dirty_rects: Vec<DeviceIntRect>,
+
+ /// Information about the state of picture cache tiles. This is only
+ /// allocated and stored if config.testing is true (such as wrench)
+ pub picture_cache_debug: PictureCacheDebugInfo,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainTexture {
+ data: String,
+ size: (DeviceIntSize, i32),
+ format: ImageFormat,
+ filter: TextureFilter,
+ has_depth: bool,
+ is_array: bool,
+}
+
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainRenderer {
+ device_size: Option<DeviceIntSize>,
+ gpu_cache: PlainTexture,
+ gpu_cache_frame_id: FrameId,
+ textures: FastHashMap<CacheTextureId, PlainTexture>,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainExternalResources {
+ images: Vec<ExternalCaptureImage>
+}
+
+#[cfg(feature = "replay")]
+enum CapturedExternalImageData {
+ NativeTexture(gl::GLuint),
+ Buffer(Arc<Vec<u8>>),
+}
+
+#[cfg(feature = "replay")]
+struct DummyExternalImageHandler {
+ data: FastHashMap<(ExternalImageId, u8), (CapturedExternalImageData, TexelRect)>,
+}
+
+#[cfg(feature = "replay")]
+impl ExternalImageHandler for DummyExternalImageHandler {
+ fn lock(&mut self, key: ExternalImageId, channel_index: u8, _rendering: ImageRendering) -> ExternalImage {
+ let (ref captured_data, ref uv) = self.data[&(key, channel_index)];
+ ExternalImage {
+ uv: *uv,
+ source: match *captured_data {
+ CapturedExternalImageData::NativeTexture(tid) => ExternalImageSource::NativeTexture(tid),
+ CapturedExternalImageData::Buffer(ref arc) => ExternalImageSource::RawData(&*arc),
+ }
+ }
+ }
+ fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {}
+}
+
+#[derive(Default)]
+pub struct PipelineInfo {
+ pub epochs: FastHashMap<(PipelineId, DocumentId), Epoch>,
+ pub removed_pipelines: Vec<(PipelineId, DocumentId)>,
+}
+
+impl Renderer {
+ #[cfg(feature = "capture")]
+ fn save_texture(
+ texture: &Texture, name: &str, root: &PathBuf, device: &mut Device
+ ) -> PlainTexture {
+ use std::fs;
+ use std::io::Write;
+
+ let short_path = format!("textures/{}.raw", name);
+
+ let bytes_per_pixel = texture.get_format().bytes_per_pixel();
+ let read_format = texture.get_format();
+ let rect_size = texture.get_dimensions();
+
+ let mut file = fs::File::create(root.join(&short_path))
+ .expect(&format!("Unable to create {}", short_path));
+ let bytes_per_layer = (rect_size.width * rect_size.height * bytes_per_pixel) as usize;
+ let mut data = vec![0; bytes_per_layer];
+
+ //TODO: instead of reading from an FBO with `read_pixels*`, we could
+ // read from textures directly with `get_tex_image*`.
+
+ for layer_id in 0 .. texture.get_layer_count() {
+ let rect = device_size_as_framebuffer_size(rect_size).into();
+
+ device.attach_read_texture(texture, layer_id);
+ #[cfg(feature = "png")]
+ {
+ let mut png_data;
+ let (data_ref, format) = match texture.get_format() {
+ ImageFormat::RGBAF32 => {
+ png_data = vec![0; (rect_size.width * rect_size.height * 4) as usize];
+ device.read_pixels_into(rect, ImageFormat::RGBA8, &mut png_data);
+ (&png_data, ImageFormat::RGBA8)
+ }
+ fm => (&data, fm),
+ };
+ CaptureConfig::save_png(
+ root.join(format!("textures/{}-{}.png", name, layer_id)),
+ rect_size, format,
+ None,
+ data_ref,
+ );
+ }
+ device.read_pixels_into(rect, read_format, &mut data);
+ file.write_all(&data)
+ .unwrap();
+ }
+
+ PlainTexture {
+ data: short_path,
+ size: (rect_size, texture.get_layer_count()),
+ format: texture.get_format(),
+ filter: texture.get_filter(),
+ has_depth: texture.supports_depth(),
+ is_array: texture.is_array(),
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_texture(
+ target: ImageBufferKind,
+ plain: &PlainTexture,
+ rt_info: Option<RenderTargetInfo>,
+ root: &PathBuf,
+ device: &mut Device
+ ) -> (Texture, Vec<u8>)
+ {
+ use std::fs::File;
+ use std::io::Read;
+
+ let mut texels = Vec::new();
+ File::open(root.join(&plain.data))
+ .expect(&format!("Unable to open texture at {}", plain.data))
+ .read_to_end(&mut texels)
+ .unwrap();
+
+ let texture = device.create_texture(
+ target,
+ plain.format,
+ plain.size.0.width,
+ plain.size.0.height,
+ plain.filter,
+ rt_info,
+ plain.size.1,
+ );
+ device.upload_texture_immediate(&texture, &texels);
+
+ (texture, texels)
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture(
+ &mut self,
+ config: CaptureConfig,
+ deferred_images: Vec<ExternalCaptureImage>,
+ ) {
+ use std::fs;
+ use std::io::Write;
+ use api::ExternalImageData;
+ use crate::render_api::CaptureBits;
+
+ let root = config.resource_root();
+
+ self.device.begin_frame();
+ let _gm = self.gpu_profiler.start_marker("read GPU data");
+ self.device.bind_read_target_impl(self.read_fbo);
+
+ if config.bits.contains(CaptureBits::EXTERNAL_RESOURCES) && !deferred_images.is_empty() {
+ info!("saving external images");
+ let mut arc_map = FastHashMap::<*const u8, String>::default();
+ let mut tex_map = FastHashMap::<u32, String>::default();
+ let handler = self.external_image_handler
+ .as_mut()
+ .expect("Unable to lock the external image handler!");
+ for def in &deferred_images {
+ info!("\t{}", def.short_path);
+ let ExternalImageData { id, channel_index, image_type } = def.external;
+ // The image rendering parameter is irrelevant because no filtering happens during capturing.
+ let ext_image = handler.lock(id, channel_index, ImageRendering::Auto);
+ let (data, short_path) = match ext_image.source {
+ ExternalImageSource::RawData(data) => {
+ let arc_id = arc_map.len() + 1;
+ match arc_map.entry(data.as_ptr()) {
+ Entry::Occupied(e) => {
+ (None, e.get().clone())
+ }
+ Entry::Vacant(e) => {
+ let short_path = format!("externals/d{}.raw", arc_id);
+ (Some(data.to_vec()), e.insert(short_path).clone())
+ }
+ }
+ }
+ ExternalImageSource::NativeTexture(gl_id) => {
+ let tex_id = tex_map.len() + 1;
+ match tex_map.entry(gl_id) {
+ Entry::Occupied(e) => {
+ (None, e.get().clone())
+ }
+ Entry::Vacant(e) => {
+ let target = match image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => unreachable!(),
+ };
+ info!("\t\tnative texture of target {:?}", target);
+ let layer_index = 0; //TODO: what about layered textures?
+ self.device.attach_read_texture_external(gl_id, target, layer_index);
+ let data = self.device.read_pixels(&def.descriptor);
+ let short_path = format!("externals/t{}.raw", tex_id);
+ (Some(data), e.insert(short_path).clone())
+ }
+ }
+ }
+ ExternalImageSource::Invalid => {
+ info!("\t\tinvalid source!");
+ (None, String::new())
+ }
+ };
+ if let Some(bytes) = data {
+ fs::File::create(root.join(&short_path))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&bytes)
+ .unwrap();
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(&short_path).with_extension("png"),
+ def.descriptor.size,
+ def.descriptor.format,
+ def.descriptor.stride,
+ &bytes,
+ );
+ }
+ let plain = PlainExternalImage {
+ data: short_path,
+ external: def.external,
+ uv: ext_image.uv,
+ };
+ config.serialize_for_resource(&plain, &def.short_path);
+ }
+ for def in &deferred_images {
+ handler.unlock(def.external.id, def.external.channel_index);
+ }
+ let plain_external = PlainExternalResources {
+ images: deferred_images,
+ };
+ config.serialize_for_resource(&plain_external, "external_resources");
+ }
+
+ if config.bits.contains(CaptureBits::FRAME) {
+ let path_textures = root.join("textures");
+ if !path_textures.is_dir() {
+ fs::create_dir(&path_textures).unwrap();
+ }
+
+ info!("saving GPU cache");
+ self.update_gpu_cache(); // flush pending updates
+ let mut plain_self = PlainRenderer {
+ device_size: self.device_size,
+ gpu_cache: Self::save_texture(
+ self.gpu_cache_texture.get_texture(),
+ "gpu", &root, &mut self.device,
+ ),
+ gpu_cache_frame_id: self.gpu_cache_frame_id,
+ textures: FastHashMap::default(),
+ };
+
+ info!("saving cached textures");
+ for (id, texture) in &self.texture_resolver.texture_cache_map {
+ let file_name = format!("cache-{}", plain_self.textures.len() + 1);
+ info!("\t{}", file_name);
+ let plain = Self::save_texture(texture, &file_name, &root, &mut self.device);
+ plain_self.textures.insert(*id, plain);
+ }
+
+ config.serialize_for_resource(&plain_self, "renderer");
+ }
+
+ self.device.reset_read_target();
+ self.device.end_frame();
+
+ let mut stats_file = fs::File::create(config.root.join("profiler-stats.txt"))
+ .expect(&format!("Unable to create profiler-stats.txt"));
+ if self.debug_flags.intersects(DebugFlags::PROFILER_DBG | DebugFlags::PROFILER_CAPTURE) {
+ self.profiler.dump_stats(&mut stats_file).unwrap();
+ } else {
+ writeln!(stats_file, "Turn on PROFILER_DBG or PROFILER_CAPTURE to get stats here!").unwrap();
+ }
+
+ info!("done.");
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_capture(
+ &mut self,
+ config: CaptureConfig,
+ plain_externals: Vec<PlainExternalImage>,
+ ) {
+ use std::{fs::File, io::Read};
+
+ info!("loading external buffer-backed images");
+ assert!(self.texture_resolver.external_images.is_empty());
+ let mut raw_map = FastHashMap::<String, Arc<Vec<u8>>>::default();
+ let mut image_handler = DummyExternalImageHandler {
+ data: FastHashMap::default(),
+ };
+
+ let root = config.resource_root();
+
+ // Note: this is a `SCENE` level population of the external image handlers
+ // It would put both external buffers and texture into the map.
+ // But latter are going to be overwritten later in this function
+ // if we are in the `FRAME` level.
+ for plain_ext in plain_externals {
+ let data = match raw_map.entry(plain_ext.data) {
+ Entry::Occupied(e) => e.get().clone(),
+ Entry::Vacant(e) => {
+ let mut buffer = Vec::new();
+ File::open(root.join(e.key()))
+ .expect(&format!("Unable to open {}", e.key()))
+ .read_to_end(&mut buffer)
+ .unwrap();
+ e.insert(Arc::new(buffer)).clone()
+ }
+ };
+ let ext = plain_ext.external;
+ let value = (CapturedExternalImageData::Buffer(data), plain_ext.uv);
+ image_handler.data.insert((ext.id, ext.channel_index), value);
+ }
+
+ if let Some(external_resources) = config.deserialize_for_resource::<PlainExternalResources, _>("external_resources") {
+ info!("loading external texture-backed images");
+ let mut native_map = FastHashMap::<String, gl::GLuint>::default();
+ for ExternalCaptureImage { short_path, external, descriptor } in external_resources.images {
+ let target = match external.image_type {
+ ExternalImageType::TextureHandle(target) => target,
+ ExternalImageType::Buffer => continue,
+ };
+ let plain_ext = config.deserialize_for_resource::<PlainExternalImage, _>(&short_path)
+ .expect(&format!("Unable to read {}.ron", short_path));
+ let key = (external.id, external.channel_index);
+
+ let tid = match native_map.entry(plain_ext.data) {
+ Entry::Occupied(e) => e.get().clone(),
+ Entry::Vacant(e) => {
+ //TODO: provide a way to query both the layer count and the filter from external images
+ let (layer_count, filter) = (1, TextureFilter::Linear);
+ let plain_tex = PlainTexture {
+ data: e.key().clone(),
+ size: (descriptor.size, layer_count),
+ format: descriptor.format,
+ filter,
+ has_depth: false,
+ is_array: false,
+ };
+ let t = Self::load_texture(
+ target,
+ &plain_tex,
+ None,
+ &root,
+ &mut self.device
+ );
+ let extex = t.0.into_external();
+ self.owned_external_images.insert(key, extex.clone());
+ e.insert(extex.internal_id()).clone()
+ }
+ };
+
+ let value = (CapturedExternalImageData::NativeTexture(tid), plain_ext.uv);
+ image_handler.data.insert(key, value);
+ }
+ }
+
+ self.device.begin_frame();
+ self.gpu_cache_texture.remove_texture(&mut self.device);
+
+ if let Some(renderer) = config.deserialize_for_resource::<PlainRenderer, _>("renderer") {
+ info!("loading cached textures");
+ self.device_size = renderer.device_size;
+
+ for (_id, texture) in self.texture_resolver.texture_cache_map.drain() {
+ self.device.delete_texture(texture);
+ }
+ for (id, texture) in renderer.textures {
+ info!("\t{}", texture.data);
+ let target = if texture.is_array {
+ ImageBufferKind::Texture2DArray
+ } else {
+ ImageBufferKind::Texture2D
+ };
+ let t = Self::load_texture(
+ target,
+ &texture,
+ Some(RenderTargetInfo { has_depth: texture.has_depth }),
+ &root,
+ &mut self.device
+ );
+ self.texture_resolver.texture_cache_map.insert(id, t.0);
+ }
+
+ info!("loading gpu cache");
+ let (t, gpu_cache_data) = Self::load_texture(
+ ImageBufferKind::Texture2D,
+ &renderer.gpu_cache,
+ Some(RenderTargetInfo { has_depth: false }),
+ &root,
+ &mut self.device,
+ );
+ self.gpu_cache_texture.load_from_data(t, gpu_cache_data);
+ self.gpu_cache_frame_id = renderer.gpu_cache_frame_id;
+ } else {
+ info!("loading cached textures");
+ self.device.begin_frame();
+ for (_id, texture) in self.texture_resolver.texture_cache_map.drain() {
+ self.device.delete_texture(texture);
+ }
+ }
+ self.device.end_frame();
+
+ self.external_image_handler = Some(Box::new(image_handler) as Box<_>);
+ info!("done.");
+ }
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum FramebufferKind {
+ Main,
+ Other,
+}
+
+fn should_skip_batch(kind: &BatchKind, flags: DebugFlags) -> bool {
+ match kind {
+ BatchKind::TextRun(_) => {
+ flags.contains(DebugFlags::DISABLE_TEXT_PRIMS)
+ }
+ BatchKind::Brush(BrushBatchKind::ConicGradient) |
+ BatchKind::Brush(BrushBatchKind::RadialGradient) |
+ BatchKind::Brush(BrushBatchKind::LinearGradient) => {
+ flags.contains(DebugFlags::DISABLE_GRADIENT_PRIMS)
+ }
+ _ => false,
+ }
+}
+
+impl CompositeState {
+ /// Use the client provided native compositor interface to add all picture
+ /// cache tiles to the OS compositor
+ fn composite_native(
+ &self,
+ dirty_rects: &[DeviceIntRect],
+ compositor: &mut dyn Compositor,
+ ) {
+ // Add each surface to the visual tree. z-order is implicit based on
+ // order added. Offset and clip rect apply to all tiles within this
+ // surface.
+ for surface in &self.descriptor.surfaces {
+ compositor.add_surface(
+ surface.surface_id.expect("bug: no native surface allocated"),
+ surface.transform,
+ surface.clip_rect.to_i32(),
+ surface.image_rendering,
+ );
+ }
+ compositor.start_compositing(dirty_rects, &[]);
+ }
+}
+
+mod tests {
+ #[test]
+ fn test_buffer_damage_tracker() {
+ use super::BufferDamageTracker;
+ use api::units::{DevicePoint, DeviceRect, DeviceSize};
+
+ let mut tracker = BufferDamageTracker::default();
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(3), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(4), None);
+
+ let damage1 = DeviceRect::new(DevicePoint::new(10.0, 10.0), DeviceSize::new(10.0, 10.0));
+ let damage2 = DeviceRect::new(DevicePoint::new(20.0, 20.0), DeviceSize::new(10.0, 10.0));
+ let combined = damage1.union(&damage2);
+
+ tracker.push_dirty_rect(&damage1);
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(damage1));
+ assert_eq!(tracker.get_damage_rect(3), Some(damage1));
+ assert_eq!(tracker.get_damage_rect(4), None);
+
+ tracker.push_dirty_rect(&damage2);
+ assert_eq!(tracker.get_damage_rect(0), None);
+ assert_eq!(tracker.get_damage_rect(1), Some(DeviceRect::zero()));
+ assert_eq!(tracker.get_damage_rect(2), Some(damage2));
+ assert_eq!(tracker.get_damage_rect(3), Some(combined));
+ assert_eq!(tracker.get_damage_rect(4), None);
+ }
+}
diff --git a/gfx/wr/webrender/src/renderer/shade.rs b/gfx/wr/webrender/src/renderer/shade.rs
new file mode 100644
index 0000000000..1c9a6732f1
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/shade.rs
@@ -0,0 +1,1215 @@
+/* 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 api::ImageBufferKind;
+use crate::batch::{BatchKey, BatchKind, BrushBatchKind, BatchFeatures};
+use crate::composite::CompositeSurfaceFormat;
+use crate::device::{Device, Program, ShaderError};
+use euclid::default::Transform3D;
+use crate::glyph_rasterizer::GlyphFormat;
+use crate::renderer::{
+ desc,
+ BlendMode, DebugFlags, RendererError, RendererOptions,
+ TextureSampler, VertexArrayKind, ShaderPrecacheFlags,
+};
+
+use gleam::gl::GlType;
+use time::precise_time_ns;
+
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use webrender_build::shader::{ShaderFeatures, ShaderFeatureFlags, get_shader_features};
+
+pub(crate) fn get_feature_string(kind: ImageBufferKind) -> &'static str {
+ match kind {
+ ImageBufferKind::Texture2D => "TEXTURE_2D",
+ ImageBufferKind::Texture2DArray => "TEXTURE_2D_ARRAY",
+ ImageBufferKind::TextureRect => "TEXTURE_RECT",
+ ImageBufferKind::TextureExternal => "TEXTURE_EXTERNAL",
+ }
+}
+
+fn has_platform_support(kind: ImageBufferKind, gl_type: &GlType) -> bool {
+ match (kind, gl_type) {
+ (ImageBufferKind::Texture2D, _) => true,
+ (ImageBufferKind::Texture2DArray, _) => true,
+ (ImageBufferKind::TextureRect, &GlType::Gles) => false,
+ (ImageBufferKind::TextureRect, &GlType::Gl) => true,
+ (ImageBufferKind::TextureExternal, &GlType::Gles) => true,
+ (ImageBufferKind::TextureExternal, &GlType::Gl) => false,
+ }
+}
+
+pub const IMAGE_BUFFER_KINDS: [ImageBufferKind; 4] = [
+ ImageBufferKind::Texture2D,
+ ImageBufferKind::TextureRect,
+ ImageBufferKind::TextureExternal,
+ ImageBufferKind::Texture2DArray,
+];
+
+const ADVANCED_BLEND_FEATURE: &str = "ADVANCED_BLEND";
+const ALPHA_FEATURE: &str = "ALPHA_PASS";
+const DEBUG_OVERDRAW_FEATURE: &str = "DEBUG_OVERDRAW";
+const DITHERING_FEATURE: &str = "DITHERING";
+const DUAL_SOURCE_FEATURE: &str = "DUAL_SOURCE_BLENDING";
+const FAST_PATH_FEATURE: &str = "FAST_PATH";
+
+pub(crate) enum ShaderKind {
+ Primitive,
+ Cache(VertexArrayKind),
+ ClipCache(VertexArrayKind),
+ Brush,
+ Text,
+ #[allow(dead_code)]
+ VectorStencil,
+ #[allow(dead_code)]
+ VectorCover,
+ Resolve,
+ Composite,
+ Clear,
+}
+
+pub struct LazilyCompiledShader {
+ program: Option<Program>,
+ name: &'static str,
+ kind: ShaderKind,
+ cached_projection: Transform3D<f32>,
+ features: Vec<&'static str>,
+}
+
+impl LazilyCompiledShader {
+ pub(crate) fn new(
+ kind: ShaderKind,
+ name: &'static str,
+ unsorted_features: &[&'static str],
+ device: &mut Device,
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ ) -> Result<Self, ShaderError> {
+ let mut features = unsorted_features.to_vec();
+ features.sort();
+
+ // Ensure this shader config is in the available shader list so that we get
+ // alerted if the list gets out-of-date when shaders or features are added.
+ let config = features.join(",");
+ assert!(
+ shader_list.get(name).map_or(false, |f| f.contains(&config)),
+ "shader \"{}\" with features \"{}\" not in available shader list",
+ name,
+ config,
+ );
+
+ let mut shader = LazilyCompiledShader {
+ program: None,
+ name,
+ kind,
+ //Note: this isn't really the default state, but there is no chance
+ // an actual projection passed here would accidentally match.
+ cached_projection: Transform3D::identity(),
+ features,
+ };
+
+ if precache_flags.intersects(ShaderPrecacheFlags::ASYNC_COMPILE | ShaderPrecacheFlags::FULL_COMPILE) {
+ let t0 = precise_time_ns();
+ shader.get_internal(device, precache_flags)?;
+ let t1 = precise_time_ns();
+ debug!("[C: {:.1} ms ] Precache {} {:?}",
+ (t1 - t0) as f64 / 1000000.0,
+ name,
+ unsorted_features
+ );
+ }
+
+ Ok(shader)
+ }
+
+ pub fn bind(
+ &mut self,
+ device: &mut Device,
+ projection: &Transform3D<f32>,
+ renderer_errors: &mut Vec<RendererError>,
+ ) {
+ let update_projection = self.cached_projection != *projection;
+ let program = match self.get_internal(device, ShaderPrecacheFlags::FULL_COMPILE) {
+ Ok(program) => program,
+ Err(e) => {
+ renderer_errors.push(RendererError::from(e));
+ return;
+ }
+ };
+ device.bind_program(program);
+ if update_projection {
+ device.set_uniforms(program, projection);
+ // thanks NLL for this (`program` technically borrows `self`)
+ self.cached_projection = *projection;
+ }
+ }
+
+ fn get_internal(
+ &mut self,
+ device: &mut Device,
+ precache_flags: ShaderPrecacheFlags,
+ ) -> Result<&mut Program, ShaderError> {
+ if self.program.is_none() {
+ let program = match self.kind {
+ ShaderKind::Primitive | ShaderKind::Brush | ShaderKind::Text | ShaderKind::Resolve | ShaderKind::Clear => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::Cache(..) => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::VectorStencil => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::VectorCover => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::Composite => {
+ create_prim_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ ShaderKind::ClipCache(..) => {
+ create_clip_shader(
+ self.name,
+ device,
+ &self.features,
+ )
+ }
+ };
+ self.program = Some(program?);
+ }
+
+ let program = self.program.as_mut().unwrap();
+
+ if precache_flags.contains(ShaderPrecacheFlags::FULL_COMPILE) && !program.is_initialized() {
+ let vertex_format = match self.kind {
+ ShaderKind::Primitive |
+ ShaderKind::Brush |
+ ShaderKind::Text => VertexArrayKind::Primitive,
+ ShaderKind::Cache(format) => format,
+ ShaderKind::VectorStencil => VertexArrayKind::VectorStencil,
+ ShaderKind::VectorCover => VertexArrayKind::VectorCover,
+ ShaderKind::ClipCache(format) => format,
+ ShaderKind::Resolve => VertexArrayKind::Resolve,
+ ShaderKind::Composite => VertexArrayKind::Composite,
+ ShaderKind::Clear => VertexArrayKind::Clear,
+ };
+
+ let vertex_descriptor = match vertex_format {
+ VertexArrayKind::Primitive => &desc::PRIM_INSTANCES,
+ VertexArrayKind::LineDecoration => &desc::LINE,
+ VertexArrayKind::Gradient => &desc::GRADIENT,
+ VertexArrayKind::Blur => &desc::BLUR,
+ VertexArrayKind::ClipImage => &desc::CLIP_IMAGE,
+ VertexArrayKind::ClipRect => &desc::CLIP_RECT,
+ VertexArrayKind::ClipBoxShadow => &desc::CLIP_BOX_SHADOW,
+ VertexArrayKind::VectorStencil => &desc::VECTOR_STENCIL,
+ VertexArrayKind::VectorCover => &desc::VECTOR_COVER,
+ VertexArrayKind::Border => &desc::BORDER,
+ VertexArrayKind::Scale => &desc::SCALE,
+ VertexArrayKind::Resolve => &desc::RESOLVE,
+ VertexArrayKind::SvgFilter => &desc::SVG_FILTER,
+ VertexArrayKind::Composite => &desc::COMPOSITE,
+ VertexArrayKind::Clear => &desc::CLEAR,
+ };
+
+ device.link_program(program, vertex_descriptor)?;
+ device.bind_program(program);
+ match self.kind {
+ ShaderKind::ClipCache(..) => {
+ device.bind_shader_samplers(
+ &program,
+ &[
+ ("sColor0", TextureSampler::Color0),
+ ("sTransformPalette", TextureSampler::TransformPalette),
+ ("sRenderTasks", TextureSampler::RenderTasks),
+ ("sGpuCache", TextureSampler::GpuCache),
+ ("sPrimitiveHeadersF", TextureSampler::PrimitiveHeadersF),
+ ("sPrimitiveHeadersI", TextureSampler::PrimitiveHeadersI),
+ ],
+ );
+ }
+ _ => {
+ device.bind_shader_samplers(
+ &program,
+ &[
+ ("sColor0", TextureSampler::Color0),
+ ("sColor1", TextureSampler::Color1),
+ ("sColor2", TextureSampler::Color2),
+ ("sDither", TextureSampler::Dither),
+ ("sTransformPalette", TextureSampler::TransformPalette),
+ ("sRenderTasks", TextureSampler::RenderTasks),
+ ("sGpuCache", TextureSampler::GpuCache),
+ ("sPrimitiveHeadersF", TextureSampler::PrimitiveHeadersF),
+ ("sPrimitiveHeadersI", TextureSampler::PrimitiveHeadersI),
+ ("sClipMask", TextureSampler::ClipMask),
+ ],
+ );
+ }
+ }
+ }
+
+ Ok(program)
+ }
+
+ fn deinit(self, device: &mut Device) {
+ if let Some(program) = self.program {
+ device.delete_program(program);
+ }
+ }
+}
+
+// A brush shader supports two modes:
+// opaque:
+// Used for completely opaque primitives,
+// or inside segments of partially
+// opaque primitives. Assumes no need
+// for clip masks, AA etc.
+// alpha:
+// Used for brush primitives in the alpha
+// pass. Assumes that AA should be applied
+// along the primitive edge, and also that
+// clip mask is present.
+struct BrushShader {
+ opaque: LazilyCompiledShader,
+ alpha: LazilyCompiledShader,
+ advanced_blend: Option<LazilyCompiledShader>,
+ dual_source: Option<LazilyCompiledShader>,
+ debug_overdraw: LazilyCompiledShader,
+}
+
+impl BrushShader {
+ fn new(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ use_advanced_blend: bool,
+ use_dual_source: bool,
+ ) -> Result<Self, ShaderError> {
+ let opaque_features = features.to_vec();
+ let opaque = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &opaque_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ let mut alpha_features = opaque_features.to_vec();
+ alpha_features.push(ALPHA_FEATURE);
+
+ let alpha = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &alpha_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ let advanced_blend = if use_advanced_blend {
+ let mut advanced_blend_features = alpha_features.to_vec();
+ advanced_blend_features.push(ADVANCED_BLEND_FEATURE);
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &advanced_blend_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ Some(shader)
+ } else {
+ None
+ };
+
+ let dual_source = if use_dual_source {
+ let mut dual_source_features = alpha_features.to_vec();
+ dual_source_features.push(DUAL_SOURCE_FEATURE);
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &dual_source_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ Some(shader)
+ } else {
+ None
+ };
+
+ let mut debug_overdraw_features = features.to_vec();
+ debug_overdraw_features.push(DEBUG_OVERDRAW_FEATURE);
+
+ let debug_overdraw = LazilyCompiledShader::new(
+ ShaderKind::Brush,
+ name,
+ &debug_overdraw_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ Ok(BrushShader {
+ opaque,
+ alpha,
+ advanced_blend,
+ dual_source,
+ debug_overdraw,
+ })
+ }
+
+ fn get(&mut self, blend_mode: BlendMode, features: BatchFeatures, debug_flags: DebugFlags)
+ -> &mut LazilyCompiledShader {
+ match blend_mode {
+ _ if debug_flags.contains(DebugFlags::SHOW_OVERDRAW) => &mut self.debug_overdraw,
+ BlendMode::None => &mut self.opaque,
+ BlendMode::Alpha |
+ BlendMode::PremultipliedAlpha |
+ BlendMode::PremultipliedDestOut |
+ BlendMode::SubpixelConstantTextColor(..) |
+ BlendMode::SubpixelWithBgColor => {
+ if features.contains(BatchFeatures::ALPHA_PASS) {
+ &mut self.alpha
+ } else {
+ &mut self.opaque
+ }
+ }
+ BlendMode::Advanced(_) => {
+ self.advanced_blend
+ .as_mut()
+ .expect("bug: no advanced blend shader loaded")
+ }
+ BlendMode::SubpixelDualSource => {
+ self.dual_source
+ .as_mut()
+ .expect("bug: no dual source shader loaded")
+ }
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ self.opaque.deinit(device);
+ self.alpha.deinit(device);
+ if let Some(advanced_blend) = self.advanced_blend {
+ advanced_blend.deinit(device);
+ }
+ if let Some(dual_source) = self.dual_source {
+ dual_source.deinit(device);
+ }
+ self.debug_overdraw.deinit(device);
+ }
+}
+
+pub struct TextShader {
+ simple: LazilyCompiledShader,
+ glyph_transform: LazilyCompiledShader,
+ debug_overdraw: LazilyCompiledShader,
+}
+
+impl TextShader {
+ fn new(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+ precache_flags: ShaderPrecacheFlags,
+ shader_list: &ShaderFeatures,
+ ) -> Result<Self, ShaderError> {
+ let mut simple_features = features.to_vec();
+ simple_features.push("ALPHA_PASS");
+ simple_features.push("TEXTURE_2D");
+
+ let simple = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &simple_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ let mut glyph_transform_features = features.to_vec();
+ glyph_transform_features.push("GLYPH_TRANSFORM");
+ glyph_transform_features.push("ALPHA_PASS");
+ glyph_transform_features.push("TEXTURE_2D");
+
+ let glyph_transform = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &glyph_transform_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ let mut debug_overdraw_features = features.to_vec();
+ debug_overdraw_features.push("DEBUG_OVERDRAW");
+ debug_overdraw_features.push("TEXTURE_2D");
+
+ let debug_overdraw = LazilyCompiledShader::new(
+ ShaderKind::Text,
+ name,
+ &debug_overdraw_features,
+ device,
+ precache_flags,
+ &shader_list,
+ )?;
+
+ Ok(TextShader { simple, glyph_transform, debug_overdraw })
+ }
+
+ pub fn get(
+ &mut self,
+ glyph_format: GlyphFormat,
+ debug_flags: DebugFlags,
+ ) -> &mut LazilyCompiledShader {
+ match glyph_format {
+ _ if debug_flags.contains(DebugFlags::SHOW_OVERDRAW) => &mut self.debug_overdraw,
+ GlyphFormat::Alpha |
+ GlyphFormat::Subpixel |
+ GlyphFormat::Bitmap |
+ GlyphFormat::ColorBitmap => &mut self.simple,
+ GlyphFormat::TransformedAlpha |
+ GlyphFormat::TransformedSubpixel => &mut self.glyph_transform,
+ }
+ }
+
+ fn deinit(self, device: &mut Device) {
+ self.simple.deinit(device);
+ self.glyph_transform.deinit(device);
+ self.debug_overdraw.deinit(device);
+ }
+}
+
+fn create_prim_shader(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+) -> Result<Program, ShaderError> {
+ debug!("PrimShader {}", name);
+
+ device.create_program(name, features)
+}
+
+fn create_clip_shader(
+ name: &'static str,
+ device: &mut Device,
+ features: &[&'static str],
+) -> Result<Program, ShaderError> {
+ debug!("ClipShader {}", name);
+
+ device.create_program(name, features)
+}
+
+// NB: If you add a new shader here, make sure to deinitialize it
+// in `Shaders::deinit()` below.
+pub struct Shaders {
+ // These are "cache shaders". These shaders are used to
+ // draw intermediate results to cache targets. The results
+ // of these shaders are then used by the primitive shaders.
+ pub cs_blur_a8: LazilyCompiledShader,
+ pub cs_blur_rgba8: LazilyCompiledShader,
+ pub cs_border_segment: LazilyCompiledShader,
+ pub cs_border_solid: LazilyCompiledShader,
+ pub cs_scale: Vec<Option<LazilyCompiledShader>>,
+ pub cs_line_decoration: LazilyCompiledShader,
+ pub cs_gradient: LazilyCompiledShader,
+ pub cs_svg_filter: LazilyCompiledShader,
+
+ // Brush shaders
+ brush_solid: BrushShader,
+ brush_image: Vec<Option<BrushShader>>,
+ brush_fast_image: Vec<Option<BrushShader>>,
+ brush_blend: BrushShader,
+ brush_mix_blend: BrushShader,
+ brush_yuv_image: Vec<Option<BrushShader>>,
+ brush_conic_gradient: BrushShader,
+ brush_radial_gradient: BrushShader,
+ brush_linear_gradient: BrushShader,
+ brush_opacity: BrushShader,
+ brush_opacity_aa: BrushShader,
+
+ /// These are "cache clip shaders". These shaders are used to
+ /// draw clip instances into the cached clip mask. The results
+ /// of these shaders are also used by the primitive shaders.
+ pub cs_clip_rectangle_slow: LazilyCompiledShader,
+ pub cs_clip_rectangle_fast: LazilyCompiledShader,
+ pub cs_clip_box_shadow: LazilyCompiledShader,
+ pub cs_clip_image: LazilyCompiledShader,
+
+ // The are "primitive shaders". These shaders draw and blend
+ // final results on screen. They are aware of tile boundaries.
+ // Most draw directly to the framebuffer, but some use inputs
+ // from the cache shaders to draw. Specifically, the box
+ // shadow primitive shader stretches the box shadow cache
+ // output, and the cache_image shader blits the results of
+ // a cache shader (e.g. blur) to the screen.
+ pub ps_text_run: TextShader,
+ pub ps_text_run_dual_source: Option<TextShader>,
+
+ ps_split_composite: LazilyCompiledShader,
+ pub ps_clear: LazilyCompiledShader,
+
+ // Composite shaders. These are very simple shaders used to composite
+ // picture cache tiles into the framebuffer on platforms that do not have an
+ // OS Compositor (or we cannot use it). Such an OS Compositor (such as
+ // DirectComposite or CoreAnimation) handles the composition of the picture
+ // cache tiles at a lower level (e.g. in DWM for Windows); in that case we
+ // directly hand the picture cache surfaces over to the OS Compositor, and
+ // our own Composite shaders below never run.
+ // To composite external (RGB) surfaces we need various permutations of
+ // shaders with WR_FEATURE flags on or off based on the type of image
+ // buffer we're sourcing from (see IMAGE_BUFFER_KINDS).
+ pub composite_rgba: Vec<Option<LazilyCompiledShader>>,
+ // The same set of composite shaders but with WR_FEATURE_YUV added.
+ pub composite_yuv: Vec<Option<LazilyCompiledShader>>,
+}
+
+impl Shaders {
+ pub fn new(
+ device: &mut Device,
+ gl_type: GlType,
+ options: &RendererOptions,
+ ) -> Result<Self, ShaderError> {
+ let use_dual_source_blending =
+ device.get_capabilities().supports_dual_source_blending &&
+ options.allow_dual_source_blending;
+ let use_advanced_blend_equation =
+ device.get_capabilities().supports_advanced_blend_equation &&
+ options.allow_advanced_blend_equation;
+
+ let mut shader_flags = match gl_type {
+ GlType::Gl => ShaderFeatureFlags::GL,
+ GlType::Gles => ShaderFeatureFlags::GLES | ShaderFeatureFlags::TEXTURE_EXTERNAL,
+ };
+ shader_flags.set(ShaderFeatureFlags::ADVANCED_BLEND_EQUATION, use_advanced_blend_equation);
+ shader_flags.set(ShaderFeatureFlags::DUAL_SOURCE_BLENDING, use_dual_source_blending);
+ shader_flags.set(ShaderFeatureFlags::DITHERING, options.enable_dithering);
+ let shader_list = get_shader_features(shader_flags);
+
+ let brush_solid = BrushShader::new(
+ "brush_solid",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_blend = BrushShader::new(
+ "brush_blend",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_mix_blend = BrushShader::new(
+ "brush_mix_blend",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_conic_gradient = BrushShader::new(
+ "brush_conic_gradient",
+ device,
+ if options.enable_dithering {
+ &[DITHERING_FEATURE]
+ } else {
+ &[]
+ },
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_radial_gradient = BrushShader::new(
+ "brush_radial_gradient",
+ device,
+ if options.enable_dithering {
+ &[DITHERING_FEATURE]
+ } else {
+ &[]
+ },
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_linear_gradient = BrushShader::new(
+ "brush_linear_gradient",
+ device,
+ if options.enable_dithering {
+ &[DITHERING_FEATURE]
+ } else {
+ &[]
+ },
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_opacity_aa = BrushShader::new(
+ "brush_opacity",
+ device,
+ &["ANTIALIASING"],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let brush_opacity = BrushShader::new(
+ "brush_opacity",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let cs_blur_a8 = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Blur),
+ "cs_blur",
+ &["ALPHA_TARGET"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_blur_rgba8 = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Blur),
+ "cs_blur",
+ &["COLOR_TARGET"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_svg_filter = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::SvgFilter),
+ "cs_svg_filter",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_clip_rectangle_slow = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipRect),
+ "cs_clip_rectangle",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_clip_rectangle_fast = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipRect),
+ "cs_clip_rectangle",
+ &[FAST_PATH_FEATURE],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_clip_box_shadow = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipBoxShadow),
+ "cs_clip_box_shadow",
+ &["TEXTURE_2D"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_clip_image = LazilyCompiledShader::new(
+ ShaderKind::ClipCache(VertexArrayKind::ClipImage),
+ "cs_clip_image",
+ &["TEXTURE_2D"],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let mut cs_scale = Vec::new();
+ let scale_shader_num = IMAGE_BUFFER_KINDS.len();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. scale_shader_num {
+ cs_scale.push(None);
+ }
+ for image_buffer_kind in &IMAGE_BUFFER_KINDS {
+ if has_platform_support(*image_buffer_kind, &gl_type) {
+ let feature_string = get_feature_string(*image_buffer_kind);
+
+ let mut features = Vec::new();
+ if feature_string != "" {
+ features.push(feature_string);
+ }
+
+ let shader = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Scale),
+ "cs_scale",
+ &features,
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let index = Self::get_compositing_shader_index(
+ *image_buffer_kind,
+ );
+ cs_scale[index] = Some(shader);
+ }
+ }
+
+ // TODO(gw): The split composite + text shader are special cases - the only
+ // shaders used during normal scene rendering that aren't a brush
+ // shader. Perhaps we can unify these in future?
+
+ let ps_text_run = TextShader::new("ps_text_run",
+ device,
+ &[],
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let ps_text_run_dual_source = if use_dual_source_blending {
+ let dual_source_features = vec![DUAL_SOURCE_FEATURE];
+ Some(TextShader::new("ps_text_run",
+ device,
+ &dual_source_features,
+ options.precache_flags,
+ &shader_list,
+ )?)
+ } else {
+ None
+ };
+
+ let ps_split_composite = LazilyCompiledShader::new(
+ ShaderKind::Primitive,
+ "ps_split_composite",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let ps_clear = LazilyCompiledShader::new(
+ ShaderKind::Clear,
+ "ps_clear",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ // All image configuration.
+ let mut image_features = Vec::new();
+ let mut brush_image = Vec::new();
+ let mut brush_fast_image = Vec::new();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. IMAGE_BUFFER_KINDS.len() {
+ brush_image.push(None);
+ brush_fast_image.push(None);
+ }
+ for buffer_kind in 0 .. IMAGE_BUFFER_KINDS.len() {
+ if !has_platform_support(IMAGE_BUFFER_KINDS[buffer_kind], &gl_type) {
+ continue;
+ }
+
+ let feature_string = get_feature_string(IMAGE_BUFFER_KINDS[buffer_kind]);
+ if feature_string != "" {
+ image_features.push(feature_string);
+ }
+
+ brush_fast_image[buffer_kind] = Some(BrushShader::new(
+ "brush_image",
+ device,
+ &image_features,
+ options.precache_flags,
+ &shader_list,
+ use_advanced_blend_equation,
+ use_dual_source_blending,
+ )?);
+
+ image_features.push("REPETITION");
+ image_features.push("ANTIALIASING");
+
+ brush_image[buffer_kind] = Some(BrushShader::new(
+ "brush_image",
+ device,
+ &image_features,
+ options.precache_flags,
+ &shader_list,
+ use_advanced_blend_equation,
+ use_dual_source_blending,
+ )?);
+
+ image_features.clear();
+ }
+
+ // All yuv_image configuration.
+ let mut yuv_features = Vec::new();
+ let mut rgba_features = Vec::new();
+ let yuv_shader_num = IMAGE_BUFFER_KINDS.len();
+ let mut brush_yuv_image = Vec::new();
+ let mut composite_yuv = Vec::new();
+ let mut composite_rgba = Vec::new();
+ // PrimitiveShader is not clonable. Use push() to initialize the vec.
+ for _ in 0 .. yuv_shader_num {
+ brush_yuv_image.push(None);
+ composite_yuv.push(None);
+ composite_rgba.push(None);
+ }
+ for image_buffer_kind in &IMAGE_BUFFER_KINDS {
+ if has_platform_support(*image_buffer_kind, &gl_type) {
+ yuv_features.push("YUV");
+
+ let feature_string = get_feature_string(*image_buffer_kind);
+ if feature_string != "" {
+ yuv_features.push(feature_string);
+ rgba_features.push(feature_string);
+ }
+
+ let brush_shader = BrushShader::new(
+ "brush_yuv_image",
+ device,
+ &yuv_features,
+ options.precache_flags,
+ &shader_list,
+ false /* advanced blend */,
+ false /* dual source */,
+ )?;
+
+ let composite_yuv_shader = LazilyCompiledShader::new(
+ ShaderKind::Composite,
+ "composite",
+ &yuv_features,
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let composite_rgba_shader = LazilyCompiledShader::new(
+ ShaderKind::Composite,
+ "composite",
+ &rgba_features,
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let index = Self::get_compositing_shader_index(
+ *image_buffer_kind,
+ );
+ brush_yuv_image[index] = Some(brush_shader);
+ composite_yuv[index] = Some(composite_yuv_shader);
+ composite_rgba[index] = Some(composite_rgba_shader);
+
+ yuv_features.clear();
+ rgba_features.clear()
+ }
+ }
+
+ let cs_line_decoration = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::LineDecoration),
+ "cs_line_decoration",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_gradient = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Gradient),
+ "cs_gradient",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_border_segment = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_segment",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ let cs_border_solid = LazilyCompiledShader::new(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_solid",
+ &[],
+ device,
+ options.precache_flags,
+ &shader_list,
+ )?;
+
+ Ok(Shaders {
+ cs_blur_a8,
+ cs_blur_rgba8,
+ cs_border_segment,
+ cs_line_decoration,
+ cs_gradient,
+ cs_border_solid,
+ cs_scale,
+ cs_svg_filter,
+ brush_solid,
+ brush_image,
+ brush_fast_image,
+ brush_blend,
+ brush_mix_blend,
+ brush_yuv_image,
+ brush_conic_gradient,
+ brush_radial_gradient,
+ brush_linear_gradient,
+ brush_opacity,
+ brush_opacity_aa,
+ cs_clip_rectangle_slow,
+ cs_clip_rectangle_fast,
+ cs_clip_box_shadow,
+ cs_clip_image,
+ ps_text_run,
+ ps_text_run_dual_source,
+ ps_split_composite,
+ ps_clear,
+ composite_rgba,
+ composite_yuv,
+ })
+ }
+
+ fn get_compositing_shader_index(buffer_kind: ImageBufferKind) -> usize {
+ buffer_kind as usize
+ }
+
+ pub fn get_composite_shader(
+ &mut self,
+ format: CompositeSurfaceFormat,
+ buffer_kind: ImageBufferKind,
+ ) -> &mut LazilyCompiledShader {
+ match format {
+ CompositeSurfaceFormat::Rgba => {
+ let shader_index = Self::get_compositing_shader_index(buffer_kind);
+ self.composite_rgba[shader_index]
+ .as_mut()
+ .expect("bug: unsupported rgba shader requested")
+ }
+ CompositeSurfaceFormat::Yuv => {
+ let shader_index = Self::get_compositing_shader_index(buffer_kind);
+ self.composite_yuv[shader_index]
+ .as_mut()
+ .expect("bug: unsupported yuv shader requested")
+ }
+ }
+ }
+
+ pub fn get_scale_shader(
+ &mut self,
+ buffer_kind: ImageBufferKind,
+ ) -> &mut LazilyCompiledShader {
+ let shader_index = Self::get_compositing_shader_index(buffer_kind);
+ self.cs_scale[shader_index]
+ .as_mut()
+ .expect("bug: unsupported scale shader requested")
+ }
+
+ pub fn get(&
+ mut self,
+ key: &BatchKey,
+ mut features: BatchFeatures,
+ debug_flags: DebugFlags,
+ ) -> &mut LazilyCompiledShader {
+ match key.kind {
+ BatchKind::SplitComposite => {
+ &mut self.ps_split_composite
+ }
+ BatchKind::Brush(brush_kind) => {
+ let brush_shader = match brush_kind {
+ BrushBatchKind::Solid => {
+ &mut self.brush_solid
+ }
+ BrushBatchKind::Image(image_buffer_kind) => {
+ if features.contains(BatchFeatures::ANTIALIASING) ||
+ features.contains(BatchFeatures::REPETITION) {
+
+ self.brush_image[image_buffer_kind as usize]
+ .as_mut()
+ .expect("Unsupported image shader kind")
+ } else {
+ self.brush_fast_image[image_buffer_kind as usize]
+ .as_mut()
+ .expect("Unsupported image shader kind")
+ }
+ }
+ BrushBatchKind::Blend => {
+ &mut self.brush_blend
+ }
+ BrushBatchKind::MixBlend { .. } => {
+ &mut self.brush_mix_blend
+ }
+ BrushBatchKind::ConicGradient => {
+ // Gradient brushes can optimistically use the opaque shader even
+ // with a blend mode if they don't require any features.
+ if !features.intersects(
+ BatchFeatures::ANTIALIASING
+ | BatchFeatures::REPETITION
+ | BatchFeatures::CLIP_MASK,
+ ) {
+ features.remove(BatchFeatures::ALPHA_PASS);
+ }
+ &mut self.brush_conic_gradient
+ }
+ BrushBatchKind::RadialGradient => {
+ // Gradient brushes can optimistically use the opaque shader even
+ // with a blend mode if they don't require any features.
+ if !features.intersects(
+ BatchFeatures::ANTIALIASING
+ | BatchFeatures::REPETITION
+ | BatchFeatures::CLIP_MASK,
+ ) {
+ features.remove(BatchFeatures::ALPHA_PASS);
+ }
+ &mut self.brush_radial_gradient
+ }
+ BrushBatchKind::LinearGradient => {
+ // Gradient brushes can optimistically use the opaque shader even
+ // with a blend mode if they don't require any features.
+ if !features.intersects(
+ BatchFeatures::ANTIALIASING
+ | BatchFeatures::REPETITION
+ | BatchFeatures::CLIP_MASK,
+ ) {
+ features.remove(BatchFeatures::ALPHA_PASS);
+ }
+ &mut self.brush_linear_gradient
+ }
+ BrushBatchKind::YuvImage(image_buffer_kind, ..) => {
+ let shader_index =
+ Self::get_compositing_shader_index(image_buffer_kind);
+ self.brush_yuv_image[shader_index]
+ .as_mut()
+ .expect("Unsupported YUV shader kind")
+ }
+ BrushBatchKind::Opacity => {
+ if features.contains(BatchFeatures::ANTIALIASING) {
+ &mut self.brush_opacity_aa
+ } else {
+ &mut self.brush_opacity
+ }
+ }
+ };
+ brush_shader.get(key.blend_mode, features, debug_flags)
+ }
+ BatchKind::TextRun(glyph_format) => {
+ let text_shader = match key.blend_mode {
+ BlendMode::SubpixelDualSource => self.ps_text_run_dual_source.as_mut().unwrap(),
+ _ => &mut self.ps_text_run,
+ };
+ text_shader.get(glyph_format, debug_flags)
+ }
+ }
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ for shader in self.cs_scale {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ self.cs_blur_a8.deinit(device);
+ self.cs_blur_rgba8.deinit(device);
+ self.cs_svg_filter.deinit(device);
+ self.brush_solid.deinit(device);
+ self.brush_blend.deinit(device);
+ self.brush_mix_blend.deinit(device);
+ self.brush_conic_gradient.deinit(device);
+ self.brush_radial_gradient.deinit(device);
+ self.brush_linear_gradient.deinit(device);
+ self.brush_opacity.deinit(device);
+ self.brush_opacity_aa.deinit(device);
+ self.cs_clip_rectangle_slow.deinit(device);
+ self.cs_clip_rectangle_fast.deinit(device);
+ self.cs_clip_box_shadow.deinit(device);
+ self.cs_clip_image.deinit(device);
+ self.ps_text_run.deinit(device);
+ if let Some(shader) = self.ps_text_run_dual_source {
+ shader.deinit(device);
+ }
+ for shader in self.brush_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.brush_fast_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.brush_yuv_image {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ self.cs_border_solid.deinit(device);
+ self.cs_gradient.deinit(device);
+ self.cs_line_decoration.deinit(device);
+ self.cs_border_segment.deinit(device);
+ self.ps_split_composite.deinit(device);
+ self.ps_clear.deinit(device);
+
+ for shader in self.composite_rgba {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ for shader in self.composite_yuv {
+ if let Some(shader) = shader {
+ shader.deinit(device);
+ }
+ }
+ }
+}
+
+pub type SharedShaders = Rc<RefCell<Shaders>>;
diff --git a/gfx/wr/webrender/src/renderer/vertex.rs b/gfx/wr/webrender/src/renderer/vertex.rs
new file mode 100644
index 0000000000..5ead6bf58f
--- /dev/null
+++ b/gfx/wr/webrender/src/renderer/vertex.rs
@@ -0,0 +1,976 @@
+/* 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/. */
+
+//! Rendering logic related to the vertex shaders and their states, uncluding
+//! - Vertex Array Objects
+//! - vertex layout descriptors
+//! - textures bound at vertex stage
+
+use std::{marker::PhantomData, mem, num::NonZeroUsize, ops};
+use api::units::*;
+use crate::{
+ device::{
+ Device, Texture, TextureFilter, TextureUploader, UploadPBOPool, VertexUsageHint, VAO,
+ },
+ frame_builder::Frame,
+ gpu_types::{PrimitiveHeaderI, PrimitiveHeaderF, TransformData},
+ internal_types::Swizzle,
+ render_task::RenderTaskData,
+};
+
+pub const VERTEX_TEXTURE_EXTRA_ROWS: i32 = 10;
+
+pub const MAX_VERTEX_TEXTURE_WIDTH: usize = webrender_build::MAX_VERTEX_TEXTURE_WIDTH;
+
+pub mod desc {
+ use crate::device::{VertexAttribute, VertexAttributeKind, VertexDescriptor};
+
+ pub const PRIM_INSTANCES: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[VertexAttribute {
+ name: "aData",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ }],
+ };
+
+ pub const BLUR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aBlurRenderTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aBlurSourceTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aBlurDirection",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const LINE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aLocalSize",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aWavyLineThickness",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStyle",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aAxisSelect",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const GRADIENT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStops",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ // TODO(gw): We should probably pack these as u32 colors instead
+ // of passing as full float vec4 here. It won't make much
+ // difference in real world, since these are only invoked
+ // rarely, when creating the cache.
+ VertexAttribute {
+ name: "aColor0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor2",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor3",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aAxisSelect",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aStartStop",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const BORDER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTaskOrigin",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aFlags",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aWidths",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aRadii",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipParams1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipParams2",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const SCALE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aScaleTargetRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aScaleSourceRect",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aScaleSourceLayer",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ ],
+ };
+
+ pub const CLIP_RECT: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipLocalPos",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipLocalRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipMode",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_TL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_TL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_TR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_TR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_BL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_BL",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRect_BR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipRadii_BR",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLIP_BOX_SHADOW: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipDataResourceAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aClipSrcRectSize",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipMode",
+ count: 1,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aStretchMode",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aClipDestRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLIP_IMAGE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ // common clip attributes
+ VertexAttribute {
+ name: "aClipDeviceArea",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipOrigins",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDevicePixelScale",
+ count: 1,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTransformIds",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ // specific clip attributes
+ VertexAttribute {
+ name: "aClipTileRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aClipDataResourceAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aClipLocalRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const GPU_CACHE_UPDATE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[
+ VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U16Norm,
+ },
+ VertexAttribute {
+ name: "aValue",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ instance_attributes: &[],
+ };
+
+ pub const RESOLVE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ }],
+ };
+
+ pub const SVG_FILTER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aFilterRenderTaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInput1TaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInput2TaskAddress",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterKind",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterInputCount",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterGenericInt",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aFilterExtraDataAddress",
+ count: 2,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const VECTOR_STENCIL: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aFromPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCtrlPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aToPosition",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aFromNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aCtrlNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aToNormal",
+ count: 2,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aPathID",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aPad",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const VECTOR_COVER: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aTargetRect",
+ count: 4,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aStencilOrigin",
+ count: 2,
+ kind: VertexAttributeKind::I32,
+ },
+ VertexAttribute {
+ name: "aSubpixel",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ VertexAttribute {
+ name: "aPad",
+ count: 1,
+ kind: VertexAttributeKind::U16,
+ },
+ ],
+ };
+
+ pub const COMPOSITE: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aDeviceRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aDeviceClipRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aParams",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect0",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect1",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aUvRect2",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aTextureLayers",
+ count: 3,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+
+ pub const CLEAR: VertexDescriptor = VertexDescriptor {
+ vertex_attributes: &[VertexAttribute {
+ name: "aPosition",
+ count: 2,
+ kind: VertexAttributeKind::U8Norm,
+ }],
+ instance_attributes: &[
+ VertexAttribute {
+ name: "aRect",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ VertexAttribute {
+ name: "aColor",
+ count: 4,
+ kind: VertexAttributeKind::F32,
+ },
+ ],
+ };
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum VertexArrayKind {
+ Primitive,
+ Blur,
+ ClipImage,
+ ClipRect,
+ ClipBoxShadow,
+ VectorStencil,
+ VectorCover,
+ Border,
+ Scale,
+ LineDecoration,
+ Gradient,
+ Resolve,
+ SvgFilter,
+ Composite,
+ Clear,
+}
+
+pub struct VertexDataTexture<T> {
+ texture: Option<Texture>,
+ format: api::ImageFormat,
+ _marker: PhantomData<T>,
+}
+
+impl<T> VertexDataTexture<T> {
+ pub fn new(format: api::ImageFormat) -> Self {
+ Self {
+ texture: None,
+ format,
+ _marker: PhantomData,
+ }
+ }
+
+ /// Returns a borrow of the GPU texture. Panics if it hasn't been initialized.
+ pub fn texture(&self) -> &Texture {
+ self.texture.as_ref().unwrap()
+ }
+
+ /// Returns an estimate of the GPU memory consumed by this VertexDataTexture.
+ pub fn size_in_bytes(&self) -> usize {
+ self.texture.as_ref().map_or(0, |t| t.size_in_bytes())
+ }
+
+ pub fn update<'a>(
+ &'a mut self,
+ device: &mut Device,
+ texture_uploader: &mut TextureUploader<'a>,
+ data: &mut Vec<T>,
+ ) {
+ debug_assert!(mem::size_of::<T>() % 16 == 0);
+ let texels_per_item = mem::size_of::<T>() / 16;
+ let items_per_row = MAX_VERTEX_TEXTURE_WIDTH / texels_per_item;
+ debug_assert_ne!(items_per_row, 0);
+
+ // Ensure we always end up with a texture when leaving this method.
+ let mut len = data.len();
+ if len == 0 {
+ if self.texture.is_some() {
+ return;
+ }
+ data.reserve(items_per_row);
+ len = items_per_row;
+ } else {
+ // Extend the data array to have enough capacity to upload at least
+ // a multiple of the row size. This ensures memory safety when the
+ // array is passed to OpenGL to upload to the GPU.
+ let extra = len % items_per_row;
+ if extra != 0 {
+ let padding = items_per_row - extra;
+ data.reserve(padding);
+ len += padding;
+ }
+ }
+
+ let needed_height = (len / items_per_row) as i32;
+ let existing_height = self
+ .texture
+ .as_ref()
+ .map_or(0, |t| t.get_dimensions().height);
+
+ // Create a new texture if needed.
+ //
+ // These textures are generally very small, which is why we don't bother
+ // with incremental updates and just re-upload every frame. For most pages
+ // they're one row each, and on stress tests like css-francine they end up
+ // in the 6-14 range. So we size the texture tightly to what we need (usually
+ // 1), and shrink it if the waste would be more than `VERTEX_TEXTURE_EXTRA_ROWS`
+ // rows. This helps with memory overhead, especially because there are several
+ // instances of these textures per Renderer.
+ if needed_height > existing_height
+ || needed_height + VERTEX_TEXTURE_EXTRA_ROWS < existing_height
+ {
+ // Drop the existing texture, if any.
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+
+ let texture = device.create_texture(
+ api::ImageBufferKind::Texture2D,
+ self.format,
+ MAX_VERTEX_TEXTURE_WIDTH as i32,
+ // Ensure height is at least two to work around
+ // https://bugs.chromium.org/p/angleproject/issues/detail?id=3039
+ needed_height.max(2),
+ TextureFilter::Nearest,
+ None,
+ 1,
+ );
+ self.texture = Some(texture);
+ }
+
+ // Note: the actual width can be larger than the logical one, with a few texels
+ // of each row unused at the tail. This is needed because there is still hardware
+ // (like Intel iGPUs) that prefers power-of-two sizes of textures ([1]).
+ //
+ // [1] https://software.intel.com/en-us/articles/opengl-performance-tips-power-of-two-textures-have-better-performance
+ let logical_width = if needed_height == 1 {
+ data.len() * texels_per_item
+ } else {
+ MAX_VERTEX_TEXTURE_WIDTH - (MAX_VERTEX_TEXTURE_WIDTH % texels_per_item)
+ };
+
+ let rect = DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ DeviceIntSize::new(logical_width as i32, needed_height),
+ );
+
+ debug_assert!(len <= data.capacity(), "CPU copy will read out of bounds");
+ texture_uploader.upload(
+ device,
+ self.texture(),
+ rect,
+ 0,
+ None,
+ None,
+ data.as_ptr(),
+ len,
+ );
+ }
+
+ pub fn deinit(mut self, device: &mut Device) {
+ if let Some(t) = self.texture.take() {
+ device.delete_texture(t);
+ }
+ }
+}
+
+pub struct VertexDataTextures {
+ prim_header_f_texture: VertexDataTexture<PrimitiveHeaderF>,
+ prim_header_i_texture: VertexDataTexture<PrimitiveHeaderI>,
+ transforms_texture: VertexDataTexture<TransformData>,
+ render_task_texture: VertexDataTexture<RenderTaskData>,
+}
+
+impl VertexDataTextures {
+ pub fn new() -> Self {
+ VertexDataTextures {
+ prim_header_f_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ prim_header_i_texture: VertexDataTexture::new(api::ImageFormat::RGBAI32),
+ transforms_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ render_task_texture: VertexDataTexture::new(api::ImageFormat::RGBAF32),
+ }
+ }
+
+ pub fn update(&mut self, device: &mut Device, pbo_pool: &mut UploadPBOPool, frame: &mut Frame) {
+ let mut texture_uploader = device.upload_texture(pbo_pool);
+ self.prim_header_f_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.prim_headers.headers_float,
+ );
+ self.prim_header_i_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.prim_headers.headers_int,
+ );
+ self.transforms_texture
+ .update(device, &mut texture_uploader, &mut frame.transform_palette);
+ self.render_task_texture.update(
+ device,
+ &mut texture_uploader,
+ &mut frame.render_tasks.task_data,
+ );
+
+ // Flush and drop the texture uploader now, so that
+ // we can borrow the textures to bind them.
+ texture_uploader.flush(device);
+
+ device.bind_texture(
+ super::TextureSampler::PrimitiveHeadersF,
+ &self.prim_header_f_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::PrimitiveHeadersI,
+ &self.prim_header_i_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::TransformPalette,
+ &self.transforms_texture.texture(),
+ Swizzle::default(),
+ );
+ device.bind_texture(
+ super::TextureSampler::RenderTasks,
+ &self.render_task_texture.texture(),
+ Swizzle::default(),
+ );
+ }
+
+ pub fn size_in_bytes(&self) -> usize {
+ self.prim_header_f_texture.size_in_bytes()
+ + self.prim_header_i_texture.size_in_bytes()
+ + self.transforms_texture.size_in_bytes()
+ + self.render_task_texture.size_in_bytes()
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ self.transforms_texture.deinit(device);
+ self.prim_header_f_texture.deinit(device);
+ self.prim_header_i_texture.deinit(device);
+ self.render_task_texture.deinit(device);
+ }
+}
+
+pub struct RendererVAOs {
+ prim_vao: VAO,
+ blur_vao: VAO,
+ clip_rect_vao: VAO,
+ clip_box_shadow_vao: VAO,
+ clip_image_vao: VAO,
+ border_vao: VAO,
+ line_vao: VAO,
+ scale_vao: VAO,
+ gradient_vao: VAO,
+ resolve_vao: VAO,
+ svg_filter_vao: VAO,
+ composite_vao: VAO,
+ clear_vao: VAO,
+}
+
+impl RendererVAOs {
+ pub fn new(device: &mut Device, indexed_quads: Option<NonZeroUsize>) -> Self {
+ const QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
+ const QUAD_VERTICES: [[u8; 2]; 4] = [[0, 0], [0xFF, 0], [0, 0xFF], [0xFF, 0xFF]];
+
+ let instance_divisor = if indexed_quads.is_some() { 0 } else { 1 };
+ let prim_vao = device.create_vao(&desc::PRIM_INSTANCES, instance_divisor);
+
+ device.bind_vao(&prim_vao);
+ match indexed_quads {
+ Some(count) => {
+ assert!(count.get() < u16::MAX as usize);
+ let quad_indices = (0 .. count.get() as u16)
+ .flat_map(|instance| QUAD_INDICES.iter().map(move |&index| instance * 4 + index))
+ .collect::<Vec<_>>();
+ device.update_vao_indices(&prim_vao, &quad_indices, VertexUsageHint::Static);
+ let quad_vertices = (0 .. count.get() as u16)
+ .flat_map(|_| QUAD_VERTICES.iter().cloned())
+ .collect::<Vec<_>>();
+ device.update_vao_main_vertices(&prim_vao, &quad_vertices, VertexUsageHint::Static);
+ }
+ None => {
+ device.update_vao_indices(&prim_vao, &QUAD_INDICES, VertexUsageHint::Static);
+ device.update_vao_main_vertices(&prim_vao, &QUAD_VERTICES, VertexUsageHint::Static);
+ }
+ }
+
+ RendererVAOs {
+ blur_vao: device.create_vao_with_new_instances(&desc::BLUR, &prim_vao),
+ clip_rect_vao: device.create_vao_with_new_instances(&desc::CLIP_RECT, &prim_vao),
+ clip_box_shadow_vao: device
+ .create_vao_with_new_instances(&desc::CLIP_BOX_SHADOW, &prim_vao),
+ clip_image_vao: device.create_vao_with_new_instances(&desc::CLIP_IMAGE, &prim_vao),
+ border_vao: device.create_vao_with_new_instances(&desc::BORDER, &prim_vao),
+ scale_vao: device.create_vao_with_new_instances(&desc::SCALE, &prim_vao),
+ line_vao: device.create_vao_with_new_instances(&desc::LINE, &prim_vao),
+ gradient_vao: device.create_vao_with_new_instances(&desc::GRADIENT, &prim_vao),
+ resolve_vao: device.create_vao_with_new_instances(&desc::RESOLVE, &prim_vao),
+ svg_filter_vao: device.create_vao_with_new_instances(&desc::SVG_FILTER, &prim_vao),
+ composite_vao: device.create_vao_with_new_instances(&desc::COMPOSITE, &prim_vao),
+ clear_vao: device.create_vao_with_new_instances(&desc::CLEAR, &prim_vao),
+ prim_vao,
+ }
+ }
+
+ pub fn deinit(self, device: &mut Device) {
+ device.delete_vao(self.prim_vao);
+ device.delete_vao(self.resolve_vao);
+ device.delete_vao(self.clip_rect_vao);
+ device.delete_vao(self.clip_box_shadow_vao);
+ device.delete_vao(self.clip_image_vao);
+ device.delete_vao(self.gradient_vao);
+ device.delete_vao(self.blur_vao);
+ device.delete_vao(self.line_vao);
+ device.delete_vao(self.border_vao);
+ device.delete_vao(self.scale_vao);
+ device.delete_vao(self.svg_filter_vao);
+ device.delete_vao(self.composite_vao);
+ device.delete_vao(self.clear_vao);
+ }
+}
+
+impl ops::Index<VertexArrayKind> for RendererVAOs {
+ type Output = VAO;
+ fn index(&self, kind: VertexArrayKind) -> &VAO {
+ match kind {
+ VertexArrayKind::Primitive => &self.prim_vao,
+ VertexArrayKind::ClipImage => &self.clip_image_vao,
+ VertexArrayKind::ClipRect => &self.clip_rect_vao,
+ VertexArrayKind::ClipBoxShadow => &self.clip_box_shadow_vao,
+ VertexArrayKind::Blur => &self.blur_vao,
+ VertexArrayKind::VectorStencil | VertexArrayKind::VectorCover => unreachable!(),
+ VertexArrayKind::Border => &self.border_vao,
+ VertexArrayKind::Scale => &self.scale_vao,
+ VertexArrayKind::LineDecoration => &self.line_vao,
+ VertexArrayKind::Gradient => &self.gradient_vao,
+ VertexArrayKind::Resolve => &self.resolve_vao,
+ VertexArrayKind::SvgFilter => &self.svg_filter_vao,
+ VertexArrayKind::Composite => &self.composite_vao,
+ VertexArrayKind::Clear => &self.clear_vao,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/resource_cache.rs b/gfx/wr/webrender/src/resource_cache.rs
new file mode 100644
index 0000000000..cc7b73b520
--- /dev/null
+++ b/gfx/wr/webrender/src/resource_cache.rs
@@ -0,0 +1,2097 @@
+/* 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 api::{BlobImageResources, BlobImageRequest, RasterizedBlobImage, ImageFormat};
+use api::{DebugFlags, FontInstanceKey, FontKey, FontTemplate, GlyphIndex};
+use api::{ExternalImageData, ExternalImageType, ExternalImageId, BlobImageResult, FontInstanceData};
+use api::{DirtyRect, GlyphDimensions, IdNamespace, DEFAULT_TILE_SIZE};
+use api::{ImageData, ImageDescriptor, ImageKey, ImageRendering, TileSize};
+use api::{BlobImageKey, VoidPtrToSizeFn};
+use api::{SharedFontInstanceMap, BaseFontInstance};
+use api::units::*;
+use crate::{render_api::{ClearCache, AddFont, ResourceUpdate, MemoryReport}, util::WeakTable};
+use crate::image_tiling::{compute_tile_size, compute_tile_range};
+#[cfg(feature = "capture")]
+use crate::capture::ExternalCaptureImage;
+#[cfg(feature = "replay")]
+use crate::capture::PlainExternalImage;
+#[cfg(any(feature = "replay", feature = "png", feature="capture"))]
+use crate::capture::CaptureConfig;
+use crate::composite::{NativeSurfaceId, NativeSurfaceOperation, NativeTileId, NativeSurfaceOperationDetails};
+use crate::device::TextureFilter;
+use crate::glyph_cache::GlyphCache;
+use crate::glyph_cache::GlyphCacheEntry;
+use crate::glyph_rasterizer::{GLYPH_FLASHING, FontInstance, GlyphFormat, GlyphKey, GlyphRasterizer};
+use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
+use crate::gpu_types::UvRectKind;
+use crate::internal_types::{CacheTextureId, FastHashMap, FastHashSet, TextureSource, ResourceUpdateList};
+use crate::picture::SurfaceInfo;
+use crate::profiler::{self, TransactionProfile, bytes_to_mb};
+use crate::render_backend::{FrameId, FrameStamp};
+use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder};
+use crate::render_task_cache::{RenderTaskCache, RenderTaskCacheKey, RenderTaskParent};
+use crate::render_task_cache::{RenderTaskCacheEntry, RenderTaskCacheEntryHandle};
+use euclid::point2;
+use smallvec::SmallVec;
+use std::collections::hash_map::Entry::{self, Occupied, Vacant};
+use std::collections::hash_map::{Iter, IterMut};
+use std::collections::VecDeque;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use std::collections::HashMap;
+use std::{cmp, mem};
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::os::raw::c_void;
+#[cfg(any(feature = "capture", feature = "replay"))]
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::u32;
+use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+
+// Counter for generating unique native surface ids
+static NEXT_NATIVE_SURFACE_ID: AtomicUsize = AtomicUsize::new(0);
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GlyphFetchResult {
+ pub index_in_text_run: i32,
+ pub uv_rect_address: GpuCacheAddress,
+ pub offset: DevicePoint,
+ pub size: DeviceIntSize,
+ pub scale: f32,
+}
+
+// These coordinates are always in texels.
+// They are converted to normalized ST
+// values in the vertex shader. The reason
+// for this is that the texture may change
+// dimensions (e.g. the pages in a texture
+// atlas can grow). When this happens, by
+// storing the coordinates as texel values
+// we don't need to go through and update
+// various CPU-side structures.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CacheItem {
+ pub texture_id: TextureSource,
+ pub uv_rect_handle: GpuCacheHandle,
+ pub uv_rect: DeviceIntRect,
+ pub texture_layer: i32,
+ pub user_data: [f32; 3],
+}
+
+impl CacheItem {
+ pub fn invalid() -> Self {
+ CacheItem {
+ texture_id: TextureSource::Invalid,
+ uv_rect_handle: GpuCacheHandle::new(),
+ uv_rect: DeviceIntRect::zero(),
+ texture_layer: 0,
+ user_data: [0.0, 0.0, 0.0],
+ }
+ }
+}
+
+/// Represents the backing store of an image in the cache.
+/// This storage can take several forms.
+#[derive(Clone, Debug)]
+pub enum CachedImageData {
+ /// A simple series of bytes, provided by the embedding and owned by WebRender.
+ /// The format is stored out-of-band, currently in ImageDescriptor.
+ Raw(Arc<Vec<u8>>),
+ /// An series of commands that can be rasterized into an image via an
+ /// embedding-provided callback.
+ ///
+ /// The commands are stored elsewhere and this variant is used as a placeholder.
+ Blob,
+ /// An image owned by the embedding, and referenced by WebRender. This may
+ /// take the form of a texture or a heap-allocated buffer.
+ External(ExternalImageData),
+}
+
+impl From<ImageData> for CachedImageData {
+ fn from(img_data: ImageData) -> Self {
+ match img_data {
+ ImageData::Raw(data) => CachedImageData::Raw(data),
+ ImageData::External(data) => CachedImageData::External(data),
+ }
+ }
+}
+
+impl CachedImageData {
+ /// Returns true if this represents a blob.
+ #[inline]
+ pub fn is_blob(&self) -> bool {
+ match *self {
+ CachedImageData::Blob => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true if this variant of CachedImageData should go through the texture
+ /// cache.
+ #[inline]
+ pub fn uses_texture_cache(&self) -> bool {
+ match *self {
+ CachedImageData::External(ref ext_data) => match ext_data.image_type {
+ ExternalImageType::TextureHandle(_) => false,
+ ExternalImageType::Buffer => true,
+ },
+ CachedImageData::Blob => true,
+ CachedImageData::Raw(_) => true,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageProperties {
+ pub descriptor: ImageDescriptor,
+ pub external_image: Option<ExternalImageData>,
+ pub tiling: Option<TileSize>,
+ // 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)]
+enum State {
+ Idle,
+ AddResources,
+ QueryResources,
+}
+
+/// Post scene building state.
+type RasterizedBlob = FastHashMap<TileOffset, RasterizedBlobImage>;
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct ImageGeneration(pub u32);
+
+impl ImageGeneration {
+ pub const INVALID: ImageGeneration = ImageGeneration(u32::MAX);
+}
+
+struct ImageResource {
+ data: CachedImageData,
+ descriptor: ImageDescriptor,
+ tiling: Option<TileSize>,
+ /// 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,
+ generation: ImageGeneration,
+}
+
+#[derive(Clone, Debug)]
+pub struct ImageTiling {
+ pub image_size: DeviceIntSize,
+ pub tile_size: TileSize,
+}
+
+#[derive(Default)]
+struct ImageTemplates {
+ images: FastHashMap<ImageKey, ImageResource>,
+}
+
+impl ImageTemplates {
+ fn insert(&mut self, key: ImageKey, resource: ImageResource) {
+ self.images.insert(key, resource);
+ }
+
+ fn remove(&mut self, key: ImageKey) -> Option<ImageResource> {
+ self.images.remove(&key)
+ }
+
+ fn get(&self, key: ImageKey) -> Option<&ImageResource> {
+ self.images.get(&key)
+ }
+
+ fn get_mut(&mut self, key: ImageKey) -> Option<&mut ImageResource> {
+ self.images.get_mut(&key)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle,
+ dirty_rect: ImageDirtyRect,
+ manual_eviction: bool,
+}
+
+impl CachedImageInfo {
+ fn mark_unused(&mut self, texture_cache: &mut TextureCache) {
+ if self.manual_eviction {
+ texture_cache.evict_manual_handle(&self.texture_cache_handle);
+ }
+ self.manual_eviction = false;
+ }
+}
+
+#[cfg(debug_assertions)]
+impl Drop for CachedImageInfo {
+ fn drop(&mut self) {
+ debug_assert!(!self.manual_eviction, "Manual eviction requires cleanup");
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ResourceClassCache<K: Hash + Eq, V, U: Default> {
+ resources: FastHashMap<K, V>,
+ pub user_data: U,
+}
+
+impl<K, V, U> ResourceClassCache<K, V, U>
+where
+ K: Clone + Hash + Eq + Debug,
+ U: Default,
+{
+ pub fn new() -> Self {
+ ResourceClassCache {
+ resources: FastHashMap::default(),
+ user_data: Default::default(),
+ }
+ }
+
+ pub fn get(&self, key: &K) -> &V {
+ self.resources.get(key)
+ .expect("Didn't find a cached resource with that ID!")
+ }
+
+ pub fn try_get(&self, key: &K) -> Option<&V> {
+ self.resources.get(key)
+ }
+
+ pub fn insert(&mut self, key: K, value: V) {
+ self.resources.insert(key, value);
+ }
+
+ pub fn remove(&mut self, key: &K) -> Option<V> {
+ self.resources.remove(key)
+ }
+
+ pub fn get_mut(&mut self, key: &K) -> &mut V {
+ self.resources.get_mut(key)
+ .expect("Didn't find a cached resource with that ID!")
+ }
+
+ pub fn try_get_mut(&mut self, key: &K) -> Option<&mut V> {
+ self.resources.get_mut(key)
+ }
+
+ pub fn entry(&mut self, key: K) -> Entry<K, V> {
+ self.resources.entry(key)
+ }
+
+ pub fn iter(&self) -> Iter<K, V> {
+ self.resources.iter()
+ }
+
+ pub fn iter_mut(&mut self) -> IterMut<K, V> {
+ self.resources.iter_mut()
+ }
+
+ pub fn is_empty(&mut self) -> bool {
+ self.resources.is_empty()
+ }
+
+ pub fn clear(&mut self) {
+ self.resources.clear();
+ }
+
+ pub fn retain<F>(&mut self, f: F)
+ where
+ F: FnMut(&K, &mut V) -> bool,
+ {
+ self.resources.retain(f);
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CachedImageKey {
+ pub rendering: ImageRendering,
+ pub tile: Option<TileOffset>,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct ImageRequest {
+ pub key: ImageKey,
+ pub rendering: ImageRendering,
+ pub tile: Option<TileOffset>,
+}
+
+impl ImageRequest {
+ pub fn with_tile(&self, offset: TileOffset) -> Self {
+ ImageRequest {
+ key: self.key,
+ rendering: self.rendering,
+ tile: Some(offset),
+ }
+ }
+
+ pub fn is_untiled_auto(&self) -> bool {
+ self.tile.is_none() && self.rendering == ImageRendering::Auto
+ }
+}
+
+impl Into<BlobImageRequest> for ImageRequest {
+ fn into(self) -> BlobImageRequest {
+ BlobImageRequest {
+ key: BlobImageKey(self.key),
+ tile: self.tile.unwrap(),
+ }
+ }
+}
+
+impl Into<CachedImageKey> for ImageRequest {
+ fn into(self) -> CachedImageKey {
+ CachedImageKey {
+ rendering: self.rendering,
+ tile: self.tile,
+ }
+ }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Clone, Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ImageCacheError {
+ OverLimitSize,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum ImageResult {
+ UntiledAuto(CachedImageInfo),
+ Multi(ResourceClassCache<CachedImageKey, CachedImageInfo, ()>),
+ Err(ImageCacheError),
+}
+
+impl ImageResult {
+ /// Releases any texture cache entries held alive by this ImageResult.
+ fn drop_from_cache(&mut self, texture_cache: &mut TextureCache) {
+ match *self {
+ ImageResult::UntiledAuto(ref mut entry) => {
+ entry.mark_unused(texture_cache);
+ },
+ ImageResult::Multi(ref mut entries) => {
+ for entry in entries.resources.values_mut() {
+ entry.mark_unused(texture_cache);
+ }
+ },
+ ImageResult::Err(_) => {},
+ }
+ }
+}
+
+type ImageCache = ResourceClassCache<ImageKey, ImageResult, ()>;
+
+struct Resources {
+ font_templates: FastHashMap<FontKey, FontTemplate>,
+ font_instances: SharedFontInstanceMap,
+ image_templates: ImageTemplates,
+ // We keep a set of Weak references to the fonts so that we're able to include them in memory
+ // reports even if only the OS is holding on to the Vec<u8>. PtrWeakHashSet will periodically
+ // drop any references that have gone dead.
+ weak_fonts: WeakTable
+}
+
+impl BlobImageResources for Resources {
+ fn get_font_data(&self, key: FontKey) -> &FontTemplate {
+ self.font_templates.get(&key).unwrap()
+ }
+ fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData> {
+ self.font_instances.get_font_instance_data(key)
+ }
+}
+
+// We only use this to report glyph dimensions to the user of the API, so using
+// the font instance key should be enough. If we start using it to cache dimensions
+// for internal font instances we should change the hash key accordingly.
+pub type GlyphDimensionsCache = FastHashMap<(FontInstanceKey, GlyphIndex), Option<GlyphDimensions>>;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct BlobImageRasterizerEpoch(usize);
+
+/// Internal information about allocated render targets in the pool
+struct RenderTarget {
+ size: DeviceIntSize,
+ format: ImageFormat,
+ texture_id: CacheTextureId,
+ /// If true, this is currently leant out, and not available to other passes
+ is_active: bool,
+ last_frame_used: FrameId,
+}
+
+impl RenderTarget {
+ fn size_in_bytes(&self) -> usize {
+ let bpp = self.format.bytes_per_pixel() as usize;
+ (self.size.width * self.size.height) as usize * bpp
+ }
+
+ /// Returns true if this texture was used within `threshold` frames of
+ /// the current frame.
+ pub fn used_recently(&self, current_frame_id: FrameId, threshold: usize) -> bool {
+ self.last_frame_used + threshold >= current_frame_id
+ }
+}
+
+/// High-level container for resources managed by the `RenderBackend`.
+///
+/// This includes a variety of things, including images, fonts, and glyphs,
+/// which may be stored as memory buffers, GPU textures, or handles to resources
+/// managed by the OS or other parts of WebRender.
+pub struct ResourceCache {
+ cached_glyphs: GlyphCache,
+ cached_images: ImageCache,
+ cached_render_tasks: RenderTaskCache,
+
+ resources: Resources,
+ state: State,
+ current_frame_id: FrameId,
+
+ #[cfg(feature = "capture")]
+ /// Used for capture sequences. If the resource cache is updated, then we
+ /// mark it as dirty. When the next frame is captured in the sequence, we
+ /// dump the state of the resource cache.
+ capture_dirty: bool,
+
+ pub texture_cache: TextureCache,
+
+ /// TODO(gw): We should expire (parts of) this cache semi-regularly!
+ cached_glyph_dimensions: GlyphDimensionsCache,
+ glyph_rasterizer: GlyphRasterizer,
+
+ /// The set of images that aren't present or valid in the texture cache,
+ /// and need to be rasterized and/or uploaded this frame. This includes
+ /// both blobs and regular images.
+ pending_image_requests: FastHashSet<ImageRequest>,
+
+ rasterized_blob_images: FastHashMap<BlobImageKey, RasterizedBlob>,
+
+ /// A log of the last three frames worth of deleted image keys kept
+ /// for debugging purposes.
+ deleted_blob_keys: VecDeque<Vec<BlobImageKey>>,
+
+ /// A list of queued compositor surface updates to apply next frame.
+ pending_native_surface_updates: Vec<NativeSurfaceOperation>,
+
+ image_templates_memory: usize,
+ font_templates_memory: usize,
+
+ /// A pool of render targets for use by the render task graph
+ render_target_pool: Vec<RenderTarget>,
+}
+
+impl ResourceCache {
+ pub fn new(
+ texture_cache: TextureCache,
+ glyph_rasterizer: GlyphRasterizer,
+ cached_glyphs: GlyphCache,
+ font_instances: SharedFontInstanceMap,
+ ) -> Self {
+ ResourceCache {
+ cached_glyphs,
+ cached_images: ResourceClassCache::new(),
+ cached_render_tasks: RenderTaskCache::new(),
+ resources: Resources {
+ font_instances,
+ font_templates: FastHashMap::default(),
+ image_templates: ImageTemplates::default(),
+ weak_fonts: WeakTable::new(),
+ },
+ cached_glyph_dimensions: FastHashMap::default(),
+ texture_cache,
+ state: State::Idle,
+ current_frame_id: FrameId::INVALID,
+ pending_image_requests: FastHashSet::default(),
+ glyph_rasterizer,
+ rasterized_blob_images: FastHashMap::default(),
+ // We want to keep three frames worth of delete blob keys
+ deleted_blob_keys: vec![Vec::new(), Vec::new(), Vec::new()].into(),
+ pending_native_surface_updates: Vec::new(),
+ #[cfg(feature = "capture")]
+ capture_dirty: true,
+ image_templates_memory: 0,
+ font_templates_memory: 0,
+ render_target_pool: Vec::new(),
+ }
+ }
+
+ /// Construct a resource cache for use in unit tests.
+ #[cfg(test)]
+ pub fn new_for_testing() -> Self {
+ use rayon::ThreadPoolBuilder;
+
+ let texture_cache = TextureCache::new_for_testing(
+ 4096,
+ ImageFormat::RGBA8,
+ );
+ let workers = Arc::new(ThreadPoolBuilder::new().build().unwrap());
+ let glyph_rasterizer = GlyphRasterizer::new(workers).unwrap();
+ let cached_glyphs = GlyphCache::new();
+ let font_instances = SharedFontInstanceMap::new();
+
+ ResourceCache::new(
+ texture_cache,
+ glyph_rasterizer,
+ cached_glyphs,
+ font_instances,
+ )
+ }
+
+ pub fn max_texture_size(&self) -> i32 {
+ self.texture_cache.max_texture_size()
+ }
+
+ pub fn enable_multithreading(&mut self, enable: bool) {
+ self.glyph_rasterizer.enable_multithreading(enable);
+ }
+
+ fn should_tile(limit: i32, descriptor: &ImageDescriptor, data: &CachedImageData) -> bool {
+ let size_check = descriptor.size.width > limit || descriptor.size.height > limit;
+ match *data {
+ CachedImageData::Raw(_) | CachedImageData::Blob => size_check,
+ CachedImageData::External(info) => {
+ // External handles already represent existing textures so it does
+ // not make sense to tile them into smaller ones.
+ info.image_type == ExternalImageType::Buffer && size_check
+ }
+ }
+ }
+
+ // Request the texture cache item for a cacheable render
+ // task. If the item is already cached, the texture cache
+ // handle will be returned. Otherwise, the user supplied
+ // closure will be invoked to generate the render task
+ // chain that is required to draw this task.
+ pub fn request_render_task<F>(
+ &mut self,
+ key: RenderTaskCacheKey,
+ gpu_cache: &mut GpuCache,
+ rg_builder: &mut RenderTaskGraphBuilder,
+ user_data: Option<[f32; 3]>,
+ is_opaque: bool,
+ parent: RenderTaskParent,
+ surfaces: &[SurfaceInfo],
+ f: F,
+ ) -> RenderTaskCacheEntryHandle
+ where
+ F: FnOnce(&mut RenderTaskGraphBuilder) -> RenderTaskId,
+ {
+ self.cached_render_tasks.request_render_task(
+ key,
+ &mut self.texture_cache,
+ gpu_cache,
+ rg_builder,
+ user_data,
+ is_opaque,
+ parent,
+ surfaces,
+ |render_graph| Ok(f(render_graph))
+ ).expect("Failed to request a render task from the resource cache!")
+ }
+
+ pub fn post_scene_building_update(
+ &mut self,
+ updates: Vec<ResourceUpdate>,
+ profile: &mut TransactionProfile,
+ ) {
+ // TODO, there is potential for optimization here, by processing updates in
+ // bulk rather than one by one (for example by sorting allocations by size or
+ // in a way that reduces fragmentation in the atlas).
+ #[cfg(feature = "capture")]
+ match updates.is_empty() {
+ false => self.capture_dirty = true,
+ _ => {},
+ }
+
+ for update in updates {
+ match update {
+ ResourceUpdate::AddImage(img) => {
+ if let ImageData::Raw(ref bytes) = img.data {
+ self.image_templates_memory += bytes.len();
+ profile.set(profiler::IMAGE_TEMPLATES_MEM, bytes_to_mb(self.image_templates_memory));
+ }
+ self.add_image_template(
+ img.key,
+ img.descriptor,
+ img.data.into(),
+ &img.descriptor.size.into(),
+ img.tiling,
+ );
+ profile.set(profiler::IMAGE_TEMPLATES, self.resources.image_templates.images.len());
+ }
+ ResourceUpdate::UpdateImage(img) => {
+ self.update_image_template(img.key, img.descriptor, img.data.into(), &img.dirty_rect);
+ }
+ ResourceUpdate::AddBlobImage(img) => {
+ self.add_image_template(
+ img.key.as_image(),
+ img.descriptor,
+ CachedImageData::Blob,
+ &img.visible_rect,
+ Some(img.tile_size),
+ );
+ }
+ ResourceUpdate::UpdateBlobImage(img) => {
+ self.update_image_template(
+ img.key.as_image(),
+ img.descriptor,
+ CachedImageData::Blob,
+ &to_image_dirty_rect(
+ &img.dirty_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);
+ profile.set(profiler::IMAGE_TEMPLATES, self.resources.image_templates.images.len());
+ profile.set(profiler::IMAGE_TEMPLATES_MEM, bytes_to_mb(self.image_templates_memory));
+ }
+ ResourceUpdate::DeleteBlobImage(img) => {
+ self.delete_image_template(img.as_image());
+ }
+ ResourceUpdate::DeleteFont(font) => {
+ self.delete_font_template(font);
+ profile.set(profiler::FONT_TEMPLATES, self.resources.font_templates.len());
+ profile.set(profiler::FONT_TEMPLATES_MEM, bytes_to_mb(self.font_templates_memory));
+ }
+ ResourceUpdate::DeleteFontInstance(font) => {
+ self.delete_font_instance(font);
+ }
+ ResourceUpdate::SetBlobImageVisibleArea(key, area) => {
+ self.discard_tiles_outside_visible_area(key, &area);
+ self.set_image_visible_rect(key.as_image(), &area);
+ }
+ ResourceUpdate::AddFont(font) => {
+ match font {
+ AddFont::Raw(id, bytes, index) => {
+ self.font_templates_memory += bytes.len();
+ profile.set(profiler::FONT_TEMPLATES_MEM, bytes_to_mb(self.font_templates_memory));
+ self.add_font_template(id, FontTemplate::Raw(bytes, index));
+ }
+ AddFont::Native(id, native_font_handle) => {
+ self.add_font_template(id, FontTemplate::Native(native_font_handle));
+ }
+ }
+ profile.set(profiler::FONT_TEMPLATES, self.resources.font_templates.len());
+ }
+ ResourceUpdate::AddFontInstance(..) => {
+ // Already added in ApiResources.
+ }
+ }
+ }
+ }
+
+ pub fn add_rasterized_blob_images(
+ &mut self,
+ images: Vec<(BlobImageRequest, BlobImageResult)>,
+ profile: &mut TransactionProfile,
+ ) {
+ for (request, result) in images {
+ let data = match result {
+ Ok(data) => data,
+ Err(..) => {
+ warn!("Failed to rasterize a blob image");
+ continue;
+ }
+ };
+
+ profile.add(profiler::RASTERIZED_BLOBS_PX, data.rasterized_rect.area());
+
+ // First make sure we have an entry for this key (using a placeholder
+ // if need be).
+ let tiles = self.rasterized_blob_images.entry(request.key).or_insert_with(
+ || { RasterizedBlob::default() }
+ );
+
+ tiles.insert(request.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(request.tile),
+ };
+ if let Some(entry) = entries.try_get_mut(&cached_key) {
+ entry.dirty_rect = DirtyRect::All;
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ pub fn add_font_template(&mut self, font_key: FontKey, template: FontTemplate) {
+ // Push the new font to the font renderer, and also store
+ // it locally for glyph metric requests.
+ if let FontTemplate::Raw(ref font, _) = template {
+ self.resources.weak_fonts.insert(Arc::downgrade(font));
+ }
+ self.glyph_rasterizer.add_font(font_key, template.clone());
+ self.resources.font_templates.insert(font_key, template);
+ }
+
+ pub fn delete_font_template(&mut self, font_key: FontKey) {
+ self.glyph_rasterizer.delete_font(font_key);
+ if let Some(FontTemplate::Raw(data, _)) = self.resources.font_templates.remove(&font_key) {
+ self.font_templates_memory -= data.len();
+ }
+ self.cached_glyphs
+ .clear_fonts(|font| font.font_key == font_key);
+ }
+
+ pub fn delete_font_instance(&mut self, instance_key: FontInstanceKey) {
+ self.resources.font_instances.delete_font_instance(instance_key);
+ }
+
+ pub fn get_font_instances(&self) -> SharedFontInstanceMap {
+ self.resources.font_instances.clone()
+ }
+
+ pub fn get_font_instance(&self, instance_key: FontInstanceKey) -> Option<Arc<BaseFontInstance>> {
+ self.resources.font_instances.get_font_instance(instance_key)
+ }
+
+ pub fn add_image_template(
+ &mut self,
+ image_key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: CachedImageData,
+ visible_rect: &DeviceIntRect,
+ mut tiling: Option<TileSize>,
+ ) {
+ if tiling.is_none() && Self::should_tile(self.max_texture_size(), &descriptor, &data) {
+ // We aren't going to be able to upload a texture this big, so tile it, even
+ // if tiling was not requested.
+ tiling = Some(DEFAULT_TILE_SIZE);
+ }
+
+ let resource = ImageResource {
+ descriptor,
+ data,
+ tiling,
+ visible_rect: *visible_rect,
+ generation: ImageGeneration(0),
+ };
+
+ self.resources.image_templates.insert(image_key, resource);
+ }
+
+ pub fn update_image_template(
+ &mut self,
+ image_key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: CachedImageData,
+ dirty_rect: &ImageDirtyRect,
+ ) {
+ let max_texture_size = self.max_texture_size();
+ let image = match self.resources.image_templates.get_mut(image_key) {
+ Some(res) => res,
+ None => panic!("Attempt to update non-existent image"),
+ };
+
+ let mut tiling = image.tiling;
+ if tiling.is_none() && Self::should_tile(max_texture_size, &descriptor, &data) {
+ tiling = Some(DEFAULT_TILE_SIZE);
+ }
+
+ // 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) {
+ Some(&mut ImageResult::UntiledAuto(ref mut entry)) => {
+ entry.dirty_rect = entry.dirty_rect.union(dirty_rect);
+ }
+ Some(&mut ImageResult::Multi(ref mut entries)) => {
+ for (key, entry) in entries.iter_mut() {
+ // We want the dirty rect relative to the tile and not the whole image.
+ let local_dirty_rect = match (tiling, key.tile) {
+ (Some(tile_size), Some(tile)) => {
+ dirty_rect.map(|mut rect|{
+ let tile_offset = DeviceIntPoint::new(
+ tile.x as i32,
+ tile.y as i32,
+ ) * tile_size as i32;
+ rect.origin -= tile_offset.to_vector();
+
+ let tile_rect = compute_tile_size(
+ &descriptor.size.into(),
+ tile_size,
+ tile,
+ ).into();
+
+ rect.intersection(&tile_rect).unwrap_or_else(DeviceIntRect::zero)
+ })
+ }
+ (None, Some(..)) => DirtyRect::All,
+ _ => *dirty_rect,
+ };
+ entry.dirty_rect = entry.dirty_rect.union(&local_dirty_rect);
+ }
+ }
+ _ => {}
+ }
+
+ if image.descriptor.format != descriptor.format {
+ // could be a stronger warning/error?
+ trace!("Format change {:?} -> {:?}", image.descriptor.format, descriptor.format);
+ }
+ *image = ImageResource {
+ descriptor,
+ data,
+ tiling,
+ visible_rect: descriptor.size.into(),
+ generation: ImageGeneration(image.generation.0 + 1),
+ };
+ }
+
+ pub fn delete_image_template(&mut self, image_key: ImageKey) {
+ // Remove the template.
+ let value = self.resources.image_templates.remove(image_key);
+
+ // Release the corresponding texture cache entry, if any.
+ if let Some(mut cached) = self.cached_images.remove(&image_key) {
+ cached.drop_from_cache(&mut self.texture_cache);
+ }
+
+ match value {
+ Some(image) => if image.data.is_blob() {
+ if let CachedImageData::Raw(data) = image.data {
+ self.image_templates_memory -= data.len();
+ }
+
+ let blob_key = BlobImageKey(image_key);
+ self.deleted_blob_keys.back_mut().unwrap().push(blob_key);
+ self.rasterized_blob_images.remove(&blob_key);
+ },
+ None => {
+ warn!("Delete the non-exist key");
+ debug!("key={:?}", image_key);
+ }
+ }
+ }
+
+ /// Return the current generation of an image template
+ pub fn get_image_generation(&self, key: ImageKey) -> ImageGeneration {
+ self.resources
+ .image_templates
+ .get(key)
+ .map_or(ImageGeneration::INVALID, |template| template.generation)
+ }
+
+ pub fn request_image(
+ &mut self,
+ request: ImageRequest,
+ gpu_cache: &mut GpuCache,
+ ) {
+ debug_assert_eq!(self.state, State::AddResources);
+
+ let template = match self.resources.image_templates.get(request.key) {
+ Some(template) => template,
+ None => {
+ warn!("ERROR: Trying to render deleted / non-existent key");
+ debug!("key={:?}", request.key);
+ return
+ }
+ };
+
+ // Images that don't use the texture cache can early out.
+ if !template.data.uses_texture_cache() {
+ return;
+ }
+
+ let side_size =
+ template.tiling.map_or(cmp::max(template.descriptor.size.width, template.descriptor.size.height),
+ |tile_size| tile_size as i32);
+ if side_size > self.texture_cache.max_texture_size() {
+ // The image or tiling size is too big for hardware texture size.
+ warn!("Dropping image, image:(w:{},h:{}, tile:{}) is too big for hardware!",
+ template.descriptor.size.width, template.descriptor.size.height, template.tiling.unwrap_or(0));
+ self.cached_images.insert(request.key, ImageResult::Err(ImageCacheError::OverLimitSize));
+ return;
+ }
+
+ let storage = match self.cached_images.entry(request.key) {
+ Occupied(e) => {
+ // We might have an existing untiled entry, and need to insert
+ // a second entry. In such cases we need to move the old entry
+ // out first, replacing it with a dummy entry, and then creating
+ // the tiled/multi-entry variant.
+ let entry = e.into_mut();
+ if !request.is_untiled_auto() {
+ let untiled_entry = match entry {
+ &mut ImageResult::UntiledAuto(ref mut entry) => {
+ Some(mem::replace(entry, CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ }))
+ }
+ _ => None
+ };
+
+ if let Some(untiled_entry) = untiled_entry {
+ let mut entries = ResourceClassCache::new();
+ let untiled_key = CachedImageKey {
+ rendering: ImageRendering::Auto,
+ tile: None,
+ };
+ entries.insert(untiled_key, untiled_entry);
+ *entry = ImageResult::Multi(entries);
+ }
+ }
+ entry
+ }
+ Vacant(entry) => {
+ entry.insert(if request.is_untiled_auto() {
+ ImageResult::UntiledAuto(CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ })
+ } else {
+ ImageResult::Multi(ResourceClassCache::new())
+ })
+ }
+ };
+
+ // If this image exists in the texture cache, *and* the dirty rect
+ // in the cache is empty, then it is valid to use as-is.
+ let entry = match *storage {
+ ImageResult::UntiledAuto(ref mut entry) => entry,
+ ImageResult::Multi(ref mut entries) => {
+ entries.entry(request.into())
+ .or_insert(CachedImageInfo {
+ texture_cache_handle: TextureCacheHandle::invalid(),
+ dirty_rect: DirtyRect::All,
+ manual_eviction: false,
+ })
+ },
+ ImageResult::Err(_) => panic!("Errors should already have been handled"),
+ };
+
+ let needs_upload = self.texture_cache.request(&entry.texture_cache_handle, gpu_cache);
+
+ if !needs_upload && entry.dirty_rect.is_empty() {
+ return
+ }
+
+ 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(tiles) => !tiles.contains_key(&request.tile),
+ _ => true,
+ };
+
+ assert!(!missing);
+ }
+ }
+
+ fn discard_tiles_outside_visible_area(
+ &mut self,
+ key: BlobImageKey,
+ area: &DeviceIntRect
+ ) {
+ let tile_size = match self.resources.image_templates.get(key.as_image()) {
+ Some(template) => template.tiling.unwrap(),
+ None => {
+ //println!("Missing image template (key={:?})!", key);
+ return;
+ }
+ };
+
+ let tiles = match self.rasterized_blob_images.get_mut(&key) {
+ Some(tiles) => tiles,
+ _ => { return; }
+ };
+
+ let tile_range = compute_tile_range(
+ &area,
+ tile_size,
+ );
+
+ tiles.retain(|tile, _| { tile_range.contains(*tile) });
+
+ let texture_cache = &mut self.texture_cache;
+ match self.cached_images.try_get_mut(&key.as_image()) {
+ Some(&mut ImageResult::Multi(ref mut entries)) => {
+ entries.retain(|key, entry| {
+ if key.tile.is_none() || tile_range.contains(key.tile.unwrap()) {
+ return true;
+ }
+ entry.mark_unused(texture_cache);
+ return false;
+ });
+ }
+ _ => {}
+ }
+ }
+
+ 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;
+ image.descriptor.size = rect.size;
+ }
+ }
+
+ pub fn request_glyphs(
+ &mut self,
+ mut font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ gpu_cache: &mut GpuCache,
+ ) {
+ debug_assert_eq!(self.state, State::AddResources);
+
+ self.glyph_rasterizer.prepare_font(&mut font);
+ self.glyph_rasterizer.request_glyphs(
+ &mut self.cached_glyphs,
+ font,
+ glyph_keys,
+ &mut self.texture_cache,
+ gpu_cache,
+ );
+ }
+
+ pub fn pending_updates(&mut self) -> ResourceUpdateList {
+ ResourceUpdateList {
+ texture_updates: self.texture_cache.pending_updates(),
+ native_surface_updates: mem::replace(&mut self.pending_native_surface_updates, Vec::new()),
+ }
+ }
+
+ pub fn fetch_glyphs<F>(
+ &self,
+ mut font: FontInstance,
+ glyph_keys: &[GlyphKey],
+ fetch_buffer: &mut Vec<GlyphFetchResult>,
+ gpu_cache: &mut GpuCache,
+ mut f: F,
+ ) where
+ F: FnMut(TextureSource, GlyphFormat, &[GlyphFetchResult]),
+ {
+ debug_assert_eq!(self.state, State::QueryResources);
+
+ self.glyph_rasterizer.prepare_font(&mut font);
+ let glyph_key_cache = self.cached_glyphs.get_glyph_key_cache_for_font(&font);
+
+ let mut current_texture_id = TextureSource::Invalid;
+ let mut current_glyph_format = GlyphFormat::Subpixel;
+ debug_assert!(fetch_buffer.is_empty());
+
+ for (loop_index, key) in glyph_keys.iter().enumerate() {
+ let (cache_item, glyph_format) = match *glyph_key_cache.get(key) {
+ GlyphCacheEntry::Cached(ref glyph) => {
+ (self.texture_cache.get(&glyph.texture_cache_handle), glyph.format)
+ }
+ GlyphCacheEntry::Blank | GlyphCacheEntry::Pending => continue,
+ };
+ if current_texture_id != cache_item.texture_id ||
+ current_glyph_format != glyph_format {
+ if !fetch_buffer.is_empty() {
+ f(current_texture_id, current_glyph_format, fetch_buffer);
+ fetch_buffer.clear();
+ }
+ current_texture_id = cache_item.texture_id;
+ current_glyph_format = glyph_format;
+ }
+ fetch_buffer.push(GlyphFetchResult {
+ index_in_text_run: loop_index as i32,
+ uv_rect_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+ offset: DevicePoint::new(cache_item.user_data[0], cache_item.user_data[1]),
+ size: cache_item.uv_rect.size,
+ scale: cache_item.user_data[2],
+ });
+ }
+
+ if !fetch_buffer.is_empty() {
+ f(current_texture_id, current_glyph_format, fetch_buffer);
+ fetch_buffer.clear();
+ }
+ }
+
+ pub fn get_glyph_dimensions(
+ &mut self,
+ font: &FontInstance,
+ glyph_index: GlyphIndex,
+ ) -> Option<GlyphDimensions> {
+ match self.cached_glyph_dimensions.entry((font.instance_key, glyph_index)) {
+ Occupied(entry) => *entry.get(),
+ Vacant(entry) => *entry.insert(
+ self.glyph_rasterizer
+ .get_glyph_dimensions(font, glyph_index),
+ ),
+ }
+ }
+
+ pub fn get_glyph_index(&mut self, font_key: FontKey, ch: char) -> Option<u32> {
+ self.glyph_rasterizer.get_glyph_index(font_key, ch)
+ }
+
+ #[inline]
+ pub fn get_cached_image(&self, request: ImageRequest) -> Result<CacheItem, ()> {
+ debug_assert_eq!(self.state, State::QueryResources);
+ let image_info = self.get_image_info(request)?;
+ Ok(self.get_texture_cache_item(&image_info.texture_cache_handle))
+ }
+
+ pub fn get_cached_render_task(
+ &self,
+ handle: &RenderTaskCacheEntryHandle,
+ ) -> &RenderTaskCacheEntry {
+ self.cached_render_tasks.get_cache_entry(handle)
+ }
+
+ #[inline]
+ fn get_image_info(&self, request: ImageRequest) -> Result<&CachedImageInfo, ()> {
+ // TODO(Jerry): add a debug option to visualize the corresponding area for
+ // the Err() case of CacheItem.
+ match *self.cached_images.get(&request.key) {
+ ImageResult::UntiledAuto(ref image_info) => Ok(image_info),
+ ImageResult::Multi(ref entries) => Ok(entries.get(&request.into())),
+ ImageResult::Err(_) => Err(()),
+ }
+ }
+
+ #[inline]
+ pub fn get_texture_cache_item(&self, handle: &TextureCacheHandle) -> CacheItem {
+ self.texture_cache.get(handle)
+ }
+
+ pub fn get_image_properties(&self, image_key: ImageKey) -> Option<ImageProperties> {
+ let image_template = &self.resources.image_templates.get(image_key);
+
+ image_template.map(|image_template| {
+ let external_image = match image_template.data {
+ CachedImageData::External(ext_image) => match ext_image.image_type {
+ ExternalImageType::TextureHandle(_) => Some(ext_image),
+ // external buffer uses resource_cache.
+ ExternalImageType::Buffer => None,
+ },
+ // raw and blob image are all using resource_cache.
+ CachedImageData::Raw(..) | CachedImageData::Blob => None,
+ };
+
+ ImageProperties {
+ descriptor: image_template.descriptor,
+ external_image,
+ tiling: image_template.tiling,
+ visible_rect: image_template.visible_rect,
+ }
+ })
+ }
+
+ pub fn begin_frame(&mut self, stamp: FrameStamp) {
+ profile_scope!("begin_frame");
+ debug_assert_eq!(self.state, State::Idle);
+ self.state = State::AddResources;
+ self.texture_cache.begin_frame(stamp);
+ self.cached_glyphs.begin_frame(
+ stamp,
+ &mut self.texture_cache,
+ &mut self.glyph_rasterizer,
+ );
+ self.cached_render_tasks.begin_frame(&mut self.texture_cache);
+ self.current_frame_id = stamp.frame_id();
+
+ // pop the old frame and push a new one
+ self.deleted_blob_keys.pop_front();
+ self.deleted_blob_keys.push_back(Vec::new());
+ }
+
+ pub fn block_until_all_resources_added(
+ &mut self,
+ gpu_cache: &mut GpuCache,
+ profile: &mut TransactionProfile,
+ ) {
+ profile_scope!("block_until_all_resources_added");
+
+ debug_assert_eq!(self.state, State::AddResources);
+ self.state = State::QueryResources;
+
+ self.glyph_rasterizer.resolve_glyphs(
+ &mut self.cached_glyphs,
+ &mut self.texture_cache,
+ gpu_cache,
+ profile,
+ );
+
+ // Apply any updates of new / updated images (incl. blobs) to the texture cache.
+ self.update_texture_cache(gpu_cache);
+ }
+
+ fn update_texture_cache(&mut self, gpu_cache: &mut GpuCache) {
+ profile_scope!("update_texture_cache");
+ for request in self.pending_image_requests.drain() {
+ let image_template = self.resources.image_templates.get_mut(request.key).unwrap();
+ debug_assert!(image_template.data.uses_texture_cache());
+
+ let mut updates: SmallVec<[(CachedImageData, Option<DeviceIntRect>); 1]> = SmallVec::new();
+
+ match image_template.data {
+ CachedImageData::Raw(..) | CachedImageData::External(..) => {
+ // Safe to clone here since the Raw image data is an
+ // Arc, and the external image data is small.
+ updates.push((image_template.data.clone(), None));
+ }
+ CachedImageData::Blob => {
+ let blob_image = self.rasterized_blob_images.get_mut(&BlobImageKey(request.key)).unwrap();
+ let img = &blob_image[&request.tile.unwrap()];
+ updates.push((
+ CachedImageData::Raw(Arc::clone(&img.data)),
+ Some(img.rasterized_rect)
+ ));
+ }
+ };
+
+ for (image_data, blob_rasterized_rect) in updates {
+ let entry = match *self.cached_images.get_mut(&request.key) {
+ ImageResult::UntiledAuto(ref mut entry) => entry,
+ ImageResult::Multi(ref mut entries) => entries.get_mut(&request.into()),
+ ImageResult::Err(_) => panic!("Update requested for invalid entry")
+ };
+
+ let mut descriptor = image_template.descriptor.clone();
+ let mut dirty_rect = entry.dirty_rect.replace_with_empty();
+
+ if let Some(tile) = request.tile {
+ let tile_size = image_template.tiling.unwrap();
+ 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);
+ descriptor.offset +=
+ tile.y as i32 * tile_size as i32 * stride +
+ tile.x as i32 * tile_size as i32 * bpp;
+ }
+
+ descriptor.size = clipped_tile_size;
+ }
+
+ // If we are uploading the dirty region of a blob image we might have several
+ // rects to upload so we use each of these rasterized rects rather than the
+ // overall dirty rect of the image.
+ if let Some(rect) = blob_rasterized_rect {
+ dirty_rect = DirtyRect::Partial(rect);
+ }
+
+ let filter = match request.rendering {
+ ImageRendering::Pixelated => {
+ TextureFilter::Nearest
+ }
+ ImageRendering::Auto | ImageRendering::CrispEdges => {
+ // If the texture uses linear filtering, enable mipmaps and
+ // trilinear filtering, for better image quality. We only
+ // support this for now on textures that are not placed
+ // into the shared cache. This accounts for any image
+ // that is > 512 in either dimension, so it should cover
+ // the most important use cases. We may want to support
+ // mip-maps on shared cache items in the future.
+ if descriptor.allow_mipmaps() &&
+ descriptor.size.width > 512 &&
+ descriptor.size.height > 512 &&
+ !self.texture_cache.is_allowed_in_shared_cache(
+ TextureFilter::Linear,
+ &descriptor,
+ ) {
+ TextureFilter::Trilinear
+ } else {
+ TextureFilter::Linear
+ }
+ }
+ };
+
+ let eviction = if image_template.data.is_blob() {
+ entry.manual_eviction = true;
+ Eviction::Manual
+ } else {
+ Eviction::Auto
+ };
+
+ //Note: at this point, the dirty rectangle is local to the descriptor space
+ self.texture_cache.update(
+ &mut entry.texture_cache_handle,
+ descriptor,
+ filter,
+ Some(image_data),
+ [0.0; 3],
+ dirty_rect,
+ gpu_cache,
+ None,
+ UvRectKind::Rect,
+ eviction,
+ TargetShader::Default,
+ );
+ }
+ }
+ }
+
+ /// Queue up allocation of a new OS native compositor surface with the
+ /// specified tile size.
+ pub fn create_compositor_surface(
+ &mut self,
+ virtual_offset: DeviceIntPoint,
+ tile_size: DeviceIntSize,
+ is_opaque: bool,
+ ) -> NativeSurfaceId {
+ let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed) as u64);
+
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateSurface {
+ id,
+ virtual_offset,
+ tile_size,
+ is_opaque,
+ },
+ }
+ );
+
+ id
+ }
+
+ pub fn create_compositor_external_surface(
+ &mut self,
+ is_opaque: bool,
+ ) -> NativeSurfaceId {
+ let id = NativeSurfaceId(NEXT_NATIVE_SURFACE_ID.fetch_add(1, Ordering::Relaxed) as u64);
+
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateExternalSurface {
+ id,
+ is_opaque,
+ },
+ }
+ );
+
+ id
+ }
+
+ /// Queue up destruction of an existing native OS surface. This is used when
+ /// a picture cache surface is dropped or resized.
+ pub fn destroy_compositor_surface(
+ &mut self,
+ id: NativeSurfaceId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::DestroySurface {
+ id,
+ }
+ }
+ );
+ }
+
+ /// Queue construction of a native compositor tile on a given surface.
+ pub fn create_compositor_tile(
+ &mut self,
+ id: NativeTileId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::CreateTile {
+ id,
+ },
+ }
+ );
+ }
+
+ /// Queue destruction of a native compositor tile.
+ pub fn destroy_compositor_tile(
+ &mut self,
+ id: NativeTileId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::DestroyTile {
+ id,
+ },
+ }
+ );
+ }
+
+ pub fn attach_compositor_external_image(
+ &mut self,
+ id: NativeSurfaceId,
+ external_image: ExternalImageId,
+ ) {
+ self.pending_native_surface_updates.push(
+ NativeSurfaceOperation {
+ details: NativeSurfaceOperationDetails::AttachExternalImage {
+ id,
+ external_image,
+ },
+ }
+ );
+ }
+
+
+ pub fn end_frame(&mut self, profile: &mut TransactionProfile) {
+ debug_assert_eq!(self.state, State::QueryResources);
+ profile_scope!("end_frame");
+ self.state = State::Idle;
+
+ // GC the render target pool, if it's currently > 64 MB in size.
+ //
+ // We use a simple scheme whereby we drop any texture that hasn't been used
+ // in the last 60 frames, until we are below the size threshold. This should
+ // generally prevent any sustained build-up of unused textures, unless we don't
+ // generate frames for a long period. This can happen when the window is
+ // minimized, and we probably want to flush all the WebRender caches in that case [1].
+ // There is also a second "red line" memory threshold which prevents
+ // memory exhaustion if many render targets are allocated within a small
+ // number of frames. For now this is set at 320 MB (10x the normal memory threshold).
+ //
+ // [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1494099
+ self.gc_render_targets(
+ 64 * 1024 * 1024,
+ 32 * 1024 * 1024 * 10,
+ 60,
+ );
+
+ self.texture_cache.end_frame(profile);
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ GLYPH_FLASHING.store(flags.contains(DebugFlags::GLYPH_FLASHING), std::sync::atomic::Ordering::Relaxed);
+ self.texture_cache.set_debug_flags(flags);
+ }
+
+ pub fn clear(&mut self, what: ClearCache) {
+ if what.contains(ClearCache::IMAGES) {
+ for (_key, mut cached) in self.cached_images.resources.drain() {
+ cached.drop_from_cache(&mut self.texture_cache);
+ }
+ }
+ if what.contains(ClearCache::GLYPHS) {
+ self.cached_glyphs.clear();
+ }
+ if what.contains(ClearCache::GLYPH_DIMENSIONS) {
+ self.cached_glyph_dimensions.clear();
+ }
+ if what.contains(ClearCache::RENDER_TASKS) {
+ self.cached_render_tasks.clear();
+ }
+ if what.contains(ClearCache::TEXTURE_CACHE) {
+ self.texture_cache.clear_all();
+ }
+ if what.contains(ClearCache::RENDER_TARGETS) {
+ self.clear_render_target_pool();
+ }
+ }
+
+ pub fn clear_namespace(&mut self, namespace: IdNamespace) {
+ self.clear_images(|k| k.0 == namespace);
+
+ self.resources.font_instances.clear_namespace(namespace);
+
+ for &key in self.resources.font_templates.keys().filter(|key| key.0 == namespace) {
+ self.glyph_rasterizer.delete_font(key);
+ }
+ self.resources
+ .font_templates
+ .retain(|key, _| key.0 != namespace);
+ self.cached_glyphs
+ .clear_fonts(|font| font.font_key.0 == namespace);
+ }
+
+ /// Reports the CPU heap usage of this ResourceCache.
+ ///
+ /// NB: It would be much better to use the derive(MallocSizeOf) machinery
+ /// here, but the Arcs complicate things. The two ways to handle that would
+ /// be to either (a) Implement MallocSizeOf manually for the things that own
+ /// them and manually avoid double-counting, or (b) Use the "seen this pointer
+ /// yet" machinery from the proper malloc_size_of crate. We can do this if/when
+ /// more accurate memory reporting on these resources becomes a priority.
+ pub fn report_memory(&self, op: VoidPtrToSizeFn) -> MemoryReport {
+ let mut report = MemoryReport::default();
+
+ let mut seen_fonts = std::collections::HashSet::new();
+ // Measure fonts. We only need the templates here, because the instances
+ // don't have big buffers.
+ for (_, font) in self.resources.font_templates.iter() {
+ if let FontTemplate::Raw(ref raw, _) = font {
+ report.fonts += unsafe { op(raw.as_ptr() as *const c_void) };
+ seen_fonts.insert(raw.as_ptr());
+ }
+ }
+
+ for font in self.resources.weak_fonts.iter() {
+ if !seen_fonts.contains(&font.as_ptr()) {
+ report.weak_fonts += unsafe { op(font.as_ptr() as *const c_void) };
+ }
+ }
+
+ // Measure images.
+ for (_, image) in self.resources.image_templates.images.iter() {
+ report.images += match image.data {
+ CachedImageData::Raw(ref v) => unsafe { op(v.as_ptr() as *const c_void) },
+ CachedImageData::Blob | CachedImageData::External(..) => 0,
+ }
+ }
+
+ // Mesure rasterized blobs.
+ // TODO(gw): Temporarily disabled while we roll back a crash. We can re-enable
+ // these when that crash is fixed.
+ /*
+ for (_, image) in self.rasterized_blob_images.iter() {
+ let mut accumulate = |b: &RasterizedBlobImage| {
+ report.rasterized_blobs += unsafe { op(b.data.as_ptr() as *const c_void) };
+ };
+ match image {
+ RasterizedBlob::Tiled(map) => map.values().for_each(&mut accumulate),
+ RasterizedBlob::NonTiled(vec) => vec.iter().for_each(&mut accumulate),
+ };
+ }
+ */
+
+ report
+ }
+
+ /// Properly deletes all images matching the predicate.
+ fn clear_images<F: Fn(&ImageKey) -> bool>(&mut self, f: F) {
+ let keys = self.resources.image_templates.images.keys().filter(|k| f(*k))
+ .cloned().collect::<SmallVec<[ImageKey; 16]>>();
+
+ for key in keys {
+ self.delete_image_template(key);
+ }
+
+ #[cfg(features="leak_checks")]
+ let check_leaks = true;
+ #[cfg(not(features="leak_checks"))]
+ let check_leaks = false;
+
+ if check_leaks {
+ let blob_f = |key: &BlobImageKey| { f(&key.as_image()) };
+ assert!(!self.resources.image_templates.images.keys().any(&f));
+ assert!(!self.cached_images.resources.keys().any(&f));
+ assert!(!self.rasterized_blob_images.keys().any(&blob_f));
+ }
+ }
+
+ /// Get a render target from the pool, or allocate a new one if none are
+ /// currently available that match the requested parameters.
+ pub fn get_or_create_render_target_from_pool(
+ &mut self,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ ) -> CacheTextureId {
+ for target in &mut self.render_target_pool {
+ if target.size == size &&
+ target.format == format &&
+ !target.is_active {
+ // Found a target that's not currently in use which matches. Update
+ // the last_frame_used for GC purposes.
+ target.is_active = true;
+ target.last_frame_used = self.current_frame_id;
+ return target.texture_id;
+ }
+ }
+
+ // Need to create a new render target and add it to the pool
+
+ let texture_id = self.texture_cache.alloc_render_target(
+ size,
+ format,
+ );
+
+ self.render_target_pool.push(RenderTarget {
+ size,
+ format,
+ texture_id,
+ is_active: true,
+ last_frame_used: self.current_frame_id,
+ });
+
+ texture_id
+ }
+
+ /// Return a render target to the pool.
+ pub fn return_render_target_to_pool(
+ &mut self,
+ id: CacheTextureId,
+ ) {
+ let target = self.render_target_pool
+ .iter_mut()
+ .find(|t| t.texture_id == id)
+ .expect("bug: invalid render target id");
+
+ assert!(target.is_active);
+ target.is_active = false;
+ }
+
+ /// Clear all current render targets (e.g. on memory pressure)
+ fn clear_render_target_pool(
+ &mut self,
+ ) {
+ for target in self.render_target_pool.drain(..) {
+ debug_assert!(!target.is_active);
+ self.texture_cache.free_render_target(target.texture_id);
+ }
+ }
+
+ /// Garbage collect and remove old render targets from the pool that haven't
+ /// been used for some time.
+ fn gc_render_targets(
+ &mut self,
+ total_bytes_threshold: usize,
+ total_bytes_red_line_threshold: usize,
+ frames_threshold: usize,
+ ) {
+ // Get the total GPU memory size used by the current render target pool
+ let mut rt_pool_size_in_bytes: usize = self.render_target_pool
+ .iter()
+ .map(|t| t.size_in_bytes())
+ .sum();
+
+ // If the total size of the pool is less than the threshold, don't bother
+ // trying to GC any targets
+ if rt_pool_size_in_bytes <= total_bytes_threshold {
+ return;
+ }
+
+ // Sort the current pool by age, so that we remove oldest textures first
+ self.render_target_pool.sort_by_key(|t| t.last_frame_used);
+
+ // We can't just use retain() because `RenderTarget` requires manual cleanup.
+ let mut retained_targets = SmallVec::<[RenderTarget; 8]>::new();
+
+ for target in self.render_target_pool.drain(..) {
+ assert!(!target.is_active);
+
+ // Drop oldest textures until we are under the allowed size threshold.
+ // However, if it's been used in very recently, it is always kept around,
+ // which ensures we don't thrash texture allocations on pages that do
+ // require a very large render target pool and are regularly changing.
+ let above_red_line = rt_pool_size_in_bytes > total_bytes_red_line_threshold;
+ let above_threshold = rt_pool_size_in_bytes > total_bytes_threshold;
+ let used_recently = target.used_recently(self.current_frame_id, frames_threshold);
+ let used_this_frame = target.last_frame_used == self.current_frame_id;
+
+ if !used_this_frame && (above_red_line || (above_threshold && !used_recently)) {
+ rt_pool_size_in_bytes -= target.size_in_bytes();
+ self.texture_cache.free_render_target(target.texture_id);
+ } else {
+ retained_targets.push(target);
+ }
+ }
+
+ self.render_target_pool.extend(retained_targets);
+ }
+
+ #[cfg(test)]
+ pub fn validate_surfaces(
+ &self,
+ expected_surfaces: &[(i32, i32, ImageFormat)],
+ ) {
+ assert_eq!(expected_surfaces.len(), self.render_target_pool.len());
+
+ for (expected, surface) in expected_surfaces.iter().zip(self.render_target_pool.iter()) {
+ assert_eq!(DeviceIntSize::new(expected.0, expected.1), surface.size);
+ assert_eq!(expected.2, surface.format);
+ }
+ }
+}
+
+impl Drop for ResourceCache {
+ fn drop(&mut self) {
+ self.clear_images(|_| true);
+ }
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainFontTemplate {
+ data: String,
+ index: u32,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PlainImageTemplate {
+ data: String,
+ descriptor: ImageDescriptor,
+ tiling: Option<TileSize>,
+ generation: ImageGeneration,
+}
+
+#[cfg(any(feature = "capture", feature = "replay"))]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PlainResources {
+ font_templates: FastHashMap<FontKey, PlainFontTemplate>,
+ font_instances: HashMap<FontInstanceKey, Arc<BaseFontInstance>>,
+ image_templates: FastHashMap<ImageKey, PlainImageTemplate>,
+}
+
+#[cfg(feature = "capture")]
+#[derive(Serialize)]
+pub struct PlainCacheRef<'a> {
+ current_frame_id: FrameId,
+ glyphs: &'a GlyphCache,
+ glyph_dimensions: &'a GlyphDimensionsCache,
+ images: &'a ImageCache,
+ render_tasks: &'a RenderTaskCache,
+ textures: &'a TextureCache,
+}
+
+#[cfg(feature = "replay")]
+#[derive(Deserialize)]
+pub struct PlainCacheOwn {
+ current_frame_id: FrameId,
+ glyphs: GlyphCache,
+ glyph_dimensions: GlyphDimensionsCache,
+ images: ImageCache,
+ render_tasks: RenderTaskCache,
+ textures: TextureCache,
+}
+
+#[cfg(feature = "replay")]
+const NATIVE_FONT: &'static [u8] = include_bytes!("../res/Proggy.ttf");
+
+// This currently only casts the unit but will soon apply an offset
+fn to_image_dirty_rect(blob_dirty_rect: &BlobDirtyRect) -> ImageDirtyRect {
+ match *blob_dirty_rect {
+ DirtyRect::Partial(rect) => DirtyRect::Partial(
+ DeviceIntRect {
+ origin: DeviceIntPoint::new(rect.origin.x, rect.origin.y),
+ size: DeviceIntSize::new(rect.size.width, rect.size.height),
+ }
+ ),
+ DirtyRect::All => DirtyRect::All,
+ }
+}
+
+impl ResourceCache {
+ #[cfg(feature = "capture")]
+ pub fn save_capture(
+ &mut self, root: &PathBuf
+ ) -> (PlainResources, Vec<ExternalCaptureImage>) {
+ use std::fs;
+ use std::io::Write;
+
+ info!("saving resource cache");
+ let res = &self.resources;
+ let path_fonts = root.join("fonts");
+ if !path_fonts.is_dir() {
+ fs::create_dir(&path_fonts).unwrap();
+ }
+ let path_images = root.join("images");
+ if !path_images.is_dir() {
+ fs::create_dir(&path_images).unwrap();
+ }
+ let path_blobs = root.join("blobs");
+ if !path_blobs.is_dir() {
+ fs::create_dir(&path_blobs).unwrap();
+ }
+ let path_externals = root.join("externals");
+ if !path_externals.is_dir() {
+ fs::create_dir(&path_externals).unwrap();
+ }
+
+ info!("\tfont templates");
+ let mut font_paths = FastHashMap::default();
+ for template in res.font_templates.values() {
+ let data: &[u8] = match *template {
+ FontTemplate::Raw(ref arc, _) => arc,
+ FontTemplate::Native(_) => continue,
+ };
+ let font_id = res.font_templates.len() + 1;
+ let entry = match font_paths.entry(data.as_ptr()) {
+ Entry::Occupied(_) => continue,
+ Entry::Vacant(e) => e,
+ };
+ let file_name = format!("{}.raw", font_id);
+ let short_path = format!("fonts/{}", file_name);
+ fs::File::create(path_fonts.join(file_name))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(data)
+ .unwrap();
+ entry.insert(short_path);
+ }
+
+ info!("\timage templates");
+ let mut image_paths = FastHashMap::default();
+ let mut other_paths = FastHashMap::default();
+ let mut num_blobs = 0;
+ let mut external_images = Vec::new();
+ for (&key, template) in res.image_templates.images.iter() {
+ let desc = &template.descriptor;
+ match template.data {
+ CachedImageData::Raw(ref arc) => {
+ let image_id = image_paths.len() + 1;
+ let entry = match image_paths.entry(arc.as_ptr()) {
+ Entry::Occupied(_) => continue,
+ Entry::Vacant(e) => e,
+ };
+
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(format!("images/{}.png", image_id)),
+ desc.size,
+ desc.format,
+ desc.stride,
+ &arc,
+ );
+ let file_name = format!("{}.raw", image_id);
+ let short_path = format!("images/{}", file_name);
+ fs::File::create(path_images.join(file_name))
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&*arc)
+ .unwrap();
+ entry.insert(short_path);
+ }
+ CachedImageData::Blob => {
+ warn!("Tiled blob images aren't supported yet");
+ let result = RasterizedBlobImage {
+ rasterized_rect: desc.size.into(),
+ data: Arc::new(vec![0; desc.compute_total_size() as usize])
+ };
+
+ assert_eq!(result.rasterized_rect.size, desc.size);
+ assert_eq!(result.data.len(), desc.compute_total_size() as usize);
+
+ num_blobs += 1;
+ #[cfg(feature = "png")]
+ CaptureConfig::save_png(
+ root.join(format!("blobs/{}.png", num_blobs)),
+ desc.size,
+ desc.format,
+ desc.stride,
+ &result.data,
+ );
+ let file_name = format!("{}.raw", num_blobs);
+ let short_path = format!("blobs/{}", file_name);
+ let full_path = path_blobs.clone().join(&file_name);
+ fs::File::create(full_path)
+ .expect(&format!("Unable to create {}", short_path))
+ .write_all(&result.data)
+ .unwrap();
+ other_paths.insert(key, short_path);
+ }
+ CachedImageData::External(ref ext) => {
+ let short_path = format!("externals/{}", external_images.len() + 1);
+ other_paths.insert(key, short_path.clone());
+ external_images.push(ExternalCaptureImage {
+ short_path,
+ descriptor: desc.clone(),
+ external: ext.clone(),
+ });
+ }
+ }
+ }
+
+ let resources = PlainResources {
+ font_templates: res.font_templates
+ .iter()
+ .map(|(key, template)| {
+ (*key, match *template {
+ FontTemplate::Raw(ref arc, index) => {
+ PlainFontTemplate {
+ data: font_paths[&arc.as_ptr()].clone(),
+ index,
+ }
+ }
+ #[cfg(not(target_os = "macos"))]
+ FontTemplate::Native(ref native) => {
+ PlainFontTemplate {
+ data: native.path.to_string_lossy().to_string(),
+ index: native.index,
+ }
+ }
+ #[cfg(target_os = "macos")]
+ FontTemplate::Native(ref native) => {
+ PlainFontTemplate {
+ data: native.0.postscript_name().to_string(),
+ index: 0,
+ }
+ }
+ })
+ })
+ .collect(),
+ font_instances: res.font_instances.clone_map(),
+ image_templates: res.image_templates.images
+ .iter()
+ .map(|(key, template)| {
+ (*key, PlainImageTemplate {
+ data: match template.data {
+ CachedImageData::Raw(ref arc) => image_paths[&arc.as_ptr()].clone(),
+ _ => other_paths[key].clone(),
+ },
+ descriptor: template.descriptor.clone(),
+ tiling: template.tiling,
+ generation: template.generation,
+ })
+ })
+ .collect(),
+ };
+
+ (resources, external_images)
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn save_caches(&self, _root: &PathBuf) -> PlainCacheRef {
+ PlainCacheRef {
+ current_frame_id: self.current_frame_id,
+ glyphs: &self.cached_glyphs,
+ glyph_dimensions: &self.cached_glyph_dimensions,
+ images: &self.cached_images,
+ render_tasks: &self.cached_render_tasks,
+ textures: &self.texture_cache,
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn load_capture(
+ &mut self,
+ resources: PlainResources,
+ caches: Option<PlainCacheOwn>,
+ config: &CaptureConfig,
+ ) -> Vec<PlainExternalImage> {
+ use std::{fs, path::Path};
+ use crate::texture_cache::TextureCacheConfig;
+
+ info!("loading resource cache");
+ //TODO: instead of filling the local path to Arc<data> map as we process
+ // each of the resource types, we could go through all of the local paths
+ // and fill out the map as the first step.
+ let mut raw_map = FastHashMap::<String, Arc<Vec<u8>>>::default();
+
+ self.clear(ClearCache::all());
+ self.clear_images(|_| true);
+
+ match caches {
+ Some(cached) => {
+ self.current_frame_id = cached.current_frame_id;
+ self.cached_glyphs = cached.glyphs;
+ self.cached_glyph_dimensions = cached.glyph_dimensions;
+ self.cached_images = cached.images;
+ self.cached_render_tasks = cached.render_tasks;
+ self.texture_cache = cached.textures;
+ }
+ None => {
+ self.current_frame_id = FrameId::INVALID;
+ self.texture_cache = TextureCache::new(
+ self.texture_cache.max_texture_size(),
+ self.texture_cache.default_picture_tile_size(),
+ self.texture_cache.color_formats(),
+ self.texture_cache.swizzle_settings(),
+ &TextureCacheConfig::DEFAULT,
+ );
+ }
+ }
+
+ self.glyph_rasterizer.reset();
+ let res = &mut self.resources;
+ res.font_templates.clear();
+ res.font_instances.set(resources.font_instances);
+ res.image_templates.images.clear();
+
+ info!("\tfont templates...");
+ let root = config.resource_root();
+ let native_font_replacement = Arc::new(NATIVE_FONT.to_vec());
+ for (key, plain_template) in resources.font_templates {
+ let arc = match raw_map.entry(plain_template.data) {
+ Entry::Occupied(e) => {
+ e.get().clone()
+ }
+ Entry::Vacant(e) => {
+ let file_path = if Path::new(e.key()).is_absolute() {
+ PathBuf::from(e.key())
+ } else {
+ root.join(e.key())
+ };
+ let arc = match fs::read(file_path) {
+ Ok(buffer) => Arc::new(buffer),
+ Err(err) => {
+ error!("Unable to open font template {:?}: {:?}", e.key(), err);
+ Arc::clone(&native_font_replacement)
+ }
+ };
+ e.insert(arc).clone()
+ }
+ };
+
+ let template = FontTemplate::Raw(arc, plain_template.index);
+ self.glyph_rasterizer.add_font(key, template.clone());
+ res.font_templates.insert(key, template);
+ }
+
+ info!("\timage templates...");
+ let mut external_images = Vec::new();
+ for (key, template) in resources.image_templates {
+ let data = match config.deserialize_for_resource::<PlainExternalImage, _>(&template.data) {
+ Some(plain) => {
+ let ext_data = plain.external;
+ external_images.push(plain);
+ CachedImageData::External(ext_data)
+ }
+ None => {
+ let arc = match raw_map.entry(template.data) {
+ Entry::Occupied(e) => {
+ e.get().clone()
+ }
+ Entry::Vacant(e) => {
+ let buffer = fs::read(root.join(e.key()))
+ .expect(&format!("Unable to open {}", e.key()));
+ e.insert(Arc::new(buffer))
+ .clone()
+ }
+ };
+ CachedImageData::Raw(arc)
+ }
+ };
+
+ res.image_templates.images.insert(key, ImageResource {
+ data,
+ descriptor: template.descriptor,
+ tiling: template.tiling,
+ visible_rect: template.descriptor.size.into(),
+ generation: template.generation,
+ });
+ }
+
+ external_images
+ }
+
+ #[cfg(feature = "capture")]
+ pub fn save_capture_sequence(&mut self, config: &mut CaptureConfig) -> Vec<ExternalCaptureImage> {
+ if self.capture_dirty {
+ self.capture_dirty = false;
+ config.prepare_resource();
+ let (resources, deferred) = self.save_capture(&config.resource_root());
+ config.serialize_for_resource(&resources, "plain-resources.ron");
+ deferred
+ } else {
+ Vec::new()
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/scene.rs b/gfx/wr/webrender/src/scene.rs
new file mode 100644
index 0000000000..679a7dc544
--- /dev/null
+++ b/gfx/wr/webrender/src/scene.rs
@@ -0,0 +1,347 @@
+/* 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 api::{BuiltDisplayList, DisplayListWithCache, ColorF, DynamicProperties, Epoch, FontRenderMode};
+use api::{PipelineId, PropertyBinding, PropertyBindingId, PropertyValue, MixBlendMode, StackingContext};
+use api::units::*;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::render_api::MemoryReport;
+use crate::composite::CompositorKind;
+use crate::clip::ClipStore;
+use crate::spatial_tree::SpatialTree;
+use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
+use crate::hit_test::{HitTester, HitTestingScene, HitTestingSceneStats};
+use crate::internal_types::FastHashMap;
+use crate::prim_store::{PrimitiveStore, PrimitiveStoreStats, PictureIndex};
+use crate::tile_cache::TileCacheConfig;
+use std::sync::Arc;
+
+/// Stores a map of the animated property bindings for the current display list. These
+/// can be used to animate the transform and/or opacity of a display list without
+/// re-submitting the display list itself.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SceneProperties {
+ transform_properties: FastHashMap<PropertyBindingId, LayoutTransform>,
+ float_properties: FastHashMap<PropertyBindingId, f32>,
+ color_properties: FastHashMap<PropertyBindingId, ColorF>,
+ current_properties: DynamicProperties,
+ pending_properties: Option<DynamicProperties>,
+}
+
+impl SceneProperties {
+ pub fn new() -> Self {
+ SceneProperties {
+ transform_properties: FastHashMap::default(),
+ float_properties: FastHashMap::default(),
+ color_properties: FastHashMap::default(),
+ current_properties: DynamicProperties::default(),
+ pending_properties: None,
+ }
+ }
+
+ /// Set the current property list for this display list.
+ pub fn set_properties(&mut self, properties: DynamicProperties) {
+ self.pending_properties = Some(properties);
+ }
+
+ /// Add to the current property list for this display list.
+ pub fn add_transforms(&mut self, transforms: Vec<PropertyValue<LayoutTransform>>) {
+ let mut pending_properties = self.pending_properties
+ .take()
+ .unwrap_or_default();
+
+ pending_properties.transforms.extend(transforms);
+
+ self.pending_properties = Some(pending_properties);
+ }
+
+ /// Flush any pending updates to the scene properties. Returns
+ /// true if the properties have changed since the last flush
+ /// was called. This code allows properties to be changed by
+ /// multiple set_properties and add_properties calls during a
+ /// single transaction, and still correctly determine if any
+ /// properties have changed. This can have significant power
+ /// saving implications, allowing a frame build to be skipped
+ /// if the properties haven't changed in many cases.
+ pub fn flush_pending_updates(&mut self) -> bool {
+ let mut properties_changed = false;
+
+ if let Some(ref pending_properties) = self.pending_properties {
+ if *pending_properties != self.current_properties {
+ self.transform_properties.clear();
+ self.float_properties.clear();
+ self.color_properties.clear();
+
+ for property in &pending_properties.transforms {
+ self.transform_properties
+ .insert(property.key.id, property.value);
+ }
+
+ for property in &pending_properties.floats {
+ self.float_properties
+ .insert(property.key.id, property.value);
+ }
+
+ for property in &pending_properties.colors {
+ self.color_properties
+ .insert(property.key.id, property.value);
+ }
+
+ self.current_properties = pending_properties.clone();
+ properties_changed = true;
+ }
+ }
+
+ properties_changed
+ }
+
+ /// Get the current value for a transform property.
+ pub fn resolve_layout_transform(
+ &self,
+ property: &PropertyBinding<LayoutTransform>,
+ ) -> LayoutTransform {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.transform_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ /// Get the current value for a float property.
+ pub fn resolve_float(
+ &self,
+ property: &PropertyBinding<f32>
+ ) -> f32 {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.float_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ pub fn float_properties(&self) -> &FastHashMap<PropertyBindingId, f32> {
+ &self.float_properties
+ }
+
+ /// Get the current value for a color property.
+ pub fn resolve_color(
+ &self,
+ property: &PropertyBinding<ColorF>
+ ) -> ColorF {
+ match *property {
+ PropertyBinding::Value(value) => value,
+ PropertyBinding::Binding(ref key, v) => {
+ self.color_properties
+ .get(&key.id)
+ .cloned()
+ .unwrap_or(v)
+ }
+ }
+ }
+
+ pub fn color_properties(&self) -> &FastHashMap<PropertyBindingId, ColorF> {
+ &self.color_properties
+ }
+
+}
+
+/// A representation of the layout within the display port for a given document or iframe.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub struct ScenePipeline {
+ pub pipeline_id: PipelineId,
+ pub viewport_size: LayoutSize,
+ pub background_color: Option<ColorF>,
+ pub display_list: DisplayListWithCache,
+}
+
+/// A complete representation of the layout bundling visible pipelines together.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone)]
+pub struct Scene {
+ pub root_pipeline_id: Option<PipelineId>,
+ pub pipelines: FastHashMap<PipelineId, ScenePipeline>,
+ pub pipeline_epochs: FastHashMap<PipelineId, Epoch>,
+}
+
+impl Scene {
+ pub fn new() -> Self {
+ Scene {
+ root_pipeline_id: None,
+ pipelines: FastHashMap::default(),
+ pipeline_epochs: FastHashMap::default(),
+ }
+ }
+
+ pub fn set_root_pipeline_id(&mut self, pipeline_id: PipelineId) {
+ self.root_pipeline_id = Some(pipeline_id);
+ }
+
+ pub fn set_display_list(
+ &mut self,
+ pipeline_id: PipelineId,
+ epoch: Epoch,
+ display_list: BuiltDisplayList,
+ background_color: Option<ColorF>,
+ viewport_size: LayoutSize,
+ ) {
+ // Adds a cache to the given display list. If this pipeline already had
+ // a display list before, that display list is updated and used instead.
+ let display_list = match self.pipelines.remove(&pipeline_id) {
+ Some(mut pipeline) => {
+ pipeline.display_list.update(display_list);
+ pipeline.display_list
+ }
+ None => DisplayListWithCache::new_from_list(display_list)
+ };
+
+ let new_pipeline = ScenePipeline {
+ pipeline_id,
+ viewport_size,
+ background_color,
+ display_list,
+ };
+
+ self.pipelines.insert(pipeline_id, new_pipeline);
+ self.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+
+ pub fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
+ if self.root_pipeline_id == Some(pipeline_id) {
+ self.root_pipeline_id = None;
+ }
+ self.pipelines.remove(&pipeline_id);
+ self.pipeline_epochs.remove(&pipeline_id);
+ }
+
+ pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
+ self.pipeline_epochs.insert(pipeline_id, epoch);
+ }
+
+ pub fn has_root_pipeline(&self) -> bool {
+ if let Some(ref root_id) = self.root_pipeline_id {
+ return self.pipelines.contains_key(root_id);
+ }
+
+ false
+ }
+
+ pub fn report_memory(
+ &self,
+ ops: &mut MallocSizeOfOps,
+ report: &mut MemoryReport
+ ) {
+ for (_, pipeline) in &self.pipelines {
+ report.display_list += pipeline.display_list.size_of(ops)
+ }
+ }
+}
+
+pub trait StackingContextHelpers {
+ fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode>;
+}
+
+impl StackingContextHelpers for StackingContext {
+ fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode> {
+ match self.mix_blend_mode {
+ MixBlendMode::Normal => None,
+ _ => Some(self.mix_blend_mode),
+ }
+ }
+}
+
+
+/// WebRender's internal representation of the scene.
+pub struct BuiltScene {
+ pub has_root_pipeline: bool,
+ pub pipeline_epochs: FastHashMap<PipelineId, Epoch>,
+ pub output_rect: DeviceIntRect,
+ pub background_color: Option<ColorF>,
+ pub prim_store: PrimitiveStore,
+ pub clip_store: ClipStore,
+ pub config: FrameBuilderConfig,
+ pub spatial_tree: SpatialTree,
+ pub hit_testing_scene: Arc<HitTestingScene>,
+ pub tile_cache_config: TileCacheConfig,
+ pub tile_cache_pictures: Vec<PictureIndex>,
+}
+
+impl BuiltScene {
+ pub fn empty() -> Self {
+ BuiltScene {
+ has_root_pipeline: false,
+ pipeline_epochs: FastHashMap::default(),
+ output_rect: DeviceIntRect::zero(),
+ background_color: None,
+ prim_store: PrimitiveStore::new(&PrimitiveStoreStats::empty()),
+ clip_store: ClipStore::new(),
+ spatial_tree: SpatialTree::new(),
+ hit_testing_scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
+ tile_cache_config: TileCacheConfig::new(0),
+ tile_cache_pictures: Vec::new(),
+ config: FrameBuilderConfig {
+ default_font_render_mode: FontRenderMode::Mono,
+ dual_source_blending_is_enabled: true,
+ dual_source_blending_is_supported: false,
+ chase_primitive: ChasePrimitive::Nothing,
+ testing: false,
+ gpu_supports_fast_clears: false,
+ gpu_supports_advanced_blend: false,
+ advanced_blend_is_coherent: false,
+ gpu_supports_render_target_partial_update: true,
+ batch_lookback_count: 0,
+ background_color: None,
+ compositor_kind: CompositorKind::default(),
+ tile_size_override: None,
+ max_depth_ids: 0,
+ max_target_size: 0,
+ force_invalidation: false,
+ },
+ }
+ }
+
+ /// Get the memory usage statistics to pre-allocate for the next scene.
+ pub fn get_stats(&self) -> SceneStats {
+ SceneStats {
+ prim_store_stats: self.prim_store.get_stats(),
+ hit_test_stats: self.hit_testing_scene.get_stats(),
+ }
+ }
+
+ pub fn create_hit_tester(&mut self) -> HitTester {
+ HitTester::new(
+ Arc::clone(&self.hit_testing_scene),
+ &self.spatial_tree,
+ )
+ }
+}
+
+/// Stores the allocation sizes of various arrays in the built
+/// scene. This is retrieved from the current frame builder
+/// and used to reserve an approximately correct capacity of
+/// the arrays for the next scene that is getting built.
+pub struct SceneStats {
+ pub prim_store_stats: PrimitiveStoreStats,
+ pub hit_test_stats: HitTestingSceneStats,
+}
+
+impl SceneStats {
+ pub fn empty() -> Self {
+ SceneStats {
+ prim_store_stats: PrimitiveStoreStats::empty(),
+ hit_test_stats: HitTestingSceneStats::empty(),
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/scene_builder_thread.rs b/gfx/wr/webrender/src/scene_builder_thread.rs
new file mode 100644
index 0000000000..5550a48690
--- /dev/null
+++ b/gfx/wr/webrender/src/scene_builder_thread.rs
@@ -0,0 +1,840 @@
+/* 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 api::{AsyncBlobImageRasterizer, BlobImageResult};
+use api::{DocumentId, PipelineId, ExternalEvent, BlobImageRequest};
+use api::{NotificationRequest, Checkpoint, IdNamespace, QualitySettings};
+use api::{PrimitiveKeyKind, SharedFontInstanceMap};
+use api::{GlyphDimensionRequest, GlyphIndexRequest};
+use api::channel::{unbounded_channel, single_msg_channel, Receiver, Sender};
+use api::units::*;
+use crate::render_api::{ApiMsg, FrameMsg, SceneMsg, ResourceUpdate, TransactionMsg, MemoryReport};
+#[cfg(feature = "capture")]
+use crate::capture::CaptureConfig;
+use crate::frame_builder::FrameBuilderConfig;
+use crate::scene_building::SceneBuilder;
+use crate::clip::ClipIntern;
+use crate::filterdata::FilterDataIntern;
+use crate::intern::{Internable, Interner, UpdateList};
+use crate::internal_types::{FastHashMap, FastHashSet};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::prim_store::backdrop::Backdrop;
+use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
+use crate::prim_store::image::{Image, YuvImage};
+use crate::prim_store::line_dec::LineDecoration;
+use crate::prim_store::picture::Picture;
+use crate::prim_store::text_run::TextRun;
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::SceneView;
+use crate::renderer::{PipelineInfo, SceneBuilderHooks};
+use crate::scene::{Scene, BuiltScene, SceneStats};
+use std::iter;
+use std::mem::replace;
+use time::precise_time_ns;
+use crate::util::drain_filter;
+use std::thread;
+use std::time::Duration;
+
+#[cfg(feature = "debugger")]
+use crate::debug_server;
+#[cfg(feature = "debugger")]
+use api::{BuiltDisplayListIter, DisplayItem};
+
+fn rasterize_blobs(txn: &mut TransactionMsg, is_low_priority: bool) {
+ profile_scope!("rasterize_blobs");
+
+ if let Some(ref mut rasterizer) = txn.blob_rasterizer {
+ let mut rasterized_blobs = rasterizer.rasterize(&txn.blob_requests, is_low_priority);
+ // try using the existing allocation if our current list is empty
+ if txn.rasterized_blobs.is_empty() {
+ txn.rasterized_blobs = rasterized_blobs;
+ } else {
+ txn.rasterized_blobs.append(&mut rasterized_blobs);
+ }
+ }
+}
+
+/// Represent the remaining work associated to a transaction after the scene building
+/// phase as well as the result of scene building itself if applicable.
+pub struct BuiltTransaction {
+ pub document_id: DocumentId,
+ pub built_scene: Option<BuiltScene>,
+ pub view: SceneView,
+ pub resource_updates: Vec<ResourceUpdate>,
+ pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+ pub blob_rasterizer: Option<Box<dyn AsyncBlobImageRasterizer>>,
+ pub frame_ops: Vec<FrameMsg>,
+ pub removed_pipelines: Vec<(PipelineId, DocumentId)>,
+ pub notifications: Vec<NotificationRequest>,
+ pub interner_updates: Option<InternerUpdates>,
+ pub render_frame: bool,
+ pub invalidate_rendered_frame: bool,
+ pub discard_frame_state_for_pipelines: Vec<PipelineId>,
+ pub profile: TransactionProfile,
+}
+
+#[cfg(feature = "replay")]
+pub struct LoadScene {
+ pub document_id: DocumentId,
+ pub scene: Scene,
+ pub font_instances: SharedFontInstanceMap,
+ pub view: SceneView,
+ pub config: FrameBuilderConfig,
+ pub build_frame: bool,
+ pub interners: Interners,
+}
+
+/// Message to the scene builder thread.
+pub enum SceneBuilderRequest {
+ Transactions(Vec<Box<TransactionMsg>>),
+ AddDocument(DocumentId, DeviceIntSize),
+ DeleteDocument(DocumentId),
+ GetGlyphDimensions(GlyphDimensionRequest),
+ GetGlyphIndices(GlyphIndexRequest),
+ ClearNamespace(IdNamespace),
+ SimulateLongSceneBuild(u32),
+ SimulateLongLowPrioritySceneBuild(u32),
+ ExternalEvent(ExternalEvent),
+ WakeUp,
+ ShutDown(Option<Sender<()>>),
+ Flush(Sender<()>),
+ SetFrameBuilderConfig(FrameBuilderConfig),
+ ReportMemory(Box<MemoryReport>, Sender<Box<MemoryReport>>),
+ #[cfg(feature = "capture")]
+ SaveScene(CaptureConfig),
+ #[cfg(feature = "replay")]
+ LoadScenes(Vec<LoadScene>),
+ #[cfg(feature = "capture")]
+ StartCaptureSequence(CaptureConfig),
+ #[cfg(feature = "capture")]
+ StopCaptureSequence,
+ DocumentsForDebugger
+}
+
+// Message from scene builder to render backend.
+pub enum SceneBuilderResult {
+ Transactions(Vec<Box<BuiltTransaction>>, Option<Sender<SceneSwapResult>>),
+ #[cfg(feature = "capture")]
+ CapturedTransactions(Vec<Box<BuiltTransaction>>, CaptureConfig, Option<Sender<SceneSwapResult>>),
+ ExternalEvent(ExternalEvent),
+ FlushComplete(Sender<()>),
+ DeleteDocument(DocumentId),
+ ClearNamespace(IdNamespace),
+ GetGlyphDimensions(GlyphDimensionRequest),
+ GetGlyphIndices(GlyphIndexRequest),
+ ShutDown(Option<Sender<()>>),
+ DocumentsForDebugger(String)
+}
+
+// Message from render backend to scene builder to indicate the
+// scene swap was completed. We need a separate channel for this
+// so that they don't get mixed with SceneBuilderRequest messages.
+pub enum SceneSwapResult {
+ Complete(Sender<()>),
+ Aborted,
+}
+
+macro_rules! declare_interners {
+ ( $( $name:ident : $ty:ident, )+ ) => {
+ /// This struct contains all items that can be shared between
+ /// display lists. We want to intern and share the same clips,
+ /// primitives and other things between display lists so that:
+ /// - GPU cache handles remain valid, reducing GPU cache updates.
+ /// - Comparison of primitives and pictures between two
+ /// display lists is (a) fast (b) done during scene building.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(Default)]
+ pub struct Interners {
+ $(
+ pub $name: Interner<$ty>,
+ )+
+ }
+
+ $(
+ impl AsMut<Interner<$ty>> for Interners {
+ fn as_mut(&mut self) -> &mut Interner<$ty> {
+ &mut self.$name
+ }
+ }
+ )+
+
+ pub struct InternerUpdates {
+ $(
+ pub $name: UpdateList<<$ty as Internable>::Key>,
+ )+
+ }
+
+ impl Interners {
+ /// Reports CPU heap memory used by the interners.
+ fn report_memory(
+ &self,
+ ops: &mut MallocSizeOfOps,
+ r: &mut MemoryReport,
+ ) {
+ $(
+ r.interning.interners.$name += self.$name.size_of(ops);
+ )+
+ }
+
+ fn end_frame_and_get_pending_updates(&mut self) -> InternerUpdates {
+ InternerUpdates {
+ $(
+ $name: self.$name.end_frame_and_get_pending_updates(),
+ )+
+ }
+ }
+ }
+ }
+}
+
+crate::enumerate_interners!(declare_interners);
+
+// A document in the scene builder contains the current scene,
+// as well as a persistent clip interner. This allows clips
+// to be de-duplicated, and persisted in the GPU cache between
+// display lists.
+struct Document {
+ scene: Scene,
+ interners: Interners,
+ stats: SceneStats,
+ view: SceneView,
+}
+
+impl Document {
+ fn new(device_rect: DeviceIntRect, device_pixel_ratio: f32) -> Self {
+ Document {
+ scene: Scene::new(),
+ interners: Interners::default(),
+ stats: SceneStats::empty(),
+ view: SceneView {
+ device_rect,
+ device_pixel_ratio,
+ page_zoom_factor: 1.0,
+ quality_settings: QualitySettings::default(),
+ },
+ }
+ }
+}
+
+pub struct SceneBuilderThread {
+ documents: FastHashMap<DocumentId, Document>,
+ rx: Receiver<SceneBuilderRequest>,
+ tx: Sender<ApiMsg>,
+ config: FrameBuilderConfig,
+ default_device_pixel_ratio: f32,
+ font_instances: SharedFontInstanceMap,
+ size_of_ops: Option<MallocSizeOfOps>,
+ hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ simulate_slow_ms: u32,
+ removed_pipelines: FastHashSet<PipelineId>,
+ #[cfg(feature = "capture")]
+ capture_config: Option<CaptureConfig>,
+}
+
+pub struct SceneBuilderThreadChannels {
+ rx: Receiver<SceneBuilderRequest>,
+ tx: Sender<ApiMsg>,
+}
+
+impl SceneBuilderThreadChannels {
+ pub fn new(
+ tx: Sender<ApiMsg>
+ ) -> (Self, Sender<SceneBuilderRequest>) {
+ let (in_tx, in_rx) = unbounded_channel();
+ (
+ Self {
+ rx: in_rx,
+ tx,
+ },
+ in_tx,
+ )
+ }
+}
+
+impl SceneBuilderThread {
+ pub fn new(
+ config: FrameBuilderConfig,
+ default_device_pixel_ratio: f32,
+ font_instances: SharedFontInstanceMap,
+ size_of_ops: Option<MallocSizeOfOps>,
+ hooks: Option<Box<dyn SceneBuilderHooks + Send>>,
+ channels: SceneBuilderThreadChannels,
+ ) -> Self {
+ let SceneBuilderThreadChannels { rx, tx } = channels;
+
+ Self {
+ documents: Default::default(),
+ rx,
+ tx,
+ config,
+ default_device_pixel_ratio,
+ font_instances,
+ size_of_ops,
+ hooks,
+ simulate_slow_ms: 0,
+ removed_pipelines: FastHashSet::default(),
+ #[cfg(feature = "capture")]
+ capture_config: None,
+ }
+ }
+
+ /// Send a message to the render backend thread.
+ ///
+ /// We first put something in the result queue and then send a wake-up
+ /// message to the api queue that the render backend is blocking on.
+ pub fn send(&self, msg: SceneBuilderResult) {
+ self.tx.send(ApiMsg::SceneBuilderResult(msg)).unwrap();
+ }
+
+ /// The scene builder thread's event loop.
+ pub fn run(&mut self) {
+ if let Some(ref hooks) = self.hooks {
+ hooks.register();
+ }
+
+ loop {
+ tracy_begin_frame!("scene_builder_thread");
+
+ match self.rx.recv() {
+ Ok(SceneBuilderRequest::WakeUp) => {}
+ Ok(SceneBuilderRequest::Flush(tx)) => {
+ self.send(SceneBuilderResult::FlushComplete(tx));
+ }
+ Ok(SceneBuilderRequest::Transactions(mut txns)) => {
+ let built_txns : Vec<Box<BuiltTransaction>> = txns.iter_mut()
+ .map(|txn| self.process_transaction(txn))
+ .collect();
+ #[cfg(feature = "capture")]
+ match built_txns.iter().any(|txn| txn.built_scene.is_some()) {
+ true => self.save_capture_sequence(),
+ _ => {},
+ }
+ self.forward_built_transactions(built_txns);
+ }
+ Ok(SceneBuilderRequest::AddDocument(document_id, initial_size)) => {
+ let old = self.documents.insert(document_id, Document::new(
+ initial_size.into(),
+ self.default_device_pixel_ratio,
+ ));
+ debug_assert!(old.is_none());
+ }
+ Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
+ self.documents.remove(&document_id);
+ self.send(SceneBuilderResult::DeleteDocument(document_id));
+ }
+ Ok(SceneBuilderRequest::ClearNamespace(id)) => {
+ self.documents.retain(|doc_id, _doc| doc_id.namespace_id != id);
+ self.send(SceneBuilderResult::ClearNamespace(id));
+ }
+ Ok(SceneBuilderRequest::ExternalEvent(evt)) => {
+ self.send(SceneBuilderResult::ExternalEvent(evt));
+ }
+ Ok(SceneBuilderRequest::GetGlyphDimensions(request)) => {
+ self.send(SceneBuilderResult::GetGlyphDimensions(request));
+ }
+ Ok(SceneBuilderRequest::GetGlyphIndices(request)) => {
+ self.send(SceneBuilderResult::GetGlyphIndices(request));
+ }
+ Ok(SceneBuilderRequest::ShutDown(sync)) => {
+ self.send(SceneBuilderResult::ShutDown(sync));
+ break;
+ }
+ Ok(SceneBuilderRequest::SimulateLongSceneBuild(time_ms)) => {
+ self.simulate_slow_ms = time_ms
+ }
+ Ok(SceneBuilderRequest::SimulateLongLowPrioritySceneBuild(_)) => {}
+ Ok(SceneBuilderRequest::ReportMemory(mut report, tx)) => {
+ (*report) += self.report_memory();
+ tx.send(report).unwrap();
+ }
+ Ok(SceneBuilderRequest::SetFrameBuilderConfig(cfg)) => {
+ self.config = cfg;
+ }
+ #[cfg(feature = "replay")]
+ Ok(SceneBuilderRequest::LoadScenes(msg)) => {
+ self.load_scenes(msg);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::SaveScene(config)) => {
+ self.save_scene(config);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::StartCaptureSequence(config)) => {
+ self.start_capture_sequence(config);
+ }
+ #[cfg(feature = "capture")]
+ Ok(SceneBuilderRequest::StopCaptureSequence) => {
+ // FIXME(aosmond): clear config for frames and resource cache without scene
+ // rebuild?
+ self.capture_config = None;
+ }
+ Ok(SceneBuilderRequest::DocumentsForDebugger) => {
+ let json = self.get_docs_for_debugger();
+ self.send(SceneBuilderResult::DocumentsForDebugger(json));
+ }
+ Err(_) => {
+ break;
+ }
+ }
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.poke();
+ }
+
+ tracy_end_frame!("scene_builder_thread");
+ }
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.deregister();
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_scene(&mut self, config: CaptureConfig) {
+ for (id, doc) in &self.documents {
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.interners, interners_name);
+
+ use crate::render_api::CaptureBits;
+ if config.bits.contains(CaptureBits::SCENE) {
+ let file_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.scene, file_name);
+ }
+ }
+ }
+
+ #[cfg(feature = "replay")]
+ fn load_scenes(&mut self, scenes: Vec<LoadScene>) {
+ for mut item in scenes {
+ self.config = item.config;
+
+ let mut built_scene = None;
+ let mut interner_updates = None;
+
+ if item.scene.has_root_pipeline() {
+ built_scene = Some(SceneBuilder::build(
+ &item.scene,
+ item.font_instances,
+ &item.view,
+ &self.config,
+ &mut item.interners,
+ &SceneStats::empty(),
+ ));
+
+ interner_updates = Some(
+ item.interners.end_frame_and_get_pending_updates()
+ );
+ }
+
+ self.documents.insert(
+ item.document_id,
+ Document {
+ scene: item.scene,
+ interners: item.interners,
+ stats: SceneStats::empty(),
+ view: item.view.clone(),
+ },
+ );
+
+ let txns = vec![Box::new(BuiltTransaction {
+ document_id: item.document_id,
+ render_frame: item.build_frame,
+ invalidate_rendered_frame: false,
+ built_scene,
+ view: item.view,
+ resource_updates: Vec::new(),
+ rasterized_blobs: Vec::new(),
+ blob_rasterizer: None,
+ frame_ops: Vec::new(),
+ removed_pipelines: Vec::new(),
+ discard_frame_state_for_pipelines: Vec::new(),
+ notifications: Vec::new(),
+ interner_updates,
+ profile: TransactionProfile::new(),
+ })];
+
+ self.forward_built_transactions(txns);
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn save_capture_sequence(
+ &mut self,
+ ) {
+ if let Some(ref mut config) = self.capture_config {
+ config.prepare_scene();
+ for (id, doc) in &self.documents {
+ let interners_name = format!("interners-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.interners, interners_name);
+
+ use crate::render_api::CaptureBits;
+ if config.bits.contains(CaptureBits::SCENE) {
+ let file_name = format!("scene-{}-{}", id.namespace_id.0, id.id);
+ config.serialize_for_scene(&doc.scene, file_name);
+ }
+ }
+ }
+ }
+
+ #[cfg(feature = "capture")]
+ fn start_capture_sequence(
+ &mut self,
+ config: CaptureConfig,
+ ) {
+ self.capture_config = Some(config);
+ self.save_capture_sequence();
+ }
+
+ #[cfg(feature = "debugger")]
+ fn traverse_items<'a>(
+ &self,
+ traversal: &mut BuiltDisplayListIter<'a>,
+ node: &mut debug_server::TreeNode,
+ ) {
+ loop {
+ let subtraversal = {
+ let item = match traversal.next() {
+ Some(item) => item,
+ None => break,
+ };
+
+ match *item.item() {
+ display_item @ DisplayItem::PushStackingContext(..) => {
+ let mut subtraversal = item.sub_iter();
+ let mut child_node =
+ debug_server::TreeNode::new(&display_item.debug_name().to_string());
+ self.traverse_items(&mut subtraversal, &mut child_node);
+ node.add_child(child_node);
+ Some(subtraversal)
+ }
+ DisplayItem::PopStackingContext => {
+ return;
+ }
+ display_item => {
+ node.add_item(&display_item.debug_name().to_string());
+ None
+ }
+ }
+ };
+
+ // If flatten_item created a sub-traversal, we need `traversal` to have the
+ // same state as the completed subtraversal, so we reinitialize it here.
+ if let Some(subtraversal) = subtraversal {
+ *traversal = subtraversal;
+ }
+ }
+ }
+
+ #[cfg(not(feature = "debugger"))]
+ fn get_docs_for_debugger(&self) -> String {
+ String::new()
+ }
+
+ #[cfg(feature = "debugger")]
+ fn get_docs_for_debugger(&self) -> String {
+ let mut docs = debug_server::DocumentList::new();
+
+ for (_, doc) in &self.documents {
+ let mut debug_doc = debug_server::TreeNode::new("document");
+
+ for (_, pipeline) in &doc.scene.pipelines {
+ let mut debug_dl = debug_server::TreeNode::new("display-list");
+ self.traverse_items(&mut pipeline.display_list.iter(), &mut debug_dl);
+ debug_doc.add_child(debug_dl);
+ }
+
+ docs.add(debug_doc);
+ }
+
+ serde_json::to_string(&docs).unwrap()
+ }
+
+ /// Do the bulk of the work of the scene builder thread.
+ fn process_transaction(&mut self, txn: &mut TransactionMsg) -> Box<BuiltTransaction> {
+ profile_scope!("process_transaction");
+
+ if let Some(ref hooks) = self.hooks {
+ hooks.pre_scene_build();
+ }
+
+ let doc = self.documents.get_mut(&txn.document_id).unwrap();
+ let scene = &mut doc.scene;
+
+ let mut profile = txn.profile.take();
+
+ profile.start_time(profiler::SCENE_BUILD_TIME);
+
+ let mut discard_frame_state_for_pipelines = Vec::new();
+ let mut removed_pipelines = Vec::new();
+ let mut rebuild_scene = false;
+ for message in txn.scene_ops.drain(..) {
+ match message {
+ SceneMsg::UpdateEpoch(pipeline_id, epoch) => {
+ scene.update_epoch(pipeline_id, epoch);
+ }
+ SceneMsg::SetPageZoom(factor) => {
+ doc.view.page_zoom_factor = factor.get();
+ }
+ SceneMsg::SetQualitySettings { settings } => {
+ doc.view.quality_settings = settings;
+ }
+ SceneMsg::SetDocumentView { device_rect, device_pixel_ratio } => {
+ doc.view.device_rect = device_rect;
+ doc.view.device_pixel_ratio = device_pixel_ratio;
+ }
+ SceneMsg::SetDisplayList {
+ epoch,
+ pipeline_id,
+ background,
+ viewport_size,
+ display_list,
+ preserve_frame_state,
+ } => {
+ let (builder_start_time_ns, builder_end_time_ns, send_time_ns) =
+ display_list.times();
+
+ let content_send_time = profiler::ns_to_ms(precise_time_ns() - send_time_ns);
+ let dl_build_time = profiler::ns_to_ms(builder_end_time_ns - builder_start_time_ns);
+ profile.set(profiler::CONTENT_SEND_TIME, content_send_time);
+ profile.set(profiler::DISPLAY_LIST_BUILD_TIME, dl_build_time);
+ profile.set(profiler::DISPLAY_LIST_MEM, profiler::bytes_to_mb(display_list.data().len()));
+
+ if self.removed_pipelines.contains(&pipeline_id) {
+ continue;
+ }
+
+ // Note: We could further reduce the amount of unnecessary scene
+ // building by keeping track of which pipelines are used by the
+ // scene (bug 1490751).
+ rebuild_scene = true;
+
+ scene.set_display_list(
+ pipeline_id,
+ epoch,
+ display_list,
+ background,
+ viewport_size,
+ );
+
+ if !preserve_frame_state {
+ discard_frame_state_for_pipelines.push(pipeline_id);
+ }
+ }
+ SceneMsg::SetRootPipeline(pipeline_id) => {
+ if scene.root_pipeline_id != Some(pipeline_id) {
+ rebuild_scene = true;
+ scene.set_root_pipeline_id(pipeline_id);
+ }
+ }
+ SceneMsg::RemovePipeline(pipeline_id) => {
+ scene.remove_pipeline(pipeline_id);
+ self.removed_pipelines.insert(pipeline_id);
+ removed_pipelines.push((pipeline_id, txn.document_id));
+ }
+ }
+ }
+
+ self.removed_pipelines.clear();
+
+ let mut built_scene = None;
+ let mut interner_updates = None;
+ if scene.has_root_pipeline() && rebuild_scene {
+
+ let built = SceneBuilder::build(
+ &scene,
+ self.font_instances.clone(),
+ &doc.view,
+ &self.config,
+ &mut doc.interners,
+ &doc.stats,
+ );
+
+ // Update the allocation stats for next scene
+ doc.stats = built.get_stats();
+
+ // Retrieve the list of updates from the clip interner.
+ interner_updates = Some(
+ doc.interners.end_frame_and_get_pending_updates()
+ );
+
+ built_scene = Some(built);
+ }
+
+ profile.end_time(profiler::SCENE_BUILD_TIME);
+
+
+ if !txn.blob_requests.is_empty() {
+ profile.start_time(profiler::BLOB_RASTERIZATION_TIME);
+
+ let is_low_priority = false;
+ rasterize_blobs(txn, is_low_priority);
+
+ profile.end_time(profiler::BLOB_RASTERIZATION_TIME);
+ }
+
+ drain_filter(
+ &mut txn.notifications,
+ |n| { n.when() == Checkpoint::SceneBuilt },
+ |n| { n.notify(); },
+ );
+
+ if self.simulate_slow_ms > 0 {
+ thread::sleep(Duration::from_millis(self.simulate_slow_ms as u64));
+ }
+
+ Box::new(BuiltTransaction {
+ document_id: txn.document_id,
+ render_frame: txn.generate_frame.as_bool(),
+ invalidate_rendered_frame: txn.invalidate_rendered_frame,
+ built_scene,
+ view: doc.view,
+ rasterized_blobs: replace(&mut txn.rasterized_blobs, Vec::new()),
+ resource_updates: replace(&mut txn.resource_updates, Vec::new()),
+ blob_rasterizer: replace(&mut txn.blob_rasterizer, None),
+ frame_ops: replace(&mut txn.frame_ops, Vec::new()),
+ removed_pipelines,
+ discard_frame_state_for_pipelines,
+ notifications: replace(&mut txn.notifications, Vec::new()),
+ interner_updates,
+ profile,
+ })
+ }
+
+ /// Send the results of process_transaction back to the render backend.
+ fn forward_built_transactions(&mut self, txns: Vec<Box<BuiltTransaction>>) {
+ let (pipeline_info, result_tx, result_rx) = match self.hooks {
+ Some(ref hooks) => {
+ if txns.iter().any(|txn| txn.built_scene.is_some()) {
+ let info = PipelineInfo {
+ epochs: txns.iter()
+ .filter(|txn| txn.built_scene.is_some())
+ .map(|txn| {
+ txn.built_scene.as_ref().unwrap()
+ .pipeline_epochs.iter()
+ .zip(iter::repeat(txn.document_id))
+ .map(|((&pipeline_id, &epoch), document_id)| ((pipeline_id, document_id), epoch))
+ }).flatten().collect(),
+ removed_pipelines: txns.iter()
+ .map(|txn| txn.removed_pipelines.clone())
+ .flatten().collect(),
+ };
+
+ let (tx, rx) = single_msg_channel();
+ let txn = txns.iter().find(|txn| txn.built_scene.is_some()).unwrap();
+ hooks.pre_scene_swap((txn.profile.get(profiler::SCENE_BUILD_TIME).unwrap() * 1000000.0) as u64);
+
+ (Some(info), Some(tx), Some(rx))
+ } else {
+ (None, None, None)
+ }
+ }
+ _ => (None, None, None)
+ };
+
+ let scene_swap_start_time = precise_time_ns();
+ let document_ids = txns.iter().map(|txn| txn.document_id).collect();
+ let have_resources_updates : Vec<DocumentId> = if pipeline_info.is_none() {
+ txns.iter()
+ .filter(|txn| !txn.resource_updates.is_empty() || txn.invalidate_rendered_frame)
+ .map(|txn| txn.document_id)
+ .collect()
+ } else {
+ Vec::new()
+ };
+
+ #[cfg(feature = "capture")]
+ match self.capture_config {
+ Some(ref config) => self.send(SceneBuilderResult::CapturedTransactions(txns, config.clone(), result_tx)),
+ None => self.send(SceneBuilderResult::Transactions(txns, result_tx)),
+ };
+
+ #[cfg(not(feature = "capture"))]
+ self.send(SceneBuilderResult::Transactions(txns, result_tx));
+
+ let _ = self.tx.send(ApiMsg::WakeUp);
+
+ if let Some(pipeline_info) = pipeline_info {
+ // Block until the swap is done, then invoke the hook.
+ let swap_result = result_rx.unwrap().recv();
+ let scene_swap_time = precise_time_ns() - scene_swap_start_time;
+ self.hooks.as_ref().unwrap().post_scene_swap(&document_ids,
+ pipeline_info, scene_swap_time);
+ // Once the hook is done, allow the RB thread to resume
+ if let Ok(SceneSwapResult::Complete(resume_tx)) = swap_result {
+ resume_tx.send(()).ok();
+ }
+ } else if !have_resources_updates.is_empty() {
+ if let Some(ref hooks) = self.hooks {
+ hooks.post_resource_update(&have_resources_updates);
+ }
+ } else if let Some(ref hooks) = self.hooks {
+ hooks.post_empty_scene_build();
+ }
+ }
+
+ /// Reports CPU heap memory used by the SceneBuilder.
+ fn report_memory(&mut self) -> MemoryReport {
+ let ops = self.size_of_ops.as_mut().unwrap();
+ let mut report = MemoryReport::default();
+ for doc in self.documents.values() {
+ doc.interners.report_memory(ops, &mut report);
+ doc.scene.report_memory(ops, &mut report);
+ }
+
+ report
+ }
+}
+
+/// A scene builder thread which executes expensive operations such as blob rasterization
+/// with a lower priority than the normal scene builder thread.
+///
+/// After rasterizing blobs, the secene building request is forwarded to the normal scene
+/// builder where the FrameBuilder is generated.
+pub struct LowPrioritySceneBuilderThread {
+ pub rx: Receiver<SceneBuilderRequest>,
+ pub tx: Sender<SceneBuilderRequest>,
+ pub simulate_slow_ms: u32,
+}
+
+impl LowPrioritySceneBuilderThread {
+ pub fn run(&mut self) {
+ loop {
+ match self.rx.recv() {
+ Ok(SceneBuilderRequest::Transactions(mut txns)) => {
+ let txns : Vec<Box<TransactionMsg>> = txns.drain(..)
+ .map(|txn| self.process_transaction(txn))
+ .collect();
+ self.tx.send(SceneBuilderRequest::Transactions(txns)).unwrap();
+ }
+ Ok(SceneBuilderRequest::ShutDown(sync)) => {
+ self.tx.send(SceneBuilderRequest::ShutDown(sync)).unwrap();
+ break;
+ }
+ Ok(SceneBuilderRequest::SimulateLongLowPrioritySceneBuild(time_ms)) => {
+ self.simulate_slow_ms = time_ms;
+ }
+ Ok(other) => {
+ self.tx.send(other).unwrap();
+ }
+ Err(_) => {
+ break;
+ }
+ }
+ }
+ }
+
+ fn process_transaction(&mut self, mut txn: Box<TransactionMsg>) -> Box<TransactionMsg> {
+ let is_low_priority = true;
+ rasterize_blobs(&mut txn, is_low_priority);
+ txn.blob_requests = Vec::new();
+
+ if self.simulate_slow_ms > 0 {
+ thread::sleep(Duration::from_millis(self.simulate_slow_ms as u64));
+ }
+
+ txn
+ }
+}
diff --git a/gfx/wr/webrender/src/scene_building.rs b/gfx/wr/webrender/src/scene_building.rs
new file mode 100644
index 0000000000..754fac1250
--- /dev/null
+++ b/gfx/wr/webrender/src/scene_building.rs
@@ -0,0 +1,3792 @@
+/* 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 api::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayListIter, PrimitiveFlags};
+use api::{ClipId, ColorF, CommonItemProperties, ComplexClipRegion, ComponentTransferFuncType, RasterSpace};
+use api::{DisplayItem, DisplayItemRef, ExtendMode, ExternalScrollId, FilterData, SharedFontInstanceMap};
+use api::{FilterOp, FilterPrimitive, FontInstanceKey, FontSize, GlyphInstance, GlyphOptions, GradientStop};
+use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, ColorDepth, QualitySettings};
+use api::{LineOrientation, LineStyle, NinePatchBorderSource, PipelineId, MixBlendMode, StackingContextFlags};
+use api::{PropertyBinding, ReferenceFrameKind, ScrollFrameDisplayItem, ScrollSensitivity};
+use api::{Shadow, SpaceAndClipInfo, SpatialId, StickyFrameDisplayItem, ImageMask, ItemTag};
+use api::{ClipMode, PrimitiveKeyKind, TransformStyle, YuvColorSpace, ColorRange, YuvData, TempFilterData};
+use api::{ReferenceTransformBinding, Rotation};
+use api::units::*;
+use crate::image_tiling::simplify_repeated_primitive;
+use crate::clip::{ClipChainId, ClipRegion, ClipItemKey, ClipStore, ClipItemKeyKind};
+use crate::clip::{ClipInternData, ClipNodeKind, ClipInstance, SceneClipInstance};
+use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialTree, SpatialNodeIndex};
+use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
+use crate::glyph_rasterizer::FontInstance;
+use crate::hit_test::HitTestingScene;
+use crate::intern::Interner;
+use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter};
+use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
+use crate::picture::{BlitReason, OrderedPictureChild, PrimitiveList};
+use crate::prim_store::{PrimitiveInstance, register_prim_chase_id};
+use crate::prim_store::{PrimitiveInstanceKind, NinePatchDescriptor, PrimitiveStore};
+use crate::prim_store::{InternablePrimitive, SegmentInstanceIndex, PictureIndex};
+use crate::prim_store::backdrop::Backdrop;
+use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
+use crate::prim_store::gradient::{GradientStopKey, LinearGradient, RadialGradient, RadialGradientParams, ConicGradient, ConicGradientParams};
+use crate::prim_store::image::{Image, YuvImage};
+use crate::prim_store::line_dec::{LineDecoration, LineDecorationCacheKey, get_line_decoration_size};
+use crate::prim_store::picture::{Picture, PictureCompositeKey, PictureKey};
+use crate::prim_store::text_run::TextRun;
+use crate::render_backend::SceneView;
+use crate::resource_cache::ImageRequest;
+use crate::scene::{Scene, ScenePipeline, BuiltScene, SceneStats, StackingContextHelpers};
+use crate::scene_builder_thread::Interners;
+use crate::space::SpaceSnapper;
+use crate::spatial_node::{StickyFrameInfo, ScrollFrameKind};
+use crate::tile_cache::TileCacheBuilder;
+use euclid::approxeq::ApproxEq;
+use std::{f32, mem, usize};
+use std::collections::vec_deque::VecDeque;
+use std::sync::Arc;
+use crate::util::{MaxRect, VecHelper};
+use crate::filterdata::{SFilterDataComponent, SFilterData, SFilterDataKey};
+use smallvec::SmallVec;
+
+/// The offset stack for a given reference frame.
+struct ReferenceFrameState {
+ /// A stack of current offsets from the current reference frame scope.
+ offsets: Vec<LayoutVector2D>,
+}
+
+/// Maps from stacking context layout coordinates into reference frame
+/// relative coordinates.
+struct ReferenceFrameMapper {
+ /// A stack of reference frame scopes.
+ frames: Vec<ReferenceFrameState>,
+}
+
+impl ReferenceFrameMapper {
+ fn new() -> Self {
+ ReferenceFrameMapper {
+ frames: vec![
+ ReferenceFrameState {
+ offsets: vec![
+ LayoutVector2D::zero(),
+ ],
+ }
+ ],
+ }
+ }
+
+ /// Push a new scope. This resets the current offset to zero, and is
+ /// used when a new reference frame or iframe is pushed.
+ fn push_scope(&mut self) {
+ self.frames.push(ReferenceFrameState {
+ offsets: vec![
+ LayoutVector2D::zero(),
+ ],
+ });
+ }
+
+ /// Pop a reference frame scope off the stack.
+ fn pop_scope(&mut self) {
+ self.frames.pop().unwrap();
+ }
+
+ /// Push a new offset for the current scope. This is used when
+ /// a new stacking context is pushed.
+ fn push_offset(&mut self, offset: LayoutVector2D) {
+ let frame = self.frames.last_mut().unwrap();
+ let current_offset = *frame.offsets.last().unwrap();
+ frame.offsets.push(current_offset + offset);
+ }
+
+ /// Pop a local stacking context offset from the current scope.
+ fn pop_offset(&mut self) {
+ let frame = self.frames.last_mut().unwrap();
+ frame.offsets.pop().unwrap();
+ }
+
+ /// Retrieve the current offset to allow converting a stacking context
+ /// relative coordinate to be relative to the owing reference frame.
+ /// TODO(gw): We could perhaps have separate coordinate spaces for this,
+ /// however that's going to either mean a lot of changes to
+ /// public API code, or a lot of changes to internal code.
+ /// Before doing that, we should revisit how Gecko would
+ /// prefer to provide coordinates.
+ /// TODO(gw): For now, this includes only the reference frame relative
+ /// offset. Soon, we will expand this to include the initial
+ /// scroll offsets that are now available on scroll nodes. This
+ /// will allow normalizing the coordinates even between display
+ /// lists where APZ has scrolled the content.
+ fn current_offset(&self) -> LayoutVector2D {
+ *self.frames.last().unwrap().offsets.last().unwrap()
+ }
+}
+
+/// Offsets primitives (and clips) by the external scroll offset
+/// supplied to scroll nodes.
+pub struct ScrollOffsetMapper {
+ pub current_spatial_node: SpatialNodeIndex,
+ pub current_offset: LayoutVector2D,
+}
+
+impl ScrollOffsetMapper {
+ fn new() -> Self {
+ ScrollOffsetMapper {
+ current_spatial_node: SpatialNodeIndex::INVALID,
+ current_offset: LayoutVector2D::zero(),
+ }
+ }
+
+ /// Return the accumulated external scroll offset for a spatial
+ /// node. This caches the last result, which is the common case,
+ /// or defers to the spatial tree to build the value.
+ fn external_scroll_offset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> LayoutVector2D {
+ if spatial_node_index != self.current_spatial_node {
+ self.current_spatial_node = spatial_node_index;
+ self.current_offset = spatial_tree.external_scroll_offset(spatial_node_index);
+ }
+
+ self.current_offset
+ }
+}
+
+/// A data structure that keeps track of mapping between API Ids for spatials and the indices
+/// used internally in the SpatialTree to avoid having to do HashMap lookups for primitives
+/// and clips during frame building.
+#[derive(Default)]
+pub struct NodeIdToIndexMapper {
+ spatial_node_map: FastHashMap<SpatialId, SpatialNodeIndex>,
+}
+
+impl NodeIdToIndexMapper {
+ fn add_spatial_node(&mut self, id: SpatialId, index: SpatialNodeIndex) {
+ let _old_value = self.spatial_node_map.insert(id, index);
+ debug_assert!(_old_value.is_none());
+ }
+
+ fn get_spatial_node_index(&self, id: SpatialId) -> SpatialNodeIndex {
+ self.spatial_node_map[&id]
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct CompositeOps {
+ // Requires only a single texture as input (e.g. most filters)
+ pub filters: Vec<Filter>,
+ pub filter_datas: Vec<FilterData>,
+ pub filter_primitives: Vec<FilterPrimitive>,
+
+ // Requires two source textures (e.g. mix-blend-mode)
+ pub mix_blend_mode: Option<MixBlendMode>,
+}
+
+impl CompositeOps {
+ pub fn new(
+ filters: Vec<Filter>,
+ filter_datas: Vec<FilterData>,
+ filter_primitives: Vec<FilterPrimitive>,
+ mix_blend_mode: Option<MixBlendMode>
+ ) -> Self {
+ CompositeOps {
+ filters,
+ filter_datas,
+ filter_primitives,
+ mix_blend_mode,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.filters.is_empty() &&
+ self.filter_primitives.is_empty() &&
+ self.mix_blend_mode.is_none()
+ }
+
+ /// Returns true if this CompositeOps contains any filters that affect
+ /// the content (false if no filters, or filters are all no-ops).
+ fn has_valid_filters(&self) -> bool {
+ // For each filter, create a new image with that composite mode.
+ let mut current_filter_data_index = 0;
+ for filter in &self.filters {
+ match filter {
+ Filter::ComponentTransfer => {
+ let filter_data =
+ &self.filter_datas[current_filter_data_index];
+ let filter_data = filter_data.sanitize();
+ current_filter_data_index = current_filter_data_index + 1;
+ if filter_data.is_identity() {
+ continue
+ } else {
+ return true;
+ }
+ }
+ _ => {
+ if filter.is_noop() {
+ continue;
+ } else {
+ return true;
+ }
+ }
+ }
+ }
+
+ if !self.filter_primitives.is_empty() {
+ return true;
+ }
+
+ false
+ }
+}
+
+/// Represents the current input for a picture chain builder (either a
+/// prim list from the stacking context, or a wrapped picture instance).
+enum PictureSource {
+ PrimitiveList {
+ prim_list: PrimitiveList,
+ },
+ WrappedPicture {
+ instance: PrimitiveInstance,
+ },
+}
+
+/// Helper struct to build picture chains during scene building from
+/// a flattened stacking context struct.
+struct PictureChainBuilder {
+ /// The current input source for the next picture
+ current: PictureSource,
+
+ /// Positioning node for this picture chain
+ spatial_node_index: SpatialNodeIndex,
+ /// Prim flags for any pictures in this chain
+ flags: PrimitiveFlags,
+}
+
+impl PictureChainBuilder {
+ /// Create a new picture chain builder, from a primitive list
+ fn from_prim_list(
+ prim_list: PrimitiveList,
+ flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ PictureChainBuilder {
+ current: PictureSource::PrimitiveList {
+ prim_list,
+ },
+ spatial_node_index,
+ flags,
+ }
+ }
+
+ /// Create a new picture chain builder, from a picture wrapper instance
+ fn from_instance(
+ instance: PrimitiveInstance,
+ flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Self {
+ PictureChainBuilder {
+ current: PictureSource::WrappedPicture {
+ instance,
+ },
+ flags,
+ spatial_node_index,
+ }
+ }
+
+ /// Wrap the existing content with a new picture with the given parameters
+ #[must_use]
+ fn add_picture(
+ self,
+ composite_mode: PictureCompositeMode,
+ context_3d: Picture3DContext<OrderedPictureChild>,
+ options: PictureOptions,
+ interners: &mut Interners,
+ prim_store: &mut PrimitiveStore,
+ ) -> PictureChainBuilder {
+ let prim_list = match self.current {
+ PictureSource::PrimitiveList { prim_list } => {
+ prim_list
+ }
+ PictureSource::WrappedPicture { instance } => {
+ let mut prim_list = PrimitiveList::empty();
+
+ prim_list.add_prim(
+ instance,
+ LayoutRect::zero(),
+ self.spatial_node_index,
+ self.flags,
+ );
+
+ prim_list
+ }
+ };
+
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ Some(composite_mode.clone()),
+ context_3d,
+ true,
+ self.flags,
+ prim_list,
+ self.spatial_node_index,
+ options,
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ Some(composite_mode).into(),
+ ClipChainId::NONE,
+ interners,
+ );
+
+ PictureChainBuilder {
+ current: PictureSource::WrappedPicture {
+ instance,
+ },
+ spatial_node_index: self.spatial_node_index,
+ flags: self.flags,
+ }
+ }
+
+ /// Finish building this picture chain. Set the clip chain on the outermost picture
+ fn finalize(
+ self,
+ clip_chain_id: ClipChainId,
+ interners: &mut Interners,
+ prim_store: &mut PrimitiveStore,
+ ) -> PrimitiveInstance {
+ match self.current {
+ PictureSource::WrappedPicture { mut instance } => {
+ instance.clip_set.clip_chain_id = clip_chain_id;
+ instance
+ }
+ PictureSource::PrimitiveList { prim_list } => {
+ // If no picture was created for this stacking context, create a
+ // pass-through wrapper now. This is only needed in 1-2 edge cases
+ // now, and will be removed as a follow up.
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ None,
+ Picture3DContext::Out,
+ true,
+ self.flags,
+ prim_list,
+ self.spatial_node_index,
+ PictureOptions::default(),
+ ))
+ );
+
+ create_prim_instance(
+ pic_index,
+ None.into(),
+ clip_chain_id,
+ interners,
+ )
+ }
+ }
+ }
+}
+
+bitflags! {
+ /// Slice flags
+ pub struct SliceFlags : u8 {
+ /// Slice created by a prim that has PrimitiveFlags::IS_SCROLLBAR_CONTAINER
+ const IS_SCROLLBAR = 1;
+ }
+}
+
+/// A structure that converts a serialized display list into a form that WebRender
+/// can use to later build a frame. This structure produces a BuiltScene. Public
+/// members are typically those that are destructured into the BuiltScene.
+pub struct SceneBuilder<'a> {
+ /// The scene that we are currently building.
+ scene: &'a Scene,
+
+ /// The map of all font instances.
+ font_instances: SharedFontInstanceMap,
+
+ /// The data structure that converts between ClipId/SpatialId and the various
+ /// index types that the SpatialTree uses.
+ id_to_index_mapper: NodeIdToIndexMapper,
+
+ /// A stack of stacking context properties.
+ sc_stack: Vec<FlattenedStackingContext>,
+
+ /// Stack of spatial node indices forming containing block for 3d contexts
+ containing_block_stack: Vec<SpatialNodeIndex>,
+
+ /// Stack of requested raster spaces for stacking contexts
+ raster_space_stack: Vec<RasterSpace>,
+
+ /// Maintains state for any currently active shadows
+ pending_shadow_items: VecDeque<ShadowItem>,
+
+ /// The SpatialTree that we are currently building during building.
+ pub spatial_tree: SpatialTree,
+
+ /// The store of primitives.
+ pub prim_store: PrimitiveStore,
+
+ /// Information about all primitives involved in hit testing.
+ pub hit_testing_scene: HitTestingScene,
+
+ /// The store which holds all complex clipping information.
+ pub clip_store: ClipStore,
+
+ /// The configuration to use for the FrameBuilder. We consult this in
+ /// order to determine the default font.
+ pub config: FrameBuilderConfig,
+
+ /// Reference to the set of data that is interned across display lists.
+ interners: &'a mut Interners,
+
+ /// Helper struct to map stacking context coords <-> reference frame coords.
+ rf_mapper: ReferenceFrameMapper,
+
+ /// Helper struct to map spatial nodes to external scroll offsets.
+ external_scroll_mapper: ScrollOffsetMapper,
+
+ /// The current recursion depth of iframes encountered. Used to restrict picture
+ /// caching slices to only the top-level content frame.
+ iframe_size: Vec<LayoutSize>,
+
+ /// The current quality / performance settings for this scene.
+ quality_settings: QualitySettings,
+
+ /// Maintains state about the list of tile caches being built for this scene.
+ tile_cache_builder: TileCacheBuilder,
+
+ /// A helper struct to snap local rects in device space. During frame
+ /// building we may establish new raster roots, however typically that is in
+ /// cases where we won't be applying snapping (e.g. has perspective), or in
+ /// edge cases (e.g. SVG filter) where we can accept slightly incorrect
+ /// behaviour in favour of getting the common case right.
+ snap_to_device: SpaceSnapper,
+}
+
+impl<'a> SceneBuilder<'a> {
+ pub fn build(
+ scene: &Scene,
+ font_instances: SharedFontInstanceMap,
+ view: &SceneView,
+ frame_builder_config: &FrameBuilderConfig,
+ interners: &mut Interners,
+ stats: &SceneStats,
+ ) -> BuiltScene {
+ profile_scope!("build_scene");
+
+ // We checked that the root pipeline is available on the render backend.
+ let root_pipeline_id = scene.root_pipeline_id.unwrap();
+ let root_pipeline = scene.pipelines.get(&root_pipeline_id).unwrap();
+
+ let background_color = root_pipeline
+ .background_color
+ .and_then(|color| if color.a > 0.0 { Some(color) } else { None });
+
+ let device_pixel_scale = view.accumulated_scale_factor_for_snapping();
+ let spatial_tree = SpatialTree::new();
+
+ let snap_to_device = SpaceSnapper::new(
+ ROOT_SPATIAL_NODE_INDEX,
+ device_pixel_scale,
+ );
+
+ let mut builder = SceneBuilder {
+ scene,
+ spatial_tree,
+ font_instances,
+ config: *frame_builder_config,
+ id_to_index_mapper: NodeIdToIndexMapper::default(),
+ hit_testing_scene: HitTestingScene::new(&stats.hit_test_stats),
+ pending_shadow_items: VecDeque::new(),
+ sc_stack: Vec::new(),
+ containing_block_stack: Vec::new(),
+ raster_space_stack: vec![RasterSpace::Screen],
+ prim_store: PrimitiveStore::new(&stats.prim_store_stats),
+ clip_store: ClipStore::new(),
+ interners,
+ rf_mapper: ReferenceFrameMapper::new(),
+ external_scroll_mapper: ScrollOffsetMapper::new(),
+ iframe_size: Vec::new(),
+ quality_settings: view.quality_settings,
+ tile_cache_builder: TileCacheBuilder::new(),
+ snap_to_device,
+ };
+
+ builder.build_all(&root_pipeline);
+
+ // Construct the picture cache primitive instance(s) from the tile cache builder
+ let (tile_cache_config, tile_cache_pictures) = builder.tile_cache_builder.build(
+ &builder.config,
+ &mut builder.clip_store,
+ &mut builder.prim_store,
+ );
+
+ BuiltScene {
+ has_root_pipeline: scene.has_root_pipeline(),
+ pipeline_epochs: scene.pipeline_epochs.clone(),
+ output_rect: view.device_rect.size.into(),
+ background_color,
+ hit_testing_scene: Arc::new(builder.hit_testing_scene),
+ spatial_tree: builder.spatial_tree,
+ prim_store: builder.prim_store,
+ clip_store: builder.clip_store,
+ config: builder.config,
+ tile_cache_config,
+ tile_cache_pictures,
+ }
+ }
+
+ /// Retrieve the current offset to allow converting a stacking context
+ /// relative coordinate to be relative to the owing reference frame,
+ /// also considering any external scroll offset on the provided
+ /// spatial node.
+ fn current_offset(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> LayoutVector2D {
+ // Get the current offset from stacking context <-> reference frame space.
+ let rf_offset = self.rf_mapper.current_offset();
+
+ // Get the external scroll offset, if applicable.
+ let scroll_offset = self
+ .external_scroll_mapper
+ .external_scroll_offset(
+ spatial_node_index,
+ &self.spatial_tree,
+ );
+
+ rf_offset + scroll_offset
+ }
+
+ fn build_all(&mut self, root_pipeline: &ScenePipeline) {
+ enum ContextKind<'a> {
+ Root,
+ StackingContext {
+ sc_info: StackingContextInfo,
+ },
+ ReferenceFrame,
+ Iframe {
+ parent_traversal: BuiltDisplayListIter<'a>,
+ }
+ }
+ struct BuildContext<'a> {
+ pipeline_id: PipelineId,
+ kind: ContextKind<'a>,
+ }
+
+ let root_clip_id = ClipId::root(root_pipeline.pipeline_id);
+ self.clip_store.register_clip_template(root_clip_id, root_clip_id, &[]);
+ self.clip_store.push_clip_root(Some(root_clip_id), false);
+ self.push_root(
+ root_pipeline.pipeline_id,
+ &root_pipeline.viewport_size,
+ );
+
+ let mut stack = vec![BuildContext {
+ pipeline_id: root_pipeline.pipeline_id,
+ kind: ContextKind::Root,
+ }];
+ let mut traversal = root_pipeline.display_list.iter();
+
+ 'outer: while let Some(bc) = stack.pop() {
+ loop {
+ let item = match traversal.next() {
+ Some(item) => item,
+ None => break,
+ };
+
+ match item.item() {
+ DisplayItem::PushStackingContext(ref info) => {
+ profile_scope!("build_stacking_context");
+ let spatial_node_index = self.get_space(info.spatial_id);
+ let mut subtraversal = item.sub_iter();
+ // Avoid doing unnecessary work for empty stacking contexts.
+ if subtraversal.current_stacking_context_empty() {
+ subtraversal.skip_current_stacking_context();
+ traversal = subtraversal;
+ continue;
+ }
+
+ let composition_operations = CompositeOps::new(
+ filter_ops_for_compositing(item.filters()),
+ filter_datas_for_compositing(item.filter_datas()),
+ filter_primitives_for_compositing(item.filter_primitives()),
+ info.stacking_context.mix_blend_mode_for_compositing(),
+ );
+
+ let sc_info = self.push_stacking_context(
+ composition_operations,
+ info.stacking_context.transform_style,
+ info.prim_flags,
+ spatial_node_index,
+ info.stacking_context.clip_id,
+ info.stacking_context.raster_space,
+ info.stacking_context.flags,
+ );
+
+ self.rf_mapper.push_offset(info.origin.to_vector());
+ let new_context = BuildContext {
+ pipeline_id: bc.pipeline_id,
+ kind: ContextKind::StackingContext {
+ sc_info,
+ },
+ };
+ stack.push(bc);
+ stack.push(new_context);
+
+ subtraversal.merge_debug_stats_from(&mut traversal);
+ traversal = subtraversal;
+ continue 'outer;
+ }
+ DisplayItem::PushReferenceFrame(ref info) => {
+ profile_scope!("build_reference_frame");
+ let parent_space = self.get_space(info.parent_spatial_id);
+ let mut subtraversal = item.sub_iter();
+ let current_offset = self.current_offset(parent_space);
+
+ let transform = match info.reference_frame.transform {
+ ReferenceTransformBinding::Static { binding } => binding,
+ ReferenceTransformBinding::Computed { scale_from, vertical_flip, rotation } => {
+ let content_size = &self.iframe_size.last().unwrap();
+
+ let mut transform = if let Some(scale_from) = scale_from {
+ // If we have a 90/270 degree rotation, then scale_from
+ // and content_size are in different coordinate spaces and
+ // we need to swap width/height for them to be correct.
+ match rotation {
+ Rotation::Degree0 |
+ Rotation::Degree180 => {
+ LayoutTransform::scale(
+ content_size.width / scale_from.width,
+ content_size.height / scale_from.height,
+ 1.0
+ )
+ },
+ Rotation::Degree90 |
+ Rotation::Degree270 => {
+ LayoutTransform::scale(
+ content_size.height / scale_from.width,
+ content_size.width / scale_from.height,
+ 1.0
+ )
+
+ }
+ }
+ } else {
+ LayoutTransform::identity()
+ };
+
+ if vertical_flip {
+ let content_size = &self.iframe_size.last().unwrap();
+ transform = transform
+ .then_translate(LayoutVector3D::new(0.0, content_size.height, 0.0))
+ .pre_scale(1.0, -1.0, 1.0);
+ }
+
+ let rotate = rotation.to_matrix(**content_size);
+ let transform = transform.then(&rotate);
+
+ PropertyBinding::Value(transform)
+ },
+ };
+
+ self.push_reference_frame(
+ info.reference_frame.id,
+ Some(parent_space),
+ bc.pipeline_id,
+ info.reference_frame.transform_style,
+ transform,
+ info.reference_frame.kind,
+ current_offset + info.origin.to_vector(),
+ );
+
+ self.rf_mapper.push_scope();
+ let new_context = BuildContext {
+ pipeline_id: bc.pipeline_id,
+ kind: ContextKind::ReferenceFrame,
+ };
+ stack.push(bc);
+ stack.push(new_context);
+
+ subtraversal.merge_debug_stats_from(&mut traversal);
+ traversal = subtraversal;
+ continue 'outer;
+ }
+ DisplayItem::PopReferenceFrame |
+ DisplayItem::PopStackingContext => break,
+ DisplayItem::Iframe(ref info) => {
+ profile_scope!("iframe");
+
+ let space = self.get_space(info.space_and_clip.spatial_id);
+ let (size, subtraversal) = match self.push_iframe(info, space) {
+ Some(pair) => pair,
+ None => continue,
+ };
+
+ // If this is a root iframe, force a new tile cache both before and after
+ // adding primitives for this iframe.
+ if self.iframe_size.is_empty() {
+ self.tile_cache_builder.add_tile_cache_barrier();
+ }
+
+ self.rf_mapper.push_scope();
+ self.iframe_size.push(size);
+
+ let new_context = BuildContext {
+ pipeline_id: info.pipeline_id,
+ kind: ContextKind::Iframe {
+ parent_traversal: mem::replace(&mut traversal, subtraversal),
+ },
+ };
+ stack.push(bc);
+ stack.push(new_context);
+ continue 'outer;
+ }
+ _ => {
+ self.build_item(item, bc.pipeline_id);
+ }
+ };
+ }
+
+ match bc.kind {
+ ContextKind::Root => {}
+ ContextKind::StackingContext { sc_info } => {
+ self.rf_mapper.pop_offset();
+ self.pop_stacking_context(sc_info);
+ }
+ ContextKind::ReferenceFrame => {
+ self.rf_mapper.pop_scope();
+ }
+ ContextKind::Iframe { parent_traversal } => {
+ self.iframe_size.pop();
+ self.rf_mapper.pop_scope();
+
+ self.clip_store.pop_clip_root();
+ if self.iframe_size.is_empty() {
+ self.tile_cache_builder.add_tile_cache_barrier();
+ }
+
+ traversal = parent_traversal;
+ }
+ }
+
+ // TODO: factor this out to be part of capture
+ if cfg!(feature = "display_list_stats") {
+ let stats = traversal.debug_stats();
+ let total_bytes: usize = stats.iter().map(|(_, stats)| stats.num_bytes).sum();
+ println!("item, total count, total bytes, % of DL bytes, bytes per item");
+ for (label, stats) in stats {
+ println!("{}, {}, {}kb, {}%, {}",
+ label,
+ stats.total_count,
+ stats.num_bytes / 1000,
+ ((stats.num_bytes as f32 / total_bytes.max(1) as f32) * 100.0) as usize,
+ stats.num_bytes / stats.total_count.max(1));
+ }
+ println!();
+ }
+ }
+
+ self.clip_store.pop_clip_root();
+ debug_assert!(self.sc_stack.is_empty());
+ }
+
+ fn build_sticky_frame(
+ &mut self,
+ info: &StickyFrameDisplayItem,
+ parent_node_index: SpatialNodeIndex,
+ ) {
+ let current_offset = self.current_offset(parent_node_index);
+ let frame_rect = info.bounds.translate(current_offset);
+ let sticky_frame_info = StickyFrameInfo::new(
+ frame_rect,
+ info.margins,
+ info.vertical_offset_bounds,
+ info.horizontal_offset_bounds,
+ info.previously_applied_offset,
+ );
+
+ let index = self.spatial_tree.add_sticky_frame(
+ parent_node_index,
+ sticky_frame_info,
+ info.id.pipeline_id(),
+ );
+ self.id_to_index_mapper.add_spatial_node(info.id, index);
+ }
+
+ fn build_scroll_frame(
+ &mut self,
+ info: &ScrollFrameDisplayItem,
+ parent_node_index: SpatialNodeIndex,
+ pipeline_id: PipelineId,
+ ) {
+ let current_offset = self.current_offset(parent_node_index);
+ let clip_region = ClipRegion::create_for_clip_node_with_local_clip(
+ &info.clip_rect,
+ &current_offset,
+ );
+ // Just use clip rectangle as the frame rect for this scroll frame.
+ // This is useful when calculating scroll extents for the
+ // SpatialNode::scroll(..) API as well as for properly setting sticky
+ // positioning offsets.
+ let frame_rect = clip_region.main;
+ let content_size = info.content_rect.size;
+
+ self.add_clip_node(info.clip_id, &info.parent_space_and_clip, clip_region);
+
+ self.add_scroll_frame(
+ info.scroll_frame_id,
+ parent_node_index,
+ info.external_id,
+ pipeline_id,
+ &frame_rect,
+ &content_size,
+ info.scroll_sensitivity,
+ ScrollFrameKind::Explicit,
+ info.external_scroll_offset,
+ );
+ }
+
+ fn push_iframe(
+ &mut self,
+ info: &IframeDisplayItem,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> Option<(LayoutSize, BuiltDisplayListIter<'a>)> {
+ let iframe_pipeline_id = info.pipeline_id;
+ let pipeline = match self.scene.pipelines.get(&iframe_pipeline_id) {
+ Some(pipeline) => pipeline,
+ None => {
+ debug_assert!(info.ignore_missing_pipeline);
+ return None
+ },
+ };
+
+ let current_offset = self.current_offset(spatial_node_index);
+ self.add_clip_node(
+ ClipId::root(iframe_pipeline_id),
+ &info.space_and_clip,
+ ClipRegion::create_for_clip_node_with_local_clip(
+ &info.clip_rect,
+ &current_offset,
+ ),
+ );
+
+ self.clip_store.push_clip_root(
+ Some(ClipId::root(iframe_pipeline_id)),
+ true,
+ );
+
+ let bounds = self.snap_rect(
+ &info.bounds.translate(current_offset),
+ spatial_node_index,
+ );
+
+ let spatial_node_index = self.push_reference_frame(
+ SpatialId::root_reference_frame(iframe_pipeline_id),
+ Some(spatial_node_index),
+ iframe_pipeline_id,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform,
+ bounds.origin.to_vector(),
+ );
+
+ let iframe_rect = LayoutRect::new(LayoutPoint::zero(), bounds.size);
+ let is_root_pipeline = self.iframe_size.is_empty();
+
+ self.add_scroll_frame(
+ SpatialId::root_scroll_node(iframe_pipeline_id),
+ spatial_node_index,
+ ExternalScrollId(0, iframe_pipeline_id),
+ iframe_pipeline_id,
+ &iframe_rect,
+ &bounds.size,
+ ScrollSensitivity::ScriptAndInputEvents,
+ ScrollFrameKind::PipelineRoot {
+ is_root_pipeline,
+ },
+ LayoutVector2D::zero(),
+ );
+
+ Some((bounds.size, pipeline.display_list.iter()))
+ }
+
+ fn get_space(
+ &self,
+ spatial_id: SpatialId,
+ ) -> SpatialNodeIndex {
+ self.id_to_index_mapper.get_spatial_node_index(spatial_id)
+ }
+
+ fn get_clip_chain(
+ &mut self,
+ clip_id: ClipId,
+ ) -> ClipChainId {
+ self.clip_store.get_or_build_clip_chain_id(clip_id)
+ }
+
+ fn process_common_properties(
+ &mut self,
+ common: &CommonItemProperties,
+ bounds: Option<&LayoutRect>,
+ ) -> (LayoutPrimitiveInfo, LayoutRect, SpatialNodeIndex, ClipChainId) {
+ let spatial_node_index = self.get_space(common.spatial_id);
+ let clip_chain_id = self.get_clip_chain(common.clip_id);
+
+ let current_offset = self.current_offset(spatial_node_index);
+
+ let unsnapped_clip_rect = common.clip_rect.translate(current_offset);
+ let clip_rect = self.snap_rect(
+ &unsnapped_clip_rect,
+ spatial_node_index,
+ );
+
+ let unsnapped_rect = bounds.map(|bounds| {
+ bounds.translate(current_offset)
+ });
+
+ // If no bounds rect is given, default to clip rect.
+ let rect = unsnapped_rect.map_or(clip_rect, |bounds| {
+ self.snap_rect(
+ &bounds,
+ spatial_node_index,
+ )
+ });
+
+ let layout = LayoutPrimitiveInfo {
+ rect,
+ clip_rect,
+ flags: common.flags,
+ };
+
+ (layout, unsnapped_rect.unwrap_or(unsnapped_clip_rect), spatial_node_index, clip_chain_id)
+ }
+
+ fn process_common_properties_with_bounds(
+ &mut self,
+ common: &CommonItemProperties,
+ bounds: &LayoutRect,
+ ) -> (LayoutPrimitiveInfo, LayoutRect, SpatialNodeIndex, ClipChainId) {
+ self.process_common_properties(
+ common,
+ Some(bounds),
+ )
+ }
+
+ pub fn snap_rect(
+ &mut self,
+ rect: &LayoutRect,
+ target_spatial_node: SpatialNodeIndex,
+ ) -> LayoutRect {
+ self.snap_to_device.set_target_spatial_node(
+ target_spatial_node,
+ &self.spatial_tree
+ );
+ self.snap_to_device.snap_rect(rect)
+ }
+
+ fn build_item<'b>(
+ &'b mut self,
+ item: DisplayItemRef,
+ pipeline_id: PipelineId,
+ ) {
+ match *item.item() {
+ DisplayItem::Image(ref info) => {
+ profile_scope!("image");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_image(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ layout.rect.size,
+ LayoutSize::zero(),
+ info.image_key,
+ info.image_rendering,
+ info.alpha_type,
+ info.color,
+ );
+ }
+ DisplayItem::RepeatingImage(ref info) => {
+ profile_scope!("repeating_image");
+
+ let (layout, unsnapped_rect, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let stretch_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.stretch_size,
+ );
+
+ self.add_image(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ stretch_size,
+ info.tile_spacing,
+ info.image_key,
+ info.image_rendering,
+ info.alpha_type,
+ info.color,
+ );
+ }
+ DisplayItem::YuvImage(ref info) => {
+ profile_scope!("yuv_image");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_yuv_image(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ info.yuv_data,
+ info.color_depth,
+ info.color_space,
+ info.color_range,
+ info.image_rendering,
+ );
+ }
+ DisplayItem::Text(ref info) => {
+ profile_scope!("text");
+
+ // TODO(aosmond): Snapping text primitives does not make much sense, given the
+ // primitive bounds and clip are supposed to be conservative, not definitive.
+ // E.g. they should be able to grow and not impact the output. However there
+ // are subtle interactions between the primitive origin and the glyph offset
+ // which appear to be significant (presumably due to some sort of accumulated
+ // error throughout the layers). We should fix this at some point.
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_text(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ &info.font_key,
+ &info.color,
+ item.glyphs(),
+ info.glyph_options,
+ );
+ }
+ DisplayItem::Rectangle(ref info) => {
+ profile_scope!("rect");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ Vec::new(),
+ PrimitiveKeyKind::Rectangle {
+ color: info.color.into(),
+ },
+ );
+ }
+ DisplayItem::HitTest(ref info) => {
+ profile_scope!("hit_test");
+
+ // TODO(gw): We could skip building the clip-chain here completely, as it's not used by
+ // hit-test items.
+ let (layout, _, spatial_node_index, _) = self.process_common_properties(
+ &info.common,
+ None,
+ );
+
+ // Don't add transparent rectangles to the draw list,
+ // but do consider them for hit testing. This allows
+ // specifying invisible hit testing areas.
+ self.add_primitive_to_hit_testing_list(
+ &layout,
+ spatial_node_index,
+ info.common.clip_id,
+ info.tag,
+ );
+ }
+ DisplayItem::ClearRectangle(ref info) => {
+ profile_scope!("clear");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_clear_rectangle(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ );
+ }
+ DisplayItem::Line(ref info) => {
+ profile_scope!("line");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.area,
+ );
+
+ self.add_line(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ info.wavy_line_thickness,
+ info.orientation,
+ info.color,
+ info.style,
+ );
+ }
+ DisplayItem::Gradient(ref info) => {
+ profile_scope!("gradient");
+
+ let (layout, unsnapped_rect, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ if let Some(prim_key_kind) = self.create_linear_gradient_prim(
+ &layout,
+ info.gradient.start_point,
+ info.gradient.end_point,
+ item.gradient_stops(),
+ info.gradient.extend_mode,
+ tile_size,
+ info.tile_spacing,
+ None,
+ ) {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ }
+ DisplayItem::RadialGradient(ref info) => {
+ profile_scope!("radial");
+
+ let (layout, unsnapped_rect, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ let prim_key_kind = self.create_radial_gradient_prim(
+ &layout,
+ info.gradient.center,
+ info.gradient.start_offset * info.gradient.radius.width,
+ info.gradient.end_offset * info.gradient.radius.width,
+ info.gradient.radius.width / info.gradient.radius.height,
+ item.gradient_stops(),
+ info.gradient.extend_mode,
+ tile_size,
+ info.tile_spacing,
+ None,
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ DisplayItem::ConicGradient(ref info) => {
+ profile_scope!("conic");
+
+ let (layout, unsnapped_rect, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ let tile_size = process_repeat_size(
+ &layout.rect,
+ &unsnapped_rect,
+ info.tile_size,
+ );
+
+ let prim_key_kind = self.create_conic_gradient_prim(
+ &layout,
+ info.gradient.center,
+ info.gradient.angle,
+ info.gradient.start_offset,
+ info.gradient.end_offset,
+ item.gradient_stops(),
+ info.gradient.extend_mode,
+ tile_size,
+ info.tile_spacing,
+ None,
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ Vec::new(),
+ prim_key_kind,
+ );
+ }
+ DisplayItem::BoxShadow(ref info) => {
+ profile_scope!("box_shadow");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.box_bounds,
+ );
+
+ self.add_box_shadow(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ &info.offset,
+ info.color,
+ info.blur_radius,
+ info.spread_radius,
+ info.border_radius,
+ info.clip_mode,
+ );
+ }
+ DisplayItem::Border(ref info) => {
+ profile_scope!("border");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties_with_bounds(
+ &info.common,
+ &info.bounds,
+ );
+
+ self.add_border(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ info,
+ item.gradient_stops(),
+ );
+ }
+ DisplayItem::ImageMaskClip(ref info) => {
+ profile_scope!("image_clip");
+
+ let parent_space = self.get_space(info.parent_space_and_clip.spatial_id);
+ let current_offset = self.current_offset(parent_space);
+
+ let image_mask = ImageMask {
+ rect: info.image_mask.rect.translate(current_offset),
+ ..info.image_mask
+ };
+
+ self.add_image_mask_clip_node(
+ info.id,
+ &info.parent_space_and_clip,
+ &image_mask,
+ );
+ }
+ DisplayItem::RoundedRectClip(ref info) => {
+ profile_scope!("rounded_clip");
+
+ let parent_space = self.get_space(info.parent_space_and_clip.spatial_id);
+ let current_offset = self.current_offset(parent_space);
+
+ self.add_rounded_rect_clip_node(
+ info.id,
+ &info.parent_space_and_clip,
+ &info.clip,
+ current_offset,
+ );
+ }
+ DisplayItem::RectClip(ref info) => {
+ profile_scope!("rect_clip");
+
+ let parent_space = self.get_space(info.parent_space_and_clip.spatial_id);
+ let current_offset = self.current_offset(parent_space);
+ let clip_rect = info.clip_rect.translate(current_offset);
+
+ self.add_rect_clip_node(
+ info.id,
+ &info.parent_space_and_clip,
+ &clip_rect,
+ );
+ }
+ DisplayItem::Clip(ref info) => {
+ profile_scope!("clip");
+
+ let parent_space = self.get_space(info.parent_space_and_clip.spatial_id);
+ let current_offset = self.current_offset(parent_space);
+ let clip_region = ClipRegion::create_for_clip_node(
+ info.clip_rect,
+ item.complex_clip().iter(),
+ &current_offset,
+ );
+ self.add_clip_node(info.id, &info.parent_space_and_clip, clip_region);
+ }
+ DisplayItem::ClipChain(ref info) => {
+ profile_scope!("clip_chain");
+
+ let parent = info.parent.map_or(ClipId::root(pipeline_id), |id| ClipId::ClipChain(id));
+ let mut clips: SmallVec<[SceneClipInstance; 4]> = SmallVec::new();
+
+ for clip_item in item.clip_chain_items() {
+ let template = self.clip_store.get_template(clip_item);
+ clips.extend_from_slice(&template.clips);
+ }
+
+ self.clip_store.register_clip_template(
+ ClipId::ClipChain(info.id),
+ parent,
+ &clips,
+ );
+ },
+ DisplayItem::ScrollFrame(ref info) => {
+ profile_scope!("scrollframe");
+
+ let parent_space = self.get_space(info.parent_space_and_clip.spatial_id);
+ self.build_scroll_frame(
+ info,
+ parent_space,
+ pipeline_id,
+ );
+ }
+ DisplayItem::StickyFrame(ref info) => {
+ profile_scope!("stickyframe");
+
+ let parent_space = self.get_space(info.parent_spatial_id);
+ self.build_sticky_frame(
+ info,
+ parent_space,
+ );
+ }
+ DisplayItem::BackdropFilter(ref info) => {
+ profile_scope!("backdrop");
+
+ let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties(
+ &info.common,
+ None,
+ );
+
+ let filters = filter_ops_for_compositing(item.filters());
+ let filter_datas = filter_datas_for_compositing(item.filter_datas());
+ let filter_primitives = filter_primitives_for_compositing(item.filter_primitives());
+
+ self.add_backdrop_filter(
+ spatial_node_index,
+ clip_chain_id,
+ &layout,
+ filters,
+ filter_datas,
+ filter_primitives,
+ );
+ }
+
+ // Do nothing; these are dummy items for the display list parser
+ DisplayItem::SetGradientStops |
+ DisplayItem::SetFilterOps |
+ DisplayItem::SetFilterData |
+ DisplayItem::SetFilterPrimitives => {}
+
+ // Special items that are handled in the parent method
+ DisplayItem::PushStackingContext(..) |
+ DisplayItem::PushReferenceFrame(..) |
+ DisplayItem::PopReferenceFrame |
+ DisplayItem::PopStackingContext |
+ DisplayItem::Iframe(_) => {
+ unreachable!("Handled in `build_all`")
+ }
+
+ DisplayItem::ReuseItems(key) |
+ DisplayItem::RetainedItems(key) => {
+ unreachable!("Iterator logic error: {:?}", key);
+ }
+
+ DisplayItem::PushShadow(info) => {
+ profile_scope!("push_shadow");
+
+ let spatial_node_index = self.get_space(info.space_and_clip.spatial_id);
+ let clip_chain_id = self.get_clip_chain(
+ info.space_and_clip.clip_id,
+ );
+
+ self.push_shadow(
+ info.shadow,
+ spatial_node_index,
+ clip_chain_id,
+ info.should_inflate,
+ );
+ }
+ DisplayItem::PopAllShadows => {
+ profile_scope!("pop_all_shadows");
+
+ self.pop_all_shadows();
+ }
+ }
+ }
+
+ // Given a list of clip sources, a positioning node and
+ // a parent clip chain, return a new clip chain entry.
+ // If the supplied list of clip sources is empty, then
+ // just return the parent clip chain id directly.
+ fn build_clip_chain(
+ &mut self,
+ clip_items: Vec<ClipItemKey>,
+ spatial_node_index: SpatialNodeIndex,
+ parent_clip_chain_id: ClipChainId,
+ ) -> ClipChainId {
+ if clip_items.is_empty() {
+ parent_clip_chain_id
+ } else {
+ let mut clip_chain_id = parent_clip_chain_id;
+
+ for item in clip_items {
+ // Intern this clip item, and store the handle
+ // in the clip chain node.
+ let handle = self.interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: item.kind.node_kind(),
+ }
+ });
+
+ clip_chain_id = self.clip_store.add_clip_chain_node(
+ handle,
+ spatial_node_index,
+ clip_chain_id,
+ );
+ }
+
+ clip_chain_id
+ }
+ }
+
+ /// Create a primitive and add it to the prim store. This method doesn't
+ /// add the primitive to the draw list, so can be used for creating
+ /// sub-primitives.
+ ///
+ /// TODO(djg): Can this inline into `add_interned_prim_to_draw_list`
+ fn create_primitive<P>(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ prim: P,
+ ) -> PrimitiveInstance
+ where
+ P: InternablePrimitive,
+ Interners: AsMut<Interner<P>>,
+ {
+ // Build a primitive key.
+ let prim_key = prim.into_key(info);
+
+ let current_offset = self.current_offset(spatial_node_index);
+ let interner = self.interners.as_mut();
+ let prim_data_handle = interner
+ .intern(&prim_key, || ());
+
+ let instance_kind = P::make_instance_kind(
+ prim_key,
+ prim_data_handle,
+ &mut self.prim_store,
+ current_offset,
+ );
+
+ PrimitiveInstance::new(
+ info.clip_rect,
+ instance_kind,
+ clip_chain_id,
+ )
+ }
+
+ pub fn add_primitive_to_hit_testing_list(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_id: ClipId,
+ tag: ItemTag,
+ ) {
+ self.hit_testing_scene.add_item(
+ tag,
+ info,
+ spatial_node_index,
+ clip_id,
+ &self.clip_store,
+ );
+ }
+
+ /// Add an already created primitive to the draw lists.
+ pub fn add_primitive_to_draw_list(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ flags: PrimitiveFlags,
+ ) {
+ // Add primitive to the top-most stacking context on the stack.
+ if prim_instance.is_chased() {
+ println!("\tadded to stacking context at {}", self.sc_stack.len());
+ }
+
+ // If we have a valid stacking context, the primitive gets added to that.
+ // Otherwise, it gets added to a top-level picture cache slice.
+
+ match self.sc_stack.last_mut() {
+ Some(stacking_context) => {
+ stacking_context.prim_list.add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ flags,
+ );
+ }
+ None => {
+ self.tile_cache_builder.add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ flags,
+ &self.spatial_tree,
+ &self.clip_store,
+ self.interners,
+ &self.config,
+ &self.quality_settings,
+ );
+ }
+ }
+ }
+
+ /// Convenience interface that creates a primitive entry and adds it
+ /// to the draw list.
+ fn add_nonshadowable_primitive<P>(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ clip_items: Vec<ClipItemKey>,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ {
+ if prim.is_visible() {
+ let clip_chain_id = self.build_clip_chain(
+ clip_items,
+ spatial_node_index,
+ clip_chain_id,
+ );
+ self.add_prim_to_draw_list(
+ info,
+ spatial_node_index,
+ clip_chain_id,
+ prim,
+ );
+ }
+ }
+
+ pub fn add_primitive<P>(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ clip_items: Vec<ClipItemKey>,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ ShadowItem: From<PendingPrimitive<P>>
+ {
+ // If a shadow context is not active, then add the primitive
+ // directly to the parent picture.
+ if self.pending_shadow_items.is_empty() {
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ clip_items,
+ prim,
+ );
+ } else {
+ debug_assert!(clip_items.is_empty(), "No per-prim clips expected for shadowed primitives");
+
+ // There is an active shadow context. Store as a pending primitive
+ // for processing during pop_all_shadows.
+ self.pending_shadow_items.push_back(PendingPrimitive {
+ spatial_node_index,
+ clip_chain_id,
+ info: *info,
+ prim,
+ }.into());
+ }
+ }
+
+ fn add_prim_to_draw_list<P>(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ prim: P,
+ )
+ where
+ P: InternablePrimitive,
+ Interners: AsMut<Interner<P>>,
+ {
+ let prim_instance = self.create_primitive(
+ info,
+ spatial_node_index,
+ clip_chain_id,
+ prim,
+ );
+ self.register_chase_primitive_by_rect(
+ &info.rect,
+ &prim_instance,
+ );
+ self.add_primitive_to_draw_list(
+ prim_instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ }
+
+ /// Push a new stacking context. Returns context that must be passed to pop_stacking_context().
+ fn push_stacking_context(
+ &mut self,
+ composite_ops: CompositeOps,
+ transform_style: TransformStyle,
+ prim_flags: PrimitiveFlags,
+ spatial_node_index: SpatialNodeIndex,
+ clip_id: Option<ClipId>,
+ requested_raster_space: RasterSpace,
+ flags: StackingContextFlags,
+ ) -> StackingContextInfo {
+ profile_scope!("push_stacking_context");
+
+ // Push current requested raster space on stack for prims to access
+ self.raster_space_stack.push(requested_raster_space);
+
+ // Get the transform-style of the parent stacking context,
+ // which determines if we *might* need to draw this on
+ // an intermediate surface for plane splitting purposes.
+ let (parent_is_3d, extra_3d_instance) = match self.sc_stack.last_mut() {
+ Some(ref mut sc) if sc.is_3d() => {
+ let flat_items_context_3d = match sc.context_3d {
+ Picture3DContext::In { ancestor_index, .. } => Picture3DContext::In {
+ root_data: None,
+ ancestor_index,
+ },
+ Picture3DContext::Out => panic!("Unexpected out of 3D context"),
+ };
+ // Cut the sequence of flat children before starting a child stacking context,
+ // so that the relative order between them and our current SC is preserved.
+ let extra_instance = sc.cut_item_sequence(
+ &mut self.prim_store,
+ &mut self.interners,
+ Some(PictureCompositeMode::Blit(BlitReason::PRESERVE3D)),
+ flat_items_context_3d,
+ );
+ let extra_instance = extra_instance.map(|(_, instance)| {
+ ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: sc.spatial_node_index,
+ flags: sc.prim_flags,
+ }
+ });
+ (true, extra_instance)
+ },
+ _ => (false, None),
+ };
+
+ if let Some(instance) = extra_3d_instance {
+ self.add_primitive_instance_to_3d_root(instance);
+ }
+
+ // If this is preserve-3d *or* the parent is, then this stacking
+ // context is participating in the 3d rendering context. In that
+ // case, hoist the picture up to the 3d rendering context
+ // container, so that it's rendered as a sibling with other
+ // elements in this context.
+ let participating_in_3d_context =
+ composite_ops.is_empty() &&
+ (parent_is_3d || transform_style == TransformStyle::Preserve3D);
+
+ let context_3d = if participating_in_3d_context {
+ // Get the spatial node index of the containing block, which
+ // defines the context of backface-visibility.
+ let ancestor_index = self.containing_block_stack
+ .last()
+ .cloned()
+ .unwrap_or(ROOT_SPATIAL_NODE_INDEX);
+
+ Picture3DContext::In {
+ root_data: if parent_is_3d {
+ None
+ } else {
+ Some(Vec::new())
+ },
+ ancestor_index,
+ }
+ } else {
+ Picture3DContext::Out
+ };
+
+ // Force an intermediate surface if the stacking context has a
+ // complex clip node. In the future, we may decide during
+ // prepare step to skip the intermediate surface if the
+ // clip node doesn't affect the stacking context rect.
+ let mut blit_reason = BlitReason::empty();
+
+ if flags.contains(StackingContextFlags::IS_BLEND_CONTAINER) {
+ blit_reason |= BlitReason::ISOLATE;
+ }
+
+ // If backface visibility is explicitly set, force this stacking
+ // context to be an off-screen surface. If part of a 3d context
+ // (common case) it will already be an off-screen surface. If
+ // the backface-vis is used while outside a 3d rendering context,
+ // this is an edge case.
+ if !prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE) {
+ blit_reason |= BlitReason::ISOLATE;
+ }
+
+ // If this stacking context has any complex clips, we need to draw it
+ // to an off-screen surface.
+ if let Some(clip_id) = clip_id {
+ if self.clip_store.has_complex_clips(clip_id) {
+ blit_reason |= BlitReason::CLIP;
+ }
+ }
+
+ let is_redundant = FlattenedStackingContext::is_redundant(
+ flags,
+ &context_3d,
+ &composite_ops,
+ prim_flags,
+ blit_reason,
+ self.sc_stack.last(),
+ );
+
+ let mut sc_info = StackingContextInfo {
+ pop_clip_root: false,
+ pop_stacking_context: false,
+ pop_containing_block: false,
+ };
+
+ // If this is not 3d, then it establishes an ancestor root for child 3d contexts.
+ if !participating_in_3d_context {
+ sc_info.pop_containing_block = true;
+ self.containing_block_stack.push(spatial_node_index);
+ }
+
+ // This muct be built before the push_clip_root logic below
+ let clip_chain_id = match clip_id {
+ Some(clip_id) => self.clip_store.get_or_build_clip_chain_id(clip_id),
+ None => ClipChainId::NONE,
+ };
+
+ // If this has a valid clip, it will create a new clip root
+ if let Some(clip_id) = clip_id {
+ sc_info.pop_clip_root = true;
+
+ // If this stacking context is redundant (prims will be pushed into
+ // the parent during pop) but it has a valid clip, then we need to
+ // add that clip to the current clip chain builder, so it's correctly
+ // applied to any primitives within this redundant stacking context.
+ // For the normal case, we start a new clip root, knowing that the
+ // clip on this stacking context will be pushed onto the stack during
+ // frame building.
+ if is_redundant {
+ self.clip_store.push_clip_root(Some(clip_id), true);
+ } else {
+ self.clip_store.push_clip_root(None, false);
+ }
+
+ // Push this clip id into the hit-testing scene for child primitives
+ self.hit_testing_scene.push_clip(clip_id);
+ }
+
+ // If not redundant, create a stacking context to hold primitive clusters
+ if !is_redundant {
+ sc_info.pop_stacking_context = true;
+
+ // Push the SC onto the stack, so we know how to handle things in
+ // pop_stacking_context.
+ self.sc_stack.push(FlattenedStackingContext {
+ prim_list: PrimitiveList::empty(),
+ prim_flags,
+ spatial_node_index,
+ clip_chain_id,
+ composite_ops,
+ blit_reason,
+ transform_style,
+ context_3d,
+ is_redundant,
+ is_backdrop_root: flags.contains(StackingContextFlags::IS_BACKDROP_ROOT),
+ });
+ }
+
+ sc_info
+ }
+
+ fn pop_stacking_context(
+ &mut self,
+ info: StackingContextInfo,
+ ) {
+ profile_scope!("pop_stacking_context");
+
+ // Pop off current raster space (pushed unconditionally in push_stacking_context)
+ self.raster_space_stack.pop().unwrap();
+
+ // If the stacking context formed a containing block, pop off the stack
+ if info.pop_containing_block {
+ self.containing_block_stack.pop().unwrap();
+ }
+
+ // If the stacking context established a clip root, pop off the stack
+ if info.pop_clip_root {
+ self.clip_store.pop_clip_root();
+ self.hit_testing_scene.pop_clip();
+ }
+
+ // If the stacking context was otherwise redundant, early exit
+ if !info.pop_stacking_context {
+ return;
+ }
+
+ let stacking_context = self.sc_stack.pop().unwrap();
+
+ let parent_is_empty = match self.sc_stack.last() {
+ Some(parent_sc) => {
+ assert!(!stacking_context.is_redundant);
+ parent_sc.prim_list.is_empty()
+ },
+ None => true,
+ };
+
+ let mut source = match stacking_context.context_3d {
+ // TODO(gw): For now, as soon as this picture is in
+ // a 3D context, we draw it to an intermediate
+ // surface and apply plane splitting. However,
+ // there is a large optimization opportunity here.
+ // During culling, we can check if there is actually
+ // perspective present, and skip the plane splitting
+ // completely when that is not the case.
+ Picture3DContext::In { ancestor_index, .. } => {
+ let composite_mode = Some(
+ PictureCompositeMode::Blit(BlitReason::PRESERVE3D | stacking_context.blit_reason)
+ );
+
+ // Add picture for this actual stacking context contents to render into.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ Picture3DContext::In { root_data: None, ancestor_index },
+ true,
+ stacking_context.prim_flags,
+ stacking_context.prim_list,
+ stacking_context.spatial_node_index,
+ PictureOptions::default(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ ClipChainId::NONE,
+ &mut self.interners,
+ );
+
+ PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ )
+ }
+ Picture3DContext::Out => {
+ if stacking_context.blit_reason.is_empty() {
+ PictureChainBuilder::from_prim_list(
+ stacking_context.prim_list,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ )
+ } else {
+ let composite_mode = Some(
+ PictureCompositeMode::Blit(stacking_context.blit_reason)
+ );
+
+ // Add picture for this actual stacking context contents to render into.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ Picture3DContext::Out,
+ true,
+ stacking_context.prim_flags,
+ stacking_context.prim_list,
+ stacking_context.spatial_node_index,
+ PictureOptions::default(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ ClipChainId::NONE,
+ &mut self.interners,
+ );
+
+ PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ )
+ }
+ }
+ };
+
+ // If establishing a 3d context, the `cur_instance` represents
+ // a picture with all the *trailing* immediate children elements.
+ // We append this to the preserve-3D picture set and make a container picture of them.
+ if let Picture3DContext::In { root_data: Some(mut prims), ancestor_index } = stacking_context.context_3d {
+ let instance = source.finalize(
+ ClipChainId::NONE,
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+
+ prims.push(ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: stacking_context.spatial_node_index,
+ flags: stacking_context.prim_flags,
+ });
+
+ let mut prim_list = PrimitiveList::empty();
+ for ext_prim in prims.drain(..) {
+ prim_list.add_prim(
+ ext_prim.instance,
+ LayoutRect::zero(),
+ ext_prim.spatial_node_index,
+ ext_prim.flags,
+ );
+ }
+
+ // This is the acttual picture representing our 3D hierarchy root.
+ let pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ None,
+ Picture3DContext::In {
+ root_data: Some(Vec::new()),
+ ancestor_index,
+ },
+ true,
+ stacking_context.prim_flags,
+ prim_list,
+ stacking_context.spatial_node_index,
+ PictureOptions::default(),
+ ))
+ );
+
+ let instance = create_prim_instance(
+ pic_index,
+ PictureCompositeKey::Identity,
+ ClipChainId::NONE,
+ &mut self.interners,
+ );
+
+ source = PictureChainBuilder::from_instance(
+ instance,
+ stacking_context.prim_flags,
+ stacking_context.spatial_node_index,
+ );
+ }
+
+ let has_filters = stacking_context.composite_ops.has_valid_filters();
+
+ source = self.wrap_prim_with_filters(
+ source,
+ stacking_context.composite_ops.filters,
+ stacking_context.composite_ops.filter_primitives,
+ stacking_context.composite_ops.filter_datas,
+ true,
+ );
+
+ // Same for mix-blend-mode, except we can skip if this primitive is the first in the parent
+ // stacking context.
+ // From https://drafts.fxtf.org/compositing-1/#generalformula, the formula for blending is:
+ // Cs = (1 - ab) x Cs + ab x Blend(Cb, Cs)
+ // where
+ // Cs = Source color
+ // ab = Backdrop alpha
+ // Cb = Backdrop color
+ //
+ // If we're the first primitive within a stacking context, then we can guarantee that the
+ // backdrop alpha will be 0, and then the blend equation collapses to just
+ // Cs = Cs, and the blend mode isn't taken into account at all.
+ if let (Some(mix_blend_mode), false) = (stacking_context.composite_ops.mix_blend_mode, parent_is_empty) {
+ let parent_is_isolated = match self.sc_stack.last() {
+ Some(parent_sc) => parent_sc.blit_reason.contains(BlitReason::ISOLATE),
+ None => false,
+ };
+ if parent_is_isolated {
+ let composite_mode = PictureCompositeMode::MixBlend(mix_blend_mode);
+
+ source = source.add_picture(
+ composite_mode,
+ Picture3DContext::Out,
+ PictureOptions::default(),
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+ } else {
+ // If we have a mix-blend-mode, the stacking context needs to be isolated
+ // to blend correctly as per the CSS spec.
+ // If not already isolated, we can't correctly blend.
+ warn!("found a mix-blend-mode outside a blend container, ignoring");
+ }
+ }
+
+ // Set the stacking context clip on the outermost picture in the chain,
+ // unless we already set it on the leaf picture.
+ let cur_instance = source.finalize(
+ stacking_context.clip_chain_id,
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+
+ // The primitive instance for the remainder of flat children of this SC
+ // if it's a part of 3D hierarchy but not the root of it.
+ let trailing_children_instance = match self.sc_stack.last_mut() {
+ // Preserve3D path (only relevant if there are no filters/mix-blend modes)
+ Some(ref parent_sc) if !has_filters && parent_sc.is_3d() => {
+ Some(cur_instance)
+ }
+ // Regular parenting path
+ Some(ref mut parent_sc) => {
+ parent_sc.prim_list.add_prim(
+ cur_instance,
+ LayoutRect::zero(),
+ stacking_context.spatial_node_index,
+ stacking_context.prim_flags,
+ );
+ None
+ }
+ // This must be the root stacking context
+ None => {
+ self.add_primitive_to_draw_list(
+ cur_instance,
+ LayoutRect::zero(),
+ stacking_context.spatial_node_index,
+ stacking_context.prim_flags,
+ );
+
+ None
+ }
+ };
+
+ // finally, if there any outstanding 3D primitive instances,
+ // find the 3D hierarchy root and add them there.
+ if let Some(instance) = trailing_children_instance {
+ self.add_primitive_instance_to_3d_root(ExtendedPrimitiveInstance {
+ instance,
+ spatial_node_index: stacking_context.spatial_node_index,
+ flags: stacking_context.prim_flags,
+ });
+ }
+
+ assert!(
+ self.pending_shadow_items.is_empty(),
+ "Found unpopped shadows when popping stacking context!"
+ );
+ }
+
+ pub fn push_reference_frame(
+ &mut self,
+ reference_frame_id: SpatialId,
+ parent_index: Option<SpatialNodeIndex>,
+ pipeline_id: PipelineId,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ ) -> SpatialNodeIndex {
+ let index = self.spatial_tree.add_reference_frame(
+ parent_index,
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ pipeline_id,
+ );
+ self.id_to_index_mapper.add_spatial_node(reference_frame_id, index);
+
+ index
+ }
+
+ pub fn push_root(
+ &mut self,
+ pipeline_id: PipelineId,
+ viewport_size: &LayoutSize,
+ ) {
+ if let ChasePrimitive::Id(id) = self.config.chase_primitive {
+ println!("Chasing {:?} by index", id);
+ register_prim_chase_id(id);
+ }
+
+ let spatial_node_index = self.push_reference_frame(
+ SpatialId::root_reference_frame(pipeline_id),
+ None,
+ pipeline_id,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform,
+ LayoutVector2D::zero(),
+ );
+
+ let viewport_rect = self.snap_rect(
+ &LayoutRect::new(LayoutPoint::zero(), *viewport_size),
+ spatial_node_index,
+ );
+
+ self.add_scroll_frame(
+ SpatialId::root_scroll_node(pipeline_id),
+ spatial_node_index,
+ ExternalScrollId(0, pipeline_id),
+ pipeline_id,
+ &viewport_rect,
+ &viewport_rect.size,
+ ScrollSensitivity::ScriptAndInputEvents,
+ ScrollFrameKind::PipelineRoot {
+ is_root_pipeline: true,
+ },
+ LayoutVector2D::zero(),
+ );
+ }
+
+ fn add_image_mask_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ space_and_clip: &SpaceAndClipInfo,
+ image_mask: &ImageMask,
+ ) {
+ let spatial_node_index = self.id_to_index_mapper.get_spatial_node_index(space_and_clip.spatial_id);
+
+ let snapped_mask_rect = self.snap_rect(
+ &image_mask.rect,
+ spatial_node_index,
+ );
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::image_mask(image_mask, snapped_mask_rect),
+ };
+
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: ClipNodeKind::Complex,
+ }
+ });
+
+ let instance = SceneClipInstance {
+ key: item,
+ clip: ClipInstance::new(handle, spatial_node_index),
+ };
+
+ self.clip_store.register_clip_template(
+ new_node_id,
+ space_and_clip.clip_id,
+ &[instance],
+ );
+ }
+
+ /// Add a new rectangle clip, positioned by the spatial node in the `space_and_clip`.
+ pub fn add_rect_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ space_and_clip: &SpaceAndClipInfo,
+ clip_rect: &LayoutRect,
+ ) {
+ let spatial_node_index = self.id_to_index_mapper.get_spatial_node_index(space_and_clip.spatial_id);
+
+ let snapped_clip_rect = self.snap_rect(
+ clip_rect,
+ spatial_node_index,
+ );
+
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rectangle(snapped_clip_rect, ClipMode::Clip),
+ };
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: ClipNodeKind::Rectangle,
+ }
+ });
+
+ let instance = SceneClipInstance {
+ key: item,
+ clip: ClipInstance::new(handle, spatial_node_index),
+ };
+
+ self.clip_store.register_clip_template(
+ new_node_id,
+ space_and_clip.clip_id,
+ &[instance],
+ );
+ }
+
+ pub fn add_rounded_rect_clip_node(
+ &mut self,
+ new_node_id: ClipId,
+ space_and_clip: &SpaceAndClipInfo,
+ clip: &ComplexClipRegion,
+ current_offset: LayoutVector2D,
+ ) {
+ let spatial_node_index = self.id_to_index_mapper.get_spatial_node_index(space_and_clip.spatial_id);
+
+ let snapped_region_rect = self.snap_rect(
+ &clip.rect.translate(current_offset),
+ spatial_node_index,
+ );
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ snapped_region_rect,
+ clip.radii,
+ clip.mode,
+ ),
+ };
+
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: ClipNodeKind::Complex,
+ }
+ });
+
+ let instance = SceneClipInstance {
+ key: item,
+ clip: ClipInstance::new(handle, spatial_node_index),
+ };
+
+ self.clip_store.register_clip_template(
+ new_node_id,
+ space_and_clip.clip_id,
+ &[instance],
+ );
+ }
+
+ pub fn add_clip_node<I>(
+ &mut self,
+ new_node_id: ClipId,
+ space_and_clip: &SpaceAndClipInfo,
+ clip_region: ClipRegion<I>,
+ )
+ where
+ I: IntoIterator<Item = ComplexClipRegion>
+ {
+ // Map the ClipId for the positioning node to a spatial node index.
+ let spatial_node_index = self.id_to_index_mapper.get_spatial_node_index(space_and_clip.spatial_id);
+
+ let snapped_clip_rect = self.snap_rect(
+ &clip_region.main,
+ spatial_node_index,
+ );
+ let mut instances: SmallVec<[SceneClipInstance; 4]> = SmallVec::new();
+
+ // Intern each clip item in this clip node, and add the interned
+ // handle to a clip chain node, parented to form a chain.
+ // TODO(gw): We could re-structure this to share some of the
+ // interning and chaining code.
+
+ // Build the clip sources from the supplied region.
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rectangle(snapped_clip_rect, ClipMode::Clip),
+ };
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: ClipNodeKind::Rectangle,
+ }
+ });
+ instances.push(
+ SceneClipInstance {
+ key: item,
+ clip: ClipInstance::new(handle, spatial_node_index),
+ },
+ );
+
+ for region in clip_region.complex_clips {
+ let snapped_region_rect = self.snap_rect(&region.rect, spatial_node_index);
+ let item = ClipItemKey {
+ kind: ClipItemKeyKind::rounded_rect(
+ snapped_region_rect,
+ region.radii,
+ region.mode,
+ ),
+ };
+
+ let handle = self
+ .interners
+ .clip
+ .intern(&item, || {
+ ClipInternData {
+ clip_node_kind: ClipNodeKind::Complex,
+ }
+ });
+
+ instances.push(
+ SceneClipInstance {
+ key: item,
+ clip: ClipInstance::new(handle, spatial_node_index),
+ },
+ );
+ }
+
+ self.clip_store.register_clip_template(
+ new_node_id,
+ space_and_clip.clip_id,
+ &instances,
+ );
+ }
+
+ pub fn add_scroll_frame(
+ &mut self,
+ new_node_id: SpatialId,
+ parent_node_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ pipeline_id: PipelineId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ scroll_sensitivity: ScrollSensitivity,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> SpatialNodeIndex {
+ let node_index = self.spatial_tree.add_scroll_frame(
+ parent_node_index,
+ external_id,
+ pipeline_id,
+ frame_rect,
+ content_size,
+ scroll_sensitivity,
+ frame_kind,
+ external_scroll_offset,
+ );
+ self.id_to_index_mapper.add_spatial_node(new_node_id, node_index);
+ node_index
+ }
+
+ pub fn push_shadow(
+ &mut self,
+ shadow: Shadow,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ should_inflate: bool,
+ ) {
+ // Store this shadow in the pending list, for processing
+ // during pop_all_shadows.
+ self.pending_shadow_items.push_back(ShadowItem::Shadow(PendingShadow {
+ shadow,
+ spatial_node_index,
+ clip_chain_id,
+ should_inflate,
+ }));
+ }
+
+ pub fn pop_all_shadows(
+ &mut self,
+ ) {
+ assert!(!self.pending_shadow_items.is_empty(), "popped shadows, but none were present");
+
+ let mut items = mem::replace(&mut self.pending_shadow_items, VecDeque::new());
+
+ //
+ // The pending_shadow_items queue contains a list of shadows and primitives
+ // that were pushed during the active shadow context. To process these, we:
+ //
+ // Iterate the list, popping an item from the front each iteration.
+ //
+ // If the item is a shadow:
+ // - Create a shadow picture primitive.
+ // - Add *any* primitives that remain in the item list to this shadow.
+ // If the item is a primitive:
+ // - Add that primitive as a normal item (if alpha > 0)
+ //
+
+ while let Some(item) = items.pop_front() {
+ match item {
+ ShadowItem::Shadow(pending_shadow) => {
+ // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
+ // "the image that would be generated by applying to the shadow a
+ // Gaussian blur with a standard deviation equal to half the blur radius."
+ let std_deviation = pending_shadow.shadow.blur_radius * 0.5;
+
+ // Add any primitives that come after this shadow in the item
+ // list to this shadow.
+ let mut prim_list = PrimitiveList::empty();
+ let blur_filter = Filter::Blur(std_deviation, std_deviation);
+ let blur_is_noop = blur_filter.is_noop();
+
+ for item in &items {
+ let (instance, info, spatial_node_index) = match item {
+ ShadowItem::Image(ref pending_image) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_image,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::LineDecoration(ref pending_line_dec) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_line_dec,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::NormalBorder(ref pending_border) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_border,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::Primitive(ref pending_primitive) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_primitive,
+ blur_is_noop,
+ )
+ }
+ ShadowItem::TextRun(ref pending_text_run) => {
+ self.create_shadow_prim(
+ &pending_shadow,
+ pending_text_run,
+ blur_is_noop,
+ )
+ }
+ _ => {
+ continue;
+ }
+ };
+
+ if blur_is_noop {
+ self.add_primitive_to_draw_list(
+ instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ } else {
+ prim_list.add_prim(
+ instance,
+ info.rect,
+ spatial_node_index,
+ info.flags,
+ );
+ }
+ }
+
+ // No point in adding a shadow here if there were no primitives
+ // added to the shadow.
+ if !prim_list.is_empty() {
+ // Create a picture that the shadow primitives will be added to. If the
+ // blur radius is 0, the code in Picture::prepare_for_render will
+ // detect this and mark the picture to be drawn directly into the
+ // parent picture, which avoids an intermediate surface and blur.
+ let blur_filter = Filter::Blur(std_deviation, std_deviation);
+ assert!(!blur_filter.is_noop());
+ let composite_mode = Some(PictureCompositeMode::Filter(blur_filter));
+ let composite_mode_key = composite_mode.clone().into();
+
+ // Pass through configuration information about whether WR should
+ // do the bounding rect inflation for text shadows.
+ let options = PictureOptions {
+ inflate_if_required: pending_shadow.should_inflate,
+ };
+
+ // Create the primitive to draw the shadow picture into the scene.
+ let shadow_pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode,
+ Picture3DContext::Out,
+ false,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ prim_list,
+ pending_shadow.spatial_node_index,
+ options,
+ ))
+ );
+
+ let shadow_pic_key = PictureKey::new(
+ Picture { composite_mode_key },
+ );
+
+ let shadow_prim_data_handle = self.interners
+ .picture
+ .intern(&shadow_pic_key, || ());
+
+ let shadow_prim_instance = PrimitiveInstance::new(
+ LayoutRect::max_rect(),
+ PrimitiveInstanceKind::Picture {
+ data_handle: shadow_prim_data_handle,
+ pic_index: shadow_pic_index,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ },
+ pending_shadow.clip_chain_id,
+ );
+
+ // Add the shadow primitive. This must be done before pushing this
+ // picture on to the shadow stack, to avoid infinite recursion!
+ self.add_primitive_to_draw_list(
+ shadow_prim_instance,
+ LayoutRect::zero(),
+ pending_shadow.spatial_node_index,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ );
+ }
+ }
+ ShadowItem::Image(pending_image) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_image,
+ )
+ },
+ ShadowItem::LineDecoration(pending_line_dec) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_line_dec,
+ )
+ },
+ ShadowItem::NormalBorder(pending_border) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_border,
+ )
+ },
+ ShadowItem::Primitive(pending_primitive) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_primitive,
+ )
+ },
+ ShadowItem::TextRun(pending_text_run) => {
+ self.add_shadow_prim_to_draw_list(
+ pending_text_run,
+ )
+ },
+ }
+ }
+
+ debug_assert!(items.is_empty());
+ self.pending_shadow_items = items;
+ }
+
+ fn create_shadow_prim<P>(
+ &mut self,
+ pending_shadow: &PendingShadow,
+ pending_primitive: &PendingPrimitive<P>,
+ blur_is_noop: bool,
+ ) -> (PrimitiveInstance, LayoutPrimitiveInfo, SpatialNodeIndex)
+ where
+ P: InternablePrimitive + CreateShadow,
+ Interners: AsMut<Interner<P>>,
+ {
+ // Offset the local rect and clip rect by the shadow offset. The pending
+ // primitive has already been snapped, but we will need to snap the
+ // shadow after translation. We don't need to worry about the size
+ // changing because the shadow has the same raster space as the
+ // primitive, and thus we know the size is already rounded.
+ let mut info = pending_primitive.info.clone();
+ info.rect = self.snap_rect(
+ &info.rect.translate(pending_shadow.shadow.offset),
+ pending_primitive.spatial_node_index,
+ );
+ info.clip_rect = self.snap_rect(
+ &info.clip_rect.translate(pending_shadow.shadow.offset),
+ pending_primitive.spatial_node_index,
+ );
+
+ // Construct and add a primitive for the given shadow.
+ let shadow_prim_instance = self.create_primitive(
+ &info,
+ pending_primitive.spatial_node_index,
+ pending_primitive.clip_chain_id,
+ pending_primitive.prim.create_shadow(
+ &pending_shadow.shadow,
+ blur_is_noop,
+ self.raster_space_stack.last().cloned().unwrap(),
+ ),
+ );
+
+ (shadow_prim_instance, info, pending_primitive.spatial_node_index)
+ }
+
+ fn add_shadow_prim_to_draw_list<P>(
+ &mut self,
+ pending_primitive: PendingPrimitive<P>,
+ ) where
+ P: InternablePrimitive + IsVisible,
+ Interners: AsMut<Interner<P>>,
+ {
+ // For a normal primitive, if it has alpha > 0, then we add this
+ // as a normal primitive to the parent picture.
+ if pending_primitive.prim.is_visible() {
+ self.add_prim_to_draw_list(
+ &pending_primitive.info,
+ pending_primitive.spatial_node_index,
+ pending_primitive.clip_chain_id,
+ pending_primitive.prim,
+ );
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ fn register_chase_primitive_by_rect(
+ &mut self,
+ rect: &LayoutRect,
+ prim_instance: &PrimitiveInstance,
+ ) {
+ if ChasePrimitive::LocalRect(*rect) == self.config.chase_primitive {
+ println!("Chasing {:?} by local rect", prim_instance.id);
+ register_prim_chase_id(prim_instance.id);
+ }
+ }
+
+ #[cfg(not(debug_assertions))]
+ fn register_chase_primitive_by_rect(
+ &mut self,
+ _rect: &LayoutRect,
+ _prim_instance: &PrimitiveInstance,
+ ) {
+ }
+
+ pub fn add_clear_rectangle(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ ) {
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ PrimitiveKeyKind::Clear,
+ );
+ }
+
+ pub fn add_line(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ wavy_line_thickness: f32,
+ orientation: LineOrientation,
+ color: ColorF,
+ style: LineStyle,
+ ) {
+ // For line decorations, we can construct the render task cache key
+ // here during scene building, since it doesn't depend on device
+ // pixel ratio or transform.
+ let mut info = info.clone();
+
+ let size = get_line_decoration_size(
+ &info.rect.size,
+ orientation,
+ style,
+ wavy_line_thickness,
+ );
+
+ let cache_key = size.map(|size| {
+ // If dotted, adjust the clip rect to ensure we don't draw a final
+ // partial dot.
+ if style == LineStyle::Dotted {
+ let clip_size = match orientation {
+ LineOrientation::Horizontal => {
+ LayoutSize::new(
+ size.width * (info.rect.size.width / size.width).floor(),
+ info.rect.size.height,
+ )
+ }
+ LineOrientation::Vertical => {
+ LayoutSize::new(
+ info.rect.size.width,
+ size.height * (info.rect.size.height / size.height).floor(),
+ )
+ }
+ };
+ let clip_rect = LayoutRect::new(
+ info.rect.origin,
+ clip_size,
+ );
+ info.clip_rect = clip_rect
+ .intersection(&info.clip_rect)
+ .unwrap_or_else(LayoutRect::zero);
+ }
+
+ LineDecorationCacheKey {
+ style,
+ orientation,
+ wavy_line_thickness: Au::from_f32_px(wavy_line_thickness),
+ size: size.to_au(),
+ }
+ });
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &info,
+ Vec::new(),
+ LineDecoration {
+ cache_key,
+ color: color.into(),
+ },
+ );
+ }
+
+ pub fn add_border(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ border_item: &BorderDisplayItem,
+ gradient_stops: ItemRange<GradientStop>,
+ ) {
+ match border_item.details {
+ BorderDetails::NinePatch(ref border) => {
+ let nine_patch = NinePatchDescriptor {
+ width: border.width,
+ height: border.height,
+ slice: border.slice,
+ fill: border.fill,
+ repeat_horizontal: border.repeat_horizontal,
+ repeat_vertical: border.repeat_vertical,
+ outset: border.outset.into(),
+ widths: border_item.widths.into(),
+ };
+
+ match border.source {
+ NinePatchBorderSource::Image(image_key) => {
+ let prim = ImageBorder {
+ request: ImageRequest {
+ key: image_key,
+ rendering: ImageRendering::Auto,
+ tile: None,
+ },
+ nine_patch,
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::Gradient(gradient) => {
+ let prim = match self.create_linear_gradient_prim(
+ &info,
+ gradient.start_point,
+ gradient.end_point,
+ gradient_stops,
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ ) {
+ Some(prim) => prim,
+ None => return,
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::RadialGradient(gradient) => {
+ let prim = self.create_radial_gradient_prim(
+ &info,
+ gradient.center,
+ gradient.start_offset * gradient.radius.width,
+ gradient.end_offset * gradient.radius.width,
+ gradient.radius.width / gradient.radius.height,
+ gradient_stops,
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ NinePatchBorderSource::ConicGradient(gradient) => {
+ let prim = self.create_conic_gradient_prim(
+ &info,
+ gradient.center,
+ gradient.angle,
+ gradient.start_offset,
+ gradient.end_offset,
+ gradient_stops,
+ gradient.extend_mode,
+ LayoutSize::new(border.height as f32, border.width as f32),
+ LayoutSize::zero(),
+ Some(Box::new(nine_patch)),
+ );
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ prim,
+ );
+ }
+ };
+ }
+ BorderDetails::Normal(ref border) => {
+ self.add_normal_border(
+ info,
+ border,
+ border_item.widths,
+ spatial_node_index,
+ clip_chain_id,
+ );
+ }
+ }
+ }
+
+ pub fn create_linear_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ start_point: LayoutPoint,
+ end_point: LayoutPoint,
+ stops: ItemRange<GradientStop>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ ) -> Option<LinearGradient> {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let mut max_alpha: f32 = 0.0;
+
+ let stops = stops.iter().map(|stop| {
+ max_alpha = max_alpha.max(stop.color.a);
+ GradientStopKey {
+ offset: stop.offset,
+ color: stop.color.into(),
+ }
+ }).collect();
+
+ // If all the stops have no alpha, then this
+ // gradient can't contribute to the scene.
+ if max_alpha <= 0.0 {
+ return None;
+ }
+
+ // Try to ensure that if the gradient is specified in reverse, then so long as the stops
+ // are also supplied in reverse that the rendered result will be equivalent. To do this,
+ // a reference orientation for the gradient line must be chosen, somewhat arbitrarily, so
+ // just designate the reference orientation as start < end. Aligned gradient rendering
+ // manages to produce the same result regardless of orientation, so don't worry about
+ // reversing in that case.
+ let reverse_stops = start_point.x > end_point.x ||
+ (start_point.x == end_point.x && start_point.y > end_point.y);
+
+ // To get reftests exactly matching with reverse start/end
+ // points, it's necessary to reverse the gradient
+ // line in some cases.
+ let (sp, ep) = if reverse_stops {
+ (end_point, start_point)
+ } else {
+ (start_point, end_point)
+ };
+
+ Some(LinearGradient {
+ extend_mode,
+ start_point: sp.into(),
+ end_point: ep.into(),
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ stops,
+ reverse_stops,
+ nine_patch,
+ })
+ }
+
+ pub fn create_radial_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ center: LayoutPoint,
+ start_radius: f32,
+ end_radius: f32,
+ ratio_xy: f32,
+ stops: ItemRange<GradientStop>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ ) -> RadialGradient {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let params = RadialGradientParams {
+ start_radius,
+ end_radius,
+ ratio_xy,
+ };
+
+ let stops = stops.iter().map(|stop| {
+ GradientStopKey {
+ offset: stop.offset,
+ color: stop.color.into(),
+ }
+ }).collect();
+
+ RadialGradient {
+ extend_mode,
+ center: center.into(),
+ params,
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ nine_patch,
+ stops,
+ }
+ }
+
+ pub fn create_conic_gradient_prim(
+ &mut self,
+ info: &LayoutPrimitiveInfo,
+ center: LayoutPoint,
+ angle: f32,
+ start_offset: f32,
+ end_offset: f32,
+ stops: ItemRange<GradientStop>,
+ extend_mode: ExtendMode,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ nine_patch: Option<Box<NinePatchDescriptor>>,
+ ) -> ConicGradient {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+
+ let stops = stops.iter().map(|stop| {
+ GradientStopKey {
+ offset: stop.offset,
+ color: stop.color.into(),
+ }
+ }).collect();
+
+ ConicGradient {
+ extend_mode,
+ center: center.into(),
+ params: ConicGradientParams { angle, start_offset, end_offset },
+ stretch_size: stretch_size.into(),
+ tile_spacing: tile_spacing.into(),
+ nine_patch,
+ stops,
+ }
+ }
+
+ pub fn add_text(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ prim_info: &LayoutPrimitiveInfo,
+ font_instance_key: &FontInstanceKey,
+ text_color: &ColorF,
+ glyph_range: ItemRange<GlyphInstance>,
+ glyph_options: Option<GlyphOptions>,
+ ) {
+ let offset = self.current_offset(spatial_node_index);
+
+ let text_run = {
+ let instance_map = self.font_instances.lock().unwrap();
+ let font_instance = match instance_map.get(font_instance_key) {
+ Some(instance) => instance,
+ None => {
+ warn!("Unknown font instance key");
+ debug!("key={:?}", font_instance_key);
+ return;
+ }
+ };
+
+ // Trivial early out checks
+ if font_instance.size <= FontSize::zero() {
+ return;
+ }
+
+ // TODO(gw): Use a proper algorithm to select
+ // whether this item should be rendered with
+ // subpixel AA!
+ let mut render_mode = self.config
+ .default_font_render_mode
+ .limit_by(font_instance.render_mode);
+ let mut flags = font_instance.flags;
+ if let Some(options) = glyph_options {
+ render_mode = render_mode.limit_by(options.render_mode);
+ flags |= options.flags;
+ }
+
+ let font = FontInstance::new(
+ Arc::clone(font_instance),
+ (*text_color).into(),
+ render_mode,
+ flags,
+ );
+
+ // TODO(gw): It'd be nice not to have to allocate here for creating
+ // the primitive key, when the common case is that the
+ // hash will match and we won't end up creating a new
+ // primitive template.
+ let prim_offset = prim_info.rect.origin.to_vector() - offset;
+ let glyphs = glyph_range
+ .iter()
+ .map(|glyph| {
+ GlyphInstance {
+ index: glyph.index,
+ point: glyph.point - prim_offset,
+ }
+ })
+ .collect();
+
+ // Query the current requested raster space (stack handled by push/pop
+ // stacking context).
+ let requested_raster_space = self.raster_space_stack
+ .last()
+ .cloned()
+ .unwrap();
+
+ TextRun {
+ glyphs: Arc::new(glyphs),
+ font,
+ shadow: false,
+ requested_raster_space,
+ }
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ prim_info,
+ Vec::new(),
+ text_run,
+ );
+ }
+
+ pub fn add_image(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ stretch_size: LayoutSize,
+ mut tile_spacing: LayoutSize,
+ image_key: ImageKey,
+ image_rendering: ImageRendering,
+ alpha_type: AlphaType,
+ color: ColorF,
+ ) {
+ let mut prim_rect = info.rect;
+ simplify_repeated_primitive(&stretch_size, &mut tile_spacing, &mut prim_rect);
+ let info = LayoutPrimitiveInfo {
+ rect: prim_rect,
+ .. *info
+ };
+
+ self.add_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ &info,
+ Vec::new(),
+ Image {
+ key: image_key,
+ tile_spacing: tile_spacing.into(),
+ stretch_size: stretch_size.into(),
+ color: color.into(),
+ image_rendering,
+ alpha_type,
+ },
+ );
+ }
+
+ pub fn add_yuv_image(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ yuv_data: YuvData,
+ color_depth: ColorDepth,
+ color_space: YuvColorSpace,
+ color_range: ColorRange,
+ image_rendering: ImageRendering,
+ ) {
+ let format = yuv_data.get_format();
+ let yuv_key = match yuv_data {
+ YuvData::NV12(plane_0, plane_1) => [plane_0, plane_1, ImageKey::DUMMY],
+ YuvData::PlanarYCbCr(plane_0, plane_1, plane_2) => [plane_0, plane_1, plane_2],
+ YuvData::InterleavedYCbCr(plane_0) => [plane_0, ImageKey::DUMMY, ImageKey::DUMMY],
+ };
+
+ self.add_nonshadowable_primitive(
+ spatial_node_index,
+ clip_chain_id,
+ info,
+ Vec::new(),
+ YuvImage {
+ color_depth,
+ yuv_key,
+ format,
+ color_space,
+ color_range,
+ image_rendering,
+ },
+ );
+ }
+
+ fn add_primitive_instance_to_3d_root(
+ &mut self,
+ prim: ExtendedPrimitiveInstance,
+ ) {
+ // find the 3D root and append to the children list
+ for sc in self.sc_stack.iter_mut().rev() {
+ match sc.context_3d {
+ Picture3DContext::In { root_data: Some(ref mut prims), .. } => {
+ prims.push(prim);
+ break;
+ }
+ Picture3DContext::In { .. } => {}
+ Picture3DContext::Out => panic!("Unable to find 3D root"),
+ }
+ }
+ }
+
+ pub fn add_backdrop_filter(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: &LayoutPrimitiveInfo,
+ filters: Vec<Filter>,
+ filter_datas: Vec<FilterData>,
+ filter_primitives: Vec<FilterPrimitive>,
+ ) {
+ let mut backdrop_pic_index = match self.cut_backdrop_picture() {
+ // Backdrop contains no content, so no need to add backdrop-filter
+ None => return,
+ Some(backdrop_pic_index) => backdrop_pic_index,
+ };
+
+ let backdrop_spatial_node_index = self.prim_store.pictures[backdrop_pic_index.0].spatial_node_index;
+
+ let mut instance = self.create_primitive(
+ info,
+ // TODO(cbrewster): This is a bit of a hack to help figure out the correct sizing of the backdrop
+ // region. By makings sure to include this, the clip chain instance computes the correct clip rect,
+ // but we don't actually apply the filtered backdrop clip yet (this is done to the last instance in
+ // the filter chain below).
+ backdrop_spatial_node_index,
+ clip_chain_id,
+ Backdrop {
+ pic_index: backdrop_pic_index,
+ spatial_node_index,
+ border_rect: info.rect.into(),
+ },
+ );
+
+ // We will append the filtered backdrop to the backdrop root, but we need to
+ // make sure all clips between the current stacking context and backdrop root
+ // are taken into account. So we wrap the backdrop filter instance with a picture with
+ // a clip for each stacking context.
+ for stacking_context in self.sc_stack.iter().rev().take_while(|sc| !sc.is_backdrop_root) {
+ let clip_chain_id = stacking_context.clip_chain_id;
+ let prim_flags = stacking_context.prim_flags;
+ let composite_mode = None;
+
+ let mut prim_list = PrimitiveList::empty();
+ prim_list.add_prim(
+ instance,
+ LayoutRect::zero(),
+ backdrop_spatial_node_index,
+ prim_flags,
+ );
+
+ backdrop_pic_index = PictureIndex(self.prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ Picture3DContext::Out,
+ true,
+ prim_flags,
+ prim_list,
+ backdrop_spatial_node_index,
+ PictureOptions {
+ inflate_if_required: false,
+ },
+ ))
+ );
+
+ instance = create_prim_instance(
+ backdrop_pic_index,
+ composite_mode.into(),
+ clip_chain_id,
+ &mut self.interners,
+ );
+ }
+
+ let mut source = PictureChainBuilder::from_instance(
+ instance,
+ info.flags,
+ backdrop_spatial_node_index,
+ );
+
+ source = self.wrap_prim_with_filters(
+ source,
+ filters,
+ filter_primitives,
+ filter_datas,
+ false,
+ );
+
+ // Apply filters from all stacking contexts up to, but not including the backdrop root.
+ // Gecko pushes separate stacking contexts for filters and opacity,
+ // so we must iterate through multiple stacking contexts to find all effects
+ // that need to be applied to the filtered backdrop.
+ let backdrop_root_pos = self.sc_stack.iter().rposition(|sc| sc.is_backdrop_root).expect("no backdrop root?");
+ for i in ((backdrop_root_pos + 1)..self.sc_stack.len()).rev() {
+ let stacking_context = &self.sc_stack[i];
+ let filters = stacking_context.composite_ops.filters.clone();
+ let filter_primitives = stacking_context.composite_ops.filter_primitives.clone();
+ let filter_datas = stacking_context.composite_ops.filter_datas.clone();
+
+ source = self.wrap_prim_with_filters(
+ source,
+ filters,
+ filter_primitives,
+ filter_datas,
+ false,
+ );
+ }
+
+ let filtered_instance = source.finalize(
+ clip_chain_id,
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+
+ self.sc_stack
+ .iter_mut()
+ .rev()
+ .find(|sc| sc.is_backdrop_root)
+ .unwrap()
+ .prim_list
+ .add_prim(
+ filtered_instance,
+ LayoutRect::zero(),
+ backdrop_spatial_node_index,
+ info.flags,
+ );
+ }
+
+ pub fn cut_backdrop_picture(&mut self) -> Option<PictureIndex> {
+ let mut flattened_items = None;
+ let mut backdrop_root = None;
+ let mut spatial_node_index = SpatialNodeIndex::INVALID;
+ let mut prim_flags = PrimitiveFlags::default();
+ for sc in self.sc_stack.iter_mut().rev() {
+ // Add child contents to parent stacking context
+ if let Some((_, flattened_instance)) = flattened_items.take() {
+ sc.prim_list.add_prim(
+ flattened_instance,
+ LayoutRect::zero(),
+ spatial_node_index,
+ prim_flags,
+ );
+ }
+ flattened_items = sc.cut_item_sequence(
+ &mut self.prim_store,
+ &mut self.interners,
+ None,
+ Picture3DContext::Out,
+ );
+ spatial_node_index = sc.spatial_node_index;
+ prim_flags = sc.prim_flags;
+ if sc.is_backdrop_root {
+ backdrop_root = Some(sc);
+ break;
+ }
+ }
+
+ let (pic_index, instance) = flattened_items?;
+ self.prim_store.pictures[pic_index.0].requested_composite_mode = Some(PictureCompositeMode::Blit(BlitReason::BACKDROP));
+ backdrop_root.expect("no backdrop root found")
+ .prim_list
+ .add_prim(
+ instance,
+ LayoutRect::zero(),
+ spatial_node_index,
+ prim_flags,
+ );
+
+ Some(pic_index)
+ }
+
+ #[must_use]
+ fn wrap_prim_with_filters(
+ &mut self,
+ mut source: PictureChainBuilder,
+ mut filter_ops: Vec<Filter>,
+ mut filter_primitives: Vec<FilterPrimitive>,
+ filter_datas: Vec<FilterData>,
+ inflate_if_required: bool,
+ ) -> PictureChainBuilder {
+ // TODO(cbrewster): Currently CSS and SVG filters live side by side in WebRender, but unexpected results will
+ // happen if they are used simulataneously. Gecko only provides either filter ops or filter primitives.
+ // At some point, these two should be combined and CSS filters should be expressed in terms of SVG filters.
+ assert!(filter_ops.is_empty() || filter_primitives.is_empty(),
+ "Filter ops and filter primitives are not allowed on the same stacking context.");
+
+ // For each filter, create a new image with that composite mode.
+ let mut current_filter_data_index = 0;
+ for filter in &mut filter_ops {
+ let composite_mode = match filter {
+ Filter::ComponentTransfer => {
+ let filter_data =
+ &filter_datas[current_filter_data_index];
+ let filter_data = filter_data.sanitize();
+ current_filter_data_index = current_filter_data_index + 1;
+ if filter_data.is_identity() {
+ continue
+ } else {
+ let filter_data_key = SFilterDataKey {
+ data:
+ SFilterData {
+ r_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_r_type, &filter_data.r_values),
+ g_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_g_type, &filter_data.g_values),
+ b_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_b_type, &filter_data.b_values),
+ a_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_a_type, &filter_data.a_values),
+ },
+ };
+
+ let handle = self.interners
+ .filter_data
+ .intern(&filter_data_key, || ());
+ PictureCompositeMode::ComponentTransferFilter(handle)
+ }
+ }
+ _ => {
+ if filter.is_noop() {
+ continue;
+ } else {
+ PictureCompositeMode::Filter(filter.clone())
+ }
+ }
+ };
+
+ source = source.add_picture(
+ composite_mode,
+ Picture3DContext::Out,
+ PictureOptions { inflate_if_required },
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+ }
+
+ if !filter_primitives.is_empty() {
+ let filter_datas = filter_datas.iter()
+ .map(|filter_data| filter_data.sanitize())
+ .map(|filter_data| {
+ SFilterData {
+ r_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_r_type, &filter_data.r_values),
+ g_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_g_type, &filter_data.g_values),
+ b_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_b_type, &filter_data.b_values),
+ a_func: SFilterDataComponent::from_functype_values(
+ filter_data.func_a_type, &filter_data.a_values),
+ }
+ })
+ .collect();
+
+ // Sanitize filter inputs
+ for primitive in &mut filter_primitives {
+ primitive.sanitize();
+ }
+
+ let composite_mode = PictureCompositeMode::SvgFilter(
+ filter_primitives,
+ filter_datas,
+ );
+
+ source = source.add_picture(
+ composite_mode,
+ Picture3DContext::Out,
+ PictureOptions { inflate_if_required },
+ &mut self.interners,
+ &mut self.prim_store,
+ );
+ }
+
+ source
+ }
+}
+
+
+pub trait CreateShadow {
+ fn create_shadow(
+ &self,
+ shadow: &Shadow,
+ blur_is_noop: bool,
+ current_raster_space: RasterSpace,
+ ) -> Self;
+}
+
+pub trait IsVisible {
+ fn is_visible(&self) -> bool;
+}
+
+/// A primitive instance + some extra information about the primitive. This is
+/// stored when constructing 3d rendering contexts, which involve cutting
+/// primitive lists.
+struct ExtendedPrimitiveInstance {
+ instance: PrimitiveInstance,
+ spatial_node_index: SpatialNodeIndex,
+ flags: PrimitiveFlags,
+}
+
+/// Internal tracking information about the currently pushed stacking context.
+/// Used to track what operations need to happen when a stacking context is popped.
+struct StackingContextInfo {
+ /// If true, pop an entry from the clip root stack.
+ pop_clip_root: bool,
+ /// If true, pop and entry from the containing block stack.
+ pop_containing_block: bool,
+ /// If true, pop an entry from the flattened stacking context stack.
+ pop_stacking_context: bool,
+}
+
+/// Properties of a stacking context that are maintained
+/// during creation of the scene. These structures are
+/// not persisted after the initial scene build.
+struct FlattenedStackingContext {
+ /// The list of primitive instances added to this stacking context.
+ prim_list: PrimitiveList,
+
+ /// Primitive instance flags for compositing this stacking context
+ prim_flags: PrimitiveFlags,
+
+ /// The positioning node for this stacking context
+ spatial_node_index: SpatialNodeIndex,
+
+ /// The clip chain for this stacking context
+ clip_chain_id: ClipChainId,
+
+ /// The list of filters / mix-blend-mode for this
+ /// stacking context.
+ composite_ops: CompositeOps,
+
+ /// Bitfield of reasons this stacking context needs to
+ /// be an offscreen surface.
+ blit_reason: BlitReason,
+
+ /// CSS transform-style property.
+ transform_style: TransformStyle,
+
+ /// Defines the relationship to a preserve-3D hiearachy.
+ context_3d: Picture3DContext<ExtendedPrimitiveInstance>,
+
+ /// True if this stacking context is a backdrop root.
+ is_backdrop_root: bool,
+
+ /// True if this stacking context is redundant (i.e. doesn't require a surface)
+ is_redundant: bool,
+}
+
+impl FlattenedStackingContext {
+ /// Return true if the stacking context has a valid preserve-3d property
+ pub fn is_3d(&self) -> bool {
+ self.transform_style == TransformStyle::Preserve3D && self.composite_ops.is_empty()
+ }
+
+ /// Return true if the stacking context isn't needed.
+ pub fn is_redundant(
+ sc_flags: StackingContextFlags,
+ context_3d: &Picture3DContext<ExtendedPrimitiveInstance>,
+ composite_ops: &CompositeOps,
+ prim_flags: PrimitiveFlags,
+ blit_reason: BlitReason,
+ parent: Option<&FlattenedStackingContext>,
+ ) -> bool {
+ // If this is a backdrop or blend container, it's needed
+ if sc_flags.intersects(StackingContextFlags::IS_BACKDROP_ROOT | StackingContextFlags::IS_BLEND_CONTAINER) {
+ return false;
+ }
+
+ // Any 3d context is required
+ if let Picture3DContext::In { .. } = context_3d {
+ return false;
+ }
+
+ // If any filters are present that affect the output
+ if composite_ops.has_valid_filters() {
+ return false;
+ }
+
+ // We can skip mix-blend modes if they are the first primitive in a stacking context,
+ // see pop_stacking_context for a full explanation.
+ if composite_ops.mix_blend_mode.is_some() {
+ if let Some(parent) = parent {
+ if !parent.prim_list.is_empty() {
+ return false;
+ }
+ }
+ }
+
+ // If need to isolate in surface due to clipping / mix-blend-mode
+ if !blit_reason.is_empty() {
+ return false;
+ }
+
+ // If this stacking context is a scrollbar, retain it so it can form a picture cache slice
+ if prim_flags.contains(PrimitiveFlags::IS_SCROLLBAR_CONTAINER) {
+ return false;
+ }
+
+ // It is redundant!
+ true
+ }
+
+ /// Cut the sequence of the immediate children recorded so far and generate a picture from them.
+ pub fn cut_item_sequence(
+ &mut self,
+ prim_store: &mut PrimitiveStore,
+ interners: &mut Interners,
+ composite_mode: Option<PictureCompositeMode>,
+ flat_items_context_3d: Picture3DContext<OrderedPictureChild>,
+ ) -> Option<(PictureIndex, PrimitiveInstance)> {
+ if self.prim_list.is_empty() {
+ return None
+ }
+
+ let pic_index = PictureIndex(prim_store.pictures
+ .alloc()
+ .init(PicturePrimitive::new_image(
+ composite_mode.clone(),
+ flat_items_context_3d,
+ true,
+ self.prim_flags,
+ mem::replace(&mut self.prim_list, PrimitiveList::empty()),
+ self.spatial_node_index,
+ PictureOptions::default(),
+ ))
+ );
+
+ let prim_instance = create_prim_instance(
+ pic_index,
+ composite_mode.into(),
+ self.clip_chain_id,
+ interners,
+ );
+
+ Some((pic_index, prim_instance))
+ }
+}
+
+/// A primitive that is added while a shadow context is
+/// active is stored as a pending primitive and only
+/// added to pictures during pop_all_shadows.
+pub struct PendingPrimitive<T> {
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ info: LayoutPrimitiveInfo,
+ prim: T,
+}
+
+/// As shadows are pushed, they are stored as pending
+/// shadows, and handled at once during pop_all_shadows.
+pub struct PendingShadow {
+ shadow: Shadow,
+ should_inflate: bool,
+ spatial_node_index: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+}
+
+pub enum ShadowItem {
+ Shadow(PendingShadow),
+ Image(PendingPrimitive<Image>),
+ LineDecoration(PendingPrimitive<LineDecoration>),
+ NormalBorder(PendingPrimitive<NormalBorderPrim>),
+ Primitive(PendingPrimitive<PrimitiveKeyKind>),
+ TextRun(PendingPrimitive<TextRun>),
+}
+
+impl From<PendingPrimitive<Image>> for ShadowItem {
+ fn from(image: PendingPrimitive<Image>) -> Self {
+ ShadowItem::Image(image)
+ }
+}
+
+impl From<PendingPrimitive<LineDecoration>> for ShadowItem {
+ fn from(line_dec: PendingPrimitive<LineDecoration>) -> Self {
+ ShadowItem::LineDecoration(line_dec)
+ }
+}
+
+impl From<PendingPrimitive<NormalBorderPrim>> for ShadowItem {
+ fn from(border: PendingPrimitive<NormalBorderPrim>) -> Self {
+ ShadowItem::NormalBorder(border)
+ }
+}
+
+impl From<PendingPrimitive<PrimitiveKeyKind>> for ShadowItem {
+ fn from(container: PendingPrimitive<PrimitiveKeyKind>) -> Self {
+ ShadowItem::Primitive(container)
+ }
+}
+
+impl From<PendingPrimitive<TextRun>> for ShadowItem {
+ fn from(text_run: PendingPrimitive<TextRun>) -> Self {
+ ShadowItem::TextRun(text_run)
+ }
+}
+
+fn create_prim_instance(
+ pic_index: PictureIndex,
+ composite_mode_key: PictureCompositeKey,
+ clip_chain_id: ClipChainId,
+ interners: &mut Interners,
+) -> PrimitiveInstance {
+ let pic_key = PictureKey::new(
+ Picture { composite_mode_key },
+ );
+
+ let data_handle = interners
+ .picture
+ .intern(&pic_key, || ());
+
+ PrimitiveInstance::new(
+ LayoutRect::max_rect(),
+ PrimitiveInstanceKind::Picture {
+ data_handle,
+ pic_index,
+ segment_instance_index: SegmentInstanceIndex::INVALID,
+ },
+ clip_chain_id,
+ )
+}
+
+fn filter_ops_for_compositing(
+ input_filters: ItemRange<FilterOp>,
+) -> Vec<Filter> {
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ input_filters.iter().map(|filter| filter.into()).collect()
+}
+
+fn filter_datas_for_compositing(
+ input_filter_datas: &[TempFilterData],
+) -> Vec<FilterData> {
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ let mut filter_datas = vec![];
+ for temp_filter_data in input_filter_datas {
+ let func_types : Vec<ComponentTransferFuncType> = temp_filter_data.func_types.iter().collect();
+ debug_assert!(func_types.len() == 4);
+ filter_datas.push( FilterData {
+ func_r_type: func_types[0],
+ r_values: temp_filter_data.r_values.iter().collect(),
+ func_g_type: func_types[1],
+ g_values: temp_filter_data.g_values.iter().collect(),
+ func_b_type: func_types[2],
+ b_values: temp_filter_data.b_values.iter().collect(),
+ func_a_type: func_types[3],
+ a_values: temp_filter_data.a_values.iter().collect(),
+ });
+ }
+ filter_datas
+}
+
+fn filter_primitives_for_compositing(
+ input_filter_primitives: ItemRange<FilterPrimitive>,
+) -> Vec<FilterPrimitive> {
+ // Resolve these in the flattener?
+ // TODO(gw): Now that we resolve these later on,
+ // we could probably make it a bit
+ // more efficient than cloning these here.
+ input_filter_primitives.iter().map(|primitive| primitive).collect()
+}
+
+fn process_repeat_size(
+ snapped_rect: &LayoutRect,
+ unsnapped_rect: &LayoutRect,
+ repeat_size: LayoutSize,
+) -> LayoutSize {
+ // FIXME(aosmond): The tile size is calculated based on several parameters
+ // during display list building. It may produce a slightly different result
+ // than the bounds due to floating point error accumulation, even though in
+ // theory they should be the same. We do a fuzzy check here to paper over
+ // that. It may make more sense to push the original parameters into scene
+ // building and let it do a saner calculation with more information (e.g.
+ // the snapped values).
+ const EPSILON: f32 = 0.001;
+ LayoutSize::new(
+ if repeat_size.width.approx_eq_eps(&unsnapped_rect.size.width, &EPSILON) {
+ snapped_rect.size.width
+ } else {
+ repeat_size.width
+ },
+ if repeat_size.height.approx_eq_eps(&unsnapped_rect.size.height, &EPSILON) {
+ snapped_rect.size.height
+ } else {
+ repeat_size.height
+ },
+ )
+}
diff --git a/gfx/wr/webrender/src/screen_capture.rs b/gfx/wr/webrender/src/screen_capture.rs
new file mode 100644
index 0000000000..4f2fc504b4
--- /dev/null
+++ b/gfx/wr/webrender/src/screen_capture.rs
@@ -0,0 +1,496 @@
+/* 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/. */
+
+//! Screen capture infrastructure for the Gecko Profiler and Composition Recorder.
+
+use std::collections::HashMap;
+
+use api::{ImageFormat, ImageBufferKind};
+use api::units::*;
+use gleam::gl::GlType;
+
+use crate::device::{Device, PBO, DrawTarget, ReadTarget, Texture, TextureFilter};
+use crate::internal_types::RenderTargetInfo;
+use crate::renderer::Renderer;
+use crate::util::round_up_to_multiple;
+
+/// A handle to a screenshot that is being asynchronously captured and scaled.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct AsyncScreenshotHandle(usize);
+
+/// A handle to a recorded frame that was captured.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct RecordedFrameHandle(usize);
+
+/// An asynchronously captured screenshot bound to a PBO which has not yet been mapped for copying.
+struct AsyncScreenshot {
+ /// The PBO that will contain the screenshot data.
+ pbo: PBO,
+ /// The size of the screenshot.
+ screenshot_size: DeviceIntSize,
+ /// The stride of the data in the PBO.
+ buffer_stride: usize,
+ /// Thge image format of the screenshot.
+ image_format: ImageFormat,
+}
+
+/// How the `AsyncScreenshotGrabber` captures frames.
+#[derive(Debug, Eq, PartialEq)]
+enum AsyncScreenshotGrabberMode {
+ /// Capture screenshots for the Gecko profiler.
+ ///
+ /// This mode will asynchronously scale the screenshots captured.
+ ProfilerScreenshots,
+
+ /// Capture screenshots for the CompositionRecorder.
+ ///
+ /// This mode does not scale the captured screenshots.
+ CompositionRecorder,
+}
+
+/// Renderer infrastructure for capturing screenshots and scaling them asynchronously.
+pub(in crate) struct AsyncScreenshotGrabber {
+ /// The textures used to scale screenshots.
+ scaling_textures: Vec<Texture>,
+ /// PBOs available to be used for screenshot readback.
+ available_pbos: Vec<PBO>,
+ /// PBOs containing screenshots that are awaiting readback.
+ awaiting_readback: HashMap<AsyncScreenshotHandle, AsyncScreenshot>,
+ /// The handle for the net PBO that will be inserted into `in_use_pbos`.
+ next_pbo_handle: usize,
+ /// The mode the grabber operates in.
+ mode: AsyncScreenshotGrabberMode,
+}
+
+impl Default for AsyncScreenshotGrabber {
+ fn default() -> Self {
+ AsyncScreenshotGrabber {
+ scaling_textures: Vec::new(),
+ available_pbos: Vec::new(),
+ awaiting_readback: HashMap::new(),
+ next_pbo_handle: 1,
+ mode: AsyncScreenshotGrabberMode::ProfilerScreenshots,
+ }
+ }
+}
+
+impl AsyncScreenshotGrabber {
+ /// Create a new AsyncScreenshotGrabber for the composition recorder.
+ pub fn new_composition_recorder() -> Self {
+ let mut recorder = Self::default();
+ recorder.mode = AsyncScreenshotGrabberMode::CompositionRecorder;
+
+ recorder
+ }
+
+ /// Deinitialize the allocated textures and PBOs.
+ pub fn deinit(self, device: &mut Device) {
+ for texture in self.scaling_textures {
+ device.delete_texture(texture);
+ }
+
+ for pbo in self.available_pbos {
+ device.delete_pbo(pbo);
+ }
+
+ for (_, async_screenshot) in self.awaiting_readback {
+ device.delete_pbo(async_screenshot.pbo);
+ }
+ }
+
+ /// Take a screenshot and scale it asynchronously.
+ ///
+ /// The returned handle can be used to access the mapped screenshot data via
+ /// `map_and_recycle_screenshot`.
+ /// The returned size is the size of the screenshot.
+ pub fn get_screenshot(
+ &mut self,
+ device: &mut Device,
+ window_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ image_format: ImageFormat,
+ ) -> (AsyncScreenshotHandle, DeviceIntSize) {
+ let screenshot_size = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ assert_ne!(window_rect.size.width, 0);
+ assert_ne!(window_rect.size.height, 0);
+
+ let scale = (buffer_size.width as f32 / window_rect.size.width as f32)
+ .min(buffer_size.height as f32 / window_rect.size.height as f32);
+
+ (window_rect.size.to_f32() * scale).round().to_i32()
+ }
+
+ AsyncScreenshotGrabberMode::CompositionRecorder => {
+ assert_eq!(buffer_size, window_rect.size);
+ buffer_size
+ }
+ };
+
+ assert!(screenshot_size.width <= buffer_size.width);
+ assert!(screenshot_size.height <= buffer_size.height);
+
+ // To ensure that we hit the fast path when reading from a
+ // framebuffer we must ensure that the width of the area we read
+ // is a multiple of the device's optimal pixel-transfer stride.
+ // The read_size should therefore be the screenshot_size with the width
+ // increased to a suitable value. We will also pass this value to
+ // scale_screenshot() as the min_texture_size, to ensure the texture is
+ // large enough to read from. In CompositionRecorder mode we read
+ // directly from the default framebuffer so are unable choose this size.
+ let read_size = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ let stride = (screenshot_size.width * image_format.bytes_per_pixel()) as usize;
+ let rounded = round_up_to_multiple(stride, device.optimal_pbo_stride().num_bytes(image_format));
+ let optimal_width = rounded as i32 / image_format.bytes_per_pixel();
+
+ DeviceIntSize::new(
+ optimal_width,
+ screenshot_size.height,
+ )
+ }
+ AsyncScreenshotGrabberMode::CompositionRecorder => buffer_size,
+ };
+ let required_size = read_size.area() as usize * image_format.bytes_per_pixel() as usize;
+
+ // Find an available PBO with the required size, creating a new one if necessary.
+ let pbo = {
+ let mut reusable_pbo = None;
+ while let Some(pbo) = self.available_pbos.pop() {
+ if pbo.get_reserved_size() != required_size {
+ device.delete_pbo(pbo);
+ } else {
+ reusable_pbo = Some(pbo);
+ break;
+ }
+ };
+
+ reusable_pbo.unwrap_or_else(|| device.create_pbo_with_size(required_size))
+ };
+ assert_eq!(pbo.get_reserved_size(), required_size);
+
+ let read_target = match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => {
+ self.scale_screenshot(
+ device,
+ ReadTarget::Default,
+ window_rect,
+ buffer_size,
+ read_size,
+ screenshot_size,
+ image_format,
+ 0,
+ );
+
+ ReadTarget::from_texture(&self.scaling_textures[0], 0)
+ }
+
+ AsyncScreenshotGrabberMode::CompositionRecorder => ReadTarget::Default,
+ };
+
+ device.read_pixels_into_pbo(
+ read_target,
+ DeviceIntRect::new(DeviceIntPoint::new(0, 0), read_size),
+ image_format,
+ &pbo,
+ );
+
+ let handle = AsyncScreenshotHandle(self.next_pbo_handle);
+ self.next_pbo_handle += 1;
+
+ self.awaiting_readback.insert(
+ handle,
+ AsyncScreenshot {
+ pbo,
+ screenshot_size,
+ buffer_stride: (read_size.width * image_format.bytes_per_pixel()) as usize,
+ image_format,
+ },
+ );
+
+ (handle, screenshot_size)
+ }
+
+ /// Take the screenshot in the given `ReadTarget` and scale it to `dest_size` recursively.
+ ///
+ /// Each scaling operation scales only by a factor of two to preserve quality.
+ ///
+ /// Textures are scaled such that `scaling_textures[n]` is half the size of
+ /// `scaling_textures[n+1]`.
+ ///
+ /// After the scaling completes, the final screenshot will be in
+ /// `scaling_textures[0]`.
+ ///
+ /// The size of `scaling_textures[0]` will be increased to `min_texture_size`
+ /// so that an optimally-sized area can be read from it.
+ fn scale_screenshot(
+ &mut self,
+ device: &mut Device,
+ read_target: ReadTarget,
+ read_target_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ min_texture_size: DeviceIntSize,
+ dest_size: DeviceIntSize,
+ image_format: ImageFormat,
+ level: usize,
+ ) {
+ assert_eq!(self.mode, AsyncScreenshotGrabberMode::ProfilerScreenshots);
+
+ let texture_size = {
+ let size = buffer_size * (1 << level);
+ DeviceIntSize::new(
+ size.width.max(min_texture_size.width),
+ size.height.max(min_texture_size.height),
+ )
+ };
+
+ // If we haven't created a texture for this level, or the existing
+ // texture is the wrong size, then create a new one.
+ if level == self.scaling_textures.len() || self.scaling_textures[level].get_dimensions() != texture_size {
+ let texture = device.create_texture(
+ ImageBufferKind::Texture2D,
+ image_format,
+ texture_size.width,
+ texture_size.height,
+ TextureFilter::Linear,
+ Some(RenderTargetInfo { has_depth: false }),
+ 1,
+ );
+ if level == self.scaling_textures.len() {
+ self.scaling_textures.push(texture);
+ } else {
+ let old_texture = std::mem::replace(&mut self.scaling_textures[level], texture);
+ device.delete_texture(old_texture);
+ }
+ }
+ assert_eq!(self.scaling_textures[level].get_dimensions(), texture_size);
+
+ let (read_target, read_target_rect) = if read_target_rect.size.width > 2 * dest_size.width {
+ self.scale_screenshot(
+ device,
+ read_target,
+ read_target_rect,
+ buffer_size,
+ min_texture_size,
+ dest_size * 2,
+ image_format,
+ level + 1,
+ );
+
+ (
+ ReadTarget::from_texture(&self.scaling_textures[level + 1], 0),
+ DeviceIntRect::new(DeviceIntPoint::new(0, 0), dest_size * 2),
+ )
+ } else {
+ (read_target, read_target_rect)
+ };
+
+ let draw_target = DrawTarget::from_texture(&self.scaling_textures[level], 0 as _, false);
+
+ let draw_target_rect = draw_target
+ .to_framebuffer_rect(DeviceIntRect::new(DeviceIntPoint::new(0, 0), dest_size));
+
+ let read_target_rect = device_rect_as_framebuffer_rect(&read_target_rect);
+
+ if level == 0 && !device.surface_origin_is_top_left() {
+ device.blit_render_target_invert_y(
+ read_target,
+ read_target_rect,
+ draw_target,
+ draw_target_rect,
+ );
+ } else {
+ device.blit_render_target(
+ read_target,
+ read_target_rect,
+ draw_target,
+ draw_target_rect,
+ TextureFilter::Linear,
+ );
+ }
+ }
+
+ /// Map the contents of the screenshot given by the handle and copy it into
+ /// the given buffer.
+ pub fn map_and_recycle_screenshot(
+ &mut self,
+ device: &mut Device,
+ handle: AsyncScreenshotHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ let AsyncScreenshot {
+ pbo,
+ screenshot_size,
+ buffer_stride,
+ image_format,
+ } = match self.awaiting_readback.remove(&handle) {
+ Some(screenshot) => screenshot,
+ None => return false,
+ };
+
+ let gl_type = device.gl().get_type();
+
+ let success = if let Some(bound_pbo) = device.map_pbo_for_readback(&pbo) {
+ let src_buffer = &bound_pbo.data;
+ let src_stride = buffer_stride;
+ let src_width =
+ screenshot_size.width as usize * image_format.bytes_per_pixel() as usize;
+
+ for (src_slice, dst_slice) in self
+ .iter_src_buffer_chunked(gl_type, src_buffer, src_stride)
+ .zip(dst_buffer.chunks_mut(dst_stride))
+ .take(screenshot_size.height as usize)
+ {
+ dst_slice[.. src_width].copy_from_slice(&src_slice[.. src_width]);
+ }
+
+ true
+ } else {
+ false
+ };
+
+ match self.mode {
+ AsyncScreenshotGrabberMode::ProfilerScreenshots => self.available_pbos.push(pbo),
+ AsyncScreenshotGrabberMode::CompositionRecorder => device.delete_pbo(pbo),
+ }
+
+ success
+ }
+
+ fn iter_src_buffer_chunked<'a>(
+ &self,
+ gl_type: GlType,
+ src_buffer: &'a [u8],
+ src_stride: usize,
+ ) -> Box<dyn Iterator<Item = &'a [u8]> + 'a> {
+ use AsyncScreenshotGrabberMode::*;
+
+ let is_angle = cfg!(windows) && gl_type == GlType::Gles;
+
+ if self.mode == CompositionRecorder && !is_angle {
+ // This is a non-ANGLE configuration. in this case, the recorded frames were captured
+ // upside down, so we have to flip them right side up.
+ Box::new(src_buffer.chunks(src_stride).rev())
+ } else {
+ // This is either an ANGLE configuration in the `CompositionRecorder` mode or a
+ // non-ANGLE configuration in the `ProfilerScreenshots` mode. In either case, the
+ // captured frames are right-side up.
+ Box::new(src_buffer.chunks(src_stride))
+ }
+ }
+}
+
+// Screen-capture specific Renderer impls.
+impl Renderer {
+ /// Record a frame for the Composition Recorder.
+ ///
+ /// The returned handle can be passed to `map_recorded_frame` to copy it into
+ /// a buffer.
+ /// The returned size is the size of the frame.
+ pub fn record_frame(
+ &mut self,
+ image_format: ImageFormat,
+ ) -> Option<(RecordedFrameHandle, DeviceIntSize)> {
+ let device_size = self.device_size()?;
+ self.device.begin_frame();
+
+ let (handle, _) = self
+ .async_frame_recorder
+ .get_or_insert_with(AsyncScreenshotGrabber::new_composition_recorder)
+ .get_screenshot(
+ &mut self.device,
+ DeviceIntRect::new(DeviceIntPoint::new(0, 0), device_size),
+ device_size,
+ image_format,
+ );
+
+ self.device.end_frame();
+
+ Some((RecordedFrameHandle(handle.0), device_size))
+ }
+
+ /// Map a frame captured for the composition recorder into the given buffer.
+ pub fn map_recorded_frame(
+ &mut self,
+ handle: RecordedFrameHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ if let Some(async_frame_recorder) = self.async_frame_recorder.as_mut() {
+ async_frame_recorder.map_and_recycle_screenshot(
+ &mut self.device,
+ AsyncScreenshotHandle(handle.0),
+ dst_buffer,
+ dst_stride,
+ )
+ } else {
+ false
+ }
+ }
+
+ /// Free the data structures used by the composition recorder.
+ pub fn release_composition_recorder_structures(&mut self) {
+ if let Some(async_frame_recorder) = self.async_frame_recorder.take() {
+ self.device.begin_frame();
+ async_frame_recorder.deinit(&mut self.device);
+ self.device.end_frame();
+ }
+ }
+
+ /// Take a screenshot and scale it asynchronously.
+ ///
+ /// The returned handle can be used to access the mapped screenshot data via
+ /// `map_and_recycle_screenshot`.
+ ///
+ /// The returned size is the size of the screenshot.
+ pub fn get_screenshot_async(
+ &mut self,
+ window_rect: DeviceIntRect,
+ buffer_size: DeviceIntSize,
+ image_format: ImageFormat,
+ ) -> (AsyncScreenshotHandle, DeviceIntSize) {
+ self.device.begin_frame();
+
+ let handle = self
+ .async_screenshots
+ .get_or_insert_with(AsyncScreenshotGrabber::default)
+ .get_screenshot(&mut self.device, window_rect, buffer_size, image_format);
+
+ self.device.end_frame();
+
+ handle
+ }
+
+ /// Map the contents of the screenshot given by the handle and copy it into
+ /// the given buffer.
+ pub fn map_and_recycle_screenshot(
+ &mut self,
+ handle: AsyncScreenshotHandle,
+ dst_buffer: &mut [u8],
+ dst_stride: usize,
+ ) -> bool {
+ if let Some(async_screenshots) = self.async_screenshots.as_mut() {
+ async_screenshots.map_and_recycle_screenshot(
+ &mut self.device,
+ handle,
+ dst_buffer,
+ dst_stride,
+ )
+ } else {
+ false
+ }
+ }
+
+ /// Release the screenshot grabbing structures that the profiler was using.
+ pub fn release_profiler_structures(&mut self) {
+ if let Some(async_screenshots) = self.async_screenshots.take() {
+ self.device.begin_frame();
+ async_screenshots.deinit(&mut self.device);
+ self.device.end_frame();
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/segment.rs b/gfx/wr/webrender/src/segment.rs
new file mode 100644
index 0000000000..84d19cb4fc
--- /dev/null
+++ b/gfx/wr/webrender/src/segment.rs
@@ -0,0 +1,1352 @@
+/* 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/. */
+
+//! Primitive segmentation
+//!
+//! # Overview
+//!
+//! Segmenting is the process of breaking rectangular primitives into smaller rectangular
+//! primitives in order to extract parts that could benefit from a fast paths.
+//!
+//! Typically this is used to allow fully opaque segments to be rendered in the opaque
+//! pass. For example when an opaque rectangle has a non-axis-aligned transform applied,
+//! we usually have to apply some anti-aliasing around the edges which requires alpha
+//! blending. By segmenting the edges out of the center of the primitive, we can keep a
+//! large amount of pixels in the opaque pass.
+//! Segmenting also lets us avoids rasterizing parts of clip masks that we know to have
+//! no effect or to be fully masking. For example by segmenting the corners of a rounded
+//! rectangle clip, we can optimize both rendering the mask and the primitive by only
+//! rasterize the corners in the mask and not applying any clipping to the segments of
+//! the primitive that don't overlap the borders.
+//!
+//! It is a flexible system in the sense that different sources of segmentation (for
+//! example two rounded rectangle clips) can affect the segmentation, and the possibility
+//! to segment some effects such as specific clip kinds does not necessarily mean the
+//! primitive will actually be segmented.
+//!
+//! ## Segments and clipping
+//!
+//! Segments of a primitive can be either not clipped, fully clipped, or partially clipped.
+//! In the first two case we don't need a clip mask. For each partially masked segments, a
+//! mask is rasterized using a render task. All of the interesting steps happen during frame
+//! building.
+//!
+//! - The first step is to determine the segmentation and write the associated GPU data.
+//! See `PrimitiveInstance::build_segments_if_needed` and `write_brush_segment_description`
+//! in `prim_store/mod.rs` which uses the segment builder of this module.
+//! - The second step is to generate the mask render tasks.
+//! See `BrushSegment::update_clip_task` and `RenderTask::new_mask`. For each segment that
+//! needs a mask, the contribution of all clips that affect the segment is added to the
+//! mask's render task.
+//! - Segments are assigned to batches (See `batch.rs`). Segments of a given primitive can
+//! be assigned to different batches.
+//!
+//! See also the [`clip` module documentation][clip.rs] for details about how clipping
+//! information is represented.
+//!
+//!
+//! [clip.rs]: ../clip/index.html
+//!
+
+use api::{BorderRadius, ClipMode};
+use api::units::*;
+use std::{cmp, usize};
+use crate::util::{extract_inner_rect_safe};
+use smallvec::SmallVec;
+
+bitflags! {
+ /// Each bit of the edge AA mask is:
+ /// 0, when the edge of the primitive needs to be considered for AA
+ /// 1, when the edge of the segment needs to be considered for AA
+ ///
+ /// *Note*: the bit values have to match the shader logic in
+ /// `write_transform_vertex()` function.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ #[cfg_attr(feature = "replay", derive(Deserialize))]
+ #[derive(MallocSizeOf)]
+ pub struct EdgeAaSegmentMask: u8 {
+ ///
+ const LEFT = 0x1;
+ ///
+ const TOP = 0x2;
+ ///
+ const RIGHT = 0x4;
+ ///
+ const BOTTOM = 0x8;
+ }
+}
+
+bitflags! {
+ pub struct ItemFlags: u8 {
+ const X_ACTIVE = 0x1;
+ const Y_ACTIVE = 0x2;
+ const HAS_MASK = 0x4;
+ }
+}
+
+// The segment builder outputs a list of these segments.
+#[derive(Debug, PartialEq)]
+pub struct Segment {
+ pub rect: LayoutRect,
+ pub has_mask: bool,
+ pub edge_flags: EdgeAaSegmentMask,
+ pub region_x: usize,
+ pub region_y: usize,
+}
+
+// The segment builder creates a list of x/y axis events
+// that are used to build a segment list. Right now, we
+// don't bother providing a list of *which* clip regions
+// are active for a given segment. Instead, if there is
+// any clip mask present in a segment, we will just end
+// up drawing each of the masks to that segment clip.
+// This is a fairly rare case, but we can detect this
+// in the future and only apply clip masks that are
+// relevant to each segment region.
+// TODO(gw): Provide clip region info with each segment.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd)]
+enum EventKind {
+ // Beginning of a clip (rounded) rect.
+ BeginClip,
+ // End of a clip (rounded) rect.
+ EndClip,
+ // Begin the next region in the primitive.
+ BeginRegion,
+}
+
+// Events must be ordered such that when the coordinates
+// of two events are the same, the end events are processed
+// before the begin events. This ensures that we're able
+// to detect which regions are active for a given segment.
+impl Ord for EventKind {
+ fn cmp(&self, other: &EventKind) -> cmp::Ordering {
+ match (*self, *other) {
+ (EventKind::BeginRegion, EventKind::BeginRegion) => {
+ panic!("bug: regions must be non-overlapping")
+ }
+ (EventKind::EndClip, EventKind::BeginRegion) |
+ (EventKind::BeginRegion, EventKind::BeginClip) => {
+ cmp::Ordering::Less
+ }
+ (EventKind::BeginClip, EventKind::BeginRegion) |
+ (EventKind::BeginRegion, EventKind::EndClip) => {
+ cmp::Ordering::Greater
+ }
+ (EventKind::BeginClip, EventKind::BeginClip) |
+ (EventKind::EndClip, EventKind::EndClip) => {
+ cmp::Ordering::Equal
+ }
+ (EventKind::BeginClip, EventKind::EndClip) => {
+ cmp::Ordering::Greater
+ }
+ (EventKind::EndClip, EventKind::BeginClip) => {
+ cmp::Ordering::Less
+ }
+ }
+ }
+}
+
+// A x/y event where we will create a vertex in the
+// segment builder.
+#[derive(Debug, Eq, PartialEq, PartialOrd)]
+struct Event {
+ value: Au,
+ item_index: ItemIndex,
+ kind: EventKind,
+}
+
+impl Ord for Event {
+ fn cmp(&self, other: &Event) -> cmp::Ordering {
+ self.value
+ .cmp(&other.value)
+ .then(self.kind.cmp(&other.kind))
+ }
+}
+
+impl Event {
+ fn begin(value: f32, index: usize) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ item_index: ItemIndex(index),
+ kind: EventKind::BeginClip,
+ }
+ }
+
+ fn end(value: f32, index: usize) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ item_index: ItemIndex(index),
+ kind: EventKind::EndClip,
+ }
+ }
+
+ fn region(value: f32) -> Event {
+ Event {
+ value: Au::from_f32_px(value),
+ kind: EventKind::BeginRegion,
+ item_index: ItemIndex(usize::MAX),
+ }
+ }
+
+ fn update(
+ &self,
+ flag: ItemFlags,
+ items: &mut [Item],
+ region: &mut usize,
+ ) {
+ let is_active = match self.kind {
+ EventKind::BeginClip => true,
+ EventKind::EndClip => false,
+ EventKind::BeginRegion => {
+ *region += 1;
+ return;
+ }
+ };
+
+ items[self.item_index.0].flags.set(flag, is_active);
+ }
+}
+
+// An item that provides some kind of clip region (either
+// a clip in/out rect, or a mask region).
+#[derive(Debug)]
+struct Item {
+ rect: LayoutRect,
+ mode: Option<ClipMode>,
+ flags: ItemFlags,
+}
+
+impl Item {
+ fn new(
+ rect: LayoutRect,
+ mode: Option<ClipMode>,
+ has_mask: bool,
+ ) -> Item {
+ let flags = if has_mask {
+ ItemFlags::HAS_MASK
+ } else {
+ ItemFlags::empty()
+ };
+
+ Item {
+ rect,
+ mode,
+ flags,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd)]
+struct ItemIndex(usize);
+
+// The main public interface to the segment module.
+pub struct SegmentBuilder {
+ items: Vec<Item>,
+ inner_rect: Option<LayoutRect>,
+ bounding_rect: Option<LayoutRect>,
+ has_interesting_clips: bool,
+
+ #[cfg(debug_assertions)]
+ initialized: bool,
+}
+
+impl SegmentBuilder {
+ // Create a new segment builder, supplying the primitive
+ // local rect and associated local clip rect.
+ pub fn new() -> SegmentBuilder {
+ SegmentBuilder {
+ items: Vec::with_capacity(4),
+ bounding_rect: None,
+ inner_rect: None,
+ has_interesting_clips: false,
+ #[cfg(debug_assertions)]
+ initialized: false,
+ }
+ }
+
+ pub fn initialize(
+ &mut self,
+ local_rect: LayoutRect,
+ inner_rect: Option<LayoutRect>,
+ local_clip_rect: LayoutRect,
+ ) {
+ self.items.clear();
+ self.inner_rect = inner_rect;
+ self.bounding_rect = Some(local_rect);
+
+ self.push_clip_rect(local_rect, None, ClipMode::Clip);
+ self.push_clip_rect(local_clip_rect, None, ClipMode::Clip);
+
+ // This must be set after the push_clip_rect calls above, since we
+ // want to skip segment building if those are the only clips.
+ self.has_interesting_clips = false;
+
+ #[cfg(debug_assertions)]
+ {
+ self.initialized = true;
+ }
+ }
+
+ // Push a region defined by an inner and outer rect where there
+ // is a mask required. This ensures that segments which intersect
+ // with these areas will get a clip mask task allocated. This
+ // is currently used to mark where a box-shadow region can affect
+ // the pixels of a clip-mask. It might be useful for other types
+ // such as dashed and dotted borders in the future.
+ pub fn push_mask_region(
+ &mut self,
+ outer_rect: LayoutRect,
+ inner_rect: LayoutRect,
+ inner_clip_mode: Option<ClipMode>,
+ ) {
+ self.has_interesting_clips = true;
+
+ if inner_rect.is_empty() {
+ self.items.push(Item::new(
+ outer_rect,
+ None,
+ true
+ ));
+ return;
+ }
+
+ debug_assert!(outer_rect.contains_rect(&inner_rect));
+
+ let p0 = outer_rect.origin;
+ let p1 = inner_rect.origin;
+ let p2 = inner_rect.bottom_right();
+ let p3 = outer_rect.bottom_right();
+
+ let segments = &[
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p0.y),
+ LayoutSize::new(p1.x - p0.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p0.y),
+ LayoutSize::new(p3.x - p2.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p2.y),
+ LayoutSize::new(p3.x - p2.x, p3.y - p2.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p2.y),
+ LayoutSize::new(p1.x - p0.x, p3.y - p2.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p1.x, p0.y),
+ LayoutSize::new(p2.x - p1.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p1.y),
+ LayoutSize::new(p3.x - p2.x, p2.y - p1.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p1.x, p2.y),
+ LayoutSize::new(p2.x - p1.x, p3.y - p2.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p1.y),
+ LayoutSize::new(p1.x - p0.x, p2.y - p1.y),
+ ),
+ ];
+
+ self.items.reserve(segments.len() + 1);
+
+ for segment in segments {
+ self.items.push(Item::new(
+ *segment,
+ None,
+ true
+ ));
+ }
+
+ if inner_clip_mode.is_some() {
+ self.items.push(Item::new(
+ inner_rect,
+ inner_clip_mode,
+ false,
+ ));
+ }
+ }
+
+ // Push some kind of clipping region into the segment builder.
+ // If radius is None, it's a simple rect.
+ pub fn push_clip_rect(
+ &mut self,
+ rect: LayoutRect,
+ radius: Option<BorderRadius>,
+ mode: ClipMode,
+ ) {
+ self.has_interesting_clips = true;
+
+ // Keep track of a minimal bounding rect for the set of
+ // segments that will be generated.
+ if mode == ClipMode::Clip {
+ self.bounding_rect = self.bounding_rect.and_then(|bounding_rect| {
+ bounding_rect.intersection(&rect)
+ });
+ }
+ let mode = Some(mode);
+
+ match radius {
+ Some(radius) => {
+ // For a rounded rect, try to create a nine-patch where there
+ // is a clip item for each corner, inner and edge region.
+ match extract_inner_rect_safe(&rect, &radius) {
+ Some(inner) => {
+ let p0 = rect.origin;
+ let p1 = inner.origin;
+ let p2 = inner.bottom_right();
+ let p3 = rect.bottom_right();
+
+ self.items.reserve(9);
+
+ let corner_segments = &[
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p0.y),
+ LayoutSize::new(p1.x - p0.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p0.y),
+ LayoutSize::new(p3.x - p2.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p2.y),
+ LayoutSize::new(p3.x - p2.x, p3.y - p2.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p2.y),
+ LayoutSize::new(p1.x - p0.x, p3.y - p2.y),
+ ),
+ ];
+
+ for segment in corner_segments {
+ self.items.push(Item::new(
+ *segment,
+ mode,
+ true
+ ));
+ }
+
+ let other_segments = &[
+ LayoutRect::new(
+ LayoutPoint::new(p1.x, p0.y),
+ LayoutSize::new(p2.x - p1.x, p1.y - p0.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p2.x, p1.y),
+ LayoutSize::new(p3.x - p2.x, p2.y - p1.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p1.x, p2.y),
+ LayoutSize::new(p2.x - p1.x, p3.y - p2.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p0.x, p1.y),
+ LayoutSize::new(p1.x - p0.x, p2.y - p1.y),
+ ),
+ LayoutRect::new(
+ LayoutPoint::new(p1.x, p1.y),
+ LayoutSize::new(p2.x - p1.x, p2.y - p1.y),
+ ),
+ ];
+
+ for segment in other_segments {
+ self.items.push(Item::new(
+ *segment,
+ mode,
+ false,
+ ));
+ }
+ }
+ None => {
+ // If we get here, we could not extract an inner rectangle
+ // for this clip region. This can occur in cases such as
+ // a rounded rect where the top-left and bottom-left radii
+ // result in overlapping rects. In that case, just create
+ // a single clip region for the entire rounded rect.
+ self.items.push(Item::new(
+ rect,
+ mode,
+ true,
+ ))
+ }
+ }
+ }
+ None => {
+ // For a simple rect, just create one clipping item.
+ self.items.push(Item::new(
+ rect,
+ mode,
+ false,
+ ))
+ }
+ }
+ }
+
+ // Consume this segment builder and produce a list of segments.
+ pub fn build<F>(&mut self, mut f: F) where F: FnMut(&Segment) {
+ #[cfg(debug_assertions)]
+ debug_assert!(self.initialized);
+
+ #[cfg(debug_assertions)]
+ {
+ self.initialized = false;
+ }
+
+ let bounding_rect = match self.bounding_rect {
+ Some(bounding_rect) => bounding_rect,
+ None => return,
+ };
+
+ if !self.has_interesting_clips {
+ // There were no additional clips added, so don't bother building segments.
+ // Just emit a single segment for the bounding rect of the primitive.
+ f(&Segment {
+ edge_flags: EdgeAaSegmentMask::all(),
+ region_x: 0,
+ region_y: 0,
+ has_mask: false,
+ rect: bounding_rect,
+ });
+ return
+ }
+
+ // First, filter out any items that don't intersect
+ // with the visible bounding rect.
+ self.items.retain(|item| item.rect.intersects(&bounding_rect));
+
+ // Create events for each item
+ let mut x_events : SmallVec<[Event; 4]> = SmallVec::new();
+ let mut y_events : SmallVec<[Event; 4]> = SmallVec::new();
+
+ for (item_index, item) in self.items.iter().enumerate() {
+ let p0 = item.rect.origin;
+ let p1 = item.rect.bottom_right();
+
+ x_events.push(Event::begin(p0.x, item_index));
+ x_events.push(Event::end(p1.x, item_index));
+ y_events.push(Event::begin(p0.y, item_index));
+ y_events.push(Event::end(p1.y, item_index));
+ }
+
+ // Add the region events, if provided.
+ if let Some(inner_rect) = self.inner_rect {
+ x_events.push(Event::region(inner_rect.origin.x));
+ x_events.push(Event::region(inner_rect.origin.x + inner_rect.size.width));
+
+ y_events.push(Event::region(inner_rect.origin.y));
+ y_events.push(Event::region(inner_rect.origin.y + inner_rect.size.height));
+ }
+
+ // Get the minimal bounding rect in app units. We will
+ // work in fixed point in order to avoid float precision
+ // error while handling events.
+ let p0 = LayoutPointAu::new(
+ Au::from_f32_px(bounding_rect.origin.x),
+ Au::from_f32_px(bounding_rect.origin.y),
+ );
+
+ let p1 = LayoutPointAu::new(
+ Au::from_f32_px(bounding_rect.origin.x + bounding_rect.size.width),
+ Au::from_f32_px(bounding_rect.origin.y + bounding_rect.size.height),
+ );
+
+ // Sort the events in ascending order.
+ x_events.sort();
+ y_events.sort();
+
+ // Generate segments from the event lists, by sweeping the y-axis
+ // and then the x-axis for each event. This can generate a significant
+ // number of segments, but most importantly, it ensures that there are
+ // no t-junctions in the generated segments. It's probably possible
+ // to come up with more efficient segmentation algorithms, at least
+ // for simple / common cases.
+
+ // Each coordinate is clamped to the bounds of the minimal
+ // bounding rect. This ensures that we don't generate segments
+ // outside that bounding rect, but does allow correctly handling
+ // clips where the clip region starts outside the minimal
+ // rect but still intersects with it.
+
+ let mut prev_y = clamp(p0.y, y_events[0].value, p1.y);
+ let mut region_y = 0;
+ let mut segments : SmallVec<[_; 4]> = SmallVec::new();
+ let mut x_count = 0;
+ let mut y_count = 0;
+
+ for ey in &y_events {
+ let cur_y = clamp(p0.y, ey.value, p1.y);
+
+ if cur_y != prev_y {
+ let mut prev_x = clamp(p0.x, x_events[0].value, p1.x);
+ let mut region_x = 0;
+
+ for ex in &x_events {
+ let cur_x = clamp(p0.x, ex.value, p1.x);
+
+ if cur_x != prev_x {
+ segments.push(emit_segment_if_needed(
+ prev_x,
+ prev_y,
+ cur_x,
+ cur_y,
+ region_x,
+ region_y,
+ &self.items,
+ ));
+
+ prev_x = cur_x;
+ if y_count == 0 {
+ x_count += 1;
+ }
+ }
+
+ ex.update(
+ ItemFlags::X_ACTIVE,
+ &mut self.items,
+ &mut region_x,
+ );
+ }
+
+ prev_y = cur_y;
+ y_count += 1;
+ }
+
+ ey.update(
+ ItemFlags::Y_ACTIVE,
+ &mut self.items,
+ &mut region_y,
+ );
+ }
+
+ // Run user supplied closure for each valid segment.
+ debug_assert_eq!(segments.len(), x_count * y_count);
+ for y in 0 .. y_count {
+ for x in 0 .. x_count {
+ let mut edge_flags = EdgeAaSegmentMask::empty();
+
+ if x == 0 || segments[y * x_count + x - 1].is_none() {
+ edge_flags |= EdgeAaSegmentMask::LEFT;
+ }
+ if x == x_count-1 || segments[y * x_count + x + 1].is_none() {
+ edge_flags |= EdgeAaSegmentMask::RIGHT;
+ }
+ if y == 0 || segments[(y-1) * x_count + x].is_none() {
+ edge_flags |= EdgeAaSegmentMask::TOP;
+ }
+ if y == y_count-1 || segments[(y+1) * x_count + x].is_none() {
+ edge_flags |= EdgeAaSegmentMask::BOTTOM;
+ }
+
+ if let Some(ref mut segment) = segments[y * x_count + x] {
+ segment.edge_flags = edge_flags;
+ f(segment);
+ }
+ }
+ }
+ }
+}
+
+fn clamp(low: Au, value: Au, high: Au) -> Au {
+ value.max(low).min(high)
+}
+
+fn emit_segment_if_needed(
+ x0: Au,
+ y0: Au,
+ x1: Au,
+ y1: Au,
+ region_x: usize,
+ region_y: usize,
+ items: &[Item],
+) -> Option<Segment> {
+ debug_assert!(x1 > x0);
+ debug_assert!(y1 > y0);
+
+ // TODO(gw): Don't scan the whole list of items for
+ // each segment rect. Store active list
+ // in a hash set or similar if this ever
+ // shows up in a profile.
+ let mut has_clip_mask = false;
+
+ for item in items {
+ if item.flags.contains(ItemFlags::X_ACTIVE | ItemFlags::Y_ACTIVE) {
+ has_clip_mask |= item.flags.contains(ItemFlags::HAS_MASK);
+
+ if item.mode == Some(ClipMode::ClipOut) && !item.flags.contains(ItemFlags::HAS_MASK) {
+ return None;
+ }
+ }
+ }
+
+ let segment_rect = LayoutRect::new(
+ LayoutPoint::new(
+ x0.to_f32_px(),
+ y0.to_f32_px(),
+ ),
+ LayoutSize::new(
+ (x1 - x0).to_f32_px(),
+ (y1 - y0).to_f32_px(),
+ ),
+ );
+
+ Some(Segment {
+ rect: segment_rect,
+ has_mask: has_clip_mask,
+ edge_flags: EdgeAaSegmentMask::empty(),
+ region_x,
+ region_y,
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use api::{BorderRadius, ClipMode};
+ use api::units::{LayoutPoint, LayoutRect, LayoutSize};
+ use super::{Segment, SegmentBuilder, EdgeAaSegmentMask};
+ use std::cmp;
+
+ fn rect(x0: f32, y0: f32, x1: f32, y1: f32) -> LayoutRect {
+ LayoutRect::new(
+ LayoutPoint::new(x0, y0),
+ LayoutSize::new(x1-x0, y1-y0),
+ )
+ }
+
+ fn seg(
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ has_mask: bool,
+ edge_flags: Option<EdgeAaSegmentMask>,
+ ) -> Segment {
+ seg_region(x0, y0, x1, y1, 0, 0, has_mask, edge_flags)
+ }
+
+ fn seg_region(
+ x0: f32,
+ y0: f32,
+ x1: f32,
+ y1: f32,
+ region_x: usize,
+ region_y: usize,
+ has_mask: bool,
+ edge_flags: Option<EdgeAaSegmentMask>,
+ ) -> Segment {
+ Segment {
+ rect: LayoutRect::new(
+ LayoutPoint::new(x0, y0),
+ LayoutSize::new(x1-x0, y1-y0),
+ ),
+ has_mask,
+ edge_flags: edge_flags.unwrap_or(EdgeAaSegmentMask::empty()),
+ region_x,
+ region_y,
+ }
+ }
+
+ fn segment_sorter(s0: &Segment, s1: &Segment) -> cmp::Ordering {
+ let r0 = &s0.rect;
+ let r1 = &s1.rect;
+
+ (
+ (r0.origin.x, r0.origin.y, r0.size.width, r0.size.height)
+ ).partial_cmp(&
+ (r1.origin.x, r1.origin.y, r1.size.width, r1.size.height)
+ ).unwrap()
+ }
+
+ fn seg_test(
+ local_rect: LayoutRect,
+ inner_rect: Option<LayoutRect>,
+ local_clip_rect: LayoutRect,
+ clips: &[(LayoutRect, Option<BorderRadius>, ClipMode)],
+ expected_segments: &mut [Segment]
+ ) {
+ let mut sb = SegmentBuilder::new();
+ sb.initialize(
+ local_rect,
+ inner_rect,
+ local_clip_rect,
+ );
+ sb.push_clip_rect(local_rect, None, ClipMode::Clip);
+ sb.push_clip_rect(local_clip_rect, None, ClipMode::Clip);
+ let mut segments = Vec::new();
+ for &(rect, radius, mode) in clips {
+ sb.push_clip_rect(rect, radius, mode);
+ }
+ sb.build(|segment| {
+ segments.push(Segment {
+ ..*segment
+ });
+ });
+ segments.sort_by(segment_sorter);
+ expected_segments.sort_by(segment_sorter);
+ assert_eq!(
+ segments.len(),
+ expected_segments.len(),
+ "segments\n{:?}\nexpected\n{:?}\n",
+ segments,
+ expected_segments
+ );
+ for (segment, expected) in segments.iter().zip(expected_segments.iter()) {
+ assert_eq!(segment, expected);
+ }
+ }
+
+ #[test]
+ fn segment_empty() {
+ seg_test(
+ rect(0.0, 0.0, 0.0, 0.0),
+ None,
+ rect(0.0, 0.0, 0.0, 0.0),
+ &[],
+ &mut [],
+ );
+ }
+
+ #[test]
+ fn segment_single() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(10.0, 20.0, 30.0, 40.0),
+ &[],
+ &mut [
+ seg(10.0, 20.0, 30.0, 40.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_single_clip() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(10.0, 20.0, 25.0, 35.0),
+ &[],
+ &mut [
+ seg(10.0, 20.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_inner_clip() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(15.0, 25.0, 25.0, 35.0),
+ &[],
+ &mut [
+ seg(15.0, 25.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_outer_clip() {
+ seg_test(
+ rect(15.0, 25.0, 25.0, 35.0),
+ None,
+ rect(10.0, 20.0, 30.0, 40.0),
+ &[],
+ &mut [
+ seg(15.0, 25.0, 25.0, 35.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_int() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(20.0, 10.0, 40.0, 30.0),
+ &[],
+ &mut [
+ seg(20.0, 20.0, 30.0, 30.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_disjoint() {
+ seg_test(
+ rect(10.0, 20.0, 30.0, 40.0),
+ None,
+ rect(30.0, 20.0, 50.0, 40.0),
+ &[],
+ &mut [],
+ );
+ }
+
+ #[test]
+ fn segment_clips() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 1000.0, 1000.0),
+ &[
+ (rect(20.0, 20.0, 40.0, 40.0), None, ClipMode::Clip),
+ (rect(40.0, 20.0, 60.0, 40.0), None, ClipMode::Clip),
+ ],
+ &mut [
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 1000.0, 1000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ // corners
+ seg(20.0, 20.0, 30.0, 30.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(20.0, 50.0, 30.0, 60.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 20.0, 60.0, 30.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 50.0, 60.0, 60.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner
+ seg(30.0, 30.0, 50.0, 50.0, false, None),
+
+ // edges
+ seg(30.0, 20.0, 50.0, 30.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(30.0, 50.0, 50.0, 60.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 30.0, 30.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT)),
+ seg(50.0, 30.0, 60.0, 50.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(0.0, 0.0, 20.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(20.0, 0.0, 60.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)),
+ seg(60.0, 0.0, 100.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+
+ seg(0.0, 20.0, 20.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)),
+ seg(60.0, 20.0, 100.0, 60.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT)),
+
+ seg(0.0, 60.0, 20.0, 100.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 60.0, 60.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)),
+ seg(60.0, 60.0, 100.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), Some(BorderRadius::uniform(10.0)), ClipMode::ClipOut),
+ ],
+ &mut [
+ // top row
+ seg(0.0, 0.0, 20.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(20.0, 0.0, 30.0, 20.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(30.0, 0.0, 50.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 0.0, 60.0, 20.0, false, Some(EdgeAaSegmentMask::TOP)),
+ seg(60.0, 0.0, 100.0, 20.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+
+ // left
+ seg(0.0, 20.0, 20.0, 30.0, false, Some(EdgeAaSegmentMask::LEFT)),
+ seg(0.0, 30.0, 20.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)),
+ seg(0.0, 50.0, 20.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT)),
+
+ // right
+ seg(60.0, 20.0, 100.0, 30.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ seg(60.0, 30.0, 100.0, 50.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::LEFT)),
+ seg(60.0, 50.0, 100.0, 60.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+
+ // bottom row
+ seg(0.0, 60.0, 20.0, 100.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 60.0, 30.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(30.0, 60.0, 50.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 60.0, 60.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(60.0, 60.0, 100.0, 100.0, false, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner corners
+ seg(20.0, 20.0, 30.0, 30.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 50.0, 30.0, 60.0, true, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)),
+ seg(50.0, 20.0, 60.0, 30.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(50.0, 50.0, 60.0, 60.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_in_clip_out() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(-1000.0, -1000.0, 2000.0, 2000.0),
+ &[
+ (rect(20.0, 20.0, 60.0, 60.0), None, ClipMode::Clip),
+ (rect(50.0, 50.0, 80.0, 80.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(20.0, 20.0, 50.0, 50.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(50.0, 20.0, 60.0, 50.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+ seg(20.0, 50.0, 50.0, 60.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_overlap() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 10.0, 10.0), None, ClipMode::ClipOut),
+ (rect(0.0, 0.0, 100.0, 100.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ // corners
+ seg(0.0, 90.0, 10.0, 100.0, true, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)),
+ seg(90.0, 0.0, 100.0, 10.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP)),
+ seg(90.0, 90.0, 100.0, 100.0, true, Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)),
+
+ // inner
+ seg(10.0, 10.0, 90.0, 90.0, false, None),
+
+ // edges
+ seg(10.0, 0.0, 90.0, 10.0, false, Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT)),
+ seg(10.0, 90.0, 90.0, 100.0, false, Some(EdgeAaSegmentMask::BOTTOM)),
+ seg(0.0, 10.0, 10.0, 90.0, false, Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)),
+ seg(90.0, 10.0, 100.0, 90.0, false, Some(EdgeAaSegmentMask::RIGHT)),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_rounded_clip_overlap_reverse() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::Clip),
+ (rect(0.0, 0.0, 100.0, 100.0), Some(BorderRadius::uniform(10.0)), ClipMode::Clip),
+ ],
+ &mut [
+ seg(10.0, 10.0, 90.0, 90.0, false,
+ Some(EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::TOP |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM
+ )
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_clip_in_clip_out_overlap() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::Clip),
+ (rect(10.0, 10.0, 90.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_event_order() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ None,
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 100.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg(0.0, 90.0, 100.0, 100.0, false, Some(
+ EdgeAaSegmentMask::LEFT |
+ EdgeAaSegmentMask::RIGHT |
+ EdgeAaSegmentMask::BOTTOM |
+ EdgeAaSegmentMask::TOP
+ )),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_simple() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 40.0, 60.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ ],
+ &mut [
+ seg_region(
+ 0.0, 0.0,
+ 20.0, 40.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 0.0,
+ 60.0, 40.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 60.0, 0.0,
+ 100.0, 40.0,
+ 2, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 40.0,
+ 20.0, 80.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::LEFT)
+ ),
+
+ seg_region(
+ 20.0, 40.0,
+ 60.0, 80.0,
+ 1, 1,
+ false,
+ None,
+ ),
+
+ seg_region(
+ 60.0, 40.0,
+ 100.0, 80.0,
+ 2, 1,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 80.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ seg_region(
+ 20.0, 80.0,
+ 60.0, 100.0,
+ 1, 2,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM),
+ ),
+
+ seg_region(
+ 60.0, 80.0,
+ 100.0, 100.0,
+ 2, 2,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 40.0, 60.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(0.0, 0.0, 100.0, 90.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg_region(
+ 0.0, 90.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 90.0,
+ 60.0, 100.0,
+ 1, 2,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP),
+ ),
+
+ seg_region(
+ 60.0, 90.0,
+ 100.0, 100.0,
+ 2, 2,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::TOP)
+ ),
+
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip2() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 20.0, 80.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(20.0, 20.0, 100.0, 100.0), None, ClipMode::ClipOut),
+ ],
+ &mut [
+ seg_region(
+ 0.0, 0.0,
+ 20.0, 20.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::TOP)
+ ),
+
+ seg_region(
+ 20.0, 0.0,
+ 80.0, 20.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM),
+ ),
+
+ seg_region(
+ 80.0, 0.0,
+ 100.0, 20.0,
+ 2, 0,
+ false,
+ Some(EdgeAaSegmentMask::RIGHT | EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM)
+ ),
+
+ seg_region(
+ 0.0, 20.0,
+ 20.0, 80.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT)
+ ),
+
+ seg_region(
+ 0.0, 80.0,
+ 20.0, 100.0,
+ 0, 2,
+ false,
+ Some(EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT)
+ ),
+ ],
+ );
+ }
+
+ #[test]
+ fn segment_region_clip3() {
+ seg_test(
+ rect(0.0, 0.0, 100.0, 100.0),
+ Some(rect(20.0, 20.0, 80.0, 80.0)),
+ rect(0.0, 0.0, 100.0, 100.0),
+ &[
+ (rect(10.0, 10.0, 30.0, 30.0), None, ClipMode::Clip),
+ ],
+ &mut [
+ seg_region(
+ 10.0, 10.0,
+ 20.0, 20.0,
+ 0, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::LEFT),
+ ),
+
+ seg_region(
+ 20.0, 10.0,
+ 30.0, 20.0,
+ 1, 0,
+ false,
+ Some(EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::RIGHT),
+ ),
+
+ seg_region(
+ 10.0, 20.0,
+ 20.0, 30.0,
+ 0, 1,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::LEFT),
+ ),
+
+ seg_region(
+ 20.0, 20.0,
+ 30.0, 30.0,
+ 1, 1,
+ false,
+ Some(EdgeAaSegmentMask::BOTTOM | EdgeAaSegmentMask::RIGHT),
+ ),
+ ],
+ );
+ }
+}
diff --git a/gfx/wr/webrender/src/space.rs b/gfx/wr/webrender/src/space.rs
new file mode 100644
index 0000000000..15646b478d
--- /dev/null
+++ b/gfx/wr/webrender/src/space.rs
@@ -0,0 +1,254 @@
+/* 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/. */
+
+
+//! Utilities to deal with coordinate spaces.
+
+use std::fmt;
+
+use euclid::{Transform3D, Rect, Point2D, Vector2D};
+
+use api::units::*;
+use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
+use crate::util::project_rect;
+use crate::util::{MatrixHelpers, ScaleOffset, RectHelpers, PointHelpers};
+
+
+#[derive(Debug, Clone)]
+pub struct SpaceMapper<F, T> {
+ kind: CoordinateSpaceMapping<F, T>,
+ pub ref_spatial_node_index: SpatialNodeIndex,
+ pub current_target_spatial_node_index: SpatialNodeIndex,
+ pub bounds: Rect<f32, T>,
+ visible_face: VisibleFace,
+}
+
+impl<F, T> SpaceMapper<F, T> where F: fmt::Debug {
+ pub fn new(
+ ref_spatial_node_index: SpatialNodeIndex,
+ bounds: Rect<f32, T>,
+ ) -> Self {
+ SpaceMapper {
+ kind: CoordinateSpaceMapping::Local,
+ ref_spatial_node_index,
+ current_target_spatial_node_index: ref_spatial_node_index,
+ bounds,
+ visible_face: VisibleFace::Front,
+ }
+ }
+
+ pub fn new_with_target(
+ ref_spatial_node_index: SpatialNodeIndex,
+ target_node_index: SpatialNodeIndex,
+ bounds: Rect<f32, T>,
+ spatial_tree: &SpatialTree,
+ ) -> Self {
+ let mut mapper = Self::new(ref_spatial_node_index, bounds);
+ mapper.set_target_spatial_node(target_node_index, spatial_tree);
+ mapper
+ }
+
+ pub fn set_target_spatial_node(
+ &mut self,
+ target_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) {
+ if target_node_index == self.current_target_spatial_node_index {
+ return
+ }
+
+ let ref_spatial_node = &spatial_tree.spatial_nodes[self.ref_spatial_node_index.0 as usize];
+ let target_spatial_node = &spatial_tree.spatial_nodes[target_node_index.0 as usize];
+ self.visible_face = VisibleFace::Front;
+
+ self.kind = if self.ref_spatial_node_index == target_node_index {
+ CoordinateSpaceMapping::Local
+ } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
+ let scale_offset = ref_spatial_node.content_transform
+ .inverse()
+ .accumulate(&target_spatial_node.content_transform);
+ CoordinateSpaceMapping::ScaleOffset(scale_offset)
+ } else {
+ let transform = spatial_tree
+ .get_relative_transform_with_face(
+ target_node_index,
+ self.ref_spatial_node_index,
+ Some(&mut self.visible_face),
+ )
+ .into_transform()
+ .with_source::<F>()
+ .with_destination::<T>();
+ CoordinateSpaceMapping::Transform(transform)
+ };
+
+ self.current_target_spatial_node_index = target_node_index;
+ }
+
+ pub fn get_transform(&self) -> Transform3D<f32, F, T> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Transform3D::identity()
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ scale_offset.to_transform()
+ }
+ CoordinateSpaceMapping::Transform(transform) => {
+ transform
+ }
+ }
+ }
+
+ pub fn unmap(&self, rect: &Rect<f32, T>) -> Option<Rect<f32, F>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.unmap_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.inverse_rect_footprint(rect)
+ }
+ }
+ }
+
+ pub fn map(&self, rect: &Rect<f32, F>) -> Option<Rect<f32, T>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.map_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ match project_rect(transform, rect, &self.bounds) {
+ Some(bounds) => {
+ Some(bounds)
+ }
+ None => {
+ warn!("parent relative transform can't transform the primitive rect for {:?}", rect);
+ None
+ }
+ }
+ }
+ }
+ }
+
+ // Attempt to return a rect that is contained in the mapped rect.
+ pub fn map_inner_bounds(&self, rect: &Rect<f32, F>) -> Option<Rect<f32, T>> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ Some(rect.cast_unit())
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(scale_offset.map_rect(rect))
+ }
+ CoordinateSpaceMapping::Transform(..) => {
+ // We could figure out a rect that is contained in the transformed rect but
+ // for now we do the simple thing here and bail out.
+ return None;
+ }
+ }
+ }
+
+ pub fn map_vector(&self, v: Vector2D<f32, F>) -> Vector2D<f32, T> {
+ match self.kind {
+ CoordinateSpaceMapping::Local => {
+ v.cast_unit()
+ }
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ scale_offset.map_vector(&v)
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.transform_vector2d(v)
+ }
+ }
+ }
+}
+
+
+#[derive(Clone, Debug)]
+pub struct SpaceSnapper {
+ pub ref_spatial_node_index: SpatialNodeIndex,
+ current_target_spatial_node_index: SpatialNodeIndex,
+ snapping_transform: Option<ScaleOffset>,
+ pub device_pixel_scale: DevicePixelScale,
+}
+
+impl SpaceSnapper {
+ pub fn new(
+ ref_spatial_node_index: SpatialNodeIndex,
+ device_pixel_scale: DevicePixelScale,
+ ) -> Self {
+ SpaceSnapper {
+ ref_spatial_node_index,
+ current_target_spatial_node_index: SpatialNodeIndex::INVALID,
+ snapping_transform: None,
+ device_pixel_scale,
+ }
+ }
+
+ pub fn new_with_target(
+ ref_spatial_node_index: SpatialNodeIndex,
+ target_node_index: SpatialNodeIndex,
+ device_pixel_scale: DevicePixelScale,
+ spatial_tree: &SpatialTree,
+ ) -> Self {
+ let mut snapper = SpaceSnapper {
+ ref_spatial_node_index,
+ current_target_spatial_node_index: SpatialNodeIndex::INVALID,
+ snapping_transform: None,
+ device_pixel_scale,
+ };
+
+ snapper.set_target_spatial_node(target_node_index, spatial_tree);
+ snapper
+ }
+
+ pub fn set_target_spatial_node(
+ &mut self,
+ target_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) {
+ if target_node_index == self.current_target_spatial_node_index {
+ return
+ }
+
+ let ref_spatial_node = &spatial_tree.spatial_nodes[self.ref_spatial_node_index.0 as usize];
+ let target_spatial_node = &spatial_tree.spatial_nodes[target_node_index.0 as usize];
+
+ self.current_target_spatial_node_index = target_node_index;
+ self.snapping_transform = match (ref_spatial_node.snapping_transform, target_spatial_node.snapping_transform) {
+ (Some(ref ref_scale_offset), Some(ref target_scale_offset)) => {
+ Some(ref_scale_offset
+ .inverse()
+ .accumulate(target_scale_offset)
+ .scale(self.device_pixel_scale.0))
+ }
+ _ => None,
+ };
+ }
+
+ pub fn snap_rect<F>(&self, rect: &Rect<f32, F>) -> Rect<f32, F> where F: fmt::Debug {
+ debug_assert!(self.current_target_spatial_node_index != SpatialNodeIndex::INVALID);
+ match self.snapping_transform {
+ Some(ref scale_offset) => {
+ let snapped_device_rect : DeviceRect = scale_offset.map_rect(rect).snap();
+ scale_offset.unmap_rect(&snapped_device_rect)
+ }
+ None => *rect,
+ }
+ }
+
+ pub fn snap_point<F>(&self, point: &Point2D<f32, F>) -> Point2D<f32, F> where F: fmt::Debug {
+ debug_assert!(self.current_target_spatial_node_index != SpatialNodeIndex::INVALID);
+ match self.snapping_transform {
+ Some(ref scale_offset) => {
+ let snapped_device_vector : DevicePoint = scale_offset.map_point(point).snap();
+ scale_offset.unmap_point(&snapped_device_vector)
+ }
+ None => *point,
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/spatial_node.rs b/gfx/wr/webrender/src/spatial_node.rs
new file mode 100644
index 0000000000..6df2b402e3
--- /dev/null
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -0,0 +1,963 @@
+
+/* 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 api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind, ScrollClamping, ScrollLocation};
+use api::{TransformStyle, ScrollSensitivity, StickyOffsetBounds};
+use api::units::*;
+use crate::spatial_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
+use euclid::{Point2D, Vector2D, SideOffsets2D};
+use crate::scene::SceneProperties;
+use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};
+
+#[derive(Clone, Debug)]
+pub enum SpatialNodeType {
+ /// A special kind of node that adjusts its position based on the position
+ /// of its parent node and a given set of sticky positioning offset bounds.
+ /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
+ /// https://www.w3.org/TR/css-position-3/#sticky-pos
+ StickyFrame(StickyFrameInfo),
+
+ /// Transforms it's content, but doesn't clip it. Can also be adjusted
+ /// by scroll events or setting scroll offsets.
+ ScrollFrame(ScrollFrameInfo),
+
+ /// A reference frame establishes a new coordinate space in the tree.
+ ReferenceFrame(ReferenceFrameInfo),
+}
+
+/// Contains information common among all types of SpatialTree nodes.
+#[derive(Clone, Debug)]
+pub struct SpatialNode {
+ /// The scale/offset of the viewport for this spatial node, relative to the
+ /// coordinate system. Includes any accumulated scrolling offsets from nodes
+ /// between our reference frame and this node.
+ pub viewport_transform: ScaleOffset,
+
+ /// Content scale/offset relative to the coordinate system.
+ pub content_transform: ScaleOffset,
+
+ /// Snapping scale/offset relative to the coordinate system. If None, then
+ /// we should not snap entities bound to this spatial node.
+ pub snapping_transform: Option<ScaleOffset>,
+
+ /// The axis-aligned coordinate system id of this node.
+ pub coordinate_system_id: CoordinateSystemId,
+
+ /// The current transform kind of this node.
+ pub transform_kind: TransformedRectKind,
+
+ /// Pipeline that this layer belongs to
+ pub pipeline_id: PipelineId,
+
+ /// Parent layer. If this is None, we are the root node.
+ pub parent: Option<SpatialNodeIndex>,
+
+ /// Child layers
+ pub children: Vec<SpatialNodeIndex>,
+
+ /// The type of this node and any data associated with that node type.
+ pub node_type: SpatialNodeType,
+
+ /// True if this node is transformed by an invertible transform. If not, display items
+ /// transformed by this node will not be displayed and display items not transformed by this
+ /// node will not be clipped by clips that are transformed by this node.
+ pub invertible: bool,
+
+ /// Whether this specific node is currently being async zoomed.
+ /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
+ pub is_async_zooming: bool,
+
+ /// Whether this node or any of its ancestors is being pinch zoomed.
+ /// This is calculated in update(). This will be used to decide whether
+ /// to override corresponding picture's raster space as an optimisation.
+ pub is_ancestor_or_self_zooming: bool,
+}
+
+fn compute_offset_from(
+ mut current: Option<SpatialNodeIndex>,
+ external_id: ExternalScrollId,
+ previous_spatial_nodes: &[SpatialNode],
+) -> LayoutVector2D {
+ let mut offset = LayoutVector2D::zero();
+ while let Some(parent_index) = current {
+ let ancestor = &previous_spatial_nodes[parent_index.0 as usize];
+ match ancestor.node_type {
+ SpatialNodeType::ReferenceFrame(..) => {
+ // We don't want to scroll across reference frames.
+ break;
+ },
+ SpatialNodeType::ScrollFrame(ref info) => {
+ if info.external_id == external_id {
+ break;
+ }
+
+ // External scroll offsets are not propagated across
+ // reference frame boundaries, so undo them here.
+ offset += info.offset + info.external_scroll_offset;
+ },
+ SpatialNodeType::StickyFrame(ref info) => {
+ offset += info.current_offset;
+ },
+ }
+ current = ancestor.parent;
+ }
+ offset
+}
+
+/// Snap an offset to be incorporated into a transform, where the local space
+/// may be considered the world space. We convert from world space to device
+/// space using the global device pixel scale, which may not always be correct
+/// if there are intermediate surfaces used, however those are either cases
+/// where snapping is not important (e.g. has perspective or is not axis
+/// aligned), or an edge case (e.g. SVG filters) which we can accept
+/// imperfection for now.
+fn snap_offset<OffsetUnits, ScaleUnits>(
+ offset: Vector2D<f32, OffsetUnits>,
+ scale: Vector2D<f32, ScaleUnits>,
+ global_device_pixel_scale: DevicePixelScale,
+) -> Vector2D<f32, OffsetUnits> {
+ let world_offset = Point2D::new(offset.x * scale.x, offset.y * scale.y);
+ let snapped_device_offset = (world_offset * global_device_pixel_scale).snap();
+ let snapped_world_offset = snapped_device_offset / global_device_pixel_scale;
+ Vector2D::new(
+ if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
+ if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
+ )
+}
+
+impl SpatialNode {
+ pub fn new(
+ pipeline_id: PipelineId,
+ parent_index: Option<SpatialNodeIndex>,
+ node_type: SpatialNodeType,
+ ) -> Self {
+ SpatialNode {
+ viewport_transform: ScaleOffset::identity(),
+ content_transform: ScaleOffset::identity(),
+ snapping_transform: None,
+ coordinate_system_id: CoordinateSystemId(0),
+ transform_kind: TransformedRectKind::AxisAligned,
+ parent: parent_index,
+ children: Vec::new(),
+ pipeline_id,
+ node_type,
+ invertible: true,
+ is_async_zooming: false,
+ is_ancestor_or_self_zooming: false,
+ }
+ }
+
+ pub fn new_scroll_frame(
+ pipeline_id: PipelineId,
+ parent_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ scroll_sensitivity: ScrollSensitivity,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> Self {
+ let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
+ *frame_rect,
+ scroll_sensitivity,
+ LayoutSize::new(
+ (content_size.width - frame_rect.size.width).max(0.0),
+ (content_size.height - frame_rect.size.height).max(0.0)
+ ),
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ )
+ );
+
+ Self::new(pipeline_id, Some(parent_index), node_type)
+ }
+
+ pub fn new_reference_frame(
+ parent_index: Option<SpatialNodeIndex>,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ pipeline_id: PipelineId,
+ ) -> Self {
+ let info = ReferenceFrameInfo {
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ invertible: true,
+ };
+ Self::new(pipeline_id, parent_index, SpatialNodeType::ReferenceFrame(info))
+ }
+
+ pub fn new_sticky_frame(
+ parent_index: SpatialNodeIndex,
+ sticky_frame_info: StickyFrameInfo,
+ pipeline_id: PipelineId,
+ ) -> Self {
+ Self::new(pipeline_id, Some(parent_index), SpatialNodeType::StickyFrame(sticky_frame_info))
+ }
+
+ pub fn add_child(&mut self, child: SpatialNodeIndex) {
+ self.children.push(child);
+ }
+
+ pub fn apply_old_scrolling_state(&mut self, old_scroll_info: &ScrollFrameInfo) {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => {
+ *scrolling = scrolling.combine_with_old_scroll_info(old_scroll_info);
+ }
+ _ if old_scroll_info.offset != LayoutVector2D::zero() => {
+ warn!("Tried to scroll a non-scroll node.")
+ }
+ _ => {}
+ }
+ }
+
+ pub fn set_scroll_origin(&mut self, origin: &LayoutPoint, clamp: ScrollClamping) -> bool {
+ let scrolling = match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
+ _ => {
+ warn!("Tried to scroll a non-scroll node.");
+ return false;
+ }
+ };
+
+ let normalized_offset = match clamp {
+ ScrollClamping::ToContentBounds => {
+ let scrollable_size = scrolling.scrollable_size;
+ let scrollable_width = scrollable_size.width;
+ let scrollable_height = scrollable_size.height;
+
+ if scrollable_height <= 0. && scrollable_width <= 0. {
+ return false;
+ }
+
+ let origin = LayoutPoint::new(origin.x.max(0.0), origin.y.max(0.0));
+ LayoutVector2D::new(
+ (-origin.x).max(-scrollable_width).min(0.0),
+ (-origin.y).max(-scrollable_height).min(0.0),
+ )
+ }
+ ScrollClamping::NoClamping => LayoutPoint::zero() - *origin,
+ };
+
+ let new_offset = normalized_offset - scrolling.external_scroll_offset;
+
+ if new_offset == scrolling.offset {
+ return false;
+ }
+
+ scrolling.offset = new_offset;
+ true
+ }
+
+ pub fn mark_uninvertible(
+ &mut self,
+ state: &TransformUpdateState,
+ ) {
+ self.invertible = false;
+ self.viewport_transform = ScaleOffset::identity();
+ self.content_transform = ScaleOffset::identity();
+ self.coordinate_system_id = state.current_coordinate_system_id;
+ }
+
+ pub fn update(
+ &mut self,
+ state: &mut TransformUpdateState,
+ coord_systems: &mut Vec<CoordinateSystem>,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ previous_spatial_nodes: &[SpatialNode],
+ ) {
+ // If any of our parents was not rendered, we are not rendered either and can just
+ // quit here.
+ if !state.invertible {
+ self.mark_uninvertible(state);
+ return;
+ }
+
+ self.update_transform(state, coord_systems, global_device_pixel_scale, scene_properties, previous_spatial_nodes);
+ //TODO: remove the field entirely?
+ self.transform_kind = if self.coordinate_system_id.0 == 0 {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ };
+
+ let is_parent_zooming = match self.parent {
+ Some(parent) => previous_spatial_nodes[parent.0 as usize].is_ancestor_or_self_zooming,
+ _ => false,
+ };
+ self.is_ancestor_or_self_zooming = self.is_async_zooming | is_parent_zooming;
+
+ // If this node is a reference frame, we check if it has a non-invertible matrix.
+ // For non-reference-frames we assume that they will produce only additional
+ // translations which should be invertible.
+ match self.node_type {
+ SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
+ self.mark_uninvertible(state);
+ }
+ _ => self.invertible = true,
+ }
+ }
+
+ pub fn update_transform(
+ &mut self,
+ state: &mut TransformUpdateState,
+ coord_systems: &mut Vec<CoordinateSystem>,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ previous_spatial_nodes: &[SpatialNode],
+ ) {
+ match self.node_type {
+ SpatialNodeType::ReferenceFrame(ref mut info) => {
+ let mut cs_scale_offset = ScaleOffset::identity();
+
+ if info.invertible {
+ // Resolve the transform against any property bindings.
+ let source_transform = LayoutFastTransform::from(
+ scene_properties.resolve_layout_transform(&info.source_transform)
+ );
+
+ // Do a change-basis operation on the perspective matrix using
+ // the scroll offset.
+ let source_transform = match info.kind {
+ ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
+ let scroll_offset = compute_offset_from(
+ self.parent,
+ external_id,
+ previous_spatial_nodes,
+ );
+
+ // Do a change-basis operation on the
+ // perspective matrix using the scroll offset.
+ source_transform
+ .pre_translate(scroll_offset)
+ .then_translate(-scroll_offset)
+ }
+ ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
+ ReferenceFrameKind::Transform | ReferenceFrameKind::Zoom => source_transform,
+ };
+
+ let resolved_transform =
+ LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
+ .pre_transform(&source_transform);
+
+ // The transformation for this viewport in world coordinates is the transformation for
+ // our parent reference frame, plus any accumulated scrolling offsets from nodes
+ // between our reference frame and this node. Finally, we also include
+ // whatever local transformation this reference frame provides.
+ let relative_transform = resolved_transform
+ .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale, global_device_pixel_scale))
+ .to_transform()
+ .with_destination::<LayoutPixel>();
+
+ let mut reset_cs_id = match info.transform_style {
+ TransformStyle::Preserve3D => !state.preserves_3d,
+ TransformStyle::Flat => state.preserves_3d,
+ };
+
+ // We reset the coordinate system upon either crossing the preserve-3d context boundary,
+ // or simply a 3D transformation.
+ if !reset_cs_id {
+ // Try to update our compatible coordinate system transform. If we cannot, start a new
+ // incompatible coordinate system.
+ match ScaleOffset::from_transform(&relative_transform) {
+ Some(ref scale_offset) => {
+ // We generally do not want to snap animated transforms as it causes jitter.
+ // However, we do want to snap the visual viewport offset when scrolling.
+ // Therefore only snap the transform for Zoom reference frames. This may still
+ // cause jitter when zooming, unfortunately.
+ let mut maybe_snapped = scale_offset.clone();
+ if info.kind == ReferenceFrameKind::Zoom {
+ maybe_snapped.offset = snap_offset(
+ scale_offset.offset,
+ state.coordinate_system_relative_scale_offset.scale,
+ global_device_pixel_scale
+ );
+ }
+ cs_scale_offset =
+ state.coordinate_system_relative_scale_offset.accumulate(&maybe_snapped);
+ }
+ None => reset_cs_id = true,
+ }
+ }
+ if reset_cs_id {
+ // If we break 2D axis alignment or have a perspective component, we need to start a
+ // new incompatible coordinate system with which we cannot share clips without masking.
+ let transform = relative_transform.then(
+ &state.coordinate_system_relative_scale_offset.to_transform()
+ );
+
+ // Push that new coordinate system and record the new id.
+ let coord_system = {
+ let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
+ let mut cur_transform = transform;
+ if parent_system.should_flatten {
+ cur_transform.flatten_z_output();
+ }
+ let world_transform = cur_transform.then(&parent_system.world_transform);
+ let determinant = world_transform.determinant();
+ info.invertible = determinant != 0.0 && !determinant.is_nan();
+
+ CoordinateSystem {
+ transform,
+ world_transform,
+ should_flatten: match (info.transform_style, info.kind) {
+ (TransformStyle::Flat, ReferenceFrameKind::Transform) => true,
+ (_, _) => false,
+ },
+ parent: Some(state.current_coordinate_system_id),
+ }
+ };
+ state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
+ coord_systems.push(coord_system);
+ }
+ }
+
+ // Ensure that the current coordinate system ID is propagated to child
+ // nodes, even if we encounter a node that is not invertible. This ensures
+ // that the invariant in get_relative_transform is not violated.
+ self.coordinate_system_id = state.current_coordinate_system_id;
+ self.viewport_transform = cs_scale_offset;
+ self.content_transform = cs_scale_offset;
+ self.invertible = info.invertible;
+ }
+ _ => {
+ // We calculate this here to avoid a double-borrow later.
+ let sticky_offset = self.calculate_sticky_offset(
+ &state.nearest_scrolling_ancestor_offset,
+ &state.nearest_scrolling_ancestor_viewport,
+ );
+
+ // The transformation for the bounds of our viewport is the parent reference frame
+ // transform, plus any accumulated scroll offset from our parents, plus any offset
+ // provided by our own sticky positioning.
+ let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
+ self.viewport_transform = state.coordinate_system_relative_scale_offset
+ .offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale, global_device_pixel_scale).to_untyped());
+
+ // The transformation for any content inside of us is the viewport transformation, plus
+ // whatever scrolling offset we supply as well.
+ let added_offset = accumulated_offset + self.scroll_offset();
+ self.content_transform = state.coordinate_system_relative_scale_offset
+ .offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale, global_device_pixel_scale).to_untyped());
+
+ if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
+ info.current_offset = sticky_offset;
+ }
+
+ self.coordinate_system_id = state.current_coordinate_system_id;
+ }
+ }
+ }
+
+ fn calculate_sticky_offset(
+ &self,
+ viewport_scroll_offset: &LayoutVector2D,
+ viewport_rect: &LayoutRect,
+ ) -> LayoutVector2D {
+ let info = match self.node_type {
+ SpatialNodeType::StickyFrame(ref info) => info,
+ _ => return LayoutVector2D::zero(),
+ };
+
+ if info.margins.top.is_none() && info.margins.bottom.is_none() &&
+ info.margins.left.is_none() && info.margins.right.is_none() {
+ return LayoutVector2D::zero();
+ }
+
+ // The viewport and margins of the item establishes the maximum amount that it can
+ // be offset in order to keep it on screen. Since we care about the relationship
+ // between the scrolled content and unscrolled viewport we adjust the viewport's
+ // position by the scroll offset in order to work with their relative positions on the
+ // page.
+ let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);
+
+ let mut sticky_offset = LayoutVector2D::zero();
+ if let Some(margin) = info.margins.top {
+ let top_viewport_edge = viewport_rect.min_y() + margin;
+ if sticky_rect.min_y() < top_viewport_edge {
+ // If the sticky rect is positioned above the top edge of the viewport (plus margin)
+ // we move it down so that it is fully inside the viewport.
+ sticky_offset.y = top_viewport_edge - sticky_rect.min_y();
+ } else if info.previously_applied_offset.y > 0.0 &&
+ sticky_rect.min_y() > top_viewport_edge {
+ // However, if the sticky rect is positioned *below* the top edge of the viewport
+ // and there is already some offset applied to the sticky rect's position, then
+ // we need to move it up so that it remains at the correct position. This
+ // makes sticky_offset.y negative and effectively reduces the amount of the
+ // offset that was already applied. We limit the reduction so that it can, at most,
+ // cancel out the already-applied offset, but should never end up adjusting the
+ // position the other way.
+ sticky_offset.y = top_viewport_edge - sticky_rect.min_y();
+ sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
+ }
+ }
+
+ // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
+ // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
+ // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
+ // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
+ // zero in the above block.
+ if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
+ if let Some(margin) = info.margins.bottom {
+ // If sticky_offset.y is nonzero that means we must have set it
+ // in the sticky-top handling code above, so this item must have
+ // both top and bottom sticky margins. We adjust the item's rect
+ // by the top-sticky offset, and then combine any offset from
+ // the bottom-sticky calculation into sticky_offset below.
+ sticky_rect.origin.y += sticky_offset.y;
+
+ // Same as the above case, but inverted for bottom-sticky items. Here
+ // we adjust items upwards, resulting in a negative sticky_offset.y,
+ // or reduce the already-present upward adjustment, resulting in a positive
+ // sticky_offset.y.
+ let bottom_viewport_edge = viewport_rect.max_y() - margin;
+ if sticky_rect.max_y() > bottom_viewport_edge {
+ sticky_offset.y += bottom_viewport_edge - sticky_rect.max_y();
+ } else if info.previously_applied_offset.y < 0.0 &&
+ sticky_rect.max_y() < bottom_viewport_edge {
+ sticky_offset.y += bottom_viewport_edge - sticky_rect.max_y();
+ sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
+ }
+ }
+ }
+
+ // Same as above, but for the x-axis.
+ if let Some(margin) = info.margins.left {
+ let left_viewport_edge = viewport_rect.min_x() + margin;
+ if sticky_rect.min_x() < left_viewport_edge {
+ sticky_offset.x = left_viewport_edge - sticky_rect.min_x();
+ } else if info.previously_applied_offset.x > 0.0 &&
+ sticky_rect.min_x() > left_viewport_edge {
+ sticky_offset.x = left_viewport_edge - sticky_rect.min_x();
+ sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
+ }
+ }
+
+ if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
+ if let Some(margin) = info.margins.right {
+ sticky_rect.origin.x += sticky_offset.x;
+ let right_viewport_edge = viewport_rect.max_x() - margin;
+ if sticky_rect.max_x() > right_viewport_edge {
+ sticky_offset.x += right_viewport_edge - sticky_rect.max_x();
+ } else if info.previously_applied_offset.x < 0.0 &&
+ sticky_rect.max_x() < right_viewport_edge {
+ sticky_offset.x += right_viewport_edge - sticky_rect.max_x();
+ sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
+ }
+ }
+ }
+
+ // The total "sticky offset" (which is the sum that was already applied by
+ // the calling code, stored in info.previously_applied_offset, and the extra amount we
+ // computed as a result of scrolling, stored in sticky_offset) needs to be
+ // clamped to the provided bounds.
+ let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
+ (value + adjust).max(bounds.min).min(bounds.max) - adjust
+ };
+ sticky_offset.y = clamp_adjusted(sticky_offset.y,
+ info.previously_applied_offset.y,
+ &info.vertical_offset_bounds);
+ sticky_offset.x = clamp_adjusted(sticky_offset.x,
+ info.previously_applied_offset.x,
+ &info.horizontal_offset_bounds);
+
+ sticky_offset
+ }
+
+ pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
+ if !self.invertible {
+ state.invertible = false;
+ return;
+ }
+
+ // The transformation we are passing is the transformation of the parent
+ // reference frame and the offset is the accumulated offset of all the nodes
+ // between us and the parent reference frame. If we are a reference frame,
+ // we need to reset both these values.
+ match self.node_type {
+ SpatialNodeType::StickyFrame(ref info) => {
+ // We don't translate the combined rect by the sticky offset, because sticky
+ // offsets actually adjust the node position itself, whereas scroll offsets
+ // only apply to contents inside the node.
+ state.parent_accumulated_scroll_offset += info.current_offset;
+ // We want nested sticky items to take into account the shift
+ // we applied as well.
+ state.nearest_scrolling_ancestor_offset += info.current_offset;
+ state.preserves_3d = false;
+ }
+ SpatialNodeType::ScrollFrame(ref scrolling) => {
+ state.parent_accumulated_scroll_offset += scrolling.offset;
+ state.nearest_scrolling_ancestor_offset = scrolling.offset;
+ state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
+ state.preserves_3d = false;
+ }
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
+ state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
+ state.coordinate_system_relative_scale_offset = self.content_transform;
+ let translation = -info.origin_in_parent_reference_frame;
+ state.nearest_scrolling_ancestor_viewport =
+ state.nearest_scrolling_ancestor_viewport
+ .translate(translation);
+ }
+ }
+ }
+
+ pub fn scroll(&mut self, scroll_location: ScrollLocation) -> bool {
+ // TODO(gw): This scroll method doesn't currently support
+ // scroll nodes with non-zero external scroll
+ // offsets. However, it's never used by Gecko,
+ // which is the only client that requires
+ // non-zero external scroll offsets.
+
+ let scrolling = match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
+ _ => return false,
+ };
+
+ let delta = match scroll_location {
+ ScrollLocation::Delta(delta) => delta,
+ ScrollLocation::Start => {
+ if scrolling.offset.y.round() >= 0.0 {
+ // Nothing to do on this layer.
+ return false;
+ }
+
+ scrolling.offset.y = 0.0;
+ return true;
+ }
+ ScrollLocation::End => {
+ let end_pos = -scrolling.scrollable_size.height;
+ if scrolling.offset.y.round() <= end_pos {
+ // Nothing to do on this layer.
+ return false;
+ }
+
+ scrolling.offset.y = end_pos;
+ return true;
+ }
+ };
+
+ let scrollable_width = scrolling.scrollable_size.width;
+ let scrollable_height = scrolling.scrollable_size.height;
+ let original_layer_scroll_offset = scrolling.offset;
+
+ if scrollable_width > 0. {
+ scrolling.offset.x = (scrolling.offset.x + delta.x)
+ .min(0.0)
+ .max(-scrollable_width);
+ }
+
+ if scrollable_height > 0. {
+ scrolling.offset.y = (scrolling.offset.y + delta.y)
+ .min(0.0)
+ .max(-scrollable_height);
+ }
+
+ scrolling.offset != original_layer_scroll_offset
+ }
+
+ pub fn scroll_offset(&self) -> LayoutVector2D {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset,
+ _ => LayoutVector2D::zero(),
+ }
+ }
+
+ pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(info) if info.external_id == external_id => true,
+ _ => false,
+ }
+ }
+
+ /// Updates the snapping transform.
+ pub fn update_snapping(
+ &mut self,
+ parent: Option<&SpatialNode>,
+ ) {
+ // Reset in case of an early return.
+ self.snapping_transform = None;
+
+ // We need to incorporate the parent scale/offset with the child.
+ // If the parent does not have a scale/offset, then we know we are
+ // not 2d axis aligned and thus do not need to snap its children
+ // either.
+ let parent_scale_offset = match parent {
+ Some(parent) => {
+ match parent.snapping_transform {
+ Some(scale_offset) => scale_offset,
+ None => return,
+ }
+ },
+ _ => ScaleOffset::identity(),
+ };
+
+ let scale_offset = match self.node_type {
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ match info.source_transform {
+ PropertyBinding::Value(ref value) => {
+ // We can only get a ScaleOffset if the transform is 2d axis
+ // aligned.
+ match ScaleOffset::from_transform(value) {
+ Some(scale_offset) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ .accumulate(&scale_offset)
+ }
+ None => return,
+ }
+ }
+
+ // Assume animations start at the identity transform for snapping purposes.
+ // We still want to incorporate the reference frame offset however.
+ // TODO(aosmond): Is there a better known starting point?
+ PropertyBinding::Binding(..) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ }
+ }
+ }
+ _ => ScaleOffset::identity(),
+ };
+
+ self.snapping_transform = Some(parent_scale_offset.accumulate(&scale_offset));
+ }
+
+ /// Returns true for ReferenceFrames whose source_transform is
+ /// bound to the property binding id.
+ pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
+ if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
+ if let PropertyBinding::Binding(key, _) = info.source_transform {
+ id == key.id
+ } else {
+ false
+ }
+ } else {
+ false
+ }
+ }
+}
+
+/// Defines whether we have an implicit scroll frame for a pipeline root,
+/// or an explicitly defined scroll frame from the display list.
+#[derive(Copy, Clone, Debug)]
+pub enum ScrollFrameKind {
+ PipelineRoot {
+ is_root_pipeline: bool,
+ },
+ Explicit,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct ScrollFrameInfo {
+ /// The rectangle of the viewport of this scroll frame. This is important for
+ /// positioning of items inside child StickyFrames.
+ pub viewport_rect: LayoutRect,
+
+ pub scroll_sensitivity: ScrollSensitivity,
+
+ /// Amount that this ScrollFrame can scroll in both directions.
+ pub scrollable_size: LayoutSize,
+
+ /// An external id to identify this scroll frame to API clients. This
+ /// allows setting scroll positions via the API without relying on ClipsIds
+ /// which may change between frames.
+ pub external_id: ExternalScrollId,
+
+ /// Stores whether this is a scroll frame added implicitly by WR when adding
+ /// a pipeline (either the root or an iframe). We need to exclude these
+ /// when searching for scroll roots we care about for picture caching.
+ /// TODO(gw): I think we can actually completely remove the implicit
+ /// scroll frame being added by WR, and rely on the embedder
+ /// to define scroll frames. However, that involves API changes
+ /// so we will use this as a temporary hack!
+ pub frame_kind: ScrollFrameKind,
+
+ /// Amount that visual components attached to this scroll node have been
+ /// pre-scrolled in their local coordinates.
+ pub external_scroll_offset: LayoutVector2D,
+
+ /// The negated scroll offset of this scroll node. including the
+ /// pre-scrolled amount. If, for example, a scroll node was pre-scrolled
+ /// to y=10 (10 pixels down from the initial unscrolled position), then
+ /// `external_scroll_offset` would be (0,10), and this `offset` field would
+ /// be (0,-10). If WebRender is then asked to change the scroll position by
+ /// an additional 10 pixels (without changing the pre-scroll amount in the
+ /// display list), `external_scroll_offset` would remain at (0,10) and
+ /// `offset` would change to (0,-20).
+ pub offset: LayoutVector2D,
+}
+
+/// Manages scrolling offset.
+impl ScrollFrameInfo {
+ pub fn new(
+ viewport_rect: LayoutRect,
+ scroll_sensitivity: ScrollSensitivity,
+ scrollable_size: LayoutSize,
+ external_id: ExternalScrollId,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> ScrollFrameInfo {
+ ScrollFrameInfo {
+ viewport_rect,
+ offset: -external_scroll_offset,
+ scroll_sensitivity,
+ scrollable_size,
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ }
+ }
+
+ pub fn sensitive_to_input_events(&self) -> bool {
+ match self.scroll_sensitivity {
+ ScrollSensitivity::ScriptAndInputEvents => true,
+ ScrollSensitivity::Script => false,
+ }
+ }
+
+ pub fn combine_with_old_scroll_info(
+ self,
+ old_scroll_info: &ScrollFrameInfo
+ ) -> ScrollFrameInfo {
+ ScrollFrameInfo {
+ viewport_rect: self.viewport_rect,
+ offset: old_scroll_info.offset,
+ scroll_sensitivity: self.scroll_sensitivity,
+ scrollable_size: self.scrollable_size,
+ external_id: self.external_id,
+ frame_kind: self.frame_kind,
+ external_scroll_offset: self.external_scroll_offset,
+ }
+ }
+}
+
+/// Contains information about reference frames.
+#[derive(Copy, Clone, Debug)]
+pub struct ReferenceFrameInfo {
+ /// The source transform and perspective matrices provided by the stacking context
+ /// that forms this reference frame. We maintain the property binding information
+ /// here so that we can resolve the animated transform and update the tree each
+ /// frame.
+ pub source_transform: PropertyBinding<LayoutTransform>,
+ pub transform_style: TransformStyle,
+ pub kind: ReferenceFrameKind,
+
+ /// The original, not including the transform and relative to the parent reference frame,
+ /// origin of this reference frame. This is already rolled into the `transform' property, but
+ /// we also store it here to properly transform the viewport for sticky positioning.
+ pub origin_in_parent_reference_frame: LayoutVector2D,
+
+ /// True if the resolved transform is invertible.
+ pub invertible: bool,
+}
+
+#[derive(Clone, Debug)]
+pub struct StickyFrameInfo {
+ pub frame_rect: LayoutRect,
+ pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ pub vertical_offset_bounds: StickyOffsetBounds,
+ pub horizontal_offset_bounds: StickyOffsetBounds,
+ pub previously_applied_offset: LayoutVector2D,
+ pub current_offset: LayoutVector2D,
+}
+
+impl StickyFrameInfo {
+ pub fn new(
+ frame_rect: LayoutRect,
+ margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ vertical_offset_bounds: StickyOffsetBounds,
+ horizontal_offset_bounds: StickyOffsetBounds,
+ previously_applied_offset: LayoutVector2D
+ ) -> StickyFrameInfo {
+ StickyFrameInfo {
+ frame_rect,
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ previously_applied_offset,
+ current_offset: LayoutVector2D::zero(),
+ }
+ }
+}
+
+#[test]
+fn test_cst_perspective_relative_scroll() {
+ // Verify that when computing the offset from a perspective transform
+ // to a relative scroll node that any external scroll offset is
+ // ignored. This is because external scroll offsets are not
+ // propagated across reference frame boundaries.
+
+ // It's not currently possible to verify this with a wrench reftest,
+ // since wrench doesn't understand external scroll ids. When wrench
+ // supports this, we could also verify with a reftest.
+
+ use crate::spatial_tree::SpatialTree;
+ use euclid::approxeq::ApproxEq;
+
+ let mut cst = SpatialTree::new();
+ let pipeline_id = PipelineId::dummy();
+ let ext_scroll_id = ExternalScrollId(1, pipeline_id);
+ let transform = LayoutTransform::perspective(100.0);
+
+ let root = cst.add_reference_frame(
+ None,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform,
+ LayoutVector2D::zero(),
+ pipeline_id,
+ );
+
+ let scroll_frame_1 = cst.add_scroll_frame(
+ root,
+ ext_scroll_id,
+ pipeline_id,
+ &LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollSensitivity::Script,
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::zero(),
+ );
+
+ let scroll_frame_2 = cst.add_scroll_frame(
+ scroll_frame_1,
+ ExternalScrollId(2, pipeline_id),
+ pipeline_id,
+ &LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollSensitivity::Script,
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 50.0),
+ );
+
+ let ref_frame = cst.add_reference_frame(
+ Some(scroll_frame_2),
+ TransformStyle::Preserve3D,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Perspective {
+ scrolling_relative_to: Some(ext_scroll_id),
+ },
+ LayoutVector2D::zero(),
+ pipeline_id,
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ let scroll_offset = compute_offset_from(
+ cst.spatial_nodes[ref_frame.0 as usize].parent,
+ ext_scroll_id,
+ &cst.spatial_nodes,
+ );
+
+ assert!(scroll_offset.x.approx_eq(&0.0));
+ assert!(scroll_offset.y.approx_eq(&0.0));
+}
diff --git a/gfx/wr/webrender/src/spatial_tree.rs b/gfx/wr/webrender/src/spatial_tree.rs
new file mode 100644
index 0000000000..04da9d9ea9
--- /dev/null
+++ b/gfx/wr/webrender/src/spatial_tree.rs
@@ -0,0 +1,1075 @@
+/* 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 api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle};
+use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollSensitivity};
+use api::units::*;
+use euclid::Transform3D;
+use crate::gpu_types::TransformPalette;
+use crate::internal_types::{FastHashMap, FastHashSet};
+use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
+use crate::scene::SceneProperties;
+use crate::spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
+use std::{ops, u32};
+use crate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
+
+pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
+
+/// An id that identifies coordinate systems in the SpatialTree. Each
+/// coordinate system has an id and those ids will be shared when the coordinates
+/// system are the same or are in the same axis-aligned space. This allows
+/// for optimizing mask generation.
+#[derive(Debug, Copy, Clone, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct CoordinateSystemId(pub u32);
+
+/// A node in the hierarchy of coordinate system
+/// transforms.
+#[derive(Debug)]
+pub struct CoordinateSystem {
+ pub transform: LayoutTransform,
+ pub world_transform: LayoutToWorldTransform,
+ pub should_flatten: bool,
+ pub parent: Option<CoordinateSystemId>,
+}
+
+impl CoordinateSystem {
+ fn root() -> Self {
+ CoordinateSystem {
+ transform: LayoutTransform::identity(),
+ world_transform: LayoutToWorldTransform::identity(),
+ should_flatten: false,
+ parent: None,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SpatialNodeIndex(pub u32);
+
+impl SpatialNodeIndex {
+ pub const INVALID: SpatialNodeIndex = SpatialNodeIndex(u32::MAX);
+}
+
+//Note: these have to match ROOT_REFERENCE_FRAME_SPATIAL_ID and ROOT_SCROLL_NODE_SPATIAL_ID
+pub const ROOT_SPATIAL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(0);
+const TOPMOST_SCROLL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(1);
+
+// In some cases, the conversion from CSS pixels to device pixels can result in small
+// rounding errors when calculating the scrollable distance of a scroll frame. Apply
+// a small epsilon so that we don't detect these frames as "real" scroll frames.
+const MIN_SCROLLABLE_AMOUNT: f32 = 0.01;
+
+impl SpatialNodeIndex {
+ pub fn new(index: usize) -> Self {
+ debug_assert!(index < ::std::u32::MAX as usize);
+ SpatialNodeIndex(index as u32)
+ }
+}
+
+impl CoordinateSystemId {
+ pub fn root() -> Self {
+ CoordinateSystemId(0)
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub enum VisibleFace {
+ Front,
+ Back,
+}
+
+impl Default for VisibleFace {
+ fn default() -> Self {
+ VisibleFace::Front
+ }
+}
+
+impl ops::Not for VisibleFace {
+ type Output = Self;
+ fn not(self) -> Self {
+ match self {
+ VisibleFace::Front => VisibleFace::Back,
+ VisibleFace::Back => VisibleFace::Front,
+ }
+ }
+}
+
+pub struct SpatialTree {
+ /// Nodes which determine the positions (offsets and transforms) for primitives
+ /// and clips.
+ pub spatial_nodes: Vec<SpatialNode>,
+
+ /// A list of transforms that establish new coordinate systems.
+ /// Spatial nodes only establish a new coordinate system when
+ /// they have a transform that is not a simple 2d translation.
+ coord_systems: Vec<CoordinateSystem>,
+
+ pub pending_scroll_offsets: FastHashMap<ExternalScrollId, (LayoutPoint, ScrollClamping)>,
+
+ /// A set of pipelines which should be discarded the next time this
+ /// tree is drained.
+ pub pipelines_to_discard: FastHashSet<PipelineId>,
+
+ /// Temporary stack of nodes to update when traversing the tree.
+ nodes_to_update: Vec<(SpatialNodeIndex, TransformUpdateState)>,
+}
+
+#[derive(Clone)]
+pub struct TransformUpdateState {
+ pub parent_reference_frame_transform: LayoutToWorldFastTransform,
+ pub parent_accumulated_scroll_offset: LayoutVector2D,
+ pub nearest_scrolling_ancestor_offset: LayoutVector2D,
+ pub nearest_scrolling_ancestor_viewport: LayoutRect,
+
+ /// An id for keeping track of the axis-aligned space of this node. This is used in
+ /// order to to track what kinds of clip optimizations can be done for a particular
+ /// display list item, since optimizations can usually only be done among
+ /// coordinate systems which are relatively axis aligned.
+ pub current_coordinate_system_id: CoordinateSystemId,
+
+ /// Scale and offset from the coordinate system that started this compatible coordinate system.
+ pub coordinate_system_relative_scale_offset: ScaleOffset,
+
+ /// True if this node is transformed by an invertible transform. If not, display items
+ /// transformed by this node will not be displayed and display items not transformed by this
+ /// node will not be clipped by clips that are transformed by this node.
+ pub invertible: bool,
+
+ /// True if this node is a part of Preserve3D hierarchy.
+ pub preserves_3d: bool,
+}
+
+
+/// Transformation between two nodes in the spatial tree that can sometimes be
+/// encoded more efficiently than with a full matrix.
+#[derive(Debug, Clone)]
+pub enum CoordinateSpaceMapping<Src, Dst> {
+ Local,
+ ScaleOffset(ScaleOffset),
+ Transform(Transform3D<f32, Src, Dst>),
+}
+
+impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
+ pub fn into_transform(self) -> Transform3D<f32, Src, Dst> {
+ match self {
+ CoordinateSpaceMapping::Local => Transform3D::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
+ CoordinateSpaceMapping::Transform(transform) => transform,
+ }
+ }
+
+ pub fn into_fast_transform(self) -> FastTransform<Src, Dst> {
+ match self {
+ CoordinateSpaceMapping::Local => FastTransform::identity(),
+ CoordinateSpaceMapping::ScaleOffset(scale_offset) => FastTransform::with_scale_offset(scale_offset),
+ CoordinateSpaceMapping::Transform(transform) => FastTransform::with_transform(transform),
+ }
+ }
+
+ pub fn is_perspective(&self) -> bool {
+ match *self {
+ CoordinateSpaceMapping::Local |
+ CoordinateSpaceMapping::ScaleOffset(_) => false,
+ CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
+ }
+ }
+
+ pub fn is_2d_axis_aligned(&self) -> bool {
+ match *self {
+ CoordinateSpaceMapping::Local |
+ CoordinateSpaceMapping::ScaleOffset(_) => true,
+ CoordinateSpaceMapping::Transform(ref transform) => transform.preserves_2d_axis_alignment(),
+ }
+ }
+
+ pub fn scale_factors(&self) -> (f32, f32) {
+ match *self {
+ CoordinateSpaceMapping::Local => (1.0, 1.0),
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x, scale_offset.scale.y),
+ CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
+ }
+ }
+
+ pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
+ match *self {
+ CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
+ CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+ Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
+ }
+ CoordinateSpaceMapping::Transform(ref transform) => {
+ transform.inverse().map(CoordinateSpaceMapping::Transform)
+ }
+ }
+ }
+}
+
+enum TransformScroll {
+ Scrolled,
+ Unscrolled,
+}
+
+impl SpatialTree {
+ pub fn new() -> Self {
+ SpatialTree {
+ spatial_nodes: Vec::new(),
+ coord_systems: Vec::new(),
+ pending_scroll_offsets: FastHashMap::default(),
+ pipelines_to_discard: FastHashSet::default(),
+ nodes_to_update: Vec::new(),
+ }
+ }
+
+ /// Calculate the accumulated external scroll offset for
+ /// a given spatial node.
+ pub fn external_scroll_offset(&self, node_index: SpatialNodeIndex) -> LayoutVector2D {
+ let mut offset = LayoutVector2D::zero();
+ let mut current_node = Some(node_index);
+
+ while let Some(node_index) = current_node {
+ let node = &self.spatial_nodes[node_index.0 as usize];
+
+ match node.node_type {
+ SpatialNodeType::ScrollFrame(ref scrolling) => {
+ offset += scrolling.external_scroll_offset;
+ }
+ SpatialNodeType::StickyFrame(..) => {
+ // Doesn't provide any external scroll offset
+ }
+ SpatialNodeType::ReferenceFrame(..) => {
+ // External scroll offsets are not propagated across
+ // reference frames.
+ break;
+ }
+ }
+
+ current_node = node.parent;
+ }
+
+ offset
+ }
+
+ /// Calculate the relative transform from `child_index` to `parent_index`.
+ /// This method will panic if the nodes are not connected!
+ pub fn get_relative_transform(
+ &self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
+ self.get_relative_transform_with_face(child_index, parent_index, None)
+ }
+
+ /// Calculate the relative transform from `child_index` to `parent_index`.
+ /// This method will panic if the nodes are not connected!
+ /// Also, switch the visible face to `Back` if at any stage where the
+ /// combined transform is flattened, we see the back face.
+ pub fn get_relative_transform_with_face(
+ &self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ mut visible_face: Option<&mut VisibleFace>,
+ ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
+ if child_index == parent_index {
+ return CoordinateSpaceMapping::Local;
+ }
+
+ let child = &self.spatial_nodes[child_index.0 as usize];
+ let parent = &self.spatial_nodes[parent_index.0 as usize];
+
+ if child.coordinate_system_id == parent.coordinate_system_id {
+ let scale_offset = parent.content_transform
+ .inverse()
+ .accumulate(&child.content_transform);
+ return CoordinateSpaceMapping::ScaleOffset(scale_offset);
+ }
+
+ if child_index.0 < parent_index.0 {
+ warn!("Unexpected transform queried from {:?} to {:?}, please call the graphics team!", child_index, parent_index);
+ let child_cs = &self.coord_systems[child.coordinate_system_id.0 as usize];
+ let child_transform = child.content_transform
+ .to_transform::<LayoutPixel, LayoutPixel>()
+ .then(&child_cs.world_transform);
+ let parent_cs = &self.coord_systems[parent.coordinate_system_id.0 as usize];
+ let parent_transform = parent.content_transform
+ .to_transform()
+ .then(&parent_cs.world_transform);
+
+ let result = parent_transform
+ .inverse()
+ .unwrap_or_default()
+ .then(&child_transform)
+ .with_source::<LayoutPixel>()
+ .with_destination::<LayoutPixel>();
+
+ if let Some(face) = visible_face {
+ if result.is_backface_visible() {
+ *face = VisibleFace::Back;
+ }
+ }
+ return CoordinateSpaceMapping::Transform(result);
+ }
+
+ let mut coordinate_system_id = child.coordinate_system_id;
+ let mut transform = child.content_transform.to_transform();
+
+ // we need to update the associated parameters of a transform in two cases:
+ // 1) when the flattening happens, so that we don't lose that original 3D aspects
+ // 2) when we reach the end of iteration, so that our result is up to date
+
+ while coordinate_system_id != parent.coordinate_system_id {
+ let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
+
+ if coord_system.should_flatten {
+ if let Some(ref mut face) = visible_face {
+ if transform.is_backface_visible() {
+ **face = VisibleFace::Back;
+ }
+ }
+ transform.flatten_z_output();
+ }
+
+ coordinate_system_id = coord_system.parent.expect("invalid parent!");
+ transform = transform.then(&coord_system.transform);
+ }
+
+ transform = transform.then(
+ &parent.content_transform
+ .inverse()
+ .to_transform(),
+ );
+ if let Some(face) = visible_face {
+ if transform.is_backface_visible() {
+ *face = VisibleFace::Back;
+ }
+ }
+
+ CoordinateSpaceMapping::Transform(transform)
+ }
+
+ pub fn is_relative_transform_complex(
+ &self,
+ child_index: SpatialNodeIndex,
+ parent_index: SpatialNodeIndex,
+ ) -> bool {
+ if child_index == parent_index {
+ return false;
+ }
+
+ let child = &self.spatial_nodes[child_index.0 as usize];
+ let parent = &self.spatial_nodes[parent_index.0 as usize];
+
+ child.coordinate_system_id != parent.coordinate_system_id
+ }
+
+ fn get_world_transform_impl(
+ &self,
+ index: SpatialNodeIndex,
+ scroll: TransformScroll,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ let child = &self.spatial_nodes[index.0 as usize];
+
+ if child.coordinate_system_id.0 == 0 {
+ if index == ROOT_SPATIAL_NODE_INDEX {
+ CoordinateSpaceMapping::Local
+ } else {
+ CoordinateSpaceMapping::ScaleOffset(child.content_transform)
+ }
+ } else {
+ let system = &self.coord_systems[child.coordinate_system_id.0 as usize];
+ let scale_offset = match scroll {
+ TransformScroll::Scrolled => &child.content_transform,
+ TransformScroll::Unscrolled => &child.viewport_transform,
+ };
+ let transform = scale_offset
+ .to_transform()
+ .then(&system.world_transform);
+
+ CoordinateSpaceMapping::Transform(transform)
+ }
+ }
+
+ /// Calculate the relative transform from `index` to the root.
+ pub fn get_world_transform(
+ &self,
+ index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ self.get_world_transform_impl(index, TransformScroll::Scrolled)
+ }
+
+ /// Calculate the relative transform from `index` to the root.
+ /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset.
+ pub fn get_world_viewport_transform(
+ &self,
+ index: SpatialNodeIndex,
+ ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+ self.get_world_transform_impl(index, TransformScroll::Unscrolled)
+ }
+
+ /// The root reference frame, which is the true root of the SpatialTree. Initially
+ /// this ID is not valid, which is indicated by ```spatial_nodes``` being empty.
+ pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
+ // TODO(mrobinson): We should eventually make this impossible to misuse.
+ debug_assert!(!self.spatial_nodes.is_empty());
+ ROOT_SPATIAL_NODE_INDEX
+ }
+
+ /// The root scroll node which is the first child of the root reference frame.
+ /// Initially this ID is not valid, which is indicated by ```spatial_nodes``` being empty.
+ pub fn topmost_scroll_node_index(&self) -> SpatialNodeIndex {
+ // TODO(mrobinson): We should eventually make this impossible to misuse.
+ debug_assert!(self.spatial_nodes.len() >= 1);
+ TOPMOST_SCROLL_NODE_INDEX
+ }
+
+ pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
+ let mut result = vec![];
+ for node in &self.spatial_nodes {
+ if let SpatialNodeType::ScrollFrame(info) = node.node_type {
+ result.push(ScrollNodeState {
+ id: info.external_id,
+ scroll_offset: info.offset - info.external_scroll_offset,
+ })
+ }
+ }
+ result
+ }
+
+ pub fn drain(&mut self) -> ScrollStates {
+ let mut scroll_states = FastHashMap::default();
+ for old_node in &mut self.spatial_nodes.drain(..) {
+ if self.pipelines_to_discard.contains(&old_node.pipeline_id) {
+ continue;
+ }
+
+ match old_node.node_type {
+ SpatialNodeType::ScrollFrame(info) => {
+ scroll_states.insert(info.external_id, info);
+ }
+ _ => {}
+ }
+ }
+
+ self.coord_systems.clear();
+ self.pipelines_to_discard.clear();
+ scroll_states
+ }
+
+ pub fn scroll_node(
+ &mut self,
+ origin: LayoutPoint,
+ id: ExternalScrollId,
+ clamp: ScrollClamping
+ ) -> bool {
+ for node in &mut self.spatial_nodes {
+ if node.matches_external_id(id) {
+ return node.set_scroll_origin(&origin, clamp);
+ }
+ }
+
+ self.pending_scroll_offsets.insert(id, (origin, clamp));
+ false
+ }
+
+ pub fn update_tree(
+ &mut self,
+ pan: WorldPoint,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ ) {
+ if self.spatial_nodes.is_empty() {
+ return;
+ }
+
+ profile_scope!("update_tree");
+ self.coord_systems.clear();
+ self.coord_systems.push(CoordinateSystem::root());
+
+ let root_node_index = self.root_reference_frame_index();
+ let state = TransformUpdateState {
+ parent_reference_frame_transform: LayoutVector2D::new(pan.x, pan.y).into(),
+ parent_accumulated_scroll_offset: LayoutVector2D::zero(),
+ nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
+ nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
+ current_coordinate_system_id: CoordinateSystemId::root(),
+ coordinate_system_relative_scale_offset: ScaleOffset::identity(),
+ invertible: true,
+ preserves_3d: false,
+ };
+ debug_assert!(self.nodes_to_update.is_empty());
+ self.nodes_to_update.push((root_node_index, state));
+
+ while let Some((node_index, mut state)) = self.nodes_to_update.pop() {
+ let (previous, following) = self.spatial_nodes.split_at_mut(node_index.0 as usize);
+ let node = match following.get_mut(0) {
+ Some(node) => node,
+ None => continue,
+ };
+
+ node.update(&mut state, &mut self.coord_systems, global_device_pixel_scale, scene_properties, &*previous);
+
+ if !node.children.is_empty() {
+ node.prepare_state_for_children(&mut state);
+ self.nodes_to_update.extend(node.children
+ .iter()
+ .rev()
+ .map(|child_index| (*child_index, state.clone()))
+ );
+ }
+ }
+ }
+
+ pub fn build_transform_palette(&self) -> TransformPalette {
+ profile_scope!("build_transform_palette");
+ let mut palette = TransformPalette::new(self.spatial_nodes.len());
+ //Note: getting the world transform of a node is O(1) operation
+ for i in 0 .. self.spatial_nodes.len() {
+ let index = SpatialNodeIndex(i as u32);
+ let world_transform = self.get_world_transform(index).into_transform();
+ palette.set_world_transform(index, world_transform);
+ }
+ palette
+ }
+
+ pub fn finalize_and_apply_pending_scroll_offsets(&mut self, old_states: ScrollStates) {
+ for node in &mut self.spatial_nodes {
+ let external_id = match node.node_type {
+ SpatialNodeType::ScrollFrame(ScrollFrameInfo { external_id, ..}) => external_id,
+ _ => continue,
+ };
+
+ if let Some(scrolling_state) = old_states.get(&external_id) {
+ node.apply_old_scrolling_state(scrolling_state);
+ }
+
+ if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&external_id) {
+ node.set_scroll_origin(&offset, clamping);
+ }
+ }
+ }
+
+ pub fn add_scroll_frame(
+ &mut self,
+ parent_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ pipeline_id: PipelineId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ scroll_sensitivity: ScrollSensitivity,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> SpatialNodeIndex {
+ let node = SpatialNode::new_scroll_frame(
+ pipeline_id,
+ parent_index,
+ external_id,
+ frame_rect,
+ content_size,
+ scroll_sensitivity,
+ frame_kind,
+ external_scroll_offset,
+ );
+ self.add_spatial_node(node)
+ }
+
+ pub fn add_reference_frame(
+ &mut self,
+ parent_index: Option<SpatialNodeIndex>,
+ transform_style: TransformStyle,
+ source_transform: PropertyBinding<LayoutTransform>,
+ kind: ReferenceFrameKind,
+ origin_in_parent_reference_frame: LayoutVector2D,
+ pipeline_id: PipelineId,
+ ) -> SpatialNodeIndex {
+ let node = SpatialNode::new_reference_frame(
+ parent_index,
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ pipeline_id,
+ );
+ self.add_spatial_node(node)
+ }
+
+ pub fn add_sticky_frame(
+ &mut self,
+ parent_index: SpatialNodeIndex,
+ sticky_frame_info: StickyFrameInfo,
+ pipeline_id: PipelineId,
+ ) -> SpatialNodeIndex {
+ let node = SpatialNode::new_sticky_frame(
+ parent_index,
+ sticky_frame_info,
+ pipeline_id,
+ );
+ self.add_spatial_node(node)
+ }
+
+ pub fn add_spatial_node(&mut self, mut node: SpatialNode) -> SpatialNodeIndex {
+ let index = SpatialNodeIndex::new(self.spatial_nodes.len());
+
+ // When the parent node is None this means we are adding the root.
+ if let Some(parent_index) = node.parent {
+ let parent_node = &mut self.spatial_nodes[parent_index.0 as usize];
+ parent_node.add_child(index);
+ node.update_snapping(Some(parent_node));
+ } else {
+ node.update_snapping(None);
+ }
+
+ self.spatial_nodes.push(node);
+ index
+ }
+
+ pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
+ self.pipelines_to_discard.insert(pipeline_id);
+ }
+
+ /// Check if a given spatial node is an ancestor of another spatial node.
+ pub fn is_ancestor(
+ &self,
+ maybe_parent: SpatialNodeIndex,
+ maybe_child: SpatialNodeIndex,
+ ) -> bool {
+ // Early out if same node
+ if maybe_parent == maybe_child {
+ return false;
+ }
+
+ let mut current_node = maybe_child;
+
+ while current_node != ROOT_SPATIAL_NODE_INDEX {
+ let node = &self.spatial_nodes[current_node.0 as usize];
+ current_node = node.parent.expect("bug: no parent");
+
+ if current_node == maybe_parent {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ /// Find the spatial node that is the scroll root for a given spatial node.
+ /// A scroll root is the first spatial node when found travelling up the
+ /// spatial node tree that is an explicit scroll frame.
+ pub fn find_scroll_root(
+ &self,
+ spatial_node_index: SpatialNodeIndex,
+ ) -> SpatialNodeIndex {
+ let mut real_scroll_root = ROOT_SPATIAL_NODE_INDEX;
+ let mut outermost_scroll_root = ROOT_SPATIAL_NODE_INDEX;
+ let mut node_index = spatial_node_index;
+
+ while node_index != ROOT_SPATIAL_NODE_INDEX {
+ let node = &self.spatial_nodes[node_index.0 as usize];
+ match node.node_type {
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ match info.kind {
+ ReferenceFrameKind::Zoom => {
+ // We can handle scroll nodes that pass through a zoom node
+ }
+ ReferenceFrameKind::Transform |
+ ReferenceFrameKind::Perspective { .. } => {
+ // When a reference frame is encountered, forget any scroll roots
+ // we have encountered, as they may end up with a non-axis-aligned transform.
+ real_scroll_root = ROOT_SPATIAL_NODE_INDEX;
+ outermost_scroll_root = ROOT_SPATIAL_NODE_INDEX;
+ }
+ }
+ }
+ SpatialNodeType::StickyFrame(..) => {}
+ SpatialNodeType::ScrollFrame(ref info) => {
+ match info.frame_kind {
+ ScrollFrameKind::PipelineRoot { is_root_pipeline } => {
+ // Once we encounter a pipeline root, there is no need to look further
+ if is_root_pipeline {
+ break;
+ }
+ }
+ ScrollFrameKind::Explicit => {
+ // Store the closest scroll root we find to the root, for use
+ // later on, even if it's not actually scrollable.
+ outermost_scroll_root = node_index;
+
+ // If the scroll root has no scrollable area, we don't want to
+ // consider it. This helps pages that have a nested scroll root
+ // within a redundant scroll root to avoid selecting the wrong
+ // reference spatial node for a picture cache.
+ if info.scrollable_size.width > MIN_SCROLLABLE_AMOUNT ||
+ info.scrollable_size.height > MIN_SCROLLABLE_AMOUNT {
+ // Since we are skipping redundant scroll roots, we may end up
+ // selecting inner scroll roots that are very small. There is
+ // no performance benefit to creating a slice for these roots,
+ // as they are cheap to rasterize. The size comparison is in
+ // local-space, but makes for a reasonable estimate. The value
+ // is arbitrary, but is generally small enough to ignore things
+ // like scroll roots around text input elements.
+ if info.viewport_rect.size.width > 128.0 &&
+ info.viewport_rect.size.height > 128.0 {
+ // If we've found a root that is scrollable, and a reasonable
+ // size, select that as the current root for this node
+ real_scroll_root = node_index;
+ }
+ }
+ }
+ }
+ }
+ }
+ node_index = node.parent.expect("unable to find parent node");
+ }
+
+ // If we didn't find any real (scrollable) frames, then return the outermost
+ // redundant scroll frame. This is important so that we can correctly find
+ // the clips defined on the content which should be handled when drawing the
+ // picture cache tiles (by definition these clips are ancestors of the
+ // scroll root selected for the picture cache).
+ if real_scroll_root == ROOT_SPATIAL_NODE_INDEX {
+ outermost_scroll_root
+ } else {
+ real_scroll_root
+ }
+ }
+
+ fn print_node<T: PrintTreePrinter>(
+ &self,
+ index: SpatialNodeIndex,
+ pt: &mut T,
+ ) {
+ let node = &self.spatial_nodes[index.0 as usize];
+ match node.node_type {
+ SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
+ pt.new_level(format!("StickyFrame"));
+ pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
+ }
+ SpatialNodeType::ScrollFrame(scrolling_info) => {
+ pt.new_level(format!("ScrollFrame"));
+ pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
+ pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
+ pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
+ pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
+ pt.add_item(format!("kind: {:?}", scrolling_info.frame_kind));
+ }
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ pt.new_level(format!("ReferenceFrame"));
+ pt.add_item(format!("kind: {:?}", info.kind));
+ pt.add_item(format!("transform_style: {:?}", info.transform_style));
+ pt.add_item(format!("source_transform: {:?}", info.source_transform));
+ pt.add_item(format!("origin_in_parent_reference_frame: {:?}", info.origin_in_parent_reference_frame));
+ }
+ }
+
+ pt.add_item(format!("index: {:?}", index));
+ pt.add_item(format!("content_transform: {:?}", node.content_transform));
+ pt.add_item(format!("viewport_transform: {:?}", node.viewport_transform));
+ pt.add_item(format!("snapping_transform: {:?}", node.snapping_transform));
+ pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
+
+ for child_index in &node.children {
+ self.print_node(*child_index, pt);
+ }
+
+ pt.end_level();
+ }
+
+ /// Get the visible face of the transfrom from the specified node to its parent.
+ pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
+ let node = &self.spatial_nodes[node_index.0 as usize];
+ let mut face = VisibleFace::Front;
+ if let Some(parent_index) = node.parent {
+ self.get_relative_transform_with_face(node_index, parent_index, Some(&mut face));
+ }
+ face
+ }
+
+ #[allow(dead_code)]
+ pub fn print(&self) {
+ if !self.spatial_nodes.is_empty() {
+ let mut buf = Vec::<u8>::new();
+ {
+ let mut pt = PrintTree::new_with_sink("spatial tree", &mut buf);
+ self.print_with(&mut pt);
+ }
+ // If running in Gecko, set RUST_LOG=webrender::spatial_tree=debug
+ // to get this logging to be emitted to stderr/logcat.
+ debug!("{}", std::str::from_utf8(&buf).unwrap_or("(Tree printer emitted non-utf8)"));
+ }
+ }
+}
+
+impl PrintableTree for SpatialTree {
+ fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
+ if !self.spatial_nodes.is_empty() {
+ self.print_node(self.root_reference_frame_index(), pt);
+ }
+ }
+}
+
+#[cfg(test)]
+fn add_reference_frame(
+ cst: &mut SpatialTree,
+ parent: Option<SpatialNodeIndex>,
+ transform: LayoutTransform,
+ origin_in_parent_reference_frame: LayoutVector2D,
+) -> SpatialNodeIndex {
+ cst.add_reference_frame(
+ parent,
+ TransformStyle::Preserve3D,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Transform,
+ origin_in_parent_reference_frame,
+ PipelineId::dummy(),
+ )
+}
+
+#[cfg(test)]
+fn test_pt(
+ px: f32,
+ py: f32,
+ cst: &SpatialTree,
+ child: SpatialNodeIndex,
+ parent: SpatialNodeIndex,
+ expected_x: f32,
+ expected_y: f32,
+) {
+ use euclid::approxeq::ApproxEq;
+ const EPSILON: f32 = 0.0001;
+
+ let p = LayoutPoint::new(px, py);
+ let m = cst.get_relative_transform(child, parent).into_transform();
+ let pt = m.transform_point2d(p).unwrap();
+ assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
+ pt.y.approx_eq_eps(&expected_y, &EPSILON),
+ "p: {:?} -> {:?}\nm={:?}",
+ p, pt, m,
+ );
+}
+
+#[test]
+fn test_cst_simple_translation() {
+ // Basic translations only
+
+ let mut cst = SpatialTree::new();
+
+ let root = add_reference_frame(
+ &mut cst,
+ None,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ Some(root),
+ LayoutTransform::translation(100.0, 0.0, 0.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ Some(child1),
+ LayoutTransform::translation(0.0, 50.0, 0.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ Some(child2),
+ LayoutTransform::translation(200.0, 200.0, 0.0),
+ LayoutVector2D::zero(),
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ test_pt(100.0, 100.0, &cst, child1, root, 200.0, 100.0);
+ test_pt(100.0, 100.0, &cst, child2, root, 200.0, 150.0);
+ test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 150.0);
+ test_pt(100.0, 100.0, &cst, child3, root, 400.0, 350.0);
+}
+
+#[test]
+fn test_cst_simple_scale() {
+ // Basic scale only
+
+ let mut cst = SpatialTree::new();
+
+ let root = add_reference_frame(
+ &mut cst,
+ None,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ Some(root),
+ LayoutTransform::scale(4.0, 1.0, 1.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ Some(child1),
+ LayoutTransform::scale(1.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ Some(child2),
+ LayoutTransform::scale(2.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ test_pt(100.0, 100.0, &cst, child1, root, 400.0, 100.0);
+ test_pt(100.0, 100.0, &cst, child2, root, 400.0, 200.0);
+ test_pt(100.0, 100.0, &cst, child3, root, 800.0, 400.0);
+ test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 200.0);
+ test_pt(100.0, 100.0, &cst, child3, child1, 200.0, 400.0);
+}
+
+#[test]
+fn test_cst_scale_translation() {
+ // Scale + translation
+
+ let mut cst = SpatialTree::new();
+
+ let root = add_reference_frame(
+ &mut cst,
+ None,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ Some(root),
+ LayoutTransform::translation(100.0, 50.0, 0.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child2 = add_reference_frame(
+ &mut cst,
+ Some(child1),
+ LayoutTransform::scale(2.0, 4.0, 1.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child3 = add_reference_frame(
+ &mut cst,
+ Some(child2),
+ LayoutTransform::translation(200.0, -100.0, 0.0),
+ LayoutVector2D::zero(),
+ );
+
+ let child4 = add_reference_frame(
+ &mut cst,
+ Some(child3),
+ LayoutTransform::scale(3.0, 2.0, 1.0),
+ LayoutVector2D::zero(),
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ test_pt(100.0, 100.0, &cst, child1, root, 200.0, 150.0);
+ test_pt(100.0, 100.0, &cst, child2, root, 300.0, 450.0);
+ test_pt(100.0, 100.0, &cst, child4, root, 1100.0, 450.0);
+
+ test_pt(0.0, 0.0, &cst, child4, child1, 400.0, -400.0);
+ test_pt(100.0, 100.0, &cst, child4, child1, 1000.0, 400.0);
+ test_pt(100.0, 100.0, &cst, child2, child1, 200.0, 400.0);
+
+ test_pt(100.0, 100.0, &cst, child3, child1, 600.0, 0.0);
+}
+
+#[test]
+fn test_cst_translation_rotate() {
+ // Rotation + translation
+ use euclid::Angle;
+
+ let mut cst = SpatialTree::new();
+
+ let root = add_reference_frame(
+ &mut cst,
+ None,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1 = add_reference_frame(
+ &mut cst,
+ Some(root),
+ LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(-90.0)),
+ LayoutVector2D::zero(),
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ test_pt(100.0, 0.0, &cst, child1, root, 0.0, -100.0);
+}
+
+#[test]
+fn test_is_ancestor1() {
+ let mut st = SpatialTree::new();
+
+ let root = add_reference_frame(
+ &mut st,
+ None,
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1_0 = add_reference_frame(
+ &mut st,
+ Some(root),
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child1_1 = add_reference_frame(
+ &mut st,
+ Some(child1_0),
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ let child2 = add_reference_frame(
+ &mut st,
+ Some(root),
+ LayoutTransform::identity(),
+ LayoutVector2D::zero(),
+ );
+
+ st.update_tree(
+ WorldPoint::zero(),
+ DevicePixelScale::new(1.0),
+ &SceneProperties::new(),
+ );
+
+ assert!(!st.is_ancestor(root, root));
+ assert!(!st.is_ancestor(child1_0, child1_0));
+ assert!(!st.is_ancestor(child1_1, child1_1));
+ assert!(!st.is_ancestor(child2, child2));
+
+ assert!(st.is_ancestor(root, child1_0));
+ assert!(st.is_ancestor(root, child1_1));
+ assert!(st.is_ancestor(child1_0, child1_1));
+
+ assert!(!st.is_ancestor(child1_0, root));
+ assert!(!st.is_ancestor(child1_1, root));
+ assert!(!st.is_ancestor(child1_1, child1_0));
+
+ assert!(st.is_ancestor(root, child2));
+ assert!(!st.is_ancestor(child2, root));
+
+ assert!(!st.is_ancestor(child1_0, child2));
+ assert!(!st.is_ancestor(child1_1, child2));
+ assert!(!st.is_ancestor(child2, child1_0));
+ assert!(!st.is_ancestor(child2, child1_1));
+}
diff --git a/gfx/wr/webrender/src/texture_cache.rs b/gfx/wr/webrender/src/texture_cache.rs
new file mode 100644
index 0000000000..a98e6b16a6
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_cache.rs
@@ -0,0 +1,1886 @@
+/* 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 api::{DirtyRect, ExternalImageType, ImageFormat, ImageBufferKind};
+use api::{DebugFlags, ImageDescriptor};
+use api::units::*;
+#[cfg(test)]
+use api::{DocumentId, IdNamespace};
+use crate::device::{TextureFilter, TextureFormatPair};
+use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
+use crate::gpu_cache::{GpuCache, GpuCacheHandle};
+use crate::gpu_types::{ImageSource, UvRectKind};
+use crate::internal_types::{
+ CacheTextureId, LayerIndex, Swizzle, SwizzleSettings,
+ TextureUpdateList, TextureUpdateSource, TextureSource,
+ TextureCacheAllocInfo, TextureCacheUpdate,
+};
+use crate::lru_cache::LRUCache;
+use crate::profiler::{self, TransactionProfile};
+use crate::render_backend::{FrameStamp, FrameId};
+use crate::resource_cache::{CacheItem, CachedImageData};
+use crate::texture_pack::{
+ AllocatorList,
+ AllocId,
+ AtlasAllocatorList,
+ ShelfAllocator,
+ ShelfAllocatorOptions,
+ SlabAllocator, SlabAllocatorParameters,
+};
+use smallvec::SmallVec;
+use std::cell::Cell;
+use std::{cmp, mem};
+use std::rc::Rc;
+use euclid::size2;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+
+/// Information about which shader will use the entry.
+///
+/// For batching purposes, it's beneficial to group some items in their
+/// own textures if we know that they are used by a specific shader.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TargetShader {
+ Default,
+ Text,
+}
+
+/// The size of each region in shared cache texture arrays.
+pub const TEXTURE_REGION_DIMENSIONS: i32 = 512;
+
+/// Items in the texture cache can either be standalone textures,
+/// or a sub-rect inside the shared cache.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum EntryDetails {
+ Standalone {
+ /// Number of bytes this entry allocates
+ size_in_bytes: usize,
+ },
+ Picture {
+ /// Size of the tile (used for debug clears only)
+ size: DeviceIntSize,
+ },
+ Cache {
+ /// Origin within the texture layer where this item exists.
+ origin: DeviceIntPoint,
+ /// ID of the allocation specific to its allocator.
+ alloc_id: AllocId,
+ /// The allocated size in bytes for this entry.
+ allocated_size_in_bytes: usize,
+ },
+}
+
+impl EntryDetails {
+ // TODO(gw): We can remove LayerIndex from here completely in a follow up
+ fn describe(&self) -> (LayerIndex, DeviceIntPoint) {
+ match *self {
+ EntryDetails::Standalone { .. } => (0, DeviceIntPoint::zero()),
+ EntryDetails::Picture { .. } => (0, DeviceIntPoint::zero()),
+ EntryDetails::Cache { origin, .. } => (0, origin),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum PictureCacheEntryMarker {}
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum AutoCacheEntryMarker {}
+
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ManualCacheEntryMarker {}
+
+// Stores information related to a single entry in the texture
+// cache. This is stored for each item whether it's in the shared
+// cache or a standalone texture.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct CacheEntry {
+ /// Size of the requested item, in device pixels. Does not include any
+ /// padding for alignment that the allocator may have added to this entry's
+ /// allocation.
+ size: DeviceIntSize,
+ /// Details specific to standalone or shared items.
+ details: EntryDetails,
+ /// Arbitrary user data associated with this item.
+ user_data: [f32; 3],
+ /// The last frame this item was requested for rendering.
+ // TODO(gw): This stamp is only used for picture cache tiles, and some checks
+ // in the glyph cache eviction code. We could probably remove it
+ // entirely in future (or move to EntryDetails::Picture).
+ last_access: FrameStamp,
+ /// Handle to the resource rect in the GPU cache.
+ uv_rect_handle: GpuCacheHandle,
+ /// Image format of the data that the entry expects.
+ input_format: ImageFormat,
+ filter: TextureFilter,
+ swizzle: Swizzle,
+ /// The actual device texture ID this is part of.
+ texture_id: CacheTextureId,
+ /// Optional notice when the entry is evicted from the cache.
+ eviction_notice: Option<EvictionNotice>,
+ /// The type of UV rect this entry specifies.
+ uv_rect_kind: UvRectKind,
+
+ shader: TargetShader,
+}
+
+malloc_size_of::malloc_size_of_is_0!(
+ CacheEntry,
+ AutoCacheEntryMarker, ManualCacheEntryMarker, PictureCacheEntryMarker
+);
+
+impl CacheEntry {
+ // Create a new entry for a standalone texture.
+ fn new_standalone(
+ texture_id: CacheTextureId,
+ last_access: FrameStamp,
+ params: &CacheAllocParams,
+ swizzle: Swizzle,
+ size_in_bytes: usize,
+ ) -> Self {
+ CacheEntry {
+ size: params.descriptor.size,
+ user_data: params.user_data,
+ last_access,
+ details: EntryDetails::Standalone {
+ size_in_bytes,
+ },
+ texture_id,
+ input_format: params.descriptor.format,
+ filter: params.filter,
+ swizzle,
+ uv_rect_handle: GpuCacheHandle::new(),
+ eviction_notice: None,
+ uv_rect_kind: params.uv_rect_kind,
+ shader: TargetShader::Default,
+ }
+ }
+
+ // Update the GPU cache for this texture cache entry.
+ // This ensures that the UV rect, and texture layer index
+ // are up to date in the GPU cache for vertex shaders
+ // to fetch from.
+ fn update_gpu_cache(&mut self, gpu_cache: &mut GpuCache) {
+ if let Some(mut request) = gpu_cache.request(&mut self.uv_rect_handle) {
+ let (layer_index, origin) = self.details.describe();
+ let image_source = ImageSource {
+ p0: origin.to_f32(),
+ p1: (origin + self.size).to_f32(),
+ texture_layer: layer_index as f32,
+ user_data: self.user_data,
+ uv_rect_kind: self.uv_rect_kind,
+ };
+ image_source.write_gpu_blocks(&mut request);
+ }
+ }
+
+ fn evict(&self) {
+ if let Some(eviction_notice) = self.eviction_notice.as_ref() {
+ eviction_notice.notify();
+ }
+ }
+
+ fn alternative_input_format(&self) -> ImageFormat {
+ match self.input_format {
+ ImageFormat::RGBA8 => ImageFormat::BGRA8,
+ ImageFormat::BGRA8 => ImageFormat::RGBA8,
+ other => other,
+ }
+ }
+}
+
+
+/// A texture cache handle is a weak reference to a cache entry.
+///
+/// If the handle has not been inserted into the cache yet, or if the entry was
+/// previously inserted and then evicted, lookup of the handle will fail, and
+/// the cache handle needs to re-upload this item to the texture cache (see
+/// request() below).
+
+#[derive(MallocSizeOf,Clone,PartialEq,Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TextureCacheHandle {
+ /// A fresh handle.
+ Empty,
+
+ /// A handle for a picture cache entry, evicted on every frame if not used.
+ Picture(WeakFreeListHandle<PictureCacheEntryMarker>),
+
+ /// A handle for an entry with automatic eviction.
+ Auto(WeakFreeListHandle<AutoCacheEntryMarker>),
+
+ /// A handle for an entry with manual eviction.
+ Manual(WeakFreeListHandle<ManualCacheEntryMarker>)
+}
+
+impl TextureCacheHandle {
+ pub fn invalid() -> Self {
+ TextureCacheHandle::Empty
+ }
+}
+
+/// Describes the eviction policy for a given entry in the texture cache.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum Eviction {
+ /// The entry will be evicted under the normal rules (which differ between
+ /// standalone and shared entries).
+ Auto,
+ /// The entry will not be evicted until the policy is explicitly set to a
+ /// different value.
+ Manual,
+}
+
+// An eviction notice is a shared condition useful for detecting
+// when a TextureCacheHandle gets evicted from the TextureCache.
+// It is optionally installed to the TextureCache when an update()
+// is scheduled. A single notice may be shared among any number of
+// TextureCacheHandle updates. The notice may then be subsequently
+// checked to see if any of the updates using it have been evicted.
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct EvictionNotice {
+ evicted: Rc<Cell<bool>>,
+}
+
+impl EvictionNotice {
+ fn notify(&self) {
+ self.evicted.set(true);
+ }
+
+ pub fn check(&self) -> bool {
+ if self.evicted.get() {
+ self.evicted.set(false);
+ true
+ } else {
+ false
+ }
+ }
+}
+
+/// The different budget types for the texture cache. Each type has its own
+/// memory budget. Once the budget is exceeded, entries with automatic eviction
+/// are evicted. Entries with manual eviction share the same budget but are not
+/// evicted once the budget is exceeded.
+/// Keeping separate budgets ensures that we don't evict entries from unrelated
+/// textures if one texture gets full.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(u8)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+enum BudgetType {
+ SharedColor8Linear,
+ SharedColor8Nearest,
+ SharedColor8Glyphs,
+ SharedAlpha8,
+ SharedAlpha8Glyphs,
+ SharedAlpha16,
+ Standalone,
+}
+
+impl BudgetType {
+ pub const COUNT: usize = 7;
+
+ pub const VALUES: [BudgetType; BudgetType::COUNT] = [
+ BudgetType::SharedColor8Linear,
+ BudgetType::SharedColor8Nearest,
+ BudgetType::SharedColor8Glyphs,
+ BudgetType::SharedAlpha8,
+ BudgetType::SharedAlpha8Glyphs,
+ BudgetType::SharedAlpha16,
+ BudgetType::Standalone,
+ ];
+
+ pub fn iter() -> impl Iterator<Item = BudgetType> {
+ BudgetType::VALUES.iter().cloned()
+ }
+}
+
+/// A set of lazily allocated, fixed size, texture arrays for each format the
+/// texture cache supports.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct SharedTextures {
+ color8_nearest: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha8_linear: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>,
+ alpha16_linear: AllocatorList<SlabAllocator, TextureParameters>,
+ color8_linear: AllocatorList<ShelfAllocator, TextureParameters>,
+ color8_glyphs: AllocatorList<ShelfAllocator, TextureParameters>,
+ bytes_per_texture_of_type: [i32 ; BudgetType::COUNT],
+}
+
+impl SharedTextures {
+ /// Mints a new set of shared textures.
+ fn new(color_formats: TextureFormatPair<ImageFormat>, config: &TextureCacheConfig) -> Self {
+ let mut bytes_per_texture_of_type = [0 ; BudgetType::COUNT];
+
+ // Used primarily for cached shadow masks. There can be lots of
+ // these on some pages like francine, but most pages don't use it
+ // much.
+ // Most content tends to fit into two 512x512 textures. We are
+ // conservatively using 1024x1024 to fit everything in a single
+ // texture and avoid breaking batches, but it's worth checking
+ // whether it would actually lead to a lot of batch breaks in
+ // practice.
+ let alpha8_linear = AllocatorList::new(
+ config.alpha8_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: 1,
+ alignment: size2(8, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R8),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha8 as usize] =
+ config.alpha8_texture_size * config.alpha8_texture_size;
+
+ // The cache for alpha glyphs (separate to help with batching).
+ let alpha8_glyphs = AllocatorList::new(
+ config.alpha8_glyph_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.alpha8_glyph_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(4, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R8),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha8Glyphs as usize] =
+ config.alpha8_glyph_texture_size * config.alpha8_glyph_texture_size;
+
+ // Used for experimental hdr yuv texture support, but not used in
+ // production Firefox.
+ let alpha16_linear = AllocatorList::new(
+ config.alpha16_texture_size,
+ SlabAllocatorParameters {
+ region_size: TEXTURE_REGION_DIMENSIONS,
+ },
+ TextureParameters {
+ formats: TextureFormatPair::from(ImageFormat::R16),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedAlpha16 as usize] =
+ ImageFormat::R16.bytes_per_pixel() *
+ config.alpha16_texture_size * config.alpha16_texture_size;
+
+ // The primary cache for images, etc.
+ let color8_linear = AllocatorList::new(
+ config.color8_linear_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.color8_linear_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(16, 16),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Linear as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_linear_texture_size * config.color8_linear_texture_size;
+
+ // The cache for subpixel-AA and bitmap glyphs (separate to help with batching).
+ let color8_glyphs = AllocatorList::new(
+ config.color8_glyph_texture_size,
+ ShelfAllocatorOptions {
+ num_columns: if config.color8_glyph_texture_size >= 1024 { 2 } else { 1 },
+ alignment: size2(4, 8),
+ .. ShelfAllocatorOptions::default()
+ },
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Linear,
+ },
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Glyphs as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_glyph_texture_size * config.color8_glyph_texture_size;
+
+ // Used for image-rendering: crisp. This is mostly favicons, which
+ // are small. Some other images use it too, but those tend to be
+ // larger than 512x512 and thus don't use the shared cache anyway.
+ let color8_nearest = AllocatorList::new(
+ config.color8_nearest_texture_size,
+ ShelfAllocatorOptions::default(),
+ TextureParameters {
+ formats: color_formats.clone(),
+ filter: TextureFilter::Nearest,
+ }
+ );
+ bytes_per_texture_of_type[BudgetType::SharedColor8Nearest as usize] =
+ color_formats.internal.bytes_per_pixel() *
+ config.color8_nearest_texture_size * config.color8_nearest_texture_size;
+
+ Self {
+ alpha8_linear,
+ alpha8_glyphs,
+ alpha16_linear,
+ color8_linear,
+ color8_glyphs,
+ color8_nearest,
+ bytes_per_texture_of_type,
+ }
+ }
+
+ /// Clears each texture in the set, with the given set of pending updates.
+ fn clear(&mut self, updates: &mut TextureUpdateList) {
+ let texture_dealloc_cb = &mut |texture_id| {
+ updates.push_free(texture_id);
+ };
+
+ self.alpha8_linear.clear(texture_dealloc_cb);
+ self.alpha8_glyphs.clear(texture_dealloc_cb);
+ self.alpha16_linear.clear(texture_dealloc_cb);
+ self.color8_linear.clear(texture_dealloc_cb);
+ self.color8_nearest.clear(texture_dealloc_cb);
+ self.color8_glyphs.clear(texture_dealloc_cb);
+ }
+
+ /// Returns a mutable borrow for the shared texture array matching the parameters.
+ fn select(
+ &mut self, external_format: ImageFormat, filter: TextureFilter, shader: TargetShader,
+ ) -> (&mut dyn AtlasAllocatorList<TextureParameters>, BudgetType) {
+ match external_format {
+ ImageFormat::R8 => {
+ assert_eq!(filter, TextureFilter::Linear);
+ match shader {
+ TargetShader::Text => {
+ (&mut self.alpha8_glyphs, BudgetType::SharedAlpha8Glyphs)
+ },
+ _ => (&mut self.alpha8_linear, BudgetType::SharedAlpha8),
+ }
+ }
+ ImageFormat::R16 => {
+ assert_eq!(filter, TextureFilter::Linear);
+ (&mut self.alpha16_linear, BudgetType::SharedAlpha16)
+ }
+ ImageFormat::RGBA8 |
+ ImageFormat::BGRA8 => {
+ match (filter, shader) {
+ (TextureFilter::Linear, TargetShader::Text) => {
+ (&mut self.color8_glyphs, BudgetType::SharedColor8Glyphs)
+ },
+ (TextureFilter::Linear, _) => {
+ (&mut self.color8_linear, BudgetType::SharedColor8Linear)
+ },
+ (TextureFilter::Nearest, _) => {
+ (&mut self.color8_nearest, BudgetType::SharedColor8Nearest)
+ },
+ _ => panic!("Unexpected filter {:?}", filter),
+ }
+ }
+ _ => panic!("Unexpected format {:?}", external_format),
+ }
+ }
+
+ /// How many bytes a single texture of the given type takes up, for the
+ /// configured texture sizes.
+ fn bytes_per_shared_texture(&self, budget_type: BudgetType) -> usize {
+ self.bytes_per_texture_of_type[budget_type as usize] as usize
+ }
+}
+
+/// The textures used to hold picture cache tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PictureTexture {
+ texture_id: CacheTextureId,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ is_allocated: bool,
+ last_frame_used: FrameId,
+}
+
+impl PictureTexture {
+ fn size_in_bytes(&self) -> usize {
+ let bpp = self.format.bytes_per_pixel() as usize;
+ (self.size.width * self.size.height) as usize * bpp
+ }
+}
+
+/// The textures used to hold picture cache tiles.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct PictureTextures {
+ /// Current list of textures in the pool
+ textures: Vec<PictureTexture>,
+ /// Default tile size for content tiles
+ default_tile_size: DeviceIntSize,
+ /// Number of currently allocated textures in the pool
+ allocated_texture_count: usize,
+}
+
+impl PictureTextures {
+ fn new(
+ default_tile_size: DeviceIntSize,
+ ) -> Self {
+ PictureTextures {
+ textures: Vec::new(),
+ default_tile_size,
+ allocated_texture_count: 0,
+ }
+ }
+
+ fn get_or_allocate_tile(
+ &mut self,
+ tile_size: DeviceIntSize,
+ now: FrameStamp,
+ next_texture_id: &mut CacheTextureId,
+ pending_updates: &mut TextureUpdateList,
+ ) -> CacheEntry {
+ let mut texture_id = None;
+ self.allocated_texture_count += 1;
+
+ for texture in &mut self.textures {
+ if texture.size == tile_size && !texture.is_allocated {
+ // Found a target that's not currently in use which matches. Update
+ // the last_frame_used for GC purposes.
+ texture.is_allocated = true;
+ texture.last_frame_used = FrameId::INVALID;
+ texture_id = Some(texture.texture_id);
+ break;
+ }
+ }
+
+ // Need to create a new render target and add it to the pool
+
+ let texture_id = texture_id.unwrap_or_else(|| {
+ let texture_id = *next_texture_id;
+ next_texture_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: tile_size.width,
+ height: tile_size.height,
+ format: ImageFormat::RGBA8,
+ filter: TextureFilter::Nearest,
+ layer_count: 1,
+ is_shared_cache: false,
+ has_depth: true,
+ };
+
+ pending_updates.push_alloc(texture_id, info);
+
+ self.textures.push(PictureTexture {
+ texture_id,
+ is_allocated: true,
+ format: ImageFormat::RGBA8,
+ size: tile_size,
+ last_frame_used: FrameId::INVALID,
+ });
+
+ texture_id
+ });
+
+ CacheEntry {
+ size: tile_size,
+ user_data: [0.0; 3],
+ last_access: now,
+ details: EntryDetails::Picture {
+ size: tile_size,
+ },
+ uv_rect_handle: GpuCacheHandle::new(),
+ input_format: ImageFormat::RGBA8,
+ filter: TextureFilter::Nearest,
+ swizzle: Swizzle::default(),
+ texture_id,
+ eviction_notice: None,
+ uv_rect_kind: UvRectKind::Rect,
+ shader: TargetShader::Default,
+ }
+ }
+
+ fn free_tile(
+ &mut self,
+ id: CacheTextureId,
+ current_frame_id: FrameId,
+ ) {
+ self.allocated_texture_count -= 1;
+
+ let texture = self.textures
+ .iter_mut()
+ .find(|t| t.texture_id == id)
+ .expect("bug: invalid texture id");
+
+ assert!(texture.is_allocated);
+ texture.is_allocated = false;
+
+ assert_eq!(texture.last_frame_used, FrameId::INVALID);
+ texture.last_frame_used = current_frame_id;
+ }
+
+ fn clear(&mut self, pending_updates: &mut TextureUpdateList) {
+ for texture in self.textures.drain(..) {
+ pending_updates.push_free(texture.texture_id);
+ }
+ }
+
+ fn update_profile(&self, profile: &mut TransactionProfile) {
+ // For now, this profile counter just accumulates the tiles and bytes
+ // from all picture cache textures.
+ let mut picture_tiles = 0;
+ let mut picture_bytes = 0;
+ for texture in &self.textures {
+ picture_tiles += 1;
+ picture_bytes += texture.size_in_bytes();
+ }
+ profile.set(profiler::PICTURE_TILES, picture_tiles);
+ profile.set(profiler::PICTURE_TILES_MEM, profiler::bytes_to_mb(picture_bytes));
+ }
+
+ /// Simple garbage collect of picture cache tiles
+ fn gc(
+ &mut self,
+ pending_updates: &mut TextureUpdateList,
+ ) {
+ // Allow the picture cache pool to keep 25% of the current allocated tile count
+ // as free textures to be reused. This ensures the allowed tile count is appropriate
+ // based on current window size.
+ let free_texture_count = self.textures.len() - self.allocated_texture_count;
+ let allowed_retained_count = (self.allocated_texture_count as f32 * 0.25).ceil() as usize;
+ let do_gc = free_texture_count > allowed_retained_count;
+
+ if do_gc {
+ // Sort the current pool by age, so that we remove oldest textures first
+ self.textures.sort_unstable_by_key(|t| cmp::Reverse(t.last_frame_used));
+
+ // We can't just use retain() because `PictureTexture` requires manual cleanup.
+ let mut allocated_targets = SmallVec::<[PictureTexture; 32]>::new();
+ let mut retained_targets = SmallVec::<[PictureTexture; 32]>::new();
+
+ for target in self.textures.drain(..) {
+ if target.is_allocated {
+ // Allocated targets can't be collected
+ allocated_targets.push(target);
+ } else if retained_targets.len() < allowed_retained_count {
+ // Retain the most recently used targets up to the allowed count
+ retained_targets.push(target);
+ } else {
+ // The rest of the targets get freed
+ assert_ne!(target.last_frame_used, FrameId::INVALID);
+ pending_updates.push_free(target.texture_id);
+ }
+ }
+
+ self.textures.extend(retained_targets);
+ self.textures.extend(allocated_targets);
+ }
+ }
+}
+
+/// Container struct for the various parameters used in cache allocation.
+struct CacheAllocParams {
+ descriptor: ImageDescriptor,
+ filter: TextureFilter,
+ user_data: [f32; 3],
+ uv_rect_kind: UvRectKind,
+ shader: TargetShader,
+}
+
+/// Startup parameters for the texture cache.
+///
+/// Texture sizes must be at least 512.
+#[derive(Clone)]
+pub struct TextureCacheConfig {
+ pub color8_linear_texture_size: i32,
+ pub color8_nearest_texture_size: i32,
+ pub color8_glyph_texture_size: i32,
+ pub alpha8_texture_size: i32,
+ pub alpha8_glyph_texture_size: i32,
+ pub alpha16_texture_size: i32,
+}
+
+impl TextureCacheConfig {
+ pub const DEFAULT: Self = TextureCacheConfig {
+ color8_linear_texture_size: 2048,
+ color8_nearest_texture_size: 512,
+ color8_glyph_texture_size: 2048,
+ alpha8_texture_size: 1024,
+ alpha8_glyph_texture_size: 2048,
+ alpha16_texture_size: 512,
+ };
+}
+
+/// General-purpose manager for images in GPU memory. This includes images,
+/// rasterized glyphs, rasterized blobs, cached render tasks, etc.
+///
+/// The texture cache is owned and managed by the RenderBackend thread, and
+/// produces a series of commands to manipulate the textures on the Renderer
+/// thread. These commands are executed before any rendering is performed for
+/// a given frame.
+///
+/// Entries in the texture cache are not guaranteed to live past the end of the
+/// frame in which they are requested, and may be evicted. The API supports
+/// querying whether an entry is still available.
+///
+/// The TextureCache is different from the GpuCache in that the former stores
+/// images, whereas the latter stores data and parameters for use in the shaders.
+/// This means that the texture cache can be visualized, which is a good way to
+/// understand how it works. Enabling gfx.webrender.debug.texture-cache shows a
+/// live view of its contents in Firefox.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureCache {
+ /// Set of texture arrays in different formats used for the shared cache.
+ shared_textures: SharedTextures,
+
+ /// A texture array per tile size for picture caching.
+ picture_textures: PictureTextures,
+
+ /// Maximum texture size supported by hardware.
+ max_texture_size: i32,
+
+ /// Settings on using texture unit swizzling.
+ swizzle: Option<SwizzleSettings>,
+
+ /// The current set of debug flags.
+ debug_flags: DebugFlags,
+
+ /// The next unused virtual texture ID. Monotonically increasing.
+ next_id: CacheTextureId,
+
+ /// A list of allocations and updates that need to be applied to the texture
+ /// cache in the rendering thread this frame.
+ #[cfg_attr(all(feature = "serde", any(feature = "capture", feature = "replay")), serde(skip))]
+ pending_updates: TextureUpdateList,
+
+ /// The current `FrameStamp`. Used for cache eviction policies.
+ now: FrameStamp,
+
+ /// Cache of texture cache handles with automatic lifetime management, evicted
+ /// in a least-recently-used order.
+ lru_cache: LRUCache<CacheEntry, AutoCacheEntryMarker>,
+
+ /// Cache of picture cache entries.
+ picture_cache_entries: FreeList<CacheEntry, PictureCacheEntryMarker>,
+
+ /// Strong handles for the picture_cache_entries FreeList.
+ picture_cache_handles: Vec<FreeListHandle<PictureCacheEntryMarker>>,
+
+ /// Cache of texture cache entries with manual liftime management.
+ manual_entries: FreeList<CacheEntry, ManualCacheEntryMarker>,
+
+ /// Strong handles for the manual_entries FreeList.
+ manual_handles: Vec<FreeListHandle<ManualCacheEntryMarker>>,
+
+ /// Memory usage of allocated entries in all of the shared or standalone
+ /// textures. Includes both manually and automatically evicted entries.
+ bytes_allocated: [usize ; BudgetType::COUNT],
+}
+
+impl TextureCache {
+ /// The maximum number of items that will be evicted per frame. This limit helps avoid jank
+ /// on frames where we want to evict a large number of items. Instead, we'd prefer to drop
+ /// the items incrementally over a number of frames, even if that means the total allocated
+ /// size of the cache is above the desired threshold for a small number of frames.
+ const MAX_EVICTIONS_PER_FRAME: usize = 32;
+
+ pub fn new(
+ max_texture_size: i32,
+ default_picture_tile_size: DeviceIntSize,
+ color_formats: TextureFormatPair<ImageFormat>,
+ swizzle: Option<SwizzleSettings>,
+ config: &TextureCacheConfig,
+ ) -> Self {
+ let pending_updates = TextureUpdateList::new();
+
+ // Shared texture cache controls swizzling on a per-entry basis, assuming that
+ // the texture as a whole doesn't need to be swizzled (but only some entries do).
+ // It would be possible to support this, but not needed at the moment.
+ assert!(color_formats.internal != ImageFormat::BGRA8 ||
+ swizzle.map_or(true, |s| s.bgra8_sampling_swizzle == Swizzle::default())
+ );
+
+ let next_texture_id = CacheTextureId(1);
+
+ TextureCache {
+ shared_textures: SharedTextures::new(color_formats, config),
+ picture_textures: PictureTextures::new(
+ default_picture_tile_size,
+ ),
+ max_texture_size,
+ swizzle,
+ debug_flags: DebugFlags::empty(),
+ next_id: next_texture_id,
+ pending_updates,
+ now: FrameStamp::INVALID,
+ lru_cache: LRUCache::new(BudgetType::COUNT),
+ picture_cache_entries: FreeList::new(),
+ picture_cache_handles: Vec::new(),
+ manual_entries: FreeList::new(),
+ manual_handles: Vec::new(),
+ bytes_allocated: [0 ; BudgetType::COUNT],
+ }
+ }
+
+ /// Creates a TextureCache and sets it up with a valid `FrameStamp`, which
+ /// is useful for avoiding panics when instantiating the `TextureCache`
+ /// directly from unit test code.
+ #[cfg(test)]
+ pub fn new_for_testing(
+ max_texture_size: i32,
+ image_format: ImageFormat,
+ ) -> Self {
+ let mut cache = Self::new(
+ max_texture_size,
+ crate::picture::TILE_SIZE_DEFAULT,
+ TextureFormatPair::from(image_format),
+ None,
+ &TextureCacheConfig::DEFAULT,
+ );
+ let mut now = FrameStamp::first(DocumentId::new(IdNamespace(1), 1));
+ now.advance();
+ cache.begin_frame(now);
+ cache
+ }
+
+ pub fn set_debug_flags(&mut self, flags: DebugFlags) {
+ self.debug_flags = flags;
+ }
+
+ /// Clear all entries in the texture cache. This is a fairly drastic
+ /// step that should only be called very rarely.
+ pub fn clear_all(&mut self) {
+ // Evict all manual eviction handles
+ let manual_handles = mem::replace(
+ &mut self.manual_handles,
+ Vec::new(),
+ );
+ for handle in manual_handles {
+ let entry = self.manual_entries.free(handle);
+ self.evict_impl(entry);
+ }
+
+ // Evict all picture cache handles
+ let picture_handles = mem::replace(
+ &mut self.picture_cache_handles,
+ Vec::new(),
+ );
+ for handle in picture_handles {
+ let entry = self.picture_cache_entries.free(handle);
+ self.evict_impl(entry);
+ }
+
+ // Evict all auto (LRU) cache handles
+ for budget_type in BudgetType::iter() {
+ while let Some(entry) = self.lru_cache.pop_oldest(budget_type as u8) {
+ entry.evict();
+ self.free(&entry);
+ }
+ }
+
+ // Free the picture and shared textures
+ self.picture_textures.clear(&mut self.pending_updates);
+ self.shared_textures.clear(&mut self.pending_updates);
+ self.pending_updates.note_clear();
+ }
+
+ /// Called at the beginning of each frame.
+ pub fn begin_frame(&mut self, stamp: FrameStamp) {
+ debug_assert!(!self.now.is_valid());
+ profile_scope!("begin_frame");
+ self.now = stamp;
+
+ // Texture cache eviction is done at the start of the frame. This ensures that
+ // we won't evict items that have been requested on this frame.
+ // It also frees up space in the cache for items allocated later in the frame
+ // potentially reducing texture allocations and fragmentation.
+ self.evict_items_from_cache_if_required();
+ self.expire_old_picture_cache_tiles();
+ }
+
+ pub fn end_frame(&mut self, profile: &mut TransactionProfile) {
+ debug_assert!(self.now.is_valid());
+ self.picture_textures.gc(
+ &mut self.pending_updates,
+ );
+
+ let updates = &mut self.pending_updates; // To avoid referring to self in the closure.
+ let callback = &mut|texture_id| { updates.push_free(texture_id); };
+
+ // Release of empty shared textures is done at the end of the frame. That way, if the
+ // eviction at the start of the frame frees up a texture, that is then subsequently
+ // used during the frame, we avoid doing a free/alloc for it.
+ self.shared_textures.alpha8_linear.release_empty_textures(callback);
+ self.shared_textures.alpha8_glyphs.release_empty_textures(callback);
+ self.shared_textures.alpha16_linear.release_empty_textures(callback);
+ self.shared_textures.color8_linear.release_empty_textures(callback);
+ self.shared_textures.color8_nearest.release_empty_textures(callback);
+ self.shared_textures.color8_glyphs.release_empty_textures(callback);
+
+ profile.set(profiler::TEXTURE_CACHE_A8_PIXELS, self.shared_textures.alpha8_linear.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_A8_TEXTURES, self.shared_textures.alpha8_linear.allocated_textures());
+ profile.set(profiler::TEXTURE_CACHE_A8_GLYPHS_PIXELS, self.shared_textures.alpha8_glyphs.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_A8_GLYPHS_TEXTURES, self.shared_textures.alpha8_glyphs.allocated_textures());
+ profile.set(profiler::TEXTURE_CACHE_A16_PIXELS, self.shared_textures.alpha16_linear.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_A16_TEXTURES, self.shared_textures.alpha16_linear.allocated_textures());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_LINEAR_PIXELS, self.shared_textures.color8_linear.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_LINEAR_TEXTURES, self.shared_textures.color8_linear.allocated_textures());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_NEAREST_PIXELS, self.shared_textures.color8_nearest.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_NEAREST_TEXTURES, self.shared_textures.color8_nearest.allocated_textures());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_GLYPHS_PIXELS, self.shared_textures.color8_glyphs.allocated_space());
+ profile.set(profiler::TEXTURE_CACHE_RGBA8_GLYPHS_TEXTURES, self.shared_textures.color8_glyphs.allocated_textures());
+
+ self.picture_textures.update_profile(profile);
+
+ let shared_bytes = [
+ BudgetType::SharedColor8Linear,
+ BudgetType::SharedColor8Nearest,
+ BudgetType::SharedColor8Glyphs,
+ BudgetType::SharedAlpha8,
+ BudgetType::SharedAlpha8Glyphs,
+ BudgetType::SharedAlpha16,
+ ].iter().map(|b| self.bytes_allocated[*b as usize]).sum();
+ let standalone_bytes = self.bytes_allocated[BudgetType::Standalone as usize];
+
+ profile.set(profiler::TEXTURE_CACHE_SHARED_MEM, profiler::bytes_to_mb(shared_bytes));
+ profile.set(profiler::TEXTURE_CACHE_STANDALONE_MEM, profiler::bytes_to_mb(standalone_bytes));
+
+ self.now = FrameStamp::INVALID;
+ }
+
+ // Request an item in the texture cache. All images that will
+ // be used on a frame *must* have request() called on their
+ // handle, to update the last used timestamp and ensure
+ // that resources are not flushed from the cache too early.
+ //
+ // Returns true if the image needs to be uploaded to the
+ // texture cache (either never uploaded, or has been
+ // evicted on a previous frame).
+ pub fn request(&mut self, handle: &TextureCacheHandle, gpu_cache: &mut GpuCache) -> bool {
+ let now = self.now;
+ let entry = match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Picture(handle) => {
+ self.picture_cache_entries.get_opt_mut(handle)
+ },
+ TextureCacheHandle::Auto(handle) => {
+ // Call touch rather than get_opt_mut so that the LRU index
+ // knows that the entry has been used.
+ self.lru_cache.touch(handle)
+ },
+ TextureCacheHandle::Manual(handle) => {
+ self.manual_entries.get_opt_mut(handle)
+ },
+ };
+ entry.map_or(true, |entry| {
+ // If an image is requested that is already in the cache,
+ // refresh the GPU cache data associated with this item.
+ entry.last_access = now;
+ entry.update_gpu_cache(gpu_cache);
+ false
+ })
+ }
+
+ fn get_entry_opt(&self, handle: &TextureCacheHandle) -> Option<&CacheEntry> {
+ match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Picture(handle) => self.picture_cache_entries.get_opt(handle),
+ TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt(handle),
+ TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt(handle),
+ }
+ }
+
+ fn get_entry_opt_mut(&mut self, handle: &TextureCacheHandle) -> Option<&mut CacheEntry> {
+ match handle {
+ TextureCacheHandle::Empty => None,
+ TextureCacheHandle::Picture(handle) => self.picture_cache_entries.get_opt_mut(handle),
+ TextureCacheHandle::Auto(handle) => self.lru_cache.get_opt_mut(handle),
+ TextureCacheHandle::Manual(handle) => self.manual_entries.get_opt_mut(handle),
+ }
+ }
+
+ // Returns true if the image needs to be uploaded to the
+ // texture cache (either never uploaded, or has been
+ // evicted on a previous frame).
+ pub fn needs_upload(&self, handle: &TextureCacheHandle) -> bool {
+ !self.is_allocated(handle)
+ }
+
+ pub fn max_texture_size(&self) -> i32 {
+ self.max_texture_size
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn color_formats(&self) -> TextureFormatPair<ImageFormat> {
+ self.shared_textures.color8_linear.texture_parameters().formats.clone()
+ }
+
+ #[cfg(feature = "replay")]
+ pub fn swizzle_settings(&self) -> Option<SwizzleSettings> {
+ self.swizzle
+ }
+
+ pub fn pending_updates(&mut self) -> TextureUpdateList {
+ mem::replace(&mut self.pending_updates, TextureUpdateList::new())
+ }
+
+ // Update the data stored by a given texture cache handle.
+ pub fn update(
+ &mut self,
+ handle: &mut TextureCacheHandle,
+ descriptor: ImageDescriptor,
+ filter: TextureFilter,
+ data: Option<CachedImageData>,
+ user_data: [f32; 3],
+ mut dirty_rect: ImageDirtyRect,
+ gpu_cache: &mut GpuCache,
+ eviction_notice: Option<&EvictionNotice>,
+ uv_rect_kind: UvRectKind,
+ eviction: Eviction,
+ shader: TargetShader,
+ ) {
+ debug_assert!(self.now.is_valid());
+ // Determine if we need to allocate texture cache memory
+ // for this item. We need to reallocate if any of the following
+ // is true:
+ // - Never been in the cache
+ // - Has been in the cache but was evicted.
+ // - Exists in the cache but dimensions / format have changed.
+ let realloc = match self.get_entry_opt(handle) {
+ Some(entry) => {
+ entry.size != descriptor.size || (entry.input_format != descriptor.format &&
+ entry.alternative_input_format() != descriptor.format)
+ }
+ None => {
+ // Not allocated, or was previously allocated but has been evicted.
+ true
+ }
+ };
+
+ if realloc {
+ let params = CacheAllocParams { descriptor, filter, user_data, uv_rect_kind, shader };
+ self.allocate(&params, handle, eviction);
+
+ // If we reallocated, we need to upload the whole item again.
+ dirty_rect = DirtyRect::All;
+ }
+
+ let entry = self.get_entry_opt_mut(handle)
+ .expect("BUG: There must be an entry at this handle now");
+
+ // Install the new eviction notice for this update, if applicable.
+ entry.eviction_notice = eviction_notice.cloned();
+ entry.uv_rect_kind = uv_rect_kind;
+
+ // Invalidate the contents of the resource rect in the GPU cache.
+ // This ensures that the update_gpu_cache below will add
+ // the new information to the GPU cache.
+ //TODO: only invalidate if the parameters change?
+ gpu_cache.invalidate(&entry.uv_rect_handle);
+
+ // Upload the resource rect and texture array layer.
+ entry.update_gpu_cache(gpu_cache);
+
+ // Create an update command, which the render thread processes
+ // to upload the new image data into the correct location
+ // in GPU memory.
+ if let Some(data) = data {
+ // If the swizzling is supported, we always upload in the internal
+ // texture format (thus avoiding the conversion by the driver).
+ // Otherwise, pass the external format to the driver.
+ let (_, origin) = entry.details.describe();
+ let texture_id = entry.texture_id;
+ let size = entry.size;
+ let use_upload_format = self.swizzle.is_none();
+ let op = TextureCacheUpdate::new_update(
+ data,
+ &descriptor,
+ origin,
+ size,
+ use_upload_format,
+ &dirty_rect,
+ );
+ self.pending_updates.push_update(texture_id, op);
+ }
+ }
+
+ // Check if a given texture handle has a valid allocation
+ // in the texture cache.
+ pub fn is_allocated(&self, handle: &TextureCacheHandle) -> bool {
+ self.get_entry_opt(handle).is_some()
+ }
+
+ // Check if a given texture handle was last used as recently
+ // as the specified number of previous frames.
+ pub fn is_recently_used(&self, handle: &TextureCacheHandle, margin: usize) -> bool {
+ self.get_entry_opt(handle).map_or(false, |entry| {
+ entry.last_access.frame_id() + margin >= self.now.frame_id()
+ })
+ }
+
+ // Return the allocated size of the texture handle's associated data,
+ // or otherwise indicate the handle is invalid.
+ pub fn get_allocated_size(&self, handle: &TextureCacheHandle) -> Option<usize> {
+ self.get_entry_opt(handle).map(|entry| {
+ (entry.input_format.bytes_per_pixel() * entry.size.area()) as usize
+ })
+ }
+
+ // Retrieve the details of an item in the cache. This is used
+ // during batch creation to provide the resource rect address
+ // to the shaders and texture ID to the batching logic.
+ // This function will assert in debug modes if the caller
+ // tries to get a handle that was not requested this frame.
+ pub fn get(&self, handle: &TextureCacheHandle) -> CacheItem {
+ let (texture_id, layer_index, uv_rect, swizzle, uv_rect_handle, user_data) = self.get_cache_location(handle);
+ CacheItem {
+ uv_rect_handle,
+ texture_id: TextureSource::TextureCache(
+ texture_id,
+ swizzle,
+ ),
+ uv_rect,
+ texture_layer: layer_index as i32,
+ user_data,
+ }
+ }
+
+ /// A more detailed version of get(). This allows access to the actual
+ /// device rect of the cache allocation.
+ ///
+ /// Returns a tuple identifying the texture, the layer, the region,
+ /// and its GPU handle.
+ pub fn get_cache_location(
+ &self,
+ handle: &TextureCacheHandle,
+ ) -> (CacheTextureId, LayerIndex, DeviceIntRect, Swizzle, GpuCacheHandle, [f32; 3]) {
+ let entry = self
+ .get_entry_opt(handle)
+ .expect("BUG: was dropped from cache or not updated!");
+ debug_assert_eq!(entry.last_access, self.now);
+ let (layer_index, origin) = entry.details.describe();
+ (
+ entry.texture_id,
+ layer_index as usize,
+ DeviceIntRect::new(origin, entry.size),
+ entry.swizzle,
+ entry.uv_rect_handle,
+ entry.user_data,
+ )
+ }
+
+ /// Internal helper function to evict a strong texture cache handle
+ fn evict_impl(
+ &mut self,
+ entry: CacheEntry,
+ ) {
+ entry.evict();
+ self.free(&entry);
+ }
+
+ /// Evict a texture cache handle that was previously set to be in manual
+ /// eviction mode.
+ pub fn evict_manual_handle(&mut self, handle: &TextureCacheHandle) {
+ if let TextureCacheHandle::Manual(handle) = handle {
+ // Find the strong handle that matches this weak handle. If this
+ // ever shows up in profiles, we can make it a hash (but the number
+ // of manual eviction handles is typically small).
+ // Alternatively, we could make a more forgiving FreeList variant
+ // which does not differentiate between strong and weak handles.
+ let index = self.manual_handles.iter().position(|strong_handle| {
+ strong_handle.matches(handle)
+ });
+ if let Some(index) = index {
+ let handle = self.manual_handles.swap_remove(index);
+ let entry = self.manual_entries.free(handle);
+ self.evict_impl(entry);
+ }
+ }
+ }
+
+ pub fn dump_color8_linear_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.color8_linear.dump_as_svg(output)
+ }
+
+ pub fn dump_glyphs_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.shared_textures.color8_glyphs.dump_as_svg(output)
+ }
+
+ /// Expire picture cache tiles that haven't been referenced in the last frame.
+ /// The picture cache code manually keeps tiles alive by calling `request` on
+ /// them if it wants to retain a tile that is currently not visible.
+ fn expire_old_picture_cache_tiles(&mut self) {
+ for i in (0 .. self.picture_cache_handles.len()).rev() {
+ let evict = {
+ let entry = self.picture_cache_entries.get(
+ &self.picture_cache_handles[i]
+ );
+
+ // This function is called at the beginning of the frame,
+ // so we don't yet know which picture cache tiles will be
+ // requested this frame. Therefore only evict picture cache
+ // tiles which weren't requested in the *previous* frame.
+ entry.last_access.frame_id() < self.now.frame_id() - 1
+ };
+
+ if evict {
+ let handle = self.picture_cache_handles.swap_remove(i);
+ let entry = self.picture_cache_entries.free(handle);
+ self.evict_impl(entry);
+ }
+ }
+ }
+
+ /// Get the eviction threshold, in bytes, for the given budget type.
+ fn get_eviction_threshold(&self, budget_type: BudgetType) -> usize {
+ if budget_type == BudgetType::Standalone {
+ // For standalone textures, the only reason to evict textures is
+ // to save GPU memory. Batching / draw call concerns do not apply
+ // to standalone textures, because unused textures don't cause
+ // extra draw calls.
+ return 16 * 1024 * 1024;
+ }
+
+ // For shared textures, evicting an entry only frees up GPU memory if it
+ // causes one of the shared textures to become empty.
+ // The bigger concern for shared textures is batching: The entries that
+ // are needed in the current frame should be distributed across as few
+ // shared textures as possible, to minimize the number of draw calls.
+ // Ideally we only want one or two textures per type.
+ let expected_texture_count = match budget_type {
+ BudgetType::SharedColor8Nearest | BudgetType::SharedAlpha16 => {
+ // These types are only rarely used, we don't want more than
+ // one of each.
+ 1
+ },
+
+ _ => {
+ // For the other types, having two textures is acceptable.
+ 2
+ },
+ };
+
+ // The threshold that we pick here will be compared to the number of
+ // bytes that are *occupied by entries*. And we're trying to target a
+ // certain number of textures.
+ // Unfortunately, it's hard to predict the number of needed textures
+ // purely based on number of occupied bytes: Due to the different
+ // rectangular shape of each entry, and due to decisions made by the
+ // allocator, sometimes we may need a new texture even if there are
+ // still large gaps in the existing textures.
+ // Let's assume that we have an average allocator wastage of 50%.
+ let average_used_bytes_per_texture_when_full =
+ self.shared_textures.bytes_per_shared_texture(budget_type) / 2;
+
+ // Compute the threshold.
+ // Because of varying allocator wastage, we may still need to use more
+ // than the expected number of textures; that's fine. We'll also go over
+ // the expected texture count whenever a large number of entries are
+ // needed to draw a complex frame (since we don't evict entries which
+ // are needed for the current frame), or if eviction hasn't had a chance
+ // to catch up after a large allocation burst.
+ expected_texture_count * average_used_bytes_per_texture_when_full
+ }
+
+ /// Evict old items from the shared and standalone caches, if we're over a
+ /// threshold memory usage value
+ fn evict_items_from_cache_if_required(&mut self) {
+ let previous_frame_id = self.now.frame_id() - 1;
+ let mut eviction_count = 0;
+
+ for budget in BudgetType::iter() {
+ let threshold = self.get_eviction_threshold(budget);
+ while self.should_continue_evicting(
+ self.bytes_allocated[budget as usize],
+ threshold,
+ eviction_count,
+ ) {
+ if let Some(entry) = self.lru_cache.peek_oldest(budget as u8) {
+ // Only evict this item if it wasn't used in the previous frame. The reason being that if it
+ // was used the previous frame then it will likely be used in this frame too, and we don't
+ // want to be continually evicting and reuploading the item every frame.
+ if entry.last_access.frame_id() >= previous_frame_id {
+ // Since the LRU cache is ordered by frame access, we can break out of the loop here because
+ // we know that all remaining items were also used in the previous frame (or more recently).
+ break;
+ }
+ let entry = self.lru_cache.pop_oldest(budget as u8).unwrap();
+ entry.evict();
+ self.free(&entry);
+ eviction_count += 1;
+ } else {
+ // The LRU cache is empty, all remaining items use manual
+ // eviction. In this case, there's nothing we can do until
+ // the calling code manually evicts items to reduce the
+ // allocated cache size.
+ break;
+ }
+ }
+ }
+ }
+
+ /// Returns true if texture cache eviction loop should continue
+ fn should_continue_evicting(
+ &self,
+ bytes_allocated: usize,
+ threshold: usize,
+ eviction_count: usize,
+ ) -> bool {
+ // If current memory usage is below selected threshold, we can stop evicting items
+ if bytes_allocated < threshold {
+ return false;
+ }
+
+ // If current memory usage is significantly more than the threshold, keep evicting this frame
+ if bytes_allocated > 4 * threshold {
+ return true;
+ }
+
+ // Otherwise, only allow evicting up to a certain number of items per frame. This allows evictions
+ // to be spread over a number of frames, to avoid frame spikes.
+ eviction_count < Self::MAX_EVICTIONS_PER_FRAME
+ }
+
+ // Free a cache entry from the standalone list or shared cache.
+ fn free(&mut self, entry: &CacheEntry) {
+ match entry.details {
+ EntryDetails::Picture { size } => {
+ self.picture_textures.free_tile(entry.texture_id, self.now.frame_id());
+ if self.debug_flags.contains(
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED)
+ {
+ self.pending_updates.push_debug_clear(
+ entry.texture_id,
+ DeviceIntPoint::zero(),
+ size.width,
+ size.height,
+ 0,
+ );
+ }
+ }
+ EntryDetails::Standalone { size_in_bytes, .. } => {
+ self.bytes_allocated[BudgetType::Standalone as usize] -= size_in_bytes;
+
+ // This is a standalone texture allocation. Free it directly.
+ self.pending_updates.push_free(entry.texture_id);
+ }
+ EntryDetails::Cache { origin, alloc_id, allocated_size_in_bytes } => {
+ let (allocator_list, budget_type) = self.shared_textures.select(
+ entry.input_format,
+ entry.filter,
+ entry.shader,
+ );
+
+ allocator_list.deallocate(entry.texture_id, alloc_id);
+
+ self.bytes_allocated[budget_type as usize] -= allocated_size_in_bytes;
+
+ if self.debug_flags.contains(
+ DebugFlags::TEXTURE_CACHE_DBG |
+ DebugFlags::TEXTURE_CACHE_DBG_CLEAR_EVICTED)
+ {
+ self.pending_updates.push_debug_clear(
+ entry.texture_id,
+ origin,
+ entry.size.width,
+ entry.size.height,
+ 0,
+ );
+ }
+ }
+ }
+ }
+
+ /// Allocate a block from the shared cache.
+ fn allocate_from_shared_cache(
+ &mut self,
+ params: &CacheAllocParams,
+ ) -> (CacheEntry, BudgetType) {
+ let (allocator_list, budget_type) = self.shared_textures.select(
+ params.descriptor.format,
+ params.filter,
+ params.shader,
+ );
+
+ // To avoid referring to self in the closure.
+ let next_id = &mut self.next_id;
+ let pending_updates = &mut self.pending_updates;
+
+ let (texture_id, alloc_id, allocated_rect) = allocator_list.allocate(
+ params.descriptor.size,
+ &mut |size, parameters| {
+ let texture_id = *next_id;
+ next_id.0 += 1;
+ pending_updates.push_alloc(
+ texture_id,
+ TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: size.width,
+ height: size.height,
+ format: parameters.formats.internal,
+ filter: parameters.filter,
+ layer_count: 1,
+ is_shared_cache: true,
+ has_depth: false,
+ },
+ );
+
+ texture_id
+ },
+ );
+
+ let formats = &allocator_list.texture_parameters().formats;
+
+ let swizzle = if formats.external == params.descriptor.format {
+ Swizzle::default()
+ } else {
+ match self.swizzle {
+ Some(_) => Swizzle::Bgra,
+ None => Swizzle::default(),
+ }
+ };
+
+ let bpp = formats.internal.bytes_per_pixel();
+ let allocated_size_in_bytes = (allocated_rect.size.area() * bpp) as usize;
+ self.bytes_allocated[budget_type as usize] += allocated_size_in_bytes;
+
+ (CacheEntry {
+ size: params.descriptor.size,
+ user_data: params.user_data,
+ last_access: self.now,
+ details: EntryDetails::Cache {
+ origin: allocated_rect.origin,
+ alloc_id,
+ allocated_size_in_bytes,
+ },
+ uv_rect_handle: GpuCacheHandle::new(),
+ input_format: params.descriptor.format,
+ filter: params.filter,
+ swizzle,
+ texture_id,
+ eviction_notice: None,
+ uv_rect_kind: params.uv_rect_kind,
+ shader: params.shader
+ }, budget_type)
+ }
+
+ // Returns true if the given image descriptor *may* be
+ // placed in the shared texture cache.
+ pub fn is_allowed_in_shared_cache(
+ &self,
+ filter: TextureFilter,
+ descriptor: &ImageDescriptor,
+ ) -> bool {
+ let mut allowed_in_shared_cache = true;
+
+ // Anything larger than TEXTURE_REGION_DIMENSIONS goes in a standalone texture.
+ // TODO(gw): If we find pages that suffer from batch breaks in this
+ // case, add support for storing these in a standalone
+ // texture array.
+ if descriptor.size.width > TEXTURE_REGION_DIMENSIONS ||
+ descriptor.size.height > TEXTURE_REGION_DIMENSIONS
+ {
+ allowed_in_shared_cache = false;
+ }
+
+ // TODO(gw): For now, alpha formats of the texture cache can only be linearly sampled.
+ // Nearest sampling gets a standalone texture.
+ // This is probably rare enough that it can be fixed up later.
+ if filter == TextureFilter::Nearest &&
+ descriptor.format.bytes_per_pixel() <= 2
+ {
+ allowed_in_shared_cache = false;
+ }
+
+ allowed_in_shared_cache
+ }
+
+ /// Allocate a render target via the pending updates sent to the renderer
+ pub fn alloc_render_target(
+ &mut self,
+ size: DeviceIntSize,
+ format: ImageFormat,
+ ) -> CacheTextureId {
+ let texture_id = self.next_id;
+ self.next_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: size.width,
+ height: size.height,
+ format,
+ filter: TextureFilter::Linear,
+ layer_count: 1,
+ is_shared_cache: false,
+ has_depth: false,
+ };
+
+ self.pending_updates.push_alloc(texture_id, info);
+
+ texture_id
+ }
+
+ /// Free an existing render target
+ pub fn free_render_target(
+ &mut self,
+ id: CacheTextureId,
+ ) {
+ self.pending_updates.push_free(id);
+ }
+
+ /// Allocates a new standalone cache entry.
+ fn allocate_standalone_entry(
+ &mut self,
+ params: &CacheAllocParams,
+ ) -> (CacheEntry, BudgetType) {
+ let texture_id = self.next_id;
+ self.next_id.0 += 1;
+
+ // Push a command to allocate device storage of the right size / format.
+ let info = TextureCacheAllocInfo {
+ target: ImageBufferKind::Texture2D,
+ width: params.descriptor.size.width,
+ height: params.descriptor.size.height,
+ format: params.descriptor.format,
+ filter: params.filter,
+ layer_count: 1,
+ is_shared_cache: false,
+ has_depth: false,
+ };
+
+ let size_in_bytes = (info.width * info.height * info.format.bytes_per_pixel()) as usize;
+ self.bytes_allocated[BudgetType::Standalone as usize] += size_in_bytes;
+
+ self.pending_updates.push_alloc(texture_id, info);
+
+ // Special handing for BGRA8 textures that may need to be swizzled.
+ let swizzle = if params.descriptor.format == ImageFormat::BGRA8 {
+ self.swizzle.map(|s| s.bgra8_sampling_swizzle)
+ } else {
+ None
+ };
+
+ (CacheEntry::new_standalone(
+ texture_id,
+ self.now,
+ params,
+ swizzle.unwrap_or_default(),
+ size_in_bytes,
+ ), BudgetType::Standalone)
+ }
+
+ /// Allocates a cache entry appropriate for the given parameters.
+ ///
+ /// This allocates from the shared cache unless the parameters do not meet
+ /// the shared cache requirements, in which case a standalone texture is
+ /// used.
+ fn allocate_cache_entry(
+ &mut self,
+ params: &CacheAllocParams,
+ ) -> (CacheEntry, BudgetType) {
+ assert!(!params.descriptor.size.is_empty());
+
+ // If this image doesn't qualify to go in the shared (batching) cache,
+ // allocate a standalone entry.
+ if self.is_allowed_in_shared_cache(params.filter, &params.descriptor) {
+ self.allocate_from_shared_cache(params)
+ } else {
+ self.allocate_standalone_entry(params)
+ }
+ }
+
+ /// Allocates a cache entry for the given parameters, and updates the
+ /// provided handle to point to the new entry.
+ fn allocate(
+ &mut self,
+ params: &CacheAllocParams,
+ handle: &mut TextureCacheHandle,
+ eviction: Eviction,
+ ) {
+ debug_assert!(self.now.is_valid());
+ let (new_cache_entry, budget_type) = self.allocate_cache_entry(params);
+
+ // If the handle points to a valid cache entry, we want to replace the
+ // cache entry with our newly updated location. We also need to ensure
+ // that the storage (region or standalone) associated with the previous
+ // entry here gets freed.
+ //
+ // If the handle is invalid, we need to insert the data, and append the
+ // result to the corresponding vector.
+ let old_entry = match (&mut *handle, eviction) {
+ (TextureCacheHandle::Auto(handle), Eviction::Auto) => {
+ self.lru_cache.replace_or_insert(handle, budget_type as u8, new_cache_entry)
+ },
+ (TextureCacheHandle::Manual(handle), Eviction::Manual) => {
+ let entry = self.manual_entries.get_opt_mut(handle)
+ .expect("Don't call this after evicting");
+ Some(mem::replace(entry, new_cache_entry))
+ },
+ (TextureCacheHandle::Manual(_), Eviction::Auto) |
+ (TextureCacheHandle::Auto(_), Eviction::Manual) => {
+ panic!("Can't change eviction policy after initial allocation");
+ },
+ (TextureCacheHandle::Empty, Eviction::Auto) => {
+ let new_handle = self.lru_cache.push_new(budget_type as u8, new_cache_entry);
+ *handle = TextureCacheHandle::Auto(new_handle);
+ None
+ },
+ (TextureCacheHandle::Empty, Eviction::Manual) => {
+ let manual_handle = self.manual_entries.insert(new_cache_entry);
+ let new_handle = manual_handle.weak();
+ self.manual_handles.push(manual_handle);
+ *handle = TextureCacheHandle::Manual(new_handle);
+ None
+ },
+ (TextureCacheHandle::Picture(_), _) => {
+ panic!("Picture cache entries are managed separately and shouldn't appear in this function");
+ },
+ };
+ if let Some(old_entry) = old_entry {
+ old_entry.evict();
+ self.free(&old_entry);
+ }
+ }
+
+ // Update the data stored by a given texture cache handle for picture caching specifically.
+ pub fn update_picture_cache(
+ &mut self,
+ tile_size: DeviceIntSize,
+ handle: &mut TextureCacheHandle,
+ gpu_cache: &mut GpuCache,
+ ) {
+ debug_assert!(self.now.is_valid());
+ debug_assert!(tile_size.width > 0 && tile_size.height > 0);
+
+ let need_alloc = match handle {
+ TextureCacheHandle::Empty => true,
+ TextureCacheHandle::Picture(handle) => {
+ // Check if the entry has been evicted.
+ self.picture_cache_entries.get_opt(handle).is_none()
+ },
+ TextureCacheHandle::Auto(_) | TextureCacheHandle::Manual(_) => {
+ panic!("Unexpected handle type in update_picture_cache");
+ }
+ };
+
+ if need_alloc {
+ let cache_entry = self.picture_textures.get_or_allocate_tile(
+ tile_size,
+ self.now,
+ &mut self.next_id,
+ &mut self.pending_updates,
+ );
+
+ // Add the cache entry to the picture_cache_entries FreeList.
+ let strong_handle = self.picture_cache_entries.insert(cache_entry);
+ let new_handle = strong_handle.weak();
+
+ self.picture_cache_handles.push(strong_handle);
+
+ *handle = TextureCacheHandle::Picture(new_handle);
+ }
+
+ if let TextureCacheHandle::Picture(handle) = handle {
+ // Upload the resource rect and texture array layer.
+ self.picture_cache_entries
+ .get_opt_mut(handle)
+ .expect("BUG: handle must be valid now")
+ .update_gpu_cache(gpu_cache);
+ } else {
+ panic!("The handle should be valid picture cache handle now")
+ }
+ }
+
+ pub fn shared_alpha_expected_format(&self) -> ImageFormat {
+ self.shared_textures.alpha8_linear.texture_parameters().formats.external
+ }
+
+ pub fn shared_color_expected_format(&self) -> ImageFormat {
+ self.shared_textures.color8_linear.texture_parameters().formats.external
+ }
+
+
+ pub fn default_picture_tile_size(&self) -> DeviceIntSize {
+ self.picture_textures.default_tile_size
+ }
+
+ #[cfg(test)]
+ pub fn total_allocated_bytes_for_testing(&self) -> usize {
+ BudgetType::iter().map(|b| self.bytes_allocated[b as usize]).sum()
+ }
+
+ pub fn report_memory(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.lru_cache.size_of(ops)
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct TextureParameters {
+ pub formats: TextureFormatPair<ImageFormat>,
+ pub filter: TextureFilter,
+}
+
+impl TextureCacheUpdate {
+ // Constructs a TextureCacheUpdate operation to be passed to the
+ // rendering thread in order to do an upload to the right
+ // location in the texture cache.
+ fn new_update(
+ data: CachedImageData,
+ descriptor: &ImageDescriptor,
+ origin: DeviceIntPoint,
+ size: DeviceIntSize,
+ use_upload_format: bool,
+ dirty_rect: &ImageDirtyRect,
+ ) -> TextureCacheUpdate {
+ let source = match data {
+ CachedImageData::Blob => {
+ panic!("The vector image should have been rasterized.");
+ }
+ CachedImageData::External(ext_image) => match ext_image.image_type {
+ ExternalImageType::TextureHandle(_) => {
+ panic!("External texture handle should not go through texture_cache.");
+ }
+ ExternalImageType::Buffer => TextureUpdateSource::External {
+ id: ext_image.id,
+ channel_index: ext_image.channel_index,
+ },
+ },
+ CachedImageData::Raw(bytes) => {
+ let finish = descriptor.offset +
+ descriptor.size.width * descriptor.format.bytes_per_pixel() +
+ (descriptor.size.height - 1) * descriptor.compute_stride();
+ assert!(bytes.len() >= finish as usize);
+
+ TextureUpdateSource::Bytes { data: bytes }
+ }
+ };
+ let format_override = if use_upload_format {
+ Some(descriptor.format)
+ } else {
+ None
+ };
+
+ match *dirty_rect {
+ DirtyRect::Partial(dirty) => {
+ // the dirty rectangle doesn't have to be within the area but has to intersect it, at least
+ let stride = descriptor.compute_stride();
+ let offset = descriptor.offset + dirty.origin.y * stride + dirty.origin.x * descriptor.format.bytes_per_pixel();
+
+ TextureCacheUpdate {
+ rect: DeviceIntRect::new(
+ DeviceIntPoint::new(origin.x + dirty.origin.x, origin.y + dirty.origin.y),
+ DeviceIntSize::new(
+ dirty.size.width.min(size.width - dirty.origin.x),
+ dirty.size.height.min(size.height - dirty.origin.y),
+ ),
+ ),
+ source,
+ stride: Some(stride),
+ offset,
+ format_override,
+ layer_index: 0,
+ }
+ }
+ DirtyRect::All => {
+ TextureCacheUpdate {
+ rect: DeviceIntRect::new(origin, size),
+ source,
+ stride: descriptor.stride,
+ offset: descriptor.offset,
+ format_override,
+ layer_index: 0,
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_texture_cache {
+ #[test]
+ fn check_allocation_size_balance() {
+ // Allocate some glyphs, observe the total allocation size, and free
+ // the glyphs again. Check that the total allocation size is back at the
+ // original value.
+
+ use crate::texture_cache::{TextureCache, TextureCacheHandle, Eviction, TargetShader};
+ use crate::gpu_cache::GpuCache;
+ use crate::device::TextureFilter;
+ use crate::gpu_types::UvRectKind;
+ use api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, DirtyRect};
+ use api::units::*;
+ use euclid::size2;
+ let mut gpu_cache = GpuCache::new_for_testing();
+ let mut texture_cache = TextureCache::new_for_testing(2048, ImageFormat::BGRA8);
+
+ let sizes: &[DeviceIntSize] = &[
+ size2(23, 27),
+ size2(15, 22),
+ size2(11, 5),
+ size2(20, 25),
+ size2(38, 41),
+ size2(11, 19),
+ size2(13, 21),
+ size2(37, 40),
+ size2(13, 15),
+ size2(14, 16),
+ size2(10, 9),
+ size2(25, 28),
+ ];
+
+ let bytes_at_start = texture_cache.total_allocated_bytes_for_testing();
+
+ let handles: Vec<TextureCacheHandle> = sizes.iter().map(|size| {
+ let mut texture_cache_handle = TextureCacheHandle::invalid();
+ texture_cache.request(&texture_cache_handle, &mut gpu_cache);
+ texture_cache.update(
+ &mut texture_cache_handle,
+ ImageDescriptor {
+ size: *size,
+ stride: None,
+ format: ImageFormat::BGRA8,
+ flags: ImageDescriptorFlags::empty(),
+ offset: 0,
+ },
+ TextureFilter::Linear,
+ None,
+ [0.0; 3],
+ DirtyRect::All,
+ &mut gpu_cache,
+ None,
+ UvRectKind::Rect,
+ Eviction::Manual,
+ TargetShader::Text,
+ );
+ texture_cache_handle
+ }).collect();
+
+ let bytes_after_allocating = texture_cache.total_allocated_bytes_for_testing();
+ assert!(bytes_after_allocating > bytes_at_start);
+
+ for handle in handles {
+ texture_cache.evict_manual_handle(&handle);
+ }
+
+ let bytes_at_end = texture_cache.total_allocated_bytes_for_testing();
+ assert_eq!(bytes_at_end, bytes_at_start);
+ }
+}
diff --git a/gfx/wr/webrender/src/texture_pack/guillotine.rs b/gfx/wr/webrender/src/texture_pack/guillotine.rs
new file mode 100644
index 0000000000..c71cf974e5
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_pack/guillotine.rs
@@ -0,0 +1,279 @@
+/* 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 api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
+
+//TODO: gather real-world statistics on the bin usage in order to assist the decision
+// on where to place the size thresholds.
+
+/// This is an optimization tweak to enable looking through all the free rectangles in a bin
+/// and choosing the smallest, as opposed to picking the first match.
+const FIND_SMALLEST_AREA: bool = false;
+
+const NUM_BINS: usize = 3;
+/// The minimum number of pixels on each side that we require for rects to be classified as
+/// particular bin of freelists.
+const MIN_RECT_AXIS_SIZES: [i32; NUM_BINS] = [1, 16, 32];
+
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
+struct FreeListBin(u8);
+
+#[derive(Debug, Clone, Copy)]
+struct FreeListIndex(usize);
+
+impl FreeListBin {
+ fn for_size(size: &DeviceIntSize) -> Self {
+ MIN_RECT_AXIS_SIZES
+ .iter()
+ .enumerate()
+ .rev()
+ .find(|(_, &min_size)| min_size <= size.width && min_size <= size.height)
+ .map(|(id, _)| FreeListBin(id as u8))
+ .expect("Unable to find a bin!")
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct FreeRectSlice(pub u32);
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct FreeRect {
+ slice: FreeRectSlice,
+ rect: DeviceIntRect,
+}
+
+/// A texture allocator using the guillotine algorithm.
+///
+/// See sections 2.2 and 2.2.5 in "A Thousand Ways to Pack the Bin - A Practical Approach to Two-
+/// Dimensional Rectangle Bin Packing":
+///
+/// http://clb.demon.fi/files/RectangleBinPack.pdf
+///
+/// This approach was chosen because of its simplicity and good performance.
+///
+/// Note: the allocations are spread across multiple textures, and also are binned
+/// orthogonally in order to speed up the search.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct GuillotineAllocator {
+ bins: [Vec<FreeRect>; NUM_BINS],
+}
+
+impl GuillotineAllocator {
+ pub fn new(initial_size: Option<DeviceIntSize>) -> Self {
+ let mut allocator = GuillotineAllocator {
+ bins: [
+ Vec::new(),
+ Vec::new(),
+ Vec::new(),
+ ],
+ };
+
+ if let Some(initial_size) = initial_size {
+ allocator.push(
+ FreeRectSlice(0),
+ initial_size.into(),
+ );
+ }
+
+ allocator
+ }
+
+ fn push(&mut self, slice: FreeRectSlice, rect: DeviceIntRect) {
+ let id = FreeListBin::for_size(&rect.size).0 as usize;
+ self.bins[id].push(FreeRect {
+ slice,
+ rect,
+ })
+ }
+
+ /// Find a suitable rect in the free list. We choose the smallest such rect
+ /// in terms of area (Best-Area-Fit, BAF).
+ fn find_index_of_best_rect(
+ &self,
+ requested_dimensions: &DeviceIntSize,
+ ) -> Option<(FreeListBin, FreeListIndex)> {
+ let start_bin = FreeListBin::for_size(requested_dimensions);
+ (start_bin.0 .. NUM_BINS as u8)
+ .find_map(|id| if FIND_SMALLEST_AREA {
+ let mut smallest_index_and_area = None;
+ for (candidate_index, candidate) in self.bins[id as usize].iter().enumerate() {
+ if requested_dimensions.width > candidate.rect.size.width ||
+ requested_dimensions.height > candidate.rect.size.height
+ {
+ continue;
+ }
+
+ let candidate_area = candidate.rect.size.area();
+ match smallest_index_and_area {
+ Some((_, area)) if candidate_area >= area => continue,
+ _ => smallest_index_and_area = Some((candidate_index, candidate_area)),
+ }
+ }
+
+ smallest_index_and_area
+ .map(|(index, _)| (FreeListBin(id), FreeListIndex(index)))
+ } else {
+ self.bins[id as usize]
+ .iter()
+ .position(|candidate| {
+ requested_dimensions.width <= candidate.rect.size.width &&
+ requested_dimensions.height <= candidate.rect.size.height
+ })
+ .map(|index| (FreeListBin(id), FreeListIndex(index)))
+ })
+ }
+
+ // Split that results in the single largest area (Min Area Split Rule, MINAS).
+ fn split_guillotine(&mut self, chosen: &FreeRect, requested_dimensions: &DeviceIntSize) {
+ let candidate_free_rect_to_right = DeviceIntRect::new(
+ DeviceIntPoint::new(
+ chosen.rect.origin.x + requested_dimensions.width,
+ chosen.rect.origin.y,
+ ),
+ DeviceIntSize::new(
+ chosen.rect.size.width - requested_dimensions.width,
+ requested_dimensions.height,
+ ),
+ );
+ let candidate_free_rect_to_bottom = DeviceIntRect::new(
+ DeviceIntPoint::new(
+ chosen.rect.origin.x,
+ chosen.rect.origin.y + requested_dimensions.height,
+ ),
+ DeviceIntSize::new(
+ requested_dimensions.width,
+ chosen.rect.size.height - requested_dimensions.height,
+ ),
+ );
+
+ // Guillotine the rectangle.
+ let new_free_rect_to_right;
+ let new_free_rect_to_bottom;
+ if candidate_free_rect_to_right.size.area() > candidate_free_rect_to_bottom.size.area() {
+ new_free_rect_to_right = DeviceIntRect::new(
+ candidate_free_rect_to_right.origin,
+ DeviceIntSize::new(
+ candidate_free_rect_to_right.size.width,
+ chosen.rect.size.height,
+ ),
+ );
+ new_free_rect_to_bottom = candidate_free_rect_to_bottom
+ } else {
+ new_free_rect_to_right = candidate_free_rect_to_right;
+ new_free_rect_to_bottom = DeviceIntRect::new(
+ candidate_free_rect_to_bottom.origin,
+ DeviceIntSize::new(
+ chosen.rect.size.width,
+ candidate_free_rect_to_bottom.size.height,
+ ),
+ )
+ }
+
+ // Add the guillotined rects back to the free list.
+ if !new_free_rect_to_right.is_empty() {
+ self.push(chosen.slice, new_free_rect_to_right);
+ }
+ if !new_free_rect_to_bottom.is_empty() {
+ self.push(chosen.slice, new_free_rect_to_bottom);
+ }
+ }
+
+ pub fn allocate(
+ &mut self, requested_dimensions: &DeviceIntSize
+ ) -> Option<(FreeRectSlice, DeviceIntPoint)> {
+ if requested_dimensions.width == 0 || requested_dimensions.height == 0 {
+ return Some((FreeRectSlice(0), DeviceIntPoint::new(0, 0)));
+ }
+ let (bin, index) = self.find_index_of_best_rect(requested_dimensions)?;
+
+ // Remove the rect from the free list and decide how to guillotine it.
+ let chosen = self.bins[bin.0 as usize].swap_remove(index.0);
+ self.split_guillotine(&chosen, requested_dimensions);
+
+ // Return the result.
+ Some((chosen.slice, chosen.rect.origin))
+ }
+
+ /// Add a new slice to the allocator, and immediately allocate a rect from it.
+ pub fn extend(
+ &mut self,
+ slice: FreeRectSlice,
+ total_size: DeviceIntSize,
+ requested_dimensions: DeviceIntSize,
+ ) {
+ self.split_guillotine(
+ &FreeRect { slice, rect: total_size.into() },
+ &requested_dimensions
+ );
+ }
+}
+
+#[cfg(test)]
+fn random_fill(count: usize, texture_size: i32) -> f32 {
+ use rand::{thread_rng, Rng};
+
+ let total_rect = DeviceIntRect::new(
+ DeviceIntPoint::zero(),
+ DeviceIntSize::new(texture_size, texture_size),
+ );
+ let mut rng = thread_rng();
+ let mut allocator = GuillotineAllocator::new(None);
+
+ // check for empty allocation
+ assert_eq!(
+ allocator.allocate(&DeviceIntSize::new(0, 12)),
+ Some((FreeRectSlice(0), DeviceIntPoint::zero())),
+ );
+
+ let mut slices: Vec<Vec<DeviceIntRect>> = Vec::new();
+ let mut requested_area = 0f32;
+ // fill up the allocator
+ for _ in 0 .. count {
+ let size = DeviceIntSize::new(
+ rng.gen_range(1, texture_size),
+ rng.gen_range(1, texture_size),
+ );
+ requested_area += size.area() as f32;
+
+ match allocator.allocate(&size) {
+ Some((slice, origin)) => {
+ let rect = DeviceIntRect::new(origin, size);
+ assert_eq!(None, slices[slice.0 as usize].iter().find(|r| r.intersects(&rect)));
+ assert!(total_rect.contains_rect(&rect));
+ slices[slice.0 as usize].push(rect);
+ }
+ None => {
+ allocator.extend(FreeRectSlice(slices.len() as u32), total_rect.size, size);
+ let rect = DeviceIntRect::new(DeviceIntPoint::zero(), size);
+ slices.push(vec![rect]);
+ }
+ }
+ }
+ // validate the free rects
+ for (i, free_vecs) in allocator.bins.iter().enumerate() {
+ for fr in free_vecs {
+ assert_eq!(FreeListBin(i as u8), FreeListBin::for_size(&fr.rect.size));
+ assert_eq!(None, slices[fr.slice.0 as usize].iter().find(|r| r.intersects(&fr.rect)));
+ assert!(total_rect.contains_rect(&fr.rect));
+ slices[fr.slice.0 as usize].push(fr.rect);
+ }
+ }
+
+ let allocated_area = slices.len() as f32 * (texture_size * texture_size) as f32;
+ requested_area / allocated_area
+}
+
+#[test]
+fn test_small() {
+ random_fill(100, 100);
+}
+
+#[test]
+fn test_large() {
+ random_fill(1000, 10000);
+}
diff --git a/gfx/wr/webrender/src/texture_pack/mod.rs b/gfx/wr/webrender/src/texture_pack/mod.rs
new file mode 100644
index 0000000000..21707a2e96
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_pack/mod.rs
@@ -0,0 +1,329 @@
+/* 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/. */
+
+mod guillotine;
+mod slab;
+
+pub use guillotine::*;
+pub use slab::*;
+
+/* 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 api::units::*;
+use crate::internal_types::CacheTextureId;
+use euclid::{point2, size2, default::Box2D};
+use smallvec::SmallVec;
+
+pub use etagere::AllocatorOptions as ShelfAllocatorOptions;
+pub use etagere::BucketedAtlasAllocator as BucketedShelfAllocator;
+pub use etagere::AtlasAllocator as ShelfAllocator;
+
+/// ID of an allocation within a given allocator.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AllocId(pub u32);
+
+pub trait AtlasAllocator {
+ /// Specific parameters of the allocator.
+ type Parameters;
+ /// Constructor
+ fn new(size: i32, parameters: &Self::Parameters) -> Self;
+ /// Allocate a rectangle.
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)>;
+ /// Deallocate a rectangle and return its size.
+ fn deallocate(&mut self, id: AllocId);
+ /// Return true if there is no live allocations.
+ fn is_empty(&self) -> bool;
+ /// Allocated area in pixels.
+ fn allocated_space(&self) -> i32;
+ /// Write a debug visualization of the atlas fitting in the provided rectangle.
+ ///
+ /// This is inserted in a larger dump so it shouldn't contain the xml start/end tags.
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()>;
+}
+
+pub trait AtlasAllocatorList<TextureParameters> {
+ /// Allocate a rectangle.
+ ///
+ /// If allocation fails, call the provided callback, add a new allocator to the list and try again.
+ fn allocate(
+ &mut self,
+ size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect);
+
+ /// Deallocate a rectangle and return its size.
+ fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId);
+
+ fn texture_parameters(&self) -> &TextureParameters;
+}
+
+/// A number of 2D textures (single layer), each with a number of
+/// regions that can act as a slab allocator.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct TextureUnit<Allocator> {
+ allocator: Allocator,
+ texture_id: CacheTextureId,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct AllocatorList<Allocator: AtlasAllocator, TextureParameters> {
+ units: SmallVec<[TextureUnit<Allocator>; 1]>,
+ size: i32,
+ atlas_parameters: Allocator::Parameters,
+ texture_parameters: TextureParameters,
+}
+
+impl<Allocator: AtlasAllocator, TextureParameters> AllocatorList<Allocator, TextureParameters> {
+ pub fn new(
+ size: i32,
+ atlas_parameters: Allocator::Parameters,
+ texture_parameters: TextureParameters,
+ ) -> Self {
+ AllocatorList {
+ units: SmallVec::new(),
+ size,
+ atlas_parameters,
+ texture_parameters,
+ }
+ }
+
+ pub fn allocate(
+ &mut self,
+ requested_size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect) {
+ // Try to allocate from one of the existing textures.
+ for unit in &mut self.units {
+ if let Some((alloc_id, rect)) = unit.allocator.allocate(requested_size) {
+ return (unit.texture_id, alloc_id, rect);
+ }
+ }
+
+ // Need to create a new texture to hold the allocation.
+ let texture_id = texture_alloc_cb(size2(self.size, self.size), &self.texture_parameters);
+ let unit_index = self.units.len();
+
+ self.units.push(TextureUnit {
+ allocator: Allocator::new(self.size, &self.atlas_parameters),
+ texture_id,
+ });
+
+ let (alloc_id, rect) = self.units[unit_index]
+ .allocator
+ .allocate(requested_size)
+ .unwrap();
+
+ (texture_id, alloc_id, rect)
+ }
+
+ pub fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId) {
+ let unit = self.units
+ .iter_mut()
+ .find(|unit| unit.texture_id == texture_id)
+ .expect("Unable to find the associated texture array unit");
+
+ unit.allocator.deallocate(alloc_id);
+ }
+
+ pub fn release_empty_textures<'l>(&mut self, texture_dealloc_cb: &'l mut dyn FnMut(CacheTextureId)) {
+ self.units.retain(|unit| {
+ if unit.allocator.is_empty() {
+ texture_dealloc_cb(unit.texture_id);
+
+ false
+ } else{
+ true
+ }
+ });
+ }
+
+ pub fn clear(&mut self, texture_dealloc_cb: &mut dyn FnMut(CacheTextureId)) {
+ for unit in self.units.drain(..) {
+ texture_dealloc_cb(unit.texture_id);
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn dump_as_svg(&self, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ use svg_fmt::*;
+
+ let num_arrays = self.units.len() as f32;
+
+ let text_spacing = 15.0;
+ let unit_spacing = 30.0;
+ let texture_size = self.size as f32 / 2.0;
+
+ let svg_w = unit_spacing * 2.0 + texture_size;
+ let svg_h = unit_spacing + num_arrays * (texture_size + text_spacing + unit_spacing);
+
+ writeln!(output, "{}", BeginSvg { w: svg_w, h: svg_h })?;
+
+ // Background.
+ writeln!(output,
+ " {}",
+ rectangle(0.0, 0.0, svg_w, svg_h)
+ .inflate(1.0, 1.0)
+ .fill(rgb(50, 50, 50))
+ )?;
+
+ let mut y = unit_spacing;
+ for unit in &self.units {
+ writeln!(output, " {}", text(unit_spacing, y, format!("{:?}", unit.texture_id)).color(rgb(230, 230, 230)))?;
+
+ let rect = Box2D {
+ min: point2(unit_spacing, y),
+ max: point2(unit_spacing + texture_size, y + texture_size),
+ };
+
+ unit.allocator.dump_into_svg(&rect, output)?;
+
+ y += unit_spacing + texture_size + text_spacing;
+ }
+
+ writeln!(output, "{}", EndSvg)
+ }
+
+ pub fn allocated_space(&self) -> i32 {
+ let mut accum = 0;
+ for unit in &self.units {
+ accum += unit.allocator.allocated_space();
+ }
+
+ accum
+ }
+
+ pub fn allocated_textures(&self) -> usize {
+ self.units.len()
+ }
+}
+
+impl<Allocator: AtlasAllocator, TextureParameters> AtlasAllocatorList<TextureParameters>
+for AllocatorList<Allocator, TextureParameters> {
+ fn allocate(
+ &mut self,
+ requested_size: DeviceIntSize,
+ texture_alloc_cb: &mut dyn FnMut(DeviceIntSize, &TextureParameters) -> CacheTextureId,
+ ) -> (CacheTextureId, AllocId, DeviceIntRect) {
+ self.allocate(requested_size, texture_alloc_cb)
+ }
+
+ fn deallocate(&mut self, texture_id: CacheTextureId, alloc_id: AllocId) {
+ self.deallocate(texture_id, alloc_id);
+ }
+
+ fn texture_parameters(&self) -> &TextureParameters {
+ &self.texture_parameters
+ }
+}
+
+impl AtlasAllocator for BucketedShelfAllocator {
+ type Parameters = ShelfAllocatorOptions;
+
+ fn new(size: i32, options: &Self::Parameters) -> Self {
+ BucketedShelfAllocator::with_options(size2(size, size), options)
+ }
+
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ self.allocate(size.to_untyped()).map(|alloc| {
+ (AllocId(alloc.id.serialize()), alloc.rectangle.to_rect().cast_unit())
+ })
+ }
+
+ fn deallocate(&mut self, id: AllocId) {
+ self.deallocate(etagere::AllocId::deserialize(id.0));
+ }
+
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+
+ fn allocated_space(&self) -> i32 {
+ self.allocated_space()
+ }
+
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.dump_into_svg(Some(&rect.to_i32().cast_unit()), output)
+ }
+}
+
+impl AtlasAllocator for ShelfAllocator {
+ type Parameters = ShelfAllocatorOptions;
+
+ fn new(size: i32, options: &Self::Parameters) -> Self {
+ ShelfAllocator::with_options(size2(size, size), options)
+ }
+
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ self.allocate(size.to_untyped()).map(|alloc| {
+ (AllocId(alloc.id.serialize()), alloc.rectangle.to_rect().cast_unit())
+ })
+ }
+
+ fn deallocate(&mut self, id: AllocId) {
+ self.deallocate(etagere::AllocId::deserialize(id.0));
+ }
+
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+
+ fn allocated_space(&self) -> i32 {
+ self.allocated_space()
+ }
+
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.dump_into_svg(Some(&rect.to_i32().cast_unit()), output)
+ }
+}
+
+#[test]
+fn bug_1680769() {
+ let mut allocators: AllocatorList<ShelfAllocator, ()> = AllocatorList::new(
+ 1024,
+ ShelfAllocatorOptions::default(),
+ (),
+ );
+
+ let mut allocations = Vec::new();
+ let mut next_id = CacheTextureId(0);
+ let alloc_cb = &mut |_: DeviceIntSize, _: &()| {
+ let texture_id = next_id;
+ next_id.0 += 1;
+
+ texture_id
+ };
+
+ // Make some allocations, forcing the the creation of multiple textures.
+ for _ in 0..50 {
+ allocations.push(allocators.allocate(size2(256, 256), alloc_cb));
+ }
+
+ // Deallocate everything.
+ // It should empty all atlases and we still have textures allocated because
+ // we haven't called release_empty_textures yet.
+ for alloc in allocations.drain(..) {
+ allocators.deallocate(alloc.0, alloc.1);
+ }
+
+ // Allocate something else.
+ // Bug 1680769 was causing this allocation to be duplicated and leaked in
+ // all textures.
+ allocations.push(allocators.allocate(size2(8, 8), alloc_cb));
+
+ // Deallocate all known allocations.
+ for alloc in allocations.drain(..) {
+ allocators.deallocate(alloc.0, alloc.1);
+ }
+
+ // If we have leaked items, this won't manage to remove all textures.
+ allocators.release_empty_textures(&mut |_| {});
+
+ assert_eq!(allocators.allocated_textures(), 0);
+}
diff --git a/gfx/wr/webrender/src/texture_pack/slab.rs b/gfx/wr/webrender/src/texture_pack/slab.rs
new file mode 100644
index 0000000000..6e38383397
--- /dev/null
+++ b/gfx/wr/webrender/src/texture_pack/slab.rs
@@ -0,0 +1,356 @@
+/* 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/. */
+
+#![deny(unconditional_recursion)]
+
+use super::{AtlasAllocator, AllocId};
+use api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
+use euclid::{point2, size2, default::Box2D};
+use std::cmp;
+
+fn pack_alloc_id(region_index: usize, location: TextureLocation) -> AllocId {
+ AllocId(
+ region_index as u32 & 0xFFFF
+ | (location.0 as u32) << 16
+ | (location.1 as u32) << 24
+ )
+}
+
+fn unpack_alloc_id(id: AllocId) -> (usize, TextureLocation) {
+ (
+ (id.0 & 0xFFFF) as usize,
+ TextureLocation(
+ ((id.0 >> 16) & 0xFF) as u8,
+ ((id.0 >> 24) & 0xFF) as u8,
+ ),
+ )
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Copy, Clone, PartialEq)]
+struct SlabSize {
+ width: i32,
+ height: i32,
+}
+
+impl SlabSize {
+ fn invalid() -> SlabSize {
+ SlabSize {
+ width: 0,
+ height: 0,
+ }
+ }
+
+ fn get(size: DeviceIntSize) -> SlabSize {
+ fn quantize_dimension(size: i32) -> i32 {
+ match size {
+ 0 => unreachable!(),
+ 1..=16 => 16,
+ 17..=32 => 32,
+ 33..=64 => 64,
+ 65..=128 => 128,
+ 129..=256 => 256,
+ 257..=512 => 512,
+ _ => panic!("Invalid dimensions for cache!"),
+ }
+ }
+
+
+ let x_size = quantize_dimension(size.width);
+ let y_size = quantize_dimension(size.height);
+
+ let (width, height) = match (x_size, y_size) {
+ // Special cased rectangular slab pages.
+ (512, 0..=64) => (512, 64),
+ (512, 128) => (512, 128),
+ (512, 256) => (512, 256),
+ (0..=64, 512) => (64, 512),
+ (128, 512) => (128, 512),
+ (256, 512) => (256, 512),
+
+ // If none of those fit, use a square slab size.
+ (x_size, y_size) => {
+ let square_size = cmp::max(x_size, y_size);
+ (square_size, square_size)
+ }
+ };
+
+ SlabSize {
+ width,
+ height,
+ }
+ }
+}
+
+// The x/y location within a texture region of an allocation.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct TextureLocation(pub u8, pub u8);
+
+impl TextureLocation {
+ fn new(x: i32, y: i32) -> Self {
+ debug_assert!(x >= 0 && y >= 0 && x < 0x100 && y < 0x100);
+ TextureLocation(x as u8, y as u8)
+ }
+}
+
+/// A region is a rectangular part of a texture cache texture, split into fixed-size slabs.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct TextureRegion {
+ index: usize,
+ slab_size: SlabSize,
+ offset: DeviceIntPoint,
+ free_slots: Vec<TextureLocation>,
+ total_slot_count: usize,
+}
+
+impl TextureRegion {
+ fn new(index: usize, offset: DeviceIntPoint) -> Self {
+ TextureRegion {
+ index,
+ slab_size: SlabSize::invalid(),
+ offset,
+ free_slots: Vec::new(),
+ total_slot_count: 0,
+ }
+ }
+
+ // Initialize a region to be an allocator for a specific slab size.
+ fn init(&mut self, slab_size: SlabSize, region_size: i32, empty_regions: &mut usize) {
+ debug_assert!(self.slab_size == SlabSize::invalid());
+ debug_assert!(self.free_slots.is_empty());
+
+ self.slab_size = slab_size;
+ let slots_per_x_axis = region_size / self.slab_size.width;
+ let slots_per_y_axis = region_size / self.slab_size.height;
+
+ // Add each block to a freelist.
+ for y in 0 .. slots_per_y_axis {
+ for x in 0 .. slots_per_x_axis {
+ self.free_slots.push(TextureLocation::new(x, y));
+ }
+ }
+
+ self.total_slot_count = self.free_slots.len();
+ *empty_regions -= 1;
+ }
+
+ // Deinit a region, allowing it to become a region with
+ // a different allocator size.
+ fn deinit(&mut self, empty_regions: &mut usize) {
+ self.slab_size = SlabSize::invalid();
+ self.free_slots.clear();
+ self.total_slot_count = 0;
+ *empty_regions += 1;
+ }
+
+ fn is_empty(&self) -> bool {
+ self.slab_size == SlabSize::invalid()
+ }
+
+ // Attempt to allocate a fixed size block from this region.
+ fn alloc(&mut self) -> Option<(DeviceIntPoint, TextureLocation)> {
+ debug_assert!(self.slab_size != SlabSize::invalid());
+
+ self.free_slots.pop().map(|location| {(
+ point2(
+ self.offset.x + self.slab_size.width * location.0 as i32,
+ self.offset.y + self.slab_size.height * location.1 as i32,
+ ),
+ location,
+ )})
+ }
+
+ // Free a block in this region.
+ fn free(&mut self, location: TextureLocation, empty_regions: &mut usize) {
+ self.free_slots.push(location);
+
+ // If this region is completely unused, deinit it
+ // so that it can become a different slab size
+ // as required.
+ if self.free_slots.len() == self.total_slot_count {
+ self.deinit(empty_regions);
+ }
+ }
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SlabAllocatorParameters {
+ pub region_size: i32,
+}
+
+/// A 2D texture divided into regions.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SlabAllocator {
+ regions: Vec<TextureRegion>,
+ size: i32,
+ region_size: i32,
+ empty_regions: usize,
+ allocated_space: i32,
+}
+
+impl SlabAllocator {
+ pub fn new(size: i32, options: &SlabAllocatorParameters) -> Self {
+ let regions_per_row = size / options.region_size;
+ let num_regions = (regions_per_row * regions_per_row) as usize;
+
+ let mut regions = Vec::with_capacity(num_regions);
+
+ for index in 0..num_regions {
+ let offset = point2(
+ (index as i32 % regions_per_row) * options.region_size,
+ (index as i32 / regions_per_row) * options.region_size,
+ );
+
+ regions.push(TextureRegion::new(index, offset));
+ }
+
+ SlabAllocator {
+ regions,
+ size,
+ region_size: options.region_size,
+ empty_regions: num_regions,
+ allocated_space: 0,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.empty_regions == self.regions.len()
+ }
+
+ pub fn allocated_space(&self) -> i32 {
+ self.allocated_space
+ }
+
+ // Returns the region index and allocated rect.
+ pub fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ let slab_size = SlabSize::get(size);
+
+ // Keep track of the location of an empty region,
+ // in case we need to select a new empty region
+ // after the loop.
+ let mut empty_region_index = None;
+
+ let allocated_size = size2(slab_size.width, slab_size.height);
+
+ // Run through the existing regions of this size, and see if
+ // we can find a free block in any of them.
+ for (i, region) in self.regions.iter_mut().enumerate() {
+ if region.is_empty() {
+ empty_region_index = Some(i);
+ } else if region.slab_size == slab_size {
+ if let Some((origin, location)) = region.alloc() {
+ return Some((
+ pack_alloc_id(region.index, location),
+ DeviceIntRect {
+ origin,
+ size: allocated_size,
+ }
+ ));
+ }
+ }
+ }
+
+ if let Some(empty_region_index) = empty_region_index {
+ let region = &mut self.regions[empty_region_index];
+ region.init(slab_size, self.region_size, &mut self.empty_regions);
+ let (origin, location) = region.alloc().unwrap();
+
+ return Some((
+ pack_alloc_id(region.index, location),
+ DeviceIntRect {
+ origin,
+ size: allocated_size,
+ },
+ ))
+ }
+
+ None
+ }
+
+ pub fn deallocate(&mut self, id: AllocId) {
+ let (region_index, location) = unpack_alloc_id(id);
+
+ let region = &mut self.regions[region_index];
+ region.free(location, &mut self.empty_regions);
+
+ self.allocated_space -= region.slab_size.width * region.slab_size.height;
+ }
+
+ pub fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ use svg_fmt::*;
+
+ let region_spacing = 5.0;
+ let text_spacing = 15.0;
+ let regions_per_row = (self.size / self.region_size) as usize;
+ let wh = rect.size().width.min(rect.size().height);
+ let region_wh = (wh - region_spacing) / regions_per_row as f32 - region_spacing;
+
+ let x0 = rect.min.x;
+ let y0 = rect.min.y;
+
+ for (idx, region) in self.regions.iter().enumerate() {
+ let slab_size = region.slab_size;
+ let x = x0 + (idx % regions_per_row) as f32 * (region_wh + region_spacing);
+
+ let y = y0 + text_spacing + (idx / regions_per_row) as f32 * (region_wh + region_spacing);
+
+ let texture_background = if region.is_empty() { rgb(30, 30, 30) } else { rgb(40, 40, 130) };
+ writeln!(output, " {}", rectangle(x, y, region_wh, region_wh).inflate(1.0, 1.0).fill(rgb(10, 10, 10)))?;
+ writeln!(output, " {}", rectangle(x, y, region_wh, region_wh).fill(texture_background))?;
+
+ let sw = (slab_size.width as f32 / self.region_size as f32) * region_wh;
+ let sh = (slab_size.height as f32 / self.region_size as f32) * region_wh;
+
+ for slot in &region.free_slots {
+ let sx = x + slot.0 as f32 * sw;
+ let sy = y + slot.1 as f32 * sh;
+
+ // Allocation slot.
+ writeln!(output, " {}", rectangle(sx, sy, sw, sh).inflate(-0.5, -0.5).fill(rgb(30, 30, 30)))?;
+ }
+
+ if slab_size.width != 0 {
+ let region_text = format!("{}x{}", slab_size.width, slab_size.height);
+ let tx = x + 1.0;
+ let ty = y + region_wh - 1.0;
+ writeln!(output, " {}", text(tx, ty, region_text).color(rgb(230, 230, 230)))?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl AtlasAllocator for SlabAllocator {
+ type Parameters = SlabAllocatorParameters;
+
+ fn new(size: i32, options: &Self::Parameters) -> Self {
+ SlabAllocator::new(size, options)
+ }
+
+ fn allocate(&mut self, size: DeviceIntSize) -> Option<(AllocId, DeviceIntRect)> {
+ self.allocate(size)
+ }
+
+ fn deallocate(&mut self, id: AllocId) {
+ self.deallocate(id);
+ }
+
+ fn is_empty(&self) -> bool {
+ self.is_empty()
+ }
+
+ fn allocated_space(&self) -> i32 {
+ self.allocated_space()
+ }
+
+ fn dump_into_svg(&self, rect: &Box2D<f32>, output: &mut dyn std::io::Write) -> std::io::Result<()> {
+ self.dump_into_svg(rect, output)
+ }
+}
diff --git a/gfx/wr/webrender/src/tile_cache.rs b/gfx/wr/webrender/src/tile_cache.rs
new file mode 100644
index 0000000000..5064097c4f
--- /dev/null
+++ b/gfx/wr/webrender/src/tile_cache.rs
@@ -0,0 +1,547 @@
+/* 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 api::{ColorF, PrimitiveFlags, QualitySettings};
+use api::units::*;
+use crate::clip::{ClipChainId, ClipNodeKind, ClipStore, ClipInstance};
+use crate::frame_builder::FrameBuilderConfig;
+use crate::internal_types::{FastHashMap, FastHashSet};
+use crate::picture::{PrimitiveList, PictureCompositeMode, PictureOptions, PicturePrimitive, SliceId};
+use crate::picture::{Picture3DContext, TileCacheParams, TileOffset};
+use crate::prim_store::{PrimitiveInstance, PrimitiveInstanceKind, PrimitiveStore, PictureIndex};
+use crate::scene_building::SliceFlags;
+use crate::scene_builder_thread::Interners;
+use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex, SpatialTree};
+use crate::util::VecHelper;
+
+/*
+ Types and functionality related to picture caching. In future, we'll
+ move more and more of the existing functionality out of picture.rs
+ and into here.
+ */
+
+/// Created during scene building, describes how to create a tile cache for a given slice.
+pub struct PendingTileCache {
+ /// List of primitives that are part of this slice
+ pub prim_list: PrimitiveList,
+ /// Parameters that define the tile cache (such as background color, shared clips, reference spatial node)
+ pub params: TileCacheParams,
+}
+
+/// Used during scene building to construct the list of pending tile caches.
+pub struct TileCacheBuilder {
+ /// When true, a new tile cache will be created for the next primitive.
+ need_new_tile_cache: bool,
+ /// List of tile caches that have been created so far (last in the list is currently active).
+ pending_tile_caches: Vec<PendingTileCache>,
+
+ /// Cache the previous scroll root search for a spatial node, since they are often the same.
+ prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex),
+ /// A buffer for collecting clips for a clip-chain. Retained here to avoid memory allocations in add_prim.
+ prim_clips_buffer: Vec<ClipInstance>,
+ /// Cache the last clip-chain that was added to the shared clips as it's often the same between prims.
+ last_checked_clip_chain: ClipChainId,
+}
+
+/// The output of a tile cache builder, containing all details needed to construct the
+/// tile cache(s) for the next scene, and retain tiles from the previous frame when sent
+/// send to the frame builder.
+pub struct TileCacheConfig {
+ /// Mapping of slice id to the parameters needed to construct this tile cache.
+ pub tile_caches: FastHashMap<SliceId, TileCacheParams>,
+ /// A set of any spatial nodes that are attached to either a picture cache
+ /// root, or a clip node on the picture cache primitive. These are used
+ /// to detect cases where picture caching must be disabled. This is mostly
+ /// a temporary workaround for some existing wrench tests. I don't think
+ /// Gecko ever produces picture cache slices with complex transforms, so
+ /// in future we should prevent this in the public API and remove this hack.
+ pub picture_cache_spatial_nodes: FastHashSet<SpatialNodeIndex>,
+ /// Number of picture cache slices that were created (for profiler)
+ pub picture_cache_slice_count: usize,
+}
+
+impl TileCacheConfig {
+ pub fn new(picture_cache_slice_count: usize) -> Self {
+ TileCacheConfig {
+ tile_caches: FastHashMap::default(),
+ picture_cache_spatial_nodes: FastHashSet::default(),
+ picture_cache_slice_count,
+ }
+ }
+}
+
+impl TileCacheBuilder {
+ /// Construct a new tile cache builder.
+ pub fn new() -> Self {
+ TileCacheBuilder {
+ need_new_tile_cache: true,
+ pending_tile_caches: Vec::new(),
+ prev_scroll_root_cache: (ROOT_SPATIAL_NODE_INDEX, ROOT_SPATIAL_NODE_INDEX),
+ prim_clips_buffer: Vec::new(),
+ last_checked_clip_chain: ClipChainId::INVALID,
+ }
+ }
+
+ /// Set a barrier that forces a new tile cache next time a prim is added.
+ pub fn add_tile_cache_barrier(&mut self) {
+ self.need_new_tile_cache = true;
+ }
+
+ /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions.
+ pub fn add_prim(
+ &mut self,
+ prim_instance: PrimitiveInstance,
+ prim_rect: LayoutRect,
+ spatial_node_index: SpatialNodeIndex,
+ prim_flags: PrimitiveFlags,
+ spatial_tree: &SpatialTree,
+ clip_store: &ClipStore,
+ interners: &Interners,
+ config: &FrameBuilderConfig,
+ quality_settings: &QualitySettings,
+ ) {
+ // Scrollbars and clear primitives always get their own slice
+ let is_scrollbar_container = prim_flags.contains(PrimitiveFlags::IS_SCROLLBAR_CONTAINER);
+ let is_clear_prim = match prim_instance.kind {
+ PrimitiveInstanceKind::Clear { .. } => true,
+ _ => false,
+ };
+ let requires_own_slice = is_scrollbar_container || is_clear_prim;
+
+ // Check if we want to create a new slice based on the current / next scroll root
+ let scroll_root = self.find_scroll_root(spatial_node_index, spatial_tree);
+
+ // Also create a new slice if there was a barrier previously set
+ let mut want_new_tile_cache =
+ self.need_new_tile_cache ||
+ requires_own_slice ||
+ self.pending_tile_caches.is_empty();
+
+ let current_scroll_root = self.pending_tile_caches
+ .last()
+ .map(|p| p.params.spatial_node_index);
+
+ if let Some(current_scroll_root) = current_scroll_root {
+ want_new_tile_cache |= match (current_scroll_root, scroll_root) {
+ (ROOT_SPATIAL_NODE_INDEX, ROOT_SPATIAL_NODE_INDEX) => {
+ // Both current slice and this cluster are fixed position, no need to cut
+ false
+ }
+ (ROOT_SPATIAL_NODE_INDEX, _) => {
+ // A real scroll root is being established, so create a cache slice
+ true
+ }
+ (_, ROOT_SPATIAL_NODE_INDEX) => {
+ // If quality settings force subpixel AA over performance, skip creating
+ // a slice for the fixed position element(s) here.
+ if quality_settings.force_subpixel_aa_where_possible {
+ false
+ } else {
+ // A fixed position slice is encountered within a scroll root. Only create
+ // a slice in this case if all the clips referenced by this cluster are also
+ // fixed position. There's no real point in creating slices for these cases,
+ // since we'll have to rasterize them as the scrolling clip moves anyway. It
+ // also allows us to retain subpixel AA in these cases. For these types of
+ // slices, the intra-slice dirty rect handling typically works quite well
+ // (a common case is parallax scrolling effects).
+ let mut create_slice = true;
+ let mut current_clip_chain_id = prim_instance.clip_set.clip_chain_id;
+
+ while current_clip_chain_id != ClipChainId::NONE {
+ let clip_chain_node = &clip_store.clip_chain_nodes[current_clip_chain_id.0 as usize];
+ let spatial_root = self.find_scroll_root(clip_chain_node.spatial_node_index, spatial_tree);
+ if spatial_root != ROOT_SPATIAL_NODE_INDEX {
+ create_slice = false;
+ break;
+ }
+ current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+ }
+
+ create_slice
+ }
+ }
+ (curr_scroll_root, scroll_root) => {
+ // Two scrolling roots - only need a new slice if they differ
+ curr_scroll_root != scroll_root
+ }
+ };
+
+ // Update the list of clips that apply to this primitive instance, to track which are the
+ // shared clips for this tile cache that can be applied during compositing.
+ if self.last_checked_clip_chain != prim_instance.clip_set.clip_chain_id {
+ let prim_clips_buffer = &mut self.prim_clips_buffer;
+ prim_clips_buffer.clear();
+ add_clips(
+ current_scroll_root,
+ prim_instance.clip_set.clip_chain_id,
+ prim_clips_buffer,
+ clip_store,
+ interners,
+ spatial_tree,
+ );
+
+ let current_shared_clips = &self.pending_tile_caches
+ .last()
+ .unwrap()
+ .params
+ .shared_clips;
+
+ // If the shared clips are not compatible, create a new slice.
+ // TODO(gw): Does Gecko ever supply duplicate or out-of-order
+ // shared clips? It doesn't seem to, but if it does,
+ // we will need to be more clever here to check if
+ // the shared clips are compatible.
+ want_new_tile_cache |= current_shared_clips != prim_clips_buffer;
+
+ self.last_checked_clip_chain = prim_instance.clip_set.clip_chain_id;
+ }
+ }
+
+ if want_new_tile_cache {
+ // If the page would create too many slices (an arbitrary definition where
+ // it's assumed the GPU memory + compositing overhead would be too high)
+ // then create a single picture cache for the remaining content. This at
+ // least means that we can cache small content changes efficiently when
+ // scrolling isn't occurring. Scrolling regions will be handled reasonably
+ // efficiently by the dirty rect tracking (since it's likely that if the
+ // page has so many slices there isn't a single major scroll region).
+ const MAX_CACHE_SLICES: usize = 12;
+ let slice = self.pending_tile_caches.len();
+
+ // If we have exceeded the maximum number of slices, skip creating a new
+ // one and the primitive will be added to the last slice.
+ if slice < MAX_CACHE_SLICES {
+ // When we reach the last valid slice that can be created, it is created as
+ // a fixed slice without shared clips, ensuring that we can safely add any
+ // subsequent primitives to it. This doesn't seem to occur on any real
+ // world content (only contrived test cases), where this acts as a fail safe
+ // to ensure we don't allocate too much GPU memory for surface caches.
+ // However, if we _do_ ever see this occur on real world content, we could
+ // probably consider increasing the max cache slices a bit more than the
+ // current limit.
+ let params = if slice == MAX_CACHE_SLICES-1 {
+ TileCacheParams {
+ slice,
+ slice_flags: SliceFlags::empty(),
+ spatial_node_index: ROOT_SPATIAL_NODE_INDEX,
+ background_color: None,
+ shared_clips: Vec::new(),
+ shared_clip_chain: ClipChainId::NONE,
+ virtual_surface_size: config.compositor_kind.get_virtual_surface_size(),
+ }
+ } else {
+ let slice_flags = if is_scrollbar_container {
+ SliceFlags::IS_SCROLLBAR
+ } else {
+ SliceFlags::empty()
+ };
+
+ let background_color = if slice == 0 {
+ config.background_color
+ } else {
+ None
+ };
+
+ let mut shared_clips = Vec::new();
+ add_clips(
+ scroll_root,
+ prim_instance.clip_set.clip_chain_id,
+ &mut shared_clips,
+ clip_store,
+ interners,
+ spatial_tree,
+ );
+
+ self.last_checked_clip_chain = prim_instance.clip_set.clip_chain_id;
+
+ TileCacheParams {
+ slice,
+ slice_flags,
+ spatial_node_index: scroll_root,
+ background_color,
+ shared_clips,
+ shared_clip_chain: ClipChainId::NONE,
+ virtual_surface_size: config.compositor_kind.get_virtual_surface_size(),
+ }
+ };
+
+ self.pending_tile_caches.push(PendingTileCache {
+ prim_list: PrimitiveList::empty(),
+ params,
+ });
+
+ self.need_new_tile_cache = requires_own_slice;
+ }
+ }
+
+ self.pending_tile_caches
+ .last_mut()
+ .unwrap()
+ .prim_list
+ .add_prim(
+ prim_instance,
+ prim_rect,
+ spatial_node_index,
+ prim_flags,
+ );
+ }
+
+ /// Consume this object and build the list of tile cache primitives
+ pub fn build(
+ self,
+ config: &FrameBuilderConfig,
+ clip_store: &mut ClipStore,
+ prim_store: &mut PrimitiveStore,
+ ) -> (TileCacheConfig, Vec<PictureIndex>) {
+ let mut result = TileCacheConfig::new(self.pending_tile_caches.len());
+ let mut tile_cache_pictures = Vec::new();
+
+ for pending_tile_cache in self.pending_tile_caches {
+ let pic_index = create_tile_cache(
+ pending_tile_cache.params.slice,
+ pending_tile_cache.params.slice_flags,
+ pending_tile_cache.params.spatial_node_index,
+ pending_tile_cache.prim_list,
+ pending_tile_cache.params.background_color,
+ pending_tile_cache.params.shared_clips,
+ prim_store,
+ clip_store,
+ &mut result.picture_cache_spatial_nodes,
+ config,
+ &mut result.tile_caches,
+ );
+
+ tile_cache_pictures.push(pic_index);
+ }
+
+ (result, tile_cache_pictures)
+ }
+
+ /// Find the scroll root for a given spatial node
+ fn find_scroll_root(
+ &mut self,
+ spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+ ) -> SpatialNodeIndex {
+ if self.prev_scroll_root_cache.0 == spatial_node_index {
+ return self.prev_scroll_root_cache.1;
+ }
+
+ let scroll_root = spatial_tree.find_scroll_root(spatial_node_index);
+ self.prev_scroll_root_cache = (spatial_node_index, scroll_root);
+
+ scroll_root
+ }
+}
+
+// Helper fn to collect clip handles from a given clip chain.
+fn add_clips(
+ scroll_root: SpatialNodeIndex,
+ clip_chain_id: ClipChainId,
+ prim_clips: &mut Vec<ClipInstance>,
+ clip_store: &ClipStore,
+ interners: &Interners,
+ spatial_tree: &SpatialTree,
+) {
+ let mut current_clip_chain_id = clip_chain_id;
+
+ while current_clip_chain_id != ClipChainId::NONE {
+ let clip_chain_node = &clip_store
+ .clip_chain_nodes[current_clip_chain_id.0 as usize];
+
+ let clip_node_data = &interners.clip[clip_chain_node.handle];
+ if let ClipNodeKind::Rectangle = clip_node_data.clip_node_kind {
+ if spatial_tree.is_ancestor(
+ clip_chain_node.spatial_node_index,
+ scroll_root,
+ ) {
+ prim_clips.push(ClipInstance::new(clip_chain_node.handle, clip_chain_node.spatial_node_index));
+ }
+ }
+
+ current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
+ }
+}
+
+/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance
+/// that wraps the primitive list.
+fn create_tile_cache(
+ slice: usize,
+ slice_flags: SliceFlags,
+ scroll_root: SpatialNodeIndex,
+ prim_list: PrimitiveList,
+ background_color: Option<ColorF>,
+ shared_clips: Vec<ClipInstance>,
+ prim_store: &mut PrimitiveStore,
+ clip_store: &mut ClipStore,
+ picture_cache_spatial_nodes: &mut FastHashSet<SpatialNodeIndex>,
+ frame_builder_config: &FrameBuilderConfig,
+ tile_caches: &mut FastHashMap<SliceId, TileCacheParams>,
+) -> PictureIndex {
+ // Add this spatial node to the list to check for complex transforms
+ // at the start of a frame build.
+ picture_cache_spatial_nodes.insert(scroll_root);
+
+ // Build a clip-chain for the tile cache, that contains any of the shared clips
+ // we will apply when drawing the tiles. In all cases provided by Gecko, these
+ // are rectangle clips with a scale/offset transform only, and get handled as
+ // a simple local clip rect in the vertex shader. However, this should in theory
+ // also work with any complex clips, such as rounded rects and image masks, by
+ // producing a clip mask that is applied to the picture cache tiles.
+
+ let mut parent_clip_chain_id = ClipChainId::NONE;
+ for clip_instance in &shared_clips {
+ // Add this spatial node to the list to check for complex transforms
+ // at the start of a frame build.
+ picture_cache_spatial_nodes.insert(clip_instance.spatial_node_index);
+
+ parent_clip_chain_id = clip_store.add_clip_chain_node(
+ clip_instance.handle,
+ clip_instance.spatial_node_index,
+ parent_clip_chain_id,
+ );
+ }
+
+ let slice_id = SliceId::new(slice);
+
+ // Store some information about the picture cache slice. This is used when we swap the
+ // new scene into the frame builder to either reuse existing slices, or create new ones.
+ tile_caches.insert(slice_id, TileCacheParams {
+ slice,
+ slice_flags,
+ spatial_node_index: scroll_root,
+ background_color,
+ shared_clips,
+ shared_clip_chain: parent_clip_chain_id,
+ virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(),
+ });
+
+ let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image(
+ Some(PictureCompositeMode::TileCache { slice_id }),
+ Picture3DContext::Out,
+ true,
+ PrimitiveFlags::IS_BACKFACE_VISIBLE,
+ prim_list,
+ scroll_root,
+ PictureOptions::default(),
+ ));
+
+ PictureIndex(pic_index)
+}
+
+/// Debug information about a set of picture cache slices, exposed via RenderResults
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PictureCacheDebugInfo {
+ pub slices: FastHashMap<usize, SliceDebugInfo>,
+}
+
+impl PictureCacheDebugInfo {
+ pub fn new() -> Self {
+ PictureCacheDebugInfo {
+ slices: FastHashMap::default(),
+ }
+ }
+
+ /// Convenience method to retrieve a given slice. Deliberately panics
+ /// if the slice isn't present.
+ pub fn slice(&self, slice: usize) -> &SliceDebugInfo {
+ &self.slices[&slice]
+ }
+}
+
+impl Default for PictureCacheDebugInfo {
+ fn default() -> PictureCacheDebugInfo {
+ PictureCacheDebugInfo::new()
+ }
+}
+
+/// Debug information about a set of picture cache tiles, exposed via RenderResults
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct SliceDebugInfo {
+ pub tiles: FastHashMap<TileOffset, TileDebugInfo>,
+}
+
+impl SliceDebugInfo {
+ pub fn new() -> Self {
+ SliceDebugInfo {
+ tiles: FastHashMap::default(),
+ }
+ }
+
+ /// Convenience method to retrieve a given tile. Deliberately panics
+ /// if the tile isn't present.
+ pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo {
+ &self.tiles[&TileOffset::new(x, y)]
+ }
+}
+
+/// Debug information about a tile that was dirty and was rasterized
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DirtyTileDebugInfo {
+ pub local_valid_rect: PictureRect,
+ pub local_dirty_rect: PictureRect,
+}
+
+/// Debug information about the state of a tile
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TileDebugInfo {
+ /// Tile was occluded by a tile in front of it
+ Occluded,
+ /// Tile was culled (not visible in current display port)
+ Culled,
+ /// Tile was valid (no rasterization was done) and visible
+ Valid,
+ /// Tile was dirty, and was updated
+ Dirty(DirtyTileDebugInfo),
+}
+
+impl TileDebugInfo {
+ pub fn is_occluded(&self) -> bool {
+ match self {
+ TileDebugInfo::Occluded => true,
+ TileDebugInfo::Culled |
+ TileDebugInfo::Valid |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn is_valid(&self) -> bool {
+ match self {
+ TileDebugInfo::Valid => true,
+ TileDebugInfo::Culled |
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn is_culled(&self) -> bool {
+ match self {
+ TileDebugInfo::Culled => true,
+ TileDebugInfo::Valid |
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Dirty(..) => false,
+ }
+ }
+
+ pub fn as_dirty(&self) -> &DirtyTileDebugInfo {
+ match self {
+ TileDebugInfo::Occluded |
+ TileDebugInfo::Culled |
+ TileDebugInfo::Valid => {
+ panic!("not a dirty tile!");
+ }
+ TileDebugInfo::Dirty(ref info) => {
+ info
+ }
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/util.rs b/gfx/wr/webrender/src/util.rs
new file mode 100644
index 0000000000..7e10254c90
--- /dev/null
+++ b/gfx/wr/webrender/src/util.rs
@@ -0,0 +1,1538 @@
+/* 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 api::BorderRadius;
+use api::units::*;
+use euclid::{Point2D, Rect, Size2D, Vector2D, point2, size2};
+use euclid::{default, Transform2D, Transform3D, Scale};
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps};
+use plane_split::{Clipper, Polygon};
+use std::{i32, f32, fmt, ptr};
+use std::borrow::Cow;
+use std::num::NonZeroUsize;
+use std::os::raw::c_void;
+use std::sync::Arc;
+use std::mem::replace;
+
+
+// Matches the definition of SK_ScalarNearlyZero in Skia.
+const NEARLY_ZERO: f32 = 1.0 / 4096.0;
+
+/// A typesafe helper that separates new value construction from
+/// vector growing, allowing LLVM to ideally construct the element in place.
+pub struct Allocation<'a, T: 'a> {
+ vec: &'a mut Vec<T>,
+ index: usize,
+}
+
+impl<'a, T> Allocation<'a, T> {
+ // writing is safe because alloc() ensured enough capacity
+ // and `Allocation` holds a mutable borrow to prevent anyone else
+ // from breaking this invariant.
+ #[inline(always)]
+ pub fn init(self, value: T) -> usize {
+ unsafe {
+ ptr::write(self.vec.as_mut_ptr().add(self.index), value);
+ self.vec.set_len(self.index + 1);
+ }
+ self.index
+ }
+}
+
+/// An entry into a vector, similar to `std::collections::hash_map::Entry`.
+pub enum VecEntry<'a, T: 'a> {
+ Vacant(Allocation<'a, T>),
+ Occupied(&'a mut T),
+}
+
+impl<'a, T> VecEntry<'a, T> {
+ #[inline(always)]
+ pub fn set(self, value: T) {
+ match self {
+ VecEntry::Vacant(alloc) => { alloc.init(value); }
+ VecEntry::Occupied(slot) => { *slot = value; }
+ }
+ }
+}
+
+pub trait VecHelper<T> {
+ /// Growns the vector by a single entry, returning the allocation.
+ fn alloc(&mut self) -> Allocation<T>;
+ /// Either returns an existing elemenet, or grows the vector by one.
+ /// Doesn't expect indices to be higher than the current length.
+ fn entry(&mut self, index: usize) -> VecEntry<T>;
+
+ /// Equivalent to `mem::replace(&mut vec, Vec::new())`
+ fn take(&mut self) -> Self;
+
+ /// Call clear and return self (useful for chaining with calls that move the vector).
+ fn cleared(self) -> Self;
+
+ /// Functionally equivalent to `mem::replace(&mut vec, Vec::new())` but tries
+ /// to keep the allocation in the caller if it is empty or replace it with a
+ /// pre-allocated vector.
+ fn take_and_preallocate(&mut self) -> Self;
+}
+
+impl<T> VecHelper<T> for Vec<T> {
+ fn alloc(&mut self) -> Allocation<T> {
+ let index = self.len();
+ if self.capacity() == index {
+ self.reserve(1);
+ }
+ Allocation {
+ vec: self,
+ index,
+ }
+ }
+
+ fn entry(&mut self, index: usize) -> VecEntry<T> {
+ if index < self.len() {
+ VecEntry::Occupied(unsafe {
+ self.get_unchecked_mut(index)
+ })
+ } else {
+ assert_eq!(index, self.len());
+ VecEntry::Vacant(self.alloc())
+ }
+ }
+
+ fn take(&mut self) -> Self {
+ replace(self, Vec::new())
+ }
+
+ fn cleared(mut self) -> Self {
+ self.clear();
+
+ self
+ }
+
+ fn take_and_preallocate(&mut self) -> Self {
+ let len = self.len();
+ if len == 0 {
+ self.clear();
+ return Vec::new();
+ }
+ replace(self, Vec::with_capacity(len + 8))
+ }
+}
+
+
+// Represents an optimized transform where there is only
+// a scale and translation (which are guaranteed to maintain
+// an axis align rectangle under transformation). The
+// scaling is applied first, followed by the translation.
+// TODO(gw): We should try and incorporate F <-> T units here,
+// but it's a bit tricky to do that now with the
+// way the current spatial tree works.
+#[derive(Debug, Clone, Copy, MallocSizeOf)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct ScaleOffset {
+ pub scale: default::Vector2D<f32>,
+ pub offset: default::Vector2D<f32>,
+}
+
+impl ScaleOffset {
+ pub fn identity() -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset: Vector2D::zero(),
+ }
+ }
+
+ // Construct a ScaleOffset from a transform. Returns
+ // None if the matrix is not a pure scale / translation.
+ pub fn from_transform<F, T>(
+ m: &Transform3D<f32, F, T>,
+ ) -> Option<ScaleOffset> {
+
+ // To check that we have a pure scale / translation:
+ // Every field must match an identity matrix, except:
+ // - Any value present in tx,ty
+ // - Any non-neg value present in sx,sy (avoid negative for reflection/rotation)
+
+ if m.m11 < 0.0 ||
+ m.m12.abs() > NEARLY_ZERO ||
+ m.m13.abs() > NEARLY_ZERO ||
+ m.m14.abs() > NEARLY_ZERO ||
+ m.m21.abs() > NEARLY_ZERO ||
+ m.m22 < 0.0 ||
+ m.m23.abs() > NEARLY_ZERO ||
+ m.m24.abs() > NEARLY_ZERO ||
+ m.m31.abs() > NEARLY_ZERO ||
+ m.m32.abs() > NEARLY_ZERO ||
+ (m.m33 - 1.0).abs() > NEARLY_ZERO ||
+ m.m34.abs() > NEARLY_ZERO ||
+ m.m43.abs() > NEARLY_ZERO ||
+ (m.m44 - 1.0).abs() > NEARLY_ZERO {
+ return None;
+ }
+
+ Some(ScaleOffset {
+ scale: Vector2D::new(m.m11, m.m22),
+ offset: Vector2D::new(m.m41, m.m42),
+ })
+ }
+
+ pub fn from_offset(offset: default::Vector2D<f32>) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset,
+ }
+ }
+
+ pub fn inverse(&self) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(
+ 1.0 / self.scale.x,
+ 1.0 / self.scale.y,
+ ),
+ offset: Vector2D::new(
+ -self.offset.x / self.scale.x,
+ -self.offset.y / self.scale.y,
+ ),
+ }
+ }
+
+ pub fn offset(&self, offset: default::Vector2D<f32>) -> Self {
+ self.accumulate(
+ &ScaleOffset {
+ scale: Vector2D::new(1.0, 1.0),
+ offset,
+ }
+ )
+ }
+
+ pub fn scale(&self, scale: f32) -> Self {
+ self.accumulate(
+ &ScaleOffset {
+ scale: Vector2D::new(scale, scale),
+ offset: Vector2D::zero(),
+ }
+ )
+ }
+
+ /// Produce a ScaleOffset that includes both self and other.
+ /// The 'self' ScaleOffset is applied after other.
+ /// This is equivalent to `Transform3D::pre_transform`.
+ pub fn accumulate(&self, other: &ScaleOffset) -> Self {
+ ScaleOffset {
+ scale: Vector2D::new(
+ self.scale.x * other.scale.x,
+ self.scale.y * other.scale.y,
+ ),
+ offset: Vector2D::new(
+ self.offset.x + self.scale.x * other.offset.x,
+ self.offset.y + self.scale.y * other.offset.y,
+ ),
+ }
+ }
+
+ pub fn map_rect<F, T>(&self, rect: &Rect<f32, F>) -> Rect<f32, T> {
+ Rect::new(
+ Point2D::new(
+ rect.origin.x * self.scale.x + self.offset.x,
+ rect.origin.y * self.scale.y + self.offset.y,
+ ),
+ Size2D::new(
+ rect.size.width * self.scale.x,
+ rect.size.height * self.scale.y,
+ )
+ )
+ }
+
+ pub fn unmap_rect<F, T>(&self, rect: &Rect<f32, F>) -> Rect<f32, T> {
+ Rect::new(
+ Point2D::new(
+ (rect.origin.x - self.offset.x) / self.scale.x,
+ (rect.origin.y - self.offset.y) / self.scale.y,
+ ),
+ Size2D::new(
+ rect.size.width / self.scale.x,
+ rect.size.height / self.scale.y,
+ )
+ )
+ }
+
+ pub fn map_vector<F, T>(&self, vector: &Vector2D<f32, F>) -> Vector2D<f32, T> {
+ Vector2D::new(
+ vector.x * self.scale.x,
+ vector.y * self.scale.y,
+ )
+ }
+
+ pub fn unmap_vector<F, T>(&self, vector: &Vector2D<f32, F>) -> Vector2D<f32, T> {
+ Vector2D::new(
+ vector.x / self.scale.x,
+ vector.y / self.scale.y,
+ )
+ }
+
+ pub fn map_point<F, T>(&self, point: &Point2D<f32, F>) -> Point2D<f32, T> {
+ Point2D::new(
+ point.x * self.scale.x + self.offset.x,
+ point.y * self.scale.y + self.offset.y,
+ )
+ }
+
+ pub fn unmap_point<F, T>(&self, point: &Point2D<f32, F>) -> Point2D<f32, T> {
+ Point2D::new(
+ (point.x - self.offset.x) / self.scale.x,
+ (point.y - self.offset.y) / self.scale.y,
+ )
+ }
+
+ pub fn to_transform<F, T>(&self) -> Transform3D<f32, F, T> {
+ Transform3D::new(
+ self.scale.x,
+ 0.0,
+ 0.0,
+ 0.0,
+
+ 0.0,
+ self.scale.y,
+ 0.0,
+ 0.0,
+
+ 0.0,
+ 0.0,
+ 1.0,
+ 0.0,
+
+ self.offset.x,
+ self.offset.y,
+ 0.0,
+ 1.0,
+ )
+ }
+}
+
+// TODO: Implement these in euclid!
+pub trait MatrixHelpers<Src, Dst> {
+ /// A port of the preserves2dAxisAlignment function in Skia.
+ /// Defined in the SkMatrix44 class.
+ fn preserves_2d_axis_alignment(&self) -> bool;
+ fn has_perspective_component(&self) -> bool;
+ fn has_2d_inverse(&self) -> bool;
+ /// Check if the matrix post-scaling on either the X or Y axes could cause geometry
+ /// transformed by this matrix to have scaling exceeding the supplied limit.
+ fn exceeds_2d_scale(&self, limit: f64) -> bool;
+ fn inverse_project(&self, target: &Point2D<f32, Dst>) -> Option<Point2D<f32, Src>>;
+ fn inverse_rect_footprint(&self, rect: &Rect<f32, Dst>) -> Option<Rect<f32, Src>>;
+ fn transform_kind(&self) -> TransformedRectKind;
+ fn is_simple_translation(&self) -> bool;
+ fn is_simple_2d_translation(&self) -> bool;
+ fn is_2d_scale_translation(&self) -> bool;
+ /// Return the determinant of the 2D part of the matrix.
+ fn determinant_2d(&self) -> f32;
+ /// This function returns a point in the `Src` space that projects into zero XY.
+ /// It ignores the Z coordinate and is usable for "flattened" transformations,
+ /// since they are not generally inversible.
+ fn inverse_project_2d_origin(&self) -> Option<Point2D<f32, Src>>;
+ /// Turn Z transformation into identity. This is useful when crossing "flat"
+ /// transform styled stacking contexts upon traversing the coordinate systems.
+ fn flatten_z_output(&mut self);
+
+ fn cast_unit<NewSrc, NewDst>(&self) -> Transform3D<f32, NewSrc, NewDst>;
+}
+
+impl<Src, Dst> MatrixHelpers<Src, Dst> for Transform3D<f32, Src, Dst> {
+ fn preserves_2d_axis_alignment(&self) -> bool {
+ if self.m14 != 0.0 || self.m24 != 0.0 {
+ return false;
+ }
+
+ let mut col0 = 0;
+ let mut col1 = 0;
+ let mut row0 = 0;
+ let mut row1 = 0;
+
+ if self.m11.abs() > NEARLY_ZERO {
+ col0 += 1;
+ row0 += 1;
+ }
+ if self.m12.abs() > NEARLY_ZERO {
+ col1 += 1;
+ row0 += 1;
+ }
+ if self.m21.abs() > NEARLY_ZERO {
+ col0 += 1;
+ row1 += 1;
+ }
+ if self.m22.abs() > NEARLY_ZERO {
+ col1 += 1;
+ row1 += 1;
+ }
+
+ col0 < 2 && col1 < 2 && row0 < 2 && row1 < 2
+ }
+
+ fn has_perspective_component(&self) -> bool {
+ self.m14.abs() > NEARLY_ZERO ||
+ self.m24.abs() > NEARLY_ZERO ||
+ self.m34.abs() > NEARLY_ZERO ||
+ (self.m44 - 1.0).abs() > NEARLY_ZERO
+ }
+
+ fn has_2d_inverse(&self) -> bool {
+ self.determinant_2d() != 0.0
+ }
+
+ fn exceeds_2d_scale(&self, limit: f64) -> bool {
+ let limit2 = (limit * limit) as f32;
+ self.m11 * self.m11 + self.m12 * self.m12 > limit2 ||
+ self.m21 * self.m21 + self.m22 * self.m22 > limit2
+ }
+
+ /// Find out a point in `Src` that would be projected into the `target`.
+ fn inverse_project(&self, target: &Point2D<f32, Dst>) -> Option<Point2D<f32, Src>> {
+ // form the linear equation for the hyperplane intersection
+ let m = Transform2D::<f32, Src, Dst>::new(
+ self.m11 - target.x * self.m14, self.m12 - target.y * self.m14,
+ self.m21 - target.x * self.m24, self.m22 - target.y * self.m24,
+ self.m41 - target.x * self.m44, self.m42 - target.y * self.m44,
+ );
+ let inv = m.inverse()?;
+ // we found the point, now check if it maps to the positive hemisphere
+ if inv.m31 * self.m14 + inv.m32 * self.m24 + self.m44 > 0.0 {
+ Some(Point2D::new(inv.m31, inv.m32))
+ } else {
+ None
+ }
+ }
+
+ fn inverse_rect_footprint(&self, rect: &Rect<f32, Dst>) -> Option<Rect<f32, Src>> {
+ Some(Rect::from_points(&[
+ self.inverse_project(&rect.origin)?,
+ self.inverse_project(&rect.top_right())?,
+ self.inverse_project(&rect.bottom_left())?,
+ self.inverse_project(&rect.bottom_right())?,
+ ]))
+ }
+
+ fn transform_kind(&self) -> TransformedRectKind {
+ if self.preserves_2d_axis_alignment() {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ }
+ }
+
+ fn is_simple_translation(&self) -> bool {
+ if (self.m11 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m22 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m33 - 1.0).abs() > NEARLY_ZERO ||
+ (self.m44 - 1.0).abs() > NEARLY_ZERO {
+ return false;
+ }
+
+ self.m12.abs() < NEARLY_ZERO && self.m13.abs() < NEARLY_ZERO &&
+ self.m14.abs() < NEARLY_ZERO && self.m21.abs() < NEARLY_ZERO &&
+ self.m23.abs() < NEARLY_ZERO && self.m24.abs() < NEARLY_ZERO &&
+ self.m31.abs() < NEARLY_ZERO && self.m32.abs() < NEARLY_ZERO &&
+ self.m34.abs() < NEARLY_ZERO
+ }
+
+ fn is_simple_2d_translation(&self) -> bool {
+ if !self.is_simple_translation() {
+ return false;
+ }
+
+ self.m43.abs() < NEARLY_ZERO
+ }
+
+ /* is this...
+ * X 0 0 0
+ * 0 Y 0 0
+ * 0 0 1 0
+ * a b 0 1
+ */
+ fn is_2d_scale_translation(&self) -> bool {
+ (self.m33 - 1.0).abs() < NEARLY_ZERO &&
+ (self.m44 - 1.0).abs() < NEARLY_ZERO &&
+ self.m12.abs() < NEARLY_ZERO && self.m13.abs() < NEARLY_ZERO && self.m14.abs() < NEARLY_ZERO &&
+ self.m21.abs() < NEARLY_ZERO && self.m23.abs() < NEARLY_ZERO && self.m24.abs() < NEARLY_ZERO &&
+ self.m31.abs() < NEARLY_ZERO && self.m32.abs() < NEARLY_ZERO && self.m34.abs() < NEARLY_ZERO &&
+ self.m43.abs() < NEARLY_ZERO
+ }
+
+ fn determinant_2d(&self) -> f32 {
+ self.m11 * self.m22 - self.m12 * self.m21
+ }
+
+ fn inverse_project_2d_origin(&self) -> Option<Point2D<f32, Src>> {
+ let det = self.determinant_2d();
+ if det != 0.0 {
+ let x = (self.m21 * self.m42 - self.m41 * self.m22) / det;
+ let y = (self.m12 * self.m41 - self.m11 * self.m42) / det;
+ Some(Point2D::new(x, y))
+ } else {
+ None
+ }
+ }
+
+ fn flatten_z_output(&mut self) {
+ self.m13 = 0.0;
+ self.m23 = 0.0;
+ self.m33 = 1.0;
+ self.m43 = 0.0;
+ self.m31 = 0.0;
+ self.m32 = 0.0;
+ self.m34 = 0.0;
+ }
+
+ fn cast_unit<NewSrc, NewDst>(&self) -> Transform3D<f32, NewSrc, NewDst> {
+ Transform3D::new(
+ self.m11, self.m12, self.m13, self.m14,
+ self.m21, self.m22, self.m23, self.m24,
+ self.m31, self.m32, self.m33, self.m34,
+ self.m41, self.m42, self.m43, self.m44,
+ )
+ }
+}
+
+pub trait PointHelpers<U>
+where
+ Self: Sized,
+{
+ fn snap(&self) -> Self;
+}
+
+impl<U> PointHelpers<U> for Point2D<f32, U> {
+ fn snap(&self) -> Self {
+ Point2D::new(
+ (self.x + 0.5).floor(),
+ (self.y + 0.5).floor(),
+ )
+ }
+}
+
+pub trait RectHelpers<U>
+where
+ Self: Sized,
+{
+ fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self;
+ fn snap(&self) -> Self;
+}
+
+impl<U> RectHelpers<U> for Rect<f32, U> {
+ fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
+ Rect::new(
+ Point2D::new(x0, y0),
+ Size2D::new(x1 - x0, y1 - y0),
+ )
+ }
+
+ fn snap(&self) -> Self {
+ let origin = Point2D::new(
+ (self.origin.x + 0.5).floor(),
+ (self.origin.y + 0.5).floor(),
+ );
+ Rect::new(
+ origin,
+ Size2D::new(
+ (self.origin.x + self.size.width + 0.5).floor() - origin.x,
+ (self.origin.y + self.size.height + 0.5).floor() - origin.y,
+ ),
+ )
+ }
+}
+
+pub trait VectorHelpers<U>
+where
+ Self: Sized,
+{
+ fn snap(&self) -> Self;
+}
+
+impl<U> VectorHelpers<U> for Vector2D<f32, U> {
+ fn snap(&self) -> Self {
+ Vector2D::new(
+ (self.x + 0.5).floor(),
+ (self.y + 0.5).floor(),
+ )
+ }
+}
+
+pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
+ (b - a) * t + a
+}
+
+#[repr(u32)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum TransformedRectKind {
+ AxisAligned = 0,
+ Complex = 1,
+}
+
+#[inline(always)]
+pub fn pack_as_float(value: u32) -> f32 {
+ value as f32 + 0.5
+}
+
+#[inline]
+fn extract_inner_rect_impl<U>(
+ rect: &Rect<f32, U>,
+ radii: &BorderRadius,
+ k: f32,
+) -> Option<Rect<f32, U>> {
+ // `k` defines how much border is taken into account
+ // We enforce the offsets to be rounded to pixel boundaries
+ // by `ceil`-ing and `floor`-ing them
+
+ let xl = (k * radii.top_left.width.max(radii.bottom_left.width)).ceil();
+ let xr = (rect.size.width - k * radii.top_right.width.max(radii.bottom_right.width)).floor();
+ let yt = (k * radii.top_left.height.max(radii.top_right.height)).ceil();
+ let yb =
+ (rect.size.height - k * radii.bottom_left.height.max(radii.bottom_right.height)).floor();
+
+ if xl <= xr && yt <= yb {
+ Some(Rect::new(
+ Point2D::new(rect.origin.x + xl, rect.origin.y + yt),
+ Size2D::new(xr - xl, yb - yt),
+ ))
+ } else {
+ None
+ }
+}
+
+/// Return an aligned rectangle that is inside the clip region and doesn't intersect
+/// any of the bounding rectangles of the rounded corners.
+pub fn extract_inner_rect_safe<U>(
+ rect: &Rect<f32, U>,
+ radii: &BorderRadius,
+) -> Option<Rect<f32, U>> {
+ // value of `k==1.0` is used for extraction of the corner rectangles
+ // see `SEGMENT_CORNER_*` in `clip_shared.glsl`
+ extract_inner_rect_impl(rect, radii, 1.0)
+}
+
+#[cfg(test)]
+use euclid::vec3;
+
+#[cfg(test)]
+pub mod test {
+ use super::*;
+ use euclid::default::{Point2D, Rect, Size2D, Transform3D};
+ use euclid::{Angle, approxeq::ApproxEq};
+ use std::f32::consts::PI;
+
+ #[test]
+ fn inverse_project() {
+ let m0 = Transform3D::identity();
+ let p0 = Point2D::new(1.0, 2.0);
+ // an identical transform doesn't need any inverse projection
+ assert_eq!(m0.inverse_project(&p0), Some(p0));
+ let m1 = Transform3D::rotation(0.0, 1.0, 0.0, Angle::radians(-PI / 3.0));
+ // rotation by 60 degrees would imply scaling of X component by a factor of 2
+ assert_eq!(m1.inverse_project(&p0), Some(Point2D::new(2.0, 2.0)));
+ }
+
+ #[test]
+ fn inverse_project_footprint() {
+ let m = Transform3D::new(
+ 0.477499992, 0.135000005, -1.0, 0.000624999986,
+ -0.642787635, 0.766044438, 0.0, 0.0,
+ 0.766044438, 0.642787635, 0.0, 0.0,
+ 1137.10986, 113.71286, 402.0, 0.748749971,
+ );
+ let r = Rect::new(Point2D::zero(), Size2D::new(804.0, 804.0));
+ {
+ let points = &[
+ r.origin,
+ r.top_right(),
+ r.bottom_left(),
+ r.bottom_right(),
+ ];
+ let mi = m.inverse().unwrap();
+ // In this section, we do the forward and backward transformation
+ // to confirm that its bijective.
+ // We also do the inverse projection path, and confirm it functions the same way.
+ println!("Points:");
+ for p in points {
+ let pp = m.transform_point2d_homogeneous(*p);
+ let p3 = pp.to_point3d().unwrap();
+ let pi = mi.transform_point3d_homogeneous(p3);
+ let px = pi.to_point2d().unwrap();
+ let py = m.inverse_project(&pp.to_point2d().unwrap()).unwrap();
+ println!("\t{:?} -> {:?} -> {:?} -> ({:?} -> {:?}, {:?})", p, pp, p3, pi, px, py);
+ assert!(px.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
+ assert!(py.approx_eq_eps(p, &Point2D::new(0.001, 0.001)));
+ }
+ }
+ // project
+ let rp = project_rect(&m, &r, &Rect::new(Point2D::zero(), Size2D::new(1000.0, 1000.0))).unwrap();
+ println!("Projected {:?}", rp);
+ // one of the points ends up in the negative hemisphere
+ assert_eq!(m.inverse_project(&rp.origin), None);
+ // inverse
+ if let Some(ri) = m.inverse_rect_footprint(&rp) {
+ // inverse footprint should be larger, since it doesn't know the original Z
+ assert!(ri.contains_rect(&r), "Inverse {:?}", ri);
+ }
+ }
+
+ fn validate_convert(xref: &LayoutTransform) {
+ let so = ScaleOffset::from_transform(xref).unwrap();
+ let xf = so.to_transform();
+ assert!(xref.approx_eq(&xf));
+ }
+
+ #[test]
+ fn scale_offset_convert() {
+ let xref = LayoutTransform::translation(130.0, 200.0, 0.0);
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(13.0, 8.0, 1.0);
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(0.5, 0.5, 1.0)
+ .pre_translate(LayoutVector3D::new(124.0, 38.0, 0.0));
+ validate_convert(&xref);
+
+ let xref = LayoutTransform::scale(30.0, 11.0, 1.0)
+ .then_translate(vec3(50.0, 240.0, 0.0));
+ validate_convert(&xref);
+ }
+
+ fn validate_inverse(xref: &LayoutTransform) {
+ let s0 = ScaleOffset::from_transform(xref).unwrap();
+ let s1 = s0.inverse().accumulate(&s0);
+ assert!((s1.scale.x - 1.0).abs() < NEARLY_ZERO &&
+ (s1.scale.y - 1.0).abs() < NEARLY_ZERO &&
+ s1.offset.x.abs() < NEARLY_ZERO &&
+ s1.offset.y.abs() < NEARLY_ZERO,
+ "{:?}",
+ s1);
+ }
+
+ #[test]
+ fn scale_offset_inverse() {
+ let xref = LayoutTransform::translation(130.0, 200.0, 0.0);
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::scale(13.0, 8.0, 1.0);
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::translation(124.0, 38.0, 0.0).
+ then_scale(0.5, 0.5, 1.0);
+
+ validate_inverse(&xref);
+
+ let xref = LayoutTransform::scale(30.0, 11.0, 1.0)
+ .then_translate(vec3(50.0, 240.0, 0.0));
+ validate_inverse(&xref);
+ }
+
+ fn validate_accumulate(x0: &LayoutTransform, x1: &LayoutTransform) {
+ let x = x1.then(&x0);
+
+ let s0 = ScaleOffset::from_transform(x0).unwrap();
+ let s1 = ScaleOffset::from_transform(x1).unwrap();
+
+ let s = s0.accumulate(&s1).to_transform();
+
+ assert!(x.approx_eq(&s), "{:?}\n{:?}", x, s);
+ }
+
+ #[test]
+ fn scale_offset_accumulate() {
+ let x0 = LayoutTransform::translation(130.0, 200.0, 0.0);
+ let x1 = LayoutTransform::scale(7.0, 3.0, 1.0);
+
+ validate_accumulate(&x0, &x1);
+ }
+
+ #[test]
+ fn inverse_project_2d_origin() {
+ let mut m = Transform3D::identity();
+ assert_eq!(m.inverse_project_2d_origin(), Some(Point2D::zero()));
+ m.m11 = 0.0;
+ assert_eq!(m.inverse_project_2d_origin(), None);
+ m.m21 = -2.0;
+ m.m22 = 0.0;
+ m.m12 = -0.5;
+ m.m41 = 1.0;
+ m.m42 = 0.5;
+ let origin = m.inverse_project_2d_origin().unwrap();
+ assert_eq!(origin, Point2D::new(1.0, 0.5));
+ assert_eq!(m.transform_point2d(origin), Some(Point2D::zero()));
+ }
+}
+
+pub trait MaxRect {
+ fn max_rect() -> Self;
+}
+
+impl MaxRect for DeviceIntRect {
+ fn max_rect() -> Self {
+ DeviceIntRect::new(
+ DeviceIntPoint::new(i32::MIN / 2, i32::MIN / 2),
+ DeviceIntSize::new(i32::MAX, i32::MAX),
+ )
+ }
+}
+
+impl<U> MaxRect for Rect<f32, U> {
+ fn max_rect() -> Self {
+ // Having an unlimited bounding box is fine up until we try
+ // to cast it to `i32`, where we get `-2147483648` for any
+ // values larger than or equal to 2^31.
+ //
+ // Note: clamping to i32::MIN and i32::MAX is not a solution,
+ // with explanation left as an exercise for the reader.
+ const MAX_COORD: f32 = 1.0e9;
+
+ Rect::new(
+ Point2D::new(-MAX_COORD, -MAX_COORD),
+ Size2D::new(2.0 * MAX_COORD, 2.0 * MAX_COORD),
+ )
+ }
+}
+
+/// An enum that tries to avoid expensive transformation matrix calculations
+/// when possible when dealing with non-perspective axis-aligned transformations.
+#[derive(Debug, MallocSizeOf)]
+pub enum FastTransform<Src, Dst> {
+ /// A simple offset, which can be used without doing any matrix math.
+ Offset(Vector2D<f32, Src>),
+
+ /// A 2D transformation with an inverse.
+ Transform {
+ transform: Transform3D<f32, Src, Dst>,
+ inverse: Option<Transform3D<f32, Dst, Src>>,
+ is_2d: bool,
+ },
+}
+
+impl<Src, Dst> Clone for FastTransform<Src, Dst> {
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<Src, Dst> Copy for FastTransform<Src, Dst> { }
+
+impl<Src, Dst> FastTransform<Src, Dst> {
+ pub fn identity() -> Self {
+ FastTransform::Offset(Vector2D::zero())
+ }
+
+ pub fn with_vector(offset: Vector2D<f32, Src>) -> Self {
+ FastTransform::Offset(offset)
+ }
+
+ pub fn with_scale_offset(scale_offset: ScaleOffset) -> Self {
+ if scale_offset.scale == Vector2D::new(1.0, 1.0) {
+ FastTransform::Offset(Vector2D::from_untyped(scale_offset.offset))
+ } else {
+ FastTransform::Transform {
+ transform: scale_offset.to_transform(),
+ inverse: Some(scale_offset.inverse().to_transform()),
+ is_2d: true,
+ }
+ }
+ }
+
+ #[inline(always)]
+ pub fn with_transform(transform: Transform3D<f32, Src, Dst>) -> Self {
+ if transform.is_simple_2d_translation() {
+ return FastTransform::Offset(Vector2D::new(transform.m41, transform.m42));
+ }
+ let inverse = transform.inverse();
+ let is_2d = transform.is_2d();
+ FastTransform::Transform { transform, inverse, is_2d}
+ }
+
+ pub fn to_transform(&self) -> Cow<Transform3D<f32, Src, Dst>> {
+ match *self {
+ FastTransform::Offset(offset) => Cow::Owned(
+ Transform3D::translation(offset.x, offset.y, 0.0)
+ ),
+ FastTransform::Transform { ref transform, .. } => Cow::Borrowed(transform),
+ }
+ }
+
+ /// Return true if this is an identity transform
+ #[allow(unused)]
+ pub fn is_identity(&self)-> bool {
+ match *self {
+ FastTransform::Offset(offset) => {
+ offset == Vector2D::zero()
+ }
+ FastTransform::Transform { ref transform, .. } => {
+ *transform == Transform3D::identity()
+ }
+ }
+ }
+
+ pub fn then<NewDst>(&self, other: &FastTransform<Dst, NewDst>) -> FastTransform<Src, NewDst> {
+ match *self {
+ FastTransform::Offset(offset) => match *other {
+ FastTransform::Offset(other_offset) => {
+ FastTransform::Offset(offset + other_offset * Scale::<_, _, Src>::new(1.0))
+ }
+ FastTransform::Transform { transform: ref other_transform, .. } => {
+ FastTransform::with_transform(
+ other_transform
+ .with_source::<Src>()
+ .pre_translate(offset.to_3d())
+ )
+ }
+ }
+ FastTransform::Transform { ref transform, ref inverse, is_2d } => match *other {
+ FastTransform::Offset(other_offset) => {
+ FastTransform::with_transform(
+ transform
+ .then_translate(other_offset.to_3d())
+ .with_destination::<NewDst>()
+ )
+ }
+ FastTransform::Transform { transform: ref other_transform, inverse: ref other_inverse, is_2d: other_is_2d } => {
+ FastTransform::Transform {
+ transform: transform.then(other_transform),
+ inverse: inverse.as_ref().and_then(|self_inv|
+ other_inverse.as_ref().map(|other_inv| other_inv.then(self_inv))
+ ),
+ is_2d: is_2d & other_is_2d,
+ }
+ }
+ }
+ }
+ }
+
+ pub fn pre_transform<NewSrc>(
+ &self,
+ other: &FastTransform<NewSrc, Src>
+ ) -> FastTransform<NewSrc, Dst> {
+ other.then(self)
+ }
+
+ pub fn pre_translate(&self, other_offset: Vector2D<f32, Src>) -> Self {
+ match *self {
+ FastTransform::Offset(offset) =>
+ FastTransform::Offset(offset + other_offset),
+ FastTransform::Transform { transform, .. } =>
+ FastTransform::with_transform(transform.pre_translate(other_offset.to_3d()))
+ }
+ }
+
+ pub fn then_translate(&self, other_offset: Vector2D<f32, Dst>) -> Self {
+ match *self {
+ FastTransform::Offset(offset) => {
+ FastTransform::Offset(offset + other_offset * Scale::<_, _, Src>::new(1.0))
+ }
+ FastTransform::Transform { ref transform, .. } => {
+ let transform = transform.then_translate(other_offset.to_3d());
+ FastTransform::with_transform(transform)
+ }
+ }
+ }
+
+ #[inline(always)]
+ pub fn is_backface_visible(&self) -> bool {
+ match *self {
+ FastTransform::Offset(..) => false,
+ FastTransform::Transform { inverse: None, .. } => false,
+ //TODO: fix this properly by taking "det|M33| * det|M34| > 0"
+ // see https://www.w3.org/Bugs/Public/show_bug.cgi?id=23014
+ FastTransform::Transform { inverse: Some(ref inverse), .. } => inverse.m33 < 0.0,
+ }
+ }
+
+ #[inline(always)]
+ pub fn transform_point2d(&self, point: Point2D<f32, Src>) -> Option<Point2D<f32, Dst>> {
+ match *self {
+ FastTransform::Offset(offset) => {
+ let new_point = point + offset;
+ Some(Point2D::from_untyped(new_point.to_untyped()))
+ }
+ FastTransform::Transform { ref transform, .. } => transform.transform_point2d(point),
+ }
+ }
+
+ #[inline(always)]
+ pub fn inverse(&self) -> Option<FastTransform<Dst, Src>> {
+ match *self {
+ FastTransform::Offset(offset) =>
+ Some(FastTransform::Offset(Vector2D::new(-offset.x, -offset.y))),
+ FastTransform::Transform { transform, inverse: Some(inverse), is_2d, } =>
+ Some(FastTransform::Transform {
+ transform: inverse,
+ inverse: Some(transform),
+ is_2d
+ }),
+ FastTransform::Transform { inverse: None, .. } => None,
+
+ }
+ }
+}
+
+impl<Src, Dst> From<Transform3D<f32, Src, Dst>> for FastTransform<Src, Dst> {
+ fn from(transform: Transform3D<f32, Src, Dst>) -> Self {
+ FastTransform::with_transform(transform)
+ }
+}
+
+impl<Src, Dst> From<Vector2D<f32, Src>> for FastTransform<Src, Dst> {
+ fn from(vector: Vector2D<f32, Src>) -> Self {
+ FastTransform::with_vector(vector)
+ }
+}
+
+pub type LayoutFastTransform = FastTransform<LayoutPixel, LayoutPixel>;
+pub type LayoutToWorldFastTransform = FastTransform<LayoutPixel, WorldPixel>;
+
+pub fn project_rect<F, T>(
+ transform: &Transform3D<f32, F, T>,
+ rect: &Rect<f32, F>,
+ bounds: &Rect<f32, T>,
+) -> Option<Rect<f32, T>>
+ where F: fmt::Debug
+{
+ let homogens = [
+ transform.transform_point2d_homogeneous(rect.origin),
+ transform.transform_point2d_homogeneous(rect.top_right()),
+ transform.transform_point2d_homogeneous(rect.bottom_left()),
+ transform.transform_point2d_homogeneous(rect.bottom_right()),
+ ];
+
+ // Note: we only do the full frustum collision when the polygon approaches the camera plane.
+ // Otherwise, it will be clamped to the screen bounds anyway.
+ if homogens.iter().any(|h| h.w <= 0.0 || h.w.is_nan()) {
+ let mut clipper = Clipper::new();
+ let polygon = Polygon::from_rect(*rect, 1);
+
+ let planes = match Clipper::<_, _, usize>::frustum_planes(
+ transform,
+ Some(*bounds),
+ ) {
+ Ok(planes) => planes,
+ Err(..) => return None,
+ };
+
+ for plane in planes {
+ clipper.add(plane);
+ }
+
+ let results = clipper.clip(polygon);
+ if results.is_empty() {
+ return None
+ }
+
+ Some(Rect::from_points(results
+ .into_iter()
+ // filter out parts behind the view plane
+ .flat_map(|poly| &poly.points)
+ .map(|p| {
+ let mut homo = transform.transform_point2d_homogeneous(p.to_2d());
+ homo.w = homo.w.max(0.00000001); // avoid infinite values
+ homo.to_point2d().unwrap()
+ })
+ ))
+ } else {
+ // we just checked for all the points to be in positive hemisphere, so `unwrap` is valid
+ Some(Rect::from_points(&[
+ homogens[0].to_point2d().unwrap(),
+ homogens[1].to_point2d().unwrap(),
+ homogens[2].to_point2d().unwrap(),
+ homogens[3].to_point2d().unwrap(),
+ ]))
+ }
+}
+
+pub fn raster_rect_to_device_pixels(
+ rect: RasterRect,
+ device_pixel_scale: DevicePixelScale,
+) -> DeviceRect {
+ let world_rect = rect * Scale::new(1.0);
+ let device_rect = world_rect * device_pixel_scale;
+ device_rect.round_out()
+}
+
+/// Run the first callback over all elements in the array. If the callback returns true,
+/// the element is removed from the array and moved to a second callback.
+///
+/// This is a simple implementation waiting for Vec::drain_filter to be stable.
+/// When that happens, code like:
+///
+/// let filter = |op| {
+/// match *op {
+/// Enum::Foo | Enum::Bar => true,
+/// Enum::Baz => false,
+/// }
+/// };
+/// drain_filter(
+/// &mut ops,
+/// filter,
+/// |op| {
+/// match op {
+/// Enum::Foo => { foo(); }
+/// Enum::Bar => { bar(); }
+/// Enum::Baz => { unreachable!(); }
+/// }
+/// },
+/// );
+///
+/// Can be rewritten as:
+///
+/// let filter = |op| {
+/// match *op {
+/// Enum::Foo | Enum::Bar => true,
+/// Enum::Baz => false,
+/// }
+/// };
+/// for op in ops.drain_filter(filter) {
+/// match op {
+/// Enum::Foo => { foo(); }
+/// Enum::Bar => { bar(); }
+/// Enum::Baz => { unreachable!(); }
+/// }
+/// }
+///
+/// See https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain_filter
+pub fn drain_filter<T, Filter, Action>(
+ vec: &mut Vec<T>,
+ mut filter: Filter,
+ mut action: Action,
+)
+where
+ Filter: FnMut(&mut T) -> bool,
+ Action: FnMut(T)
+{
+ let mut i = 0;
+ while i != vec.len() {
+ if filter(&mut vec[i]) {
+ action(vec.remove(i));
+ } else {
+ i += 1;
+ }
+ }
+}
+
+
+#[derive(Debug)]
+pub struct Recycler {
+ pub num_allocations: usize,
+}
+
+impl Recycler {
+ /// Maximum extra capacity that a recycled vector is allowed to have. If the actual capacity
+ /// is larger, we re-allocate the vector storage with lower capacity.
+ const MAX_EXTRA_CAPACITY_PERCENT: usize = 200;
+ /// Minimum extra capacity to keep when re-allocating the vector storage.
+ const MIN_EXTRA_CAPACITY_PERCENT: usize = 20;
+ /// Minimum sensible vector length to consider for re-allocation.
+ const MIN_VECTOR_LENGTH: usize = 16;
+
+ pub fn new() -> Self {
+ Recycler {
+ num_allocations: 0,
+ }
+ }
+
+ /// Clear a vector for re-use, while retaining the backing memory buffer. May shrink the buffer
+ /// if it's currently much larger than was actually used.
+ pub fn recycle_vec<T>(&mut self, vec: &mut Vec<T>) {
+ let extra_capacity = (vec.capacity() - vec.len()) * 100 / vec.len().max(Self::MIN_VECTOR_LENGTH);
+
+ if extra_capacity > Self::MAX_EXTRA_CAPACITY_PERCENT {
+ // Reduce capacity of the buffer if it is a lot larger than it needs to be. This prevents
+ // a frame with exceptionally large allocations to cause subsequent frames to retain
+ // more memory than they need.
+ //TODO: use `shrink_to` when it's stable
+ *vec = Vec::with_capacity(vec.len() + vec.len() * Self::MIN_EXTRA_CAPACITY_PERCENT / 100);
+ self.num_allocations += 1;
+ } else {
+ vec.clear();
+ }
+ }
+}
+
+/// Record the size of a data structure to preallocate a similar size
+/// at the next frame and avoid growing it too many time.
+#[derive(Copy, Clone, Debug)]
+pub struct Preallocator {
+ size: usize,
+}
+
+impl Preallocator {
+ pub fn new(initial_size: usize) -> Self {
+ Preallocator {
+ size: initial_size,
+ }
+ }
+
+ /// Record the size of a vector to preallocate it the next frame.
+ pub fn record_vec<T>(&mut self, vec: &Vec<T>) {
+ let len = vec.len();
+ if len > self.size {
+ self.size = len;
+ } else {
+ self.size = (self.size + len) / 2;
+ }
+ }
+
+ /// The size that we'll preallocate the vector with.
+ pub fn preallocation_size(&self) -> usize {
+ // Round up to multiple of 16 to avoid small tiny
+ // variations causing reallocations.
+ (self.size + 15) & !15
+ }
+
+ /// Preallocate vector storage.
+ ///
+ /// The preallocated amount depends on the length recorded in the last
+ /// record_vec call.
+ pub fn preallocate_vec<T>(&self, vec: &mut Vec<T>) {
+ let len = vec.len();
+ let cap = self.preallocation_size();
+ if len < cap {
+ vec.reserve(cap - len);
+ }
+ }
+}
+
+impl Default for Preallocator {
+ fn default() -> Self {
+ Self::new(0)
+ }
+}
+
+/// Arc wrapper to support measurement via MallocSizeOf.
+///
+/// Memory reporting for Arcs is tricky because of the risk of double-counting.
+/// One way to measure them is to keep a table of pointers that have already been
+/// traversed. The other way is to use knowledge of the program structure to
+/// identify which Arc instances should be measured and which should be skipped to
+/// avoid double-counting.
+///
+/// This struct implements the second approach. It identifies the "main" pointer
+/// to the Arc-ed resource, and measures the buffer as if it were an owned pointer.
+/// The programmer should ensure that there is at most one PrimaryArc for a given
+/// underlying ArcInner.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct PrimaryArc<T>(pub Arc<T>);
+
+impl<T> ::std::ops::Deref for PrimaryArc<T> {
+ type Target = Arc<T>;
+
+ #[inline]
+ fn deref(&self) -> &Arc<T> {
+ &self.0
+ }
+}
+
+impl<T> MallocShallowSizeOf for PrimaryArc<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe {
+ // This is a bit sketchy, but std::sync::Arc doesn't expose the
+ // base pointer.
+ let raw_arc_ptr: *const Arc<T> = &self.0;
+ let raw_ptr_ptr: *const *const c_void = raw_arc_ptr as _;
+ let raw_ptr = *raw_ptr_ptr;
+ (ops.size_of_op)(raw_ptr)
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for PrimaryArc<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+/// Computes the scale factors of this matrix; that is,
+/// the amounts each basis vector is scaled by.
+///
+/// This code comes from gecko gfx/2d/Matrix.h with the following
+/// modifications:
+///
+/// * Removed `xMajor` parameter.
+pub fn scale_factors<Src, Dst>(
+ mat: &Transform3D<f32, Src, Dst>
+) -> (f32, f32) {
+ // Determinant is just of the 2D component.
+ let det = mat.m11 * mat.m22 - mat.m12 * mat.m21;
+ if det == 0.0 {
+ return (0.0, 0.0);
+ }
+
+ // ignore mirroring
+ let det = det.abs();
+
+ let major = (mat.m11 * mat.m11 + mat.m12 * mat.m12).sqrt();
+ let minor = if major != 0.0 { det / major } else { 0.0 };
+
+ (major, minor)
+}
+
+/// Clamp scaling factor to a power of two.
+///
+/// This code comes from gecko gfx/thebes/gfxUtils.cpp with the following
+/// modification:
+///
+/// * logs are taken in base 2 instead of base e.
+pub fn clamp_to_scale_factor(val: f32, round_down: bool) -> f32 {
+ // Arbitary scale factor limitation. We can increase this
+ // for better scaling performance at the cost of worse
+ // quality.
+ const SCALE_RESOLUTION: f32 = 2.0;
+
+ // Negative scaling is just a flip and irrelevant to
+ // our resolution calculation.
+ let val = val.abs();
+
+ let (val, inverse) = if val < 1.0 {
+ (1.0 / val, true)
+ } else {
+ (val, false)
+ };
+
+ let power = val.log2() / SCALE_RESOLUTION.log2();
+
+ // If power is within 1e-5 of an integer, round to nearest to
+ // prevent floating point errors, otherwise round up to the
+ // next integer value.
+ let power = if (power - power.round()).abs() < 1e-5 {
+ power.round()
+ } else if inverse != round_down {
+ // Use floor when we are either inverted or rounding down, but
+ // not both.
+ power.floor()
+ } else {
+ // Otherwise, ceil when we are not inverted and not rounding
+ // down, or we are inverted and rounding down.
+ power.ceil()
+ };
+
+ let scale = SCALE_RESOLUTION.powf(power);
+
+ if inverse {
+ 1.0 / scale
+ } else {
+ scale
+ }
+}
+
+/// Rounds a value up to the nearest multiple of mul
+pub fn round_up_to_multiple(val: usize, mul: NonZeroUsize) -> usize {
+ match val % mul.get() {
+ 0 => val,
+ rem => val - rem + mul.get(),
+ }
+}
+
+
+#[macro_export]
+macro_rules! c_str {
+ ($lit:expr) => {
+ unsafe {
+ std::ffi::CStr::from_ptr(concat!($lit, "\0").as_ptr()
+ as *const std::os::raw::c_char)
+ }
+ }
+}
+
+// Find a rectangle that is contained by the sum of r1 and r2.
+pub fn conservative_union_rect<U>(r1: &Rect<f32, U>, r2: &Rect<f32, U>) -> Rect<f32, U> {
+ // +---+---+ +--+-+--+
+ // | | | | | | |
+ // | | | | | | |
+ // +---+---+ +--+-+--+
+ if r1.origin.y == r2.origin.y && r1.size.height == r2.size.height {
+ if r2.min_x() <= r1.max_x() && r2.max_x() >= r1.min_x() {
+ let origin_x = f32::min(r1.origin.x, r2.origin.x);
+ let width = f32::max(r1.max_x(), r2.max_x()) - origin_x;
+
+ return Rect {
+ origin: point2(origin_x, r1.origin.y),
+ size: size2(width, r1.size.height),
+ }
+ }
+ }
+
+ // +----+ +----+
+ // | | | |
+ // | | +----+
+ // +----+ | |
+ // | | +----+
+ // | | | |
+ // +----+ +----+
+ if r1.origin.x == r2.origin.x && r1.size.width == r2.size.width {
+ if r2.min_y() <= r1.max_y() && r2.max_y() >= r1.min_y() {
+ let origin_y = f32::min(r1.origin.y, r2.origin.y);
+ let height = f32::max(r1.max_y(), r2.max_y()) - origin_y;
+
+ return Rect {
+ origin: point2(r1.origin.x, origin_y),
+ size: size2(r1.size.width, height),
+ }
+ }
+ }
+
+ if r1.area() >= r2.area() { *r1 } else {*r2 }
+}
+
+#[test]
+fn test_conservative_union_rect() {
+ // Adjacent, x axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(4.0, 2.0), size: size2(5.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(8.0, 4.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(4.0, 2.0), size: size2(5.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(8.0, 4.0) });
+
+ // Averlapping adjacent, x axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(3.0, 2.0), size: size2(5.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(7.0, 4.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(5.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(5.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(7.0, 4.0) });
+
+ // Adjacent but not touching, x axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(6.0, 2.0), size: size2(5.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(6.0, 2.0), size: size2(5.0, 4.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(-6.0, 2.0), size: size2(1.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) });
+
+
+ // Adjacent, y axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 6.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 8.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 5.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 1.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 1.0), size: size2(3.0, 8.0) });
+
+ // Averlapping adjacent, y axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 3.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 5.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 4.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 6.0) });
+
+ // Adjacent but not touching, y axis
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 10.0), size: size2(3.0, 5.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 10.0), size: size2(3.0, 5.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 5.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(1.0, 0.0), size: size2(3.0, 3.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(1.0, 5.0), size: size2(3.0, 4.0) });
+
+
+ // Contained
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ &LayoutRect { origin: point2(0.0, 1.0), size: size2(10.0, 11.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(0.0, 1.0), size: size2(10.0, 11.0) });
+
+ let r = conservative_union_rect(
+ &LayoutRect { origin: point2(0.0, 1.0), size: size2(10.0, 11.0) },
+ &LayoutRect { origin: point2(1.0, 2.0), size: size2(3.0, 4.0) },
+ );
+ assert_eq!(r, LayoutRect { origin: point2(0.0, 1.0), size: size2(10.0, 11.0) });
+}
+
+/// This is inspired by the `weak-table` crate.
+/// It holds a Vec of weak pointers that are garbage collected as the Vec
+pub struct WeakTable {
+ inner: Vec<std::sync::Weak<Vec<u8>>>
+}
+
+impl WeakTable {
+ pub fn new() -> WeakTable {
+ WeakTable { inner: Vec::new() }
+ }
+ pub fn insert(&mut self, x: std::sync::Weak<Vec<u8>>) {
+ if self.inner.len() == self.inner.capacity() {
+ self.remove_expired();
+
+ // We want to make sure that we change capacity()
+ // even if remove_expired() removes some entries
+ // so that we don't repeatedly hit remove_expired()
+ if self.inner.len() * 3 < self.inner.capacity() {
+ // We use a different multiple for shrinking then
+ // expanding so that we we don't accidentally
+ // oscilate.
+ self.inner.shrink_to_fit();
+ } else {
+ // Otherwise double our size
+ self.inner.reserve(self.inner.len())
+ }
+ }
+ self.inner.push(x);
+ }
+
+ fn remove_expired(&mut self) {
+ self.inner.retain(|x| x.strong_count() > 0)
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = Arc<Vec<u8>>> + '_ {
+ self.inner.iter().filter_map(|x| x.upgrade())
+ }
+}
+
+#[test]
+fn weak_table() {
+ let mut tbl = WeakTable::new();
+ let mut things = Vec::new();
+ let target_count = 50;
+ for _ in 0..target_count {
+ things.push(Arc::new(vec![4]));
+ }
+ for i in &things {
+ tbl.insert(Arc::downgrade(i))
+ }
+ assert_eq!(tbl.inner.len(), target_count);
+ drop(things);
+ assert_eq!(tbl.iter().count(), 0);
+
+ // make sure that we shrink the table if it gets too big
+ // by adding a bunch of dead items
+ for _ in 0..target_count*2 {
+ tbl.insert(Arc::downgrade(&Arc::new(vec![5])))
+ }
+ assert!(tbl.inner.capacity() <= 4);
+}
diff --git a/gfx/wr/webrender/src/visibility.rs b/gfx/wr/webrender/src/visibility.rs
new file mode 100644
index 0000000000..8d225682ff
--- /dev/null
+++ b/gfx/wr/webrender/src/visibility.rs
@@ -0,0 +1,922 @@
+/* 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/. */
+
+//! # Visibility pass
+//!
+//! TODO: document what this pass does!
+//!
+
+use api::{ColorF, DebugFlags};
+use api::units::*;
+use euclid::Scale;
+use std::{usize, mem};
+use crate::image_tiling;
+use crate::segment::EdgeAaSegmentMask;
+use crate::clip::{ClipStore, ClipChainStack};
+use crate::composite::CompositeState;
+use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialTree, SpatialNodeIndex};
+use crate::clip::{ClipInstance, ClipChainInstance};
+use crate::debug_colors;
+use crate::frame_builder::FrameBuilderConfig;
+use crate::gpu_cache::GpuCache;
+use crate::internal_types::FastHashMap;
+use crate::picture::{PictureCompositeMode, ClusterFlags, SurfaceInfo, TileCacheInstance};
+use crate::picture::{PrimitiveList, SurfaceIndex, RasterConfig, SliceId};
+use crate::prim_store::{ClipTaskIndex, PictureIndex, PrimitiveInstanceKind};
+use crate::prim_store::{PrimitiveStore, PrimitiveInstance};
+use crate::prim_store::image::VisibleImageTile;
+use crate::render_backend::{DataStores, ScratchBuffer};
+use crate::resource_cache::{ResourceCache, ImageProperties, ImageRequest};
+use crate::scene::SceneProperties;
+use crate::space::{SpaceMapper, SpaceSnapper};
+use crate::internal_types::Filter;
+use crate::util::{MaxRect};
+
+pub struct FrameVisibilityContext<'a> {
+ pub spatial_tree: &'a SpatialTree,
+ pub global_screen_world_rect: WorldRect,
+ pub global_device_pixel_scale: DevicePixelScale,
+ pub surfaces: &'a [SurfaceInfo],
+ pub debug_flags: DebugFlags,
+ pub scene_properties: &'a SceneProperties,
+ pub config: FrameBuilderConfig,
+}
+
+pub struct FrameVisibilityState<'a> {
+ pub clip_store: &'a mut ClipStore,
+ pub resource_cache: &'a mut ResourceCache,
+ pub gpu_cache: &'a mut GpuCache,
+ pub scratch: &'a mut ScratchBuffer,
+ pub tile_cache: Option<Box<TileCacheInstance>>,
+ pub data_stores: &'a mut DataStores,
+ pub clip_chain_stack: ClipChainStack,
+ pub composite_state: &'a mut CompositeState,
+ /// A stack of currently active off-screen surfaces during the
+ /// visibility frame traversal.
+ pub surface_stack: Vec<SurfaceIndex>,
+}
+
+impl<'a> FrameVisibilityState<'a> {
+ pub fn push_surface(
+ &mut self,
+ surface_index: SurfaceIndex,
+ shared_clips: &[ClipInstance],
+ spatial_tree: &SpatialTree,
+ ) {
+ self.surface_stack.push(surface_index);
+ self.clip_chain_stack.push_surface(shared_clips, spatial_tree);
+ }
+
+ pub fn pop_surface(&mut self) {
+ self.surface_stack.pop().unwrap();
+ self.clip_chain_stack.pop_surface();
+ }
+}
+
+/// A bit mask describing which dirty regions a primitive is visible in.
+/// A value of 0 means not visible in any region, while a mask of 0xffff
+/// would be considered visible in all regions.
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct PrimitiveVisibilityMask {
+ bits: u16,
+}
+
+impl PrimitiveVisibilityMask {
+ /// Construct a default mask, where no regions are considered visible
+ pub fn empty() -> Self {
+ PrimitiveVisibilityMask {
+ bits: 0,
+ }
+ }
+
+ pub fn all() -> Self {
+ PrimitiveVisibilityMask {
+ bits: !0,
+ }
+ }
+
+ pub fn include(&mut self, other: PrimitiveVisibilityMask) {
+ self.bits |= other.bits;
+ }
+
+ pub fn intersects(&self, other: PrimitiveVisibilityMask) -> bool {
+ (self.bits & other.bits) != 0
+ }
+
+ /// Mark a given region index as visible
+ pub fn set_visible(&mut self, region_index: usize) {
+ debug_assert!(region_index < PrimitiveVisibilityMask::MAX_DIRTY_REGIONS);
+ self.bits |= 1 << region_index;
+ }
+
+ /// Returns true if there are no visible regions
+ pub fn is_empty(&self) -> bool {
+ self.bits == 0
+ }
+
+ /// The maximum number of supported dirty regions.
+ pub const MAX_DIRTY_REGIONS: usize = 8 * mem::size_of::<PrimitiveVisibilityMask>();
+}
+
+bitflags! {
+ /// A set of bitflags that can be set in the visibility information
+ /// for a primitive instance. This can be used to control how primitives
+ /// are treated during batching.
+ // TODO(gw): We should also move `is_compositor_surface` to be part of
+ // this flags struct.
+ #[cfg_attr(feature = "capture", derive(Serialize))]
+ pub struct PrimitiveVisibilityFlags: u16 {
+ /// Implies that this primitive covers the entire picture cache slice,
+ /// and can thus be dropped during batching and drawn with clear color.
+ const IS_BACKDROP = 1;
+ }
+}
+
+/// Contains the current state of the primitive's visibility.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub enum VisibilityState {
+ /// Uninitialized - this should never be encountered after prim reset
+ Unset,
+ /// Culled for being off-screen, or not possible to render (e.g. missing image resource)
+ Culled,
+ /// During picture cache dependency update, was found to be intersecting with one
+ /// or more visible tiles. The rect in picture cache space is stored here to allow
+ /// the detailed calculations below.
+ Coarse {
+ rect_in_pic_space: PictureRect,
+ },
+ /// Once coarse visibility is resolved, this provides a bitmask of which dirty tiles
+ /// this primitive should be rasterized into.
+ Detailed {
+ /// A mask defining which of the dirty regions this primitive is visible in.
+ visibility_mask: PrimitiveVisibilityMask,
+ },
+}
+
+/// Information stored for a visible primitive about the visible
+/// rect and associated clip information.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+pub struct PrimitiveVisibility {
+ /// The clip chain instance that was built for this primitive.
+ pub clip_chain: ClipChainInstance,
+
+ /// Current visibility state of the primitive.
+ // TODO(gw): Move more of the fields from this struct into
+ // the state enum.
+ pub state: VisibilityState,
+
+ /// An index into the clip task instances array in the primitive
+ /// store. If this is ClipTaskIndex::INVALID, then the primitive
+ /// has no clip mask. Otherwise, it may store the offset of the
+ /// global clip mask task for this primitive, or the first of
+ /// a list of clip task ids (one per segment).
+ pub clip_task_index: ClipTaskIndex,
+
+ /// A set of flags that define how this primitive should be handled
+ /// during batching of visibile primitives.
+ pub flags: PrimitiveVisibilityFlags,
+
+ /// The current combined local clip for this primitive, from
+ /// the primitive local clip above and the current clip chain.
+ pub combined_local_clip_rect: LayoutRect,
+}
+
+impl PrimitiveVisibility {
+ pub fn new() -> Self {
+ PrimitiveVisibility {
+ state: VisibilityState::Unset,
+ clip_chain: ClipChainInstance::empty(),
+ clip_task_index: ClipTaskIndex::INVALID,
+ flags: PrimitiveVisibilityFlags::empty(),
+ combined_local_clip_rect: LayoutRect::zero(),
+ }
+ }
+
+ pub fn reset(&mut self) {
+ self.state = VisibilityState::Culled;
+ self.clip_task_index = ClipTaskIndex::INVALID;
+ self.flags = PrimitiveVisibilityFlags::empty();
+ }
+}
+
+/// Update visibility pass - update each primitive visibility struct, and
+/// build the clip chain instance if appropriate.
+pub fn update_primitive_visibility(
+ store: &mut PrimitiveStore,
+ pic_index: PictureIndex,
+ parent_surface_index: SurfaceIndex,
+ world_culling_rect: &WorldRect,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
+) -> Option<PictureRect> {
+ profile_scope!("update_visibility");
+ let (mut prim_list, surface_index, apply_local_clip_rect, world_culling_rect, is_composite) = {
+ let pic = &mut store.pictures[pic_index.0];
+ let mut world_culling_rect = *world_culling_rect;
+
+ let prim_list = mem::replace(&mut pic.prim_list, PrimitiveList::empty());
+ let (surface_index, is_composite) = match pic.raster_config {
+ Some(ref raster_config) => (raster_config.surface_index, true),
+ None => (parent_surface_index, false)
+ };
+
+ match pic.raster_config {
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
+ let mut tile_cache = tile_caches
+ .remove(&slice_id)
+ .expect("bug: non-existent tile cache");
+
+ // If we have a tile cache for this picture, see if any of the
+ // relative transforms have changed, which means we need to
+ // re-map the dependencies of any child primitives.
+ world_culling_rect = tile_cache.pre_update(
+ layout_rect_as_picture_rect(&pic.estimated_local_rect),
+ surface_index,
+ frame_context,
+ frame_state,
+ );
+
+ // Push a new surface, supplying the list of clips that should be
+ // ignored, since they are handled by clipping when drawing this surface.
+ frame_state.push_surface(
+ surface_index,
+ &tile_cache.shared_clips,
+ frame_context.spatial_tree,
+ );
+ frame_state.tile_cache = Some(tile_cache);
+ }
+ _ => {
+ if is_composite {
+ frame_state.push_surface(
+ surface_index,
+ &[],
+ frame_context.spatial_tree,
+ );
+ }
+ }
+ }
+
+ (prim_list, surface_index, pic.apply_local_clip_rect, world_culling_rect, is_composite)
+ };
+
+ let surface = &frame_context.surfaces[surface_index.0 as usize];
+
+ let mut map_local_to_surface = surface
+ .map_local_to_surface
+ .clone();
+
+ let map_surface_to_world = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ surface.surface_spatial_node_index,
+ frame_context.global_screen_world_rect,
+ frame_context.spatial_tree,
+ );
+
+ let mut surface_rect = PictureRect::zero();
+
+ for cluster in &mut prim_list.clusters {
+ profile_scope!("cluster");
+ // Get the cluster and see if is visible
+ if !cluster.flags.contains(ClusterFlags::IS_VISIBLE) {
+ // Each prim instance must have reset called each frame, to clear
+ // indices into various scratch buffers. If this doesn't occur,
+ // the primitive may incorrectly be considered visible, which can
+ // cause unexpected conditions to occur later during the frame.
+ // Primitive instances are normally reset in the main loop below,
+ // but we must also reset them in the rare case that the cluster
+ // visibility has changed (due to an invalid transform and/or
+ // backface visibility changing for this cluster).
+ // TODO(gw): This is difficult to test for in CI - as a follow up,
+ // we should add a debug flag that validates the prim
+ // instance is always reset every frame to catch similar
+ // issues in future.
+ for prim_instance in &mut prim_list.prim_instances[cluster.prim_range()] {
+ prim_instance.reset();
+ }
+ continue;
+ }
+
+ map_local_to_surface.set_target_spatial_node(
+ cluster.spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ for prim_instance in &mut prim_list.prim_instances[cluster.prim_range()] {
+ prim_instance.reset();
+
+ if prim_instance.is_chased() {
+ #[cfg(debug_assertions)] // needed for ".id" part
+ println!("\tpreparing {:?} in {:?}", prim_instance.id, pic_index);
+ println!("\t{:?}", prim_instance.kind);
+ }
+
+ let (is_passthrough, prim_local_rect, prim_shadowed_rect) = match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ let (is_visible, is_passthrough) = {
+ let pic = &store.pictures[pic_index.0];
+ (pic.is_visible(), pic.raster_config.is_none())
+ };
+
+ if !is_visible {
+ continue;
+ }
+
+ if is_passthrough {
+ frame_state.clip_chain_stack.push_clip(
+ prim_instance.clip_set.clip_chain_id,
+ frame_state.clip_store,
+ );
+ }
+
+ let pic_surface_rect = update_primitive_visibility(
+ store,
+ pic_index,
+ surface_index,
+ &world_culling_rect,
+ frame_context,
+ frame_state,
+ tile_caches,
+ );
+
+ if is_passthrough {
+ frame_state.clip_chain_stack.pop_clip();
+ }
+
+ let pic = &store.pictures[pic_index.0];
+
+ if prim_instance.is_chased() && pic.estimated_local_rect != pic.precise_local_rect {
+ println!("\testimate {:?} adjusted to {:?}", pic.estimated_local_rect, pic.precise_local_rect);
+ }
+
+ let mut shadow_rect = pic.precise_local_rect;
+ match pic.raster_config {
+ Some(ref rc) => match rc.composite_mode {
+ // If we have a drop shadow filter, we also need to include the shadow in
+ // our shadowed local rect for the purpose of calculating the size of the
+ // picture.
+ PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => {
+ for shadow in shadows {
+ shadow_rect = shadow_rect.union(&pic.precise_local_rect.translate(shadow.offset));
+ }
+ }
+ _ => {}
+ }
+ None => {
+ // If the primitive does not have its own raster config, we need to
+ // propogate the surface rect calculation to the parent.
+ if let Some(ref rect) = pic_surface_rect {
+ surface_rect = surface_rect.union(rect);
+ }
+ }
+ }
+
+ (is_passthrough, pic.precise_local_rect, shadow_rect)
+ }
+ _ => {
+ let prim_data = &frame_state.data_stores.as_common_data(&prim_instance);
+
+ (false, prim_data.prim_rect, prim_data.prim_rect)
+ }
+ };
+
+ if is_passthrough {
+ // Pass through pictures are always considered visible in all dirty tiles.
+ prim_instance.vis.state = VisibilityState::Detailed {
+ visibility_mask: PrimitiveVisibilityMask::all(),
+ };
+ } else {
+ if prim_local_rect.size.width <= 0.0 || prim_local_rect.size.height <= 0.0 {
+ if prim_instance.is_chased() {
+ println!("\tculled for zero local rectangle");
+ }
+ continue;
+ }
+
+ // Inflate the local rect for this primitive by the inflation factor of
+ // the picture context and include the shadow offset. This ensures that
+ // even if the primitive itstore is not visible, any effects from the
+ // blur radius or shadow will be correctly taken into account.
+ let inflation_factor = surface.inflation_factor;
+ let local_rect = prim_shadowed_rect
+ .inflate(inflation_factor, inflation_factor)
+ .intersection(&prim_instance.clip_set.local_clip_rect);
+ let local_rect = match local_rect {
+ Some(local_rect) => local_rect,
+ None => {
+ if prim_instance.is_chased() {
+ println!("\tculled for being out of the local clip rectangle: {:?}",
+ prim_instance.clip_set.local_clip_rect);
+ }
+ continue;
+ }
+ };
+
+ // Include the clip chain for this primitive in the current stack.
+ frame_state.clip_chain_stack.push_clip(
+ prim_instance.clip_set.clip_chain_id,
+ frame_state.clip_store,
+ );
+
+ frame_state.clip_store.set_active_clips(
+ prim_instance.clip_set.local_clip_rect,
+ cluster.spatial_node_index,
+ map_local_to_surface.ref_spatial_node_index,
+ frame_state.clip_chain_stack.current_clips_array(),
+ &frame_context.spatial_tree,
+ &frame_state.data_stores.clip,
+ );
+
+ let clip_chain = frame_state
+ .clip_store
+ .build_clip_chain_instance(
+ local_rect,
+ &map_local_to_surface,
+ &map_surface_to_world,
+ &frame_context.spatial_tree,
+ frame_state.gpu_cache,
+ frame_state.resource_cache,
+ surface.device_pixel_scale,
+ &world_culling_rect,
+ &mut frame_state.data_stores.clip,
+ true,
+ prim_instance.is_chased(),
+ );
+
+ // Ensure the primitive clip is popped
+ frame_state.clip_chain_stack.pop_clip();
+
+ prim_instance.vis.clip_chain = match clip_chain {
+ Some(clip_chain) => clip_chain,
+ None => {
+ if prim_instance.is_chased() {
+ println!("\tunable to build the clip chain, skipping");
+ }
+ continue;
+ }
+ };
+
+ if prim_instance.is_chased() {
+ println!("\teffective clip chain from {:?} {}",
+ prim_instance.vis.clip_chain.clips_range,
+ if apply_local_clip_rect { "(applied)" } else { "" },
+ );
+ println!("\tpicture rect {:?} @{:?}",
+ prim_instance.vis.clip_chain.pic_clip_rect,
+ prim_instance.vis.clip_chain.pic_spatial_node_index,
+ );
+ }
+
+ prim_instance.vis.combined_local_clip_rect = if apply_local_clip_rect {
+ prim_instance.vis.clip_chain.local_clip_rect
+ } else {
+ prim_instance.clip_set.local_clip_rect
+ };
+
+ if prim_instance.vis.combined_local_clip_rect.size.is_empty() {
+ if prim_instance.is_chased() {
+ println!("\tculled for zero local clip rectangle");
+ }
+ continue;
+ }
+
+ // Include the visible area for primitive, including any shadows, in
+ // the area affected by the surface.
+ match prim_instance.vis.combined_local_clip_rect.intersection(&local_rect) {
+ Some(visible_rect) => {
+ if let Some(rect) = map_local_to_surface.map(&visible_rect) {
+ surface_rect = surface_rect.union(&rect);
+ }
+ }
+ None => {
+ if prim_instance.is_chased() {
+ println!("\tculled for zero visible rectangle");
+ }
+ continue;
+ }
+ }
+
+ match frame_state.tile_cache {
+ Some(ref mut tile_cache) => {
+ // TODO(gw): Refactor how tile_cache is stored in frame_state
+ // so that we can pass frame_state directly to
+ // update_prim_dependencies, rather than splitting borrows.
+ tile_cache.update_prim_dependencies(
+ prim_instance,
+ cluster.spatial_node_index,
+ prim_local_rect,
+ frame_context,
+ frame_state.data_stores,
+ frame_state.clip_store,
+ &store.pictures,
+ frame_state.resource_cache,
+ &store.color_bindings,
+ &frame_state.surface_stack,
+ &mut frame_state.composite_state,
+ );
+ }
+ None => {
+ // When picture cache is not in use, cull against the main world culling rect only.
+ let clipped_world_rect = calculate_prim_clipped_world_rect(
+ &prim_instance.vis.clip_chain.pic_clip_rect,
+ &world_culling_rect,
+ &map_surface_to_world,
+ );
+
+ prim_instance.vis.state = match clipped_world_rect {
+ Some(_) => {
+ VisibilityState::Detailed {
+ visibility_mask: PrimitiveVisibilityMask::all(),
+ }
+ }
+ None => {
+ VisibilityState::Culled
+ }
+ };
+ }
+ }
+
+ // Skip post visibility prim update if this primitive was culled above.
+ match prim_instance.vis.state {
+ VisibilityState::Unset => panic!("bug: invalid state"),
+ VisibilityState::Culled => continue,
+ VisibilityState::Coarse { .. } | VisibilityState::Detailed { .. } => {}
+ }
+
+ // When the debug display is enabled, paint a colored rectangle around each
+ // primitive.
+ if frame_context.debug_flags.contains(::api::DebugFlags::PRIMITIVE_DBG) {
+ let debug_color = match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { .. } => ColorF::TRANSPARENT,
+ PrimitiveInstanceKind::TextRun { .. } => debug_colors::RED,
+ PrimitiveInstanceKind::LineDecoration { .. } => debug_colors::PURPLE,
+ PrimitiveInstanceKind::NormalBorder { .. } |
+ PrimitiveInstanceKind::ImageBorder { .. } => debug_colors::ORANGE,
+ PrimitiveInstanceKind::Rectangle { .. } => ColorF { r: 0.8, g: 0.8, b: 0.8, a: 0.5 },
+ PrimitiveInstanceKind::YuvImage { .. } => debug_colors::BLUE,
+ PrimitiveInstanceKind::Image { .. } => debug_colors::BLUE,
+ PrimitiveInstanceKind::LinearGradient { .. } => debug_colors::PINK,
+ PrimitiveInstanceKind::RadialGradient { .. } => debug_colors::PINK,
+ PrimitiveInstanceKind::ConicGradient { .. } => debug_colors::PINK,
+ PrimitiveInstanceKind::Clear { .. } => debug_colors::CYAN,
+ PrimitiveInstanceKind::Backdrop { .. } => debug_colors::MEDIUMAQUAMARINE,
+ };
+ if debug_color.a != 0.0 {
+ if let Some(rect) = calculate_prim_clipped_world_rect(
+ &prim_instance.vis.clip_chain.pic_clip_rect,
+ &world_culling_rect,
+ &map_surface_to_world,
+ ) {
+ let debug_rect = rect * frame_context.global_device_pixel_scale;
+ frame_state.scratch.primitive.push_debug_rect(debug_rect, debug_color, debug_color.scale_alpha(0.5));
+ }
+ }
+ } else if frame_context.debug_flags.contains(::api::DebugFlags::OBSCURE_IMAGES) {
+ let is_image = matches!(
+ prim_instance.kind,
+ PrimitiveInstanceKind::Image { .. } | PrimitiveInstanceKind::YuvImage { .. }
+ );
+ if is_image {
+ // We allow "small" images, since they're generally UI elements.
+ if let Some(rect) = calculate_prim_clipped_world_rect(
+ &prim_instance.vis.clip_chain.pic_clip_rect,
+ &world_culling_rect,
+ &map_surface_to_world,
+ ) {
+ let rect = rect * frame_context.global_device_pixel_scale;
+ if rect.size.width > 70.0 && rect.size.height > 70.0 {
+ frame_state.scratch.primitive.push_debug_rect(rect, debug_colors::PURPLE, debug_colors::PURPLE);
+ }
+ }
+ }
+ }
+
+ if prim_instance.is_chased() {
+ println!("\tvisible with {:?}", prim_instance.vis.combined_local_clip_rect);
+ }
+
+ // TODO(gw): This should probably be an instance method on PrimitiveInstance?
+ update_prim_post_visibility(
+ store,
+ prim_instance,
+ cluster.spatial_node_index,
+ world_culling_rect,
+ &map_surface_to_world,
+ frame_context,
+ frame_state,
+ );
+ }
+ }
+ }
+
+ // Similar to above, pop either the clip chain or root entry off the current clip stack.
+ if is_composite {
+ frame_state.pop_surface();
+ }
+
+ let pic = &mut store.pictures[pic_index.0];
+ pic.prim_list = prim_list;
+
+ // If the local rect changed (due to transforms in child primitives) then
+ // invalidate the GPU cache location to re-upload the new local rect
+ // and stretch size. Drop shadow filters also depend on the local rect
+ // size for the extra GPU cache data handle.
+ // TODO(gw): In future, if we support specifying a flag which gets the
+ // stretch size from the segment rect in the shaders, we can
+ // remove this invalidation here completely.
+ if let Some(ref rc) = pic.raster_config {
+ // Inflate the local bounding rect if required by the filter effect.
+ // This inflaction factor is to be applied to the surface itstore.
+ if pic.options.inflate_if_required {
+ // The picture's local rect is calculated as the union of the
+ // snapped primitive rects, which should result in a snapped
+ // local rect, unless it was inflated. This is also done during
+ // surface configuration when calculating the picture's
+ // estimated local rect.
+ let snap_pic_to_raster = SpaceSnapper::new_with_target(
+ surface.raster_spatial_node_index,
+ pic.spatial_node_index,
+ surface.device_pixel_scale,
+ frame_context.spatial_tree,
+ );
+
+ surface_rect = rc.composite_mode.inflate_picture_rect(surface_rect, surface.scale_factors);
+ surface_rect = snap_pic_to_raster.snap_rect(&surface_rect);
+ }
+
+ // Layout space for the picture is picture space from the
+ // perspective of its child primitives.
+ pic.precise_local_rect = surface_rect * Scale::new(1.0);
+
+ // If the precise rect changed since last frame, we need to invalidate
+ // any segments and gpu cache handles for drop-shadows.
+ // TODO(gw): Requiring storage of the `prev_precise_local_rect` here
+ // is a total hack. It's required because `prev_precise_local_rect`
+ // gets written to twice (during initial vis pass and also during
+ // prepare pass). The proper longer term fix for this is to make
+ // use of the conservative picture rect for segmenting (which should
+ // be done during scene building).
+ if pic.precise_local_rect != pic.prev_precise_local_rect {
+ match rc.composite_mode {
+ PictureCompositeMode::Filter(Filter::DropShadows(..)) => {
+ for handle in &pic.extra_gpu_data_handles {
+ frame_state.gpu_cache.invalidate(handle);
+ }
+ }
+ _ => {}
+ }
+ // Invalidate any segments built for this picture, since the local
+ // rect has changed.
+ pic.segments_are_valid = false;
+ pic.prev_precise_local_rect = pic.precise_local_rect;
+ }
+
+ if let PictureCompositeMode::TileCache { .. } = rc.composite_mode {
+ let mut tile_cache = frame_state.tile_cache.take().unwrap();
+
+ // Build the dirty region(s) for this tile cache.
+ tile_cache.post_update(
+ frame_context,
+ frame_state,
+ );
+
+ tile_caches.insert(SliceId::new(tile_cache.slice), tile_cache);
+ }
+
+ None
+ } else {
+ let parent_surface = &frame_context.surfaces[parent_surface_index.0 as usize];
+ let map_surface_to_parent_surface = SpaceMapper::new_with_target(
+ parent_surface.surface_spatial_node_index,
+ surface.surface_spatial_node_index,
+ PictureRect::max_rect(),
+ frame_context.spatial_tree,
+ );
+ map_surface_to_parent_surface.map(&surface_rect)
+ }
+}
+
+
+fn update_prim_post_visibility(
+ store: &mut PrimitiveStore,
+ prim_instance: &mut PrimitiveInstance,
+ prim_spatial_node_index: SpatialNodeIndex,
+ world_culling_rect: WorldRect,
+ map_surface_to_world: &SpaceMapper<PicturePixel, WorldPixel>,
+ frame_context: &FrameVisibilityContext,
+ frame_state: &mut FrameVisibilityState,
+) {
+ profile_scope!("update_prim_post_visibility");
+ match prim_instance.kind {
+ PrimitiveInstanceKind::Picture { pic_index, .. } => {
+ let pic = &mut store.pictures[pic_index.0];
+ // If this picture has a surface, determine the clipped bounding rect for it to
+ // minimize the size of the render target that is required.
+ if let Some(ref mut raster_config) = pic.raster_config {
+ raster_config.clipped_bounding_rect = map_surface_to_world
+ .map(&prim_instance.vis.clip_chain.pic_clip_rect)
+ .and_then(|rect| {
+ rect.intersection(&world_culling_rect)
+ })
+ .unwrap_or(WorldRect::zero());
+ }
+ }
+ PrimitiveInstanceKind::TextRun { .. } => {
+ // Text runs can't request resources early here, as we don't
+ // know until TileCache::post_update() whether we are drawing
+ // on an opaque surface.
+ // TODO(gw): We might be able to detect simple cases of this earlier,
+ // during the picture traversal. But it's probably not worth it?
+ }
+ PrimitiveInstanceKind::Image { data_handle, image_instance_index, .. } => {
+ let prim_data = &mut frame_state.data_stores.image[data_handle];
+ let common_data = &mut prim_data.common;
+ let image_data = &mut prim_data.kind;
+ let image_instance = &mut store.images[image_instance_index];
+
+ let image_properties = frame_state
+ .resource_cache
+ .get_image_properties(image_data.key);
+
+ let request = ImageRequest {
+ key: image_data.key,
+ rendering: image_data.image_rendering,
+ tile: None,
+ };
+
+ match image_properties {
+ Some(ImageProperties { tiling: None, .. }) => {
+
+ frame_state.resource_cache.request_image(
+ request,
+ frame_state.gpu_cache,
+ );
+ }
+ Some(ImageProperties { tiling: Some(tile_size), visible_rect, .. }) => {
+ image_instance.visible_tiles.clear();
+ // 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
+ // rect and we want to clip these extra parts out.
+ let prim_info = &prim_instance.vis;
+ let tight_clip_rect = prim_info
+ .combined_local_clip_rect
+ .intersection(&common_data.prim_rect).unwrap();
+ image_instance.tight_local_clip_rect = tight_clip_rect;
+
+ let visible_rect = compute_conservative_visible_rect(
+ &prim_instance.vis.clip_chain,
+ world_culling_rect,
+ prim_spatial_node_index,
+ frame_context.spatial_tree,
+ );
+
+ let base_edge_flags = edge_flags_for_tile_spacing(&image_data.tile_spacing);
+
+ let stride = image_data.stretch_size + image_data.tile_spacing;
+
+ // We are performing the decomposition on the CPU here, no need to
+ // have it in the shader.
+ common_data.may_need_repetition = false;
+
+ let repetitions = image_tiling::repetitions(
+ &common_data.prim_rect,
+ &visible_rect,
+ stride,
+ );
+
+ for image_tiling::Repetition { origin, edge_flags } in repetitions {
+ let edge_flags = base_edge_flags | edge_flags;
+
+ let layout_image_rect = LayoutRect {
+ origin,
+ size: image_data.stretch_size,
+ };
+
+ let tiles = image_tiling::tiles(
+ &layout_image_rect,
+ &visible_rect,
+ &active_rect,
+ tile_size as i32,
+ );
+
+ for tile in tiles {
+ frame_state.resource_cache.request_image(
+ request.with_tile(tile.offset),
+ frame_state.gpu_cache,
+ );
+
+ image_instance.visible_tiles.push(VisibleImageTile {
+ tile_offset: tile.offset,
+ edge_flags: tile.edge_flags & edge_flags,
+ local_rect: tile.rect,
+ local_clip_rect: tight_clip_rect,
+ });
+ }
+ }
+
+ if image_instance.visible_tiles.is_empty() {
+ // Mark as invisible
+ prim_instance.clear_visibility();
+ }
+ }
+ None => {}
+ }
+ }
+ PrimitiveInstanceKind::ImageBorder { data_handle, .. } => {
+ let prim_data = &mut frame_state.data_stores.image_border[data_handle];
+ prim_data.kind.request_resources(
+ frame_state.resource_cache,
+ frame_state.gpu_cache,
+ );
+ }
+ PrimitiveInstanceKind::YuvImage { data_handle, .. } => {
+ let prim_data = &mut frame_state.data_stores.yuv_image[data_handle];
+ prim_data.kind.request_resources(
+ frame_state.resource_cache,
+ frame_state.gpu_cache,
+ );
+ }
+ _ => {}
+ }
+}
+
+fn edge_flags_for_tile_spacing(tile_spacing: &LayoutSize) -> EdgeAaSegmentMask {
+ let mut flags = EdgeAaSegmentMask::empty();
+
+ if tile_spacing.width > 0.0 {
+ flags |= EdgeAaSegmentMask::LEFT | EdgeAaSegmentMask::RIGHT;
+ }
+ if tile_spacing.height > 0.0 {
+ flags |= EdgeAaSegmentMask::TOP | EdgeAaSegmentMask::BOTTOM;
+ }
+
+ flags
+}
+
+pub fn compute_conservative_visible_rect(
+ clip_chain: &ClipChainInstance,
+ world_culling_rect: WorldRect,
+ prim_spatial_node_index: SpatialNodeIndex,
+ spatial_tree: &SpatialTree,
+) -> LayoutRect {
+ // Mapping from picture space -> world space
+ let map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target(
+ ROOT_SPATIAL_NODE_INDEX,
+ clip_chain.pic_spatial_node_index,
+ world_culling_rect,
+ spatial_tree,
+ );
+
+ // Mapping from local space -> picture space
+ let map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel> = SpaceMapper::new_with_target(
+ clip_chain.pic_spatial_node_index,
+ prim_spatial_node_index,
+ PictureRect::max_rect(),
+ spatial_tree,
+ );
+
+ // Unmap the world culling rect from world -> picture space. If this mapping fails due
+ // to matrix weirdness, best we can do is use the clip chain's local clip rect.
+ let pic_culling_rect = match map_pic_to_world.unmap(&world_culling_rect) {
+ Some(rect) => rect,
+ None => return clip_chain.local_clip_rect,
+ };
+
+ // Intersect the unmapped world culling rect with the primitive's clip chain rect that
+ // is in picture space (the clip-chain already takes into account the bounds of the
+ // primitive local_rect and local_clip_rect). If there is no intersection here, the
+ // primitive is not visible at all.
+ let pic_culling_rect = match pic_culling_rect.intersection(&clip_chain.pic_clip_rect) {
+ Some(rect) => rect,
+ None => return LayoutRect::zero(),
+ };
+
+ // Unmap the picture culling rect from picture -> local space. If this mapping fails due
+ // to matrix weirdness, best we can do is use the clip chain's local clip rect.
+ match map_local_to_pic.unmap(&pic_culling_rect) {
+ Some(rect) => rect,
+ None => clip_chain.local_clip_rect,
+ }
+}
+
+fn calculate_prim_clipped_world_rect(
+ pic_clip_rect: &PictureRect,
+ world_culling_rect: &WorldRect,
+ map_surface_to_world: &SpaceMapper<PicturePixel, WorldPixel>,
+) -> Option<WorldRect> {
+ map_surface_to_world
+ .map(pic_clip_rect)
+ .and_then(|world_rect| {
+ world_rect.intersection(world_culling_rect)
+ })
+}