/* 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, ExternalScrollId, FontRenderMode, ImageKey, MinimapData, PremultipliedColorF};
use api::units::*;
use plane_split::BspSplitter;
use crate::batch::{BatchBuilder, AlphaBatchBuilder, AlphaBatchContainer};
use crate::clip::{ClipStore, ClipTree};
use crate::command_buffer::{PrimitiveCommand, CommandBufferList, CommandBufferIndex};
use crate::{debug_colors, ChunkPool};
use crate::spatial_node::SpatialNodeType;
use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
use crate::composite::{CompositorKind, CompositeState, CompositeStatePreallocator};
use crate::debug_item::DebugItem;
use crate::gpu_cache::{GpuCache, GpuCacheHandle};
use crate::gpu_types::{PrimitiveHeaders, TransformPalette, ZBufferIdGenerator};
use crate::gpu_types::{QuadSegment, TransformData};
use crate::internal_types::{FastHashMap, PlaneSplitter, FrameId, FrameStamp};
use crate::picture::{DirtyRegion, SliceId, TileCacheInstance};
use crate::picture::{SurfaceInfo, SurfaceIndex, ResolvedSurfaceTexture};
use crate::picture::{SubpixelMode, RasterConfig, PictureCompositeMode};
use crate::prepare::prepare_picture;
use crate::prim_store::{PictureIndex, PrimitiveScratchBuffer};
use crate::prim_store::{DeferredResolve, PrimitiveInstance};
use crate::profiler::{self, TransactionProfile};
use crate::render_backend::{DataStores, ScratchBuffer};
use crate::renderer::{GpuBufferF, GpuBufferBuilderF, GpuBufferI, GpuBufferBuilderI, GpuBufferBuilder};
use crate::render_target::{PictureCacheTarget, PictureCacheTargetKind};
use crate::render_target::{RenderTargetContext, RenderTargetKind, RenderTarget};
use crate::render_task_graph::{Pass, RenderTaskGraph, RenderTaskId, SubPassSurface};
use crate::render_task_graph::{RenderPass, RenderTaskGraphBuilder};
use crate::render_task::{RenderTaskKind, StaticRenderTaskSurface};
use crate::resource_cache::ResourceCache;
use crate::scene::{BuiltScene, SceneProperties};
use crate::space::SpaceMapper;
use crate::segment::SegmentBuilder;
use crate::surface::SurfaceBuilder;
use std::sync::Arc;
use std::{f32, mem};
use crate::util::{MaxRect, VecHelper, Preallocator};
use crate::visibility::{update_prim_visibility, FrameVisibilityState, FrameVisibilityContext};
use crate::internal_types::{FrameVec, FrameMemory};

#[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,
    /// 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,
    /// Whether ImageBufferKind::TextureExternal images must first be copied
    /// to a regular texture before rendering.
    pub external_images_require_copy: bool,
    pub batch_lookback_count: usize,
    pub background_color: Option<ColorF>,
    pub compositor_kind: CompositorKind,
    pub tile_size_override: Option<DeviceIntSize>,
    pub max_surface_override: Option<usize>,
    pub max_depth_ids: i32,
    pub max_target_size: i32,
    pub force_invalidation: bool,
    pub is_software: bool,
    pub low_quality_pinch_zoom: bool,
    pub max_shared_surface_size: i32,
    pub enable_dithering: bool,
    pub precise_linear_gradients: bool,
    pub precise_radial_gradients: bool,
    pub precise_conic_gradients: 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 cut-out rectangle primitives.
    /// This is used to 'cut out' overlay tiles where a compositor
    /// surface exists.
    pub default_black_rect_handle: GpuCacheHandle,
}

impl FrameGlobalResources {
    pub fn empty() -> Self {
        FrameGlobalResources {
            default_image_handle: GpuCacheHandle::new(),
            default_black_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_black_rect_handle) {
            request.push(PremultipliedColorF::BLACK);
        }
    }
}

pub struct FrameScratchBuffer {
    dirty_region_stack: Vec<DirtyRegion>,
    surface_stack: Vec<(PictureIndex, SurfaceIndex)>,
}

impl Default for FrameScratchBuffer {
    fn default() -> Self {
        FrameScratchBuffer {
            dirty_region_stack: Vec::new(),
            surface_stack: Vec::new(),
        }
    }
}

impl FrameScratchBuffer {
    pub fn begin_frame(&mut self) {
        self.dirty_region_stack.clear();
        self.surface_stack.clear();
    }
}

/// 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,
    #[cfg_attr(feature = "capture", serde(skip))]
    plane_splitters: Vec<PlaneSplitter>,
}

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 root_spatial_node_index: SpatialNodeIndex,
}

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,
    pub plane_splitters: &'a mut [PlaneSplitter],
    pub surface_builder: SurfaceBuilder,
    pub cmd_buffers: &'a mut CommandBufferList,
    pub clip_tree: &'a ClipTree,
    pub frame_gpu_data: &'a mut GpuBufferBuilder,
    /// When using a render task to produce pixels that are associated with
    /// an image key (for example snapshotted pictures), inserting the image
    /// key / task id association in this hashmap allows the image item to
    /// register a dependency to the render task. This ensures that the
    /// render task is produced before the image that renders it if they
    /// are happening in the same frame.
    /// This mechanism relies on the item producing the render task to be
    /// traversed before the image that displays it (in other words, the
    /// picture must appear before the image in the display list).
    pub image_dependencies: FastHashMap<ImageKey, RenderTaskId>,
    pub visited_pictures: &'a mut [bool],
}

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();
    }

    /// Push a primitive command to a set of command buffers
    pub fn push_prim(
        &mut self,
        cmd: &PrimitiveCommand,
        spatial_node_index: SpatialNodeIndex,
        targets: &[CommandBufferIndex],
    ) {
        for cmd_buffer_index in targets {
            let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
            cmd_buffer.add_prim(cmd, spatial_node_index);
        }
    }

    /// Push a command to a set of command buffers
    pub fn push_cmd(
        &mut self,
        cmd: &PrimitiveCommand,
        targets: &[CommandBufferIndex],
    ) {
        for cmd_buffer_index in targets {
            let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
            cmd_buffer.add_cmd(cmd);
        }
    }

    /// Set the active list of segments in a set of command buffers
    pub fn set_segments(
        &mut self,
        segments: &[QuadSegment],
        targets: &[CommandBufferIndex],
    ) {
        for cmd_buffer_index in targets {
            let cmd_buffer = self.cmd_buffers.get_mut(*cmd_buffer_index);
            cmd_buffer.set_segments(segments);
        }
    }
}

/// Immutable context of a picture when processing children.
#[derive(Debug)]
pub struct PictureContext {
    pub pic_index: PictureIndex,
    pub surface_spatial_node_index: SpatialNodeIndex,
    pub raster_spatial_node_index: SpatialNodeIndex,
    pub visibility_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_vis: SpaceMapper<PicturePixel, VisPixel>,
}

impl FrameBuilder {
    pub fn new() -> Self {
        FrameBuilder {
            globals: FrameGlobalResources::empty(),
            prim_headers_prealloc: Preallocator::new(0),
            composite_state_prealloc: CompositeStatePreallocator::default(),
            plane_splitters: Vec::new(),
        }
    }

    /// 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,
        present: bool,
        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_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
        spatial_tree: &SpatialTree,
        cmd_buffers: &mut CommandBufferList,
        frame_gpu_data: &mut GpuBufferBuilder,
        frame_memory: &FrameMemory,
        profile: &mut TransactionProfile,
    ) {
        profile_scope!("build_layer_screen_rects_and_cull_layers");

        let render_picture_cache_slices = present;

        let root_spatial_node_index = spatial_tree.root_reference_frame_index();

        const MAX_CLIP_COORD: f32 = 1.0e9;

        // Reset all plane splitters. These are retained from frame to frame to reduce
        // per-frame allocations
        self.plane_splitters.resize_with(scene.num_plane_splitters, BspSplitter::new);
        for splitter in &mut self.plane_splitters {
            splitter.reset();
        }

        let frame_context = FrameBuildingContext {
            global_device_pixel_scale,
            scene_properties,
            global_screen_world_rect,
            spatial_tree,
            max_local_clip: LayoutRect {
                min: LayoutPoint::new(-MAX_CLIP_COORD, -MAX_CLIP_COORD),
                max: LayoutPoint::new(MAX_CLIP_COORD, MAX_CLIP_COORD),
            },
            debug_flags,
            fb_config: &scene.config,
            root_spatial_node_index,
        };

        scene.picture_graph.build_update_passes(
            &mut scene.prim_store.pictures,
            &frame_context,
        );

        scene.picture_graph.assign_surfaces(
            &mut scene.prim_store.pictures,
            &mut scene.surfaces,
            tile_caches,
            &frame_context,
        );

        // Add a "fake" surface that we will use as parent for
        // snapshotted pictures.
        let root_spatial_node = frame_context.spatial_tree.root_reference_frame_index();
        let snapshot_surface = SurfaceIndex(scene.surfaces.len());
        scene.surfaces.push(SurfaceInfo::new(
            root_spatial_node,
            root_spatial_node,
            WorldRect::max_rect(),
            &frame_context.spatial_tree,
            euclid::Scale::new(1.0),
            (1.0, 1.0),
            (1.0, 1.0),
            false,
            false,
        ));

        scene.picture_graph.propagate_bounding_rects(
            &mut scene.prim_store.pictures,
            &mut scene.surfaces,
            &frame_context,
        );

        // In order to handle picture snapshots consistently we need
        // the visibility and prepare passes to visit them first before
        // traversing the scene. This ensures that out-of-view snapshots
        // are rendered and that snapshots are consistently produced
        // relative to the root spatial node.
        // However it means that the visibility and prepare passes may
        // visit some pictures multiple times, so we keep track of visited
        // pictures during each traversal to avoid that.
        let n_pics = scene.prim_store.pictures.len();
        let mut visited_pictures = frame_memory.new_vec_with_capacity(n_pics);
        for _ in 0..n_pics {
            visited_pictures.push(false);
        }

        {
            profile_scope!("UpdateVisibility");
            profile_marker!("UpdateVisibility");
            profile.start_time(profiler::FRAME_VISIBILITY_TIME);

            let visibility_context = FrameVisibilityContext {
                global_device_pixel_scale,
                spatial_tree,
                global_screen_world_rect,
                debug_flags,
                scene_properties,
                config: scene.config,
                root_spatial_node_index,
            };

            for pic_index in scene.snapshot_pictures.iter() {
                let mut visibility_state = FrameVisibilityState {
                    clip_store: &mut scene.clip_store,
                    resource_cache,
                    gpu_cache,
                    data_stores,
                    clip_tree: &mut scene.clip_tree,
                    composite_state,
                    rg_builder,
                    prim_instances: &mut scene.prim_instances,
                    surfaces: &mut scene.surfaces,
                    surface_stack: scratch.frame.surface_stack.take(),
                    profile,
                    scratch,
                    visited_pictures: &mut visited_pictures,
                };

                let world_culling_rect = WorldRect::max_rect();

                // For now, snapshots are updated every frame. For the
                // pictures displaying the snapshot via images pick up
                // the changes, we have to make sure that the image's
                // generation counter is incremented early in the frame,
                // before the main visibility pass visits the image items.
                let pic = &scene.prim_store.pictures[pic_index.0];
                let snapshot = pic.snapshot
                    .unwrap();
                let key = snapshot.key.as_image();
                visibility_state.resource_cache
                    .increment_image_generation(key);

                if let Some(node) = pic.clip_root {
                    visibility_state.clip_tree.push_clip_root_node(node);
                }
                update_prim_visibility(
                    *pic_index,
                    None,
                    &world_culling_rect,
                    &scene.prim_store,
                    true,
                    &visibility_context,
                    &mut visibility_state,
                    &mut None,
                );
                if scene.prim_store.pictures[pic_index.0].clip_root.is_some() {
                    visibility_state.clip_tree.pop_clip_root();
                }
            }

            for pic_index in scene.tile_cache_pictures.iter().rev() {
                if !render_picture_cache_slices {
                    break;
                }
                let pic = &mut scene.prim_store.pictures[pic_index.0];

                match pic.raster_config {
                    Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
                        let tile_cache = tile_caches
                            .get_mut(&slice_id)
                            .expect("bug: non-existent tile cache");

                        let mut visibility_state = FrameVisibilityState {
                            clip_store: &mut scene.clip_store,
                            resource_cache,
                            gpu_cache,
                            data_stores,
                            clip_tree: &mut scene.clip_tree,
                            composite_state,
                            rg_builder,
                            prim_instances: &mut scene.prim_instances,
                            surfaces: &mut scene.surfaces,
                            surface_stack: scratch.frame.surface_stack.take(),
                            profile,
                            scratch,
                            visited_pictures: &mut visited_pictures,
                        };

                        // 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.
                        let world_culling_rect = tile_cache.pre_update(
                            surface_index,
                            &visibility_context,
                            &mut visibility_state,
                        );

                        // Push a new surface, supplying the list of clips that should be
                        // ignored, since they are handled by clipping when drawing this surface.
                        visibility_state.push_surface(
                            *pic_index,
                            surface_index,
                        );
                        visibility_state.clip_tree.push_clip_root_node(tile_cache.shared_clip_node_id);

                        update_prim_visibility(
                            *pic_index,
                            None,
                            &world_culling_rect,
                            &scene.prim_store,
                            true,
                            &visibility_context,
                            &mut visibility_state,
                            &mut Some(tile_cache),
                        );

                        // Build the dirty region(s) for this tile cache.
                        tile_cache.post_update(
                            &visibility_context,
                            &mut visibility_state.composite_state,
                            &mut visibility_state.resource_cache,
                        );

                        visibility_state.clip_tree.pop_clip_root();
                        visibility_state.pop_surface();
                        visibility_state.scratch.frame.surface_stack = visibility_state.surface_stack.take();
                    }
                    _ => {
                        panic!("bug: not a tile cache");
                    }
                }
            }

            profile.end_time(profiler::FRAME_VISIBILITY_TIME);
        }

        profile.start_time(profiler::FRAME_PREPARE_TIME);

        // Reset the visited pictures for the prepare pass.
        visited_pictures.clear();
        for _ in 0..n_pics {
            visited_pictures.push(false);
        }
        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 scene.surfaces,
            dirty_region_stack: scratch.frame.dirty_region_stack.take(),
            composite_state,
            num_visible_primitives: 0,
            plane_splitters: &mut self.plane_splitters,
            surface_builder: SurfaceBuilder::new(),
            cmd_buffers,
            clip_tree: &mut scene.clip_tree,
            frame_gpu_data,
            image_dependencies: FastHashMap::default(),
            visited_pictures: &mut visited_pictures,
        };


        if !scene.snapshot_pictures.is_empty() {
            // Push a default dirty region which does not cull any
            // primitive.
            let mut default_dirty_region = DirtyRegion::new(
                root_spatial_node_index,
                root_spatial_node_index,
            );
            default_dirty_region.add_dirty_region(
                PictureRect::max_rect(),
                frame_context.spatial_tree,
            );
            frame_state.push_dirty_region(default_dirty_region);

            frame_state.surface_builder.push_surface(
                snapshot_surface,
                false,
                PictureRect::max_rect(),
                None,
                frame_state.surfaces,
                frame_state.rg_builder,
            );
        }

        for pic_index in &scene.snapshot_pictures {

            prepare_picture(
                *pic_index,
                &mut scene.prim_store,
                Some(snapshot_surface),
                SubpixelMode::Allow,
                &frame_context,
                &mut frame_state,
                data_stores,
                &mut scratch.primitive,
                tile_caches,
                &mut scene.prim_instances
            );
        }

        if !scene.snapshot_pictures.is_empty() {
            frame_state.surface_builder.pop_empty_surface();
            frame_state.pop_dirty_region();
        }

        // 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,
            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 !render_picture_cache_slices {
                break;
            }

            prepare_picture(
                *pic_index,
                &mut scene.prim_store,
                None,
                SubpixelMode::Allow,
                &frame_context,
                &mut frame_state,
                data_stores,
                &mut scratch.primitive,
                tile_caches,
                &mut scene.prim_instances
            );
        }

        frame_state.pop_dirty_region();
        frame_state.surface_builder.finalize();
        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();

        {
            profile_marker!("BlockOnResources");

            resource_cache.block_until_all_resources_added(
                gpu_cache,
                profile,
            );
        }
    }

    pub fn build(
        &mut self,
        scene: &mut BuiltScene,
        present: bool,
        resource_cache: &mut ResourceCache,
        gpu_cache: &mut GpuCache,
        rg_builder: &mut RenderTaskGraphBuilder,
        stamp: FrameStamp,
        device_origin: DeviceIntPoint,
        scene_properties: &SceneProperties,
        data_stores: &mut DataStores,
        scratch: &mut ScratchBuffer,
        debug_flags: DebugFlags,
        tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
        spatial_tree: &mut SpatialTree,
        dirty_rects_are_valid: bool,
        profile: &mut TransactionProfile,
        minimap_data: FastHashMap<ExternalScrollId, MinimapData>,
        chunk_pool: Arc<ChunkPool>,
    ) -> Frame {
        profile_scope!("build");
        profile_marker!("BuildFrame");

        let mut frame_memory = FrameMemory::new(chunk_pool, stamp.frame_id());

        profile.set(profiler::PRIMITIVES, scene.prim_instances.len());
        profile.set(profiler::PICTURE_CACHE_SLICES, scene.tile_cache_config.picture_cache_slice_count);
        scratch.begin_frame();
        gpu_cache.begin_frame(stamp);
        resource_cache.begin_frame(stamp, gpu_cache, profile);

        // TODO(gw): Follow up patches won't clear this, as they'll be assigned
        //           statically during scene building.
        scene.surfaces.clear();

        self.globals.update(gpu_cache);

        spatial_tree.update_tree(scene_properties);
        let mut transform_palette = spatial_tree.build_transform_palette(&frame_memory);
        scene.clip_store.begin_frame(&mut scratch.clip_store);

        rg_builder.begin_frame(stamp.frame_id());

        // TODO(dp): Remove me completely!!
        let global_device_pixel_scale = DevicePixelScale::new(1.0);

        let output_size = scene.output_rect.size();
        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,
            scene.config.max_depth_ids,
            dirty_rects_are_valid,
            scene.config.low_quality_pinch_zoom,
            &frame_memory,
        );

        self.composite_state_prealloc.preallocate(&mut composite_state);

        let mut cmd_buffers = CommandBufferList::new();

        // TODO(gw): Recycle backing vec buffers for gpu buffer builder between frames
        let mut gpu_buffer_builder = GpuBufferBuilder {
            f32: GpuBufferBuilderF::new(&frame_memory),
            i32: GpuBufferBuilderI::new(&frame_memory),
        };

        self.build_layer_screen_rects_and_cull_layers(
            scene,
            present,
            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_caches,
            spatial_tree,
            &mut cmd_buffers,
            &mut gpu_buffer_builder,
            &frame_memory,
            profile,
        );

        self.render_minimap(&mut scratch.primitive, &spatial_tree, minimap_data);

        profile.start_time(profiler::FRAME_BATCHING_TIME);

        let mut deferred_resolves = frame_memory.new_vec();

        // Finish creating the frame graph and build it.
        let render_tasks = rg_builder.end_frame(
            resource_cache,
            gpu_cache,
            &mut deferred_resolves,
            scene.config.max_shared_surface_size,
            &frame_memory,
        );

        let mut passes = frame_memory.new_vec();
        let mut has_texture_cache_tasks = false;
        let mut prim_headers = PrimitiveHeaders::new(&frame_memory);
        self.prim_headers_prealloc.preallocate_framevec(&mut prim_headers.headers_int);
        self.prim_headers_prealloc.preallocate_framevec(&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_supported;

            for pass in render_tasks.passes.iter().rev() {
                let mut ctx = RenderTargetContext {
                    global_device_pixel_scale,
                    prim_store: &scene.prim_store,
                    clip_store: &scene.clip_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,
                    data_stores,
                    surfaces: &scene.surfaces,
                    scratch: &mut scratch.primitive,
                    screen_world_rect,
                    globals: &self.globals,
                    tile_caches,
                    root_spatial_node_index: spatial_tree.root_reference_frame_index(),
                    frame_memory: &mut frame_memory,
                };

                let pass = build_render_pass(
                    pass,
                    output_size,
                    &mut ctx,
                    gpu_cache,
                    &mut gpu_buffer_builder,
                    &render_tasks,
                    &scene.clip_store,
                    &mut transform_palette,
                    &mut prim_headers,
                    &mut z_generator,
                    scene.config.gpu_supports_fast_clears,
                    &scene.prim_instances,
                    &cmd_buffers,
                );

                has_texture_cache_tasks |= !pass.texture_cache.is_empty();
                has_texture_cache_tasks |= !pass.picture_cache.is_empty();

                passes.push(pass);
            }

            if present {
                let mut ctx = RenderTargetContext {
                    global_device_pixel_scale,
                    clip_store: &scene.clip_store,
                    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,
                    data_stores,
                    surfaces: &scene.surfaces,
                    scratch: &mut scratch.primitive,
                    screen_world_rect,
                    globals: &self.globals,
                    tile_caches,
                    root_spatial_node_index: spatial_tree.root_reference_frame_index(),
                    frame_memory: &mut frame_memory,
                };

                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(&prim_headers.headers_int);
        self.composite_state_prealloc.record(&composite_state);

        composite_state.end_frame();
        scene.clip_store.end_frame(&mut scratch.clip_store);
        scratch.end_frame();

        let gpu_buffer_f = gpu_buffer_builder.f32.finalize(&render_tasks);
        let gpu_buffer_i = gpu_buffer_builder.i32.finalize(&render_tasks);

        Frame {
            device_rect: DeviceIntRect::from_origin_and_size(
                device_origin,
                scene.output_rect.size(),
            ),
            present,
            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,
            gpu_buffer_f,
            gpu_buffer_i,
            allocator_memory: frame_memory,
        }
    }

    fn render_minimap(
        &self,
        scratch: &mut PrimitiveScratchBuffer,
        spatial_tree: &SpatialTree,
        minimap_data_store: FastHashMap<ExternalScrollId, MinimapData>) {
      // TODO: Replace minimap_data_store with Option<FastHastMap>?
      if minimap_data_store.is_empty() {
        return
      }

      // In our main walk over the spatial tree (below), for nodes inside a
      // subtree rooted at a root-content node, we need some information from
      // that enclosing root-content node. To collect this information, do an
      // preliminary walk over the spatial tree now and collect the root-content
      // info in a HashMap.
      struct RootContentInfo {
        transform: LayoutToWorldTransform,
        clip: LayoutRect
      }
      let mut root_content_info = FastHashMap::<ExternalScrollId, RootContentInfo>::default();
      spatial_tree.visit_nodes(|index, node| {
        if let SpatialNodeType::ScrollFrame(ref scroll_frame_info) = node.node_type {
          if let Some(minimap_data) = minimap_data_store.get(&scroll_frame_info.external_id) {
            if minimap_data.is_root_content {
              let transform = spatial_tree.get_world_viewport_transform(index).into_transform();
              root_content_info.insert(scroll_frame_info.external_id, RootContentInfo{
                transform,
                clip: scroll_frame_info.viewport_rect
              });
            }
          }
        }
      });

      // This is the main walk over the spatial tree. For every scroll frame node which
      // has minimap data, compute the rects we want to render for that minimap in world
      // coordinates and add them to `scratch.debug_items`.
      spatial_tree.visit_nodes(|index, node| {
        if let SpatialNodeType::ScrollFrame(ref scroll_frame_info) = node.node_type {
          if let Some(minimap_data) = minimap_data_store.get(&scroll_frame_info.external_id) {
            const HORIZONTAL_PADDING: f32 = 5.0;
            const VERTICAL_PADDING: f32 = 10.0;
            const PAGE_BORDER_COLOR: ColorF = debug_colors::BLACK;
            const BACKGROUND_COLOR: ColorF = ColorF { r: 0.3, g: 0.3, b: 0.3, a: 0.3};
            const DISPLAYPORT_BACKGROUND_COLOR: ColorF = ColorF { r: 1.0, g: 1.0, b: 1.0, a: 0.4};
            const LAYOUT_PORT_COLOR: ColorF = debug_colors::RED;
            const VISUAL_PORT_COLOR: ColorF = debug_colors::BLUE;
            const DISPLAYPORT_COLOR: ColorF = debug_colors::LIME;

            let viewport = scroll_frame_info.viewport_rect;

            // Scale the minimap to make it 100px wide (if there's space), and the full height
            // of the scroll frame's viewport, minus some padding. Position it at the left edge
            // of the scroll frame's viewport.
            let scale_factor_x = 100f32.min(viewport.width() - (2.0 * HORIZONTAL_PADDING))
                                   / minimap_data.scrollable_rect.width();
            let scale_factor_y = (viewport.height() - (2.0 * VERTICAL_PADDING))
                                / minimap_data.scrollable_rect.height();
            if scale_factor_x <= 0.0 || scale_factor_y <= 0.0 {
              return;
            }
            let transform = LayoutTransform::scale(scale_factor_x, scale_factor_y, 1.0)
                .then_translate(LayoutVector3D::new(HORIZONTAL_PADDING, VERTICAL_PADDING, 0.0))
                .then_translate(LayoutVector3D::new(viewport.min.x, viewport.min.y, 0.0));

            // Transforms for transforming rects in this scroll frame's local coordintes, to world coordinates.
            // For scroll frames inside a root-content subtree, we apply this transform in two parts
            // (local to root-content, and root-content to world), so that we can make additional
            // adjustments in root-content space. For scroll frames outside of a root-content subtree,
            // the entire world transform will be in `local_to_root_content`.
            let world_transform = spatial_tree
                .get_world_viewport_transform(index)
                .into_transform();
            let mut local_to_root_content =
                world_transform.with_destination::<LayoutPixel>();
            let mut root_content_to_world = LayoutToWorldTransform::default();
            let mut root_content_clip = None;
            if minimap_data.root_content_scroll_id != 0 {
              if let Some(RootContentInfo{transform: root_content_transform, clip}) = root_content_info.get(&ExternalScrollId(minimap_data.root_content_scroll_id, minimap_data.root_content_pipeline_id)) {
                // Exclude the root-content node's zoom transform from `local_to_root_content`.
                // This ensures that the minimap remains unaffected by pinch-zooming
                // (in essence, remaining attached to the *visual* viewport, rather than to
                // the *layout* viewport which is what happens by default).
                let zoom_transform = minimap_data.zoom_transform;
                local_to_root_content = world_transform
                  .then(&root_content_transform.inverse().unwrap())
                  .then(&zoom_transform.inverse().unwrap());
                root_content_to_world = root_content_transform.clone();
                root_content_clip = Some(clip);
              }
            }

            let mut add_rect = |rect, border, fill| -> Option<()> {
              const STROKE_WIDTH: f32 = 2.0;
              // Place rect in scroll frame's local coordinate space
              let transformed_rect = transform.outer_transformed_box2d(&rect)?;

              // Transform to world coordinates, using root-content coords as an intermediate step.
              let mut root_content_rect = local_to_root_content.outer_transformed_box2d(&transformed_rect)?;
              // In root-content coords, apply the root content node's viewport clip.
              // This prevents subframe minimaps from leaking into the chrome area when the root
              // scroll frame is scrolled.
              // TODO: The minimaps of nested subframes can still leak outside of the viewports of
              // their containing subframes. Should have a more proper fix for this.
              if let Some(clip) = root_content_clip {
                root_content_rect = root_content_rect.intersection(clip)?;
              }
              let world_rect = root_content_to_world.outer_transformed_box2d(&root_content_rect)?;

              scratch.push_debug_rect_with_stroke_width(world_rect, border, STROKE_WIDTH);

              // Add world coordinate rects to scratch.debug_items
              if let Some(fill_color) = fill {
                let interior_world_rect = WorldRect::new(
                    world_rect.min + WorldVector2D::new(STROKE_WIDTH, STROKE_WIDTH),
                    world_rect.max - WorldVector2D::new(STROKE_WIDTH, STROKE_WIDTH)
                );
                scratch.push_debug_rect(interior_world_rect * DevicePixelScale::new(1.0), 1, border, fill_color);
              }

              Some(())
            };

            add_rect(minimap_data.scrollable_rect, PAGE_BORDER_COLOR, Some(BACKGROUND_COLOR));
            add_rect(minimap_data.displayport, DISPLAYPORT_COLOR, Some(DISPLAYPORT_BACKGROUND_COLOR));
            // Only render a distinct layout viewport for the root content.
            // For other scroll frames, the visual and layout viewports coincide.
            if minimap_data.is_root_content {
              add_rect(minimap_data.layout_viewport, LAYOUT_PORT_COLOR, None);
            }
            add_rect(minimap_data.visual_viewport, VISUAL_PORT_COLOR, None);
          }
        }
      });
    }

    fn build_composite_pass(
        &self,
        scene: &BuiltScene,
        ctx: &RenderTargetContext,
        gpu_cache: &mut GpuCache,
        deferred_resolves: &mut FrameVec<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(
                        ctx.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.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,
    gpu_buffer_builder: &mut GpuBufferBuilder,
    render_tasks: &RenderTaskGraph,
    clip_store: &ClipStore,
    transforms: &mut TransformPalette,
    prim_headers: &mut PrimitiveHeaders,
    z_generator: &mut ZBufferIdGenerator,
    gpu_supports_fast_clears: bool,
    prim_instances: &[PrimitiveInstance],
    cmd_buffers: &CommandBufferList,
) -> 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.
    let mut pass = RenderPass::new(src_pass, ctx.frame_memory);

    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 = RenderTarget::new(
                            RenderTargetKind::Color,
                            false,
                            texture_id,
                            screen_size,
                            gpu_supports_fast_clears,
                            Some(used_rect),
                            &ctx.frame_memory,
                        );

                        for task_id in &sub_pass.task_ids {
                            target.add_task(
                                *task_id,
                                ctx,
                                gpu_cache,
                                gpu_buffer_builder,
                                render_tasks,
                                clip_store,
                                transforms,
                            );
                        }

                        pass.color.targets.push(target);
                    }
                    RenderTargetKind::Alpha => {
                        let mut target = RenderTarget::new(
                            RenderTargetKind::Alpha,
                            false,
                            texture_id,
                            screen_size,
                            gpu_supports_fast_clears,
                            Some(used_rect),
                            &ctx.frame_memory,
                        );

                        for task_id in &sub_pass.task_ids {
                            target.add_task(
                                *task_id,
                                ctx,
                                gpu_cache,
                                gpu_buffer_builder,
                                render_tasks,
                                clip_store,
                                transforms,
                            );
                        }

                        pass.alpha.targets.push(target);
                    }
                }
            }
            SubPassSurface::Persistent { surface: StaticRenderTaskSurface::PictureCache { ref surface, .. }, .. } => {
                assert_eq!(sub_pass.task_ids.len(), 1);
                let task_id = sub_pass.task_ids[0];
                let task = &render_tasks[task_id];
                let target_rect = task.get_target_rect();

                match task.kind {
                    RenderTaskKind::Picture(ref pic_task) => {
                        let cmd_buffer = cmd_buffers.get(pic_task.cmd_buffer_index);
                        let mut dirty_rect = pic_task.scissor_rect.expect("bug: must be set for cache tasks");
                        let mut valid_rect = pic_task.valid_rect.expect("bug: must be set for cache tasks");

                        // If we have a surface size, clip the dirty and vaild rects
                        // to that size. This ensures that native compositors will
                        // pass sanity checks (Bug 1971296).
                        if let ResolvedSurfaceTexture::Native { size, .. } = surface {
                            let surface_size_rect = <DeviceIntRect>::from_size(*size);
                            dirty_rect = dirty_rect.intersection(&surface_size_rect).unwrap_or_default();
                            valid_rect = valid_rect.intersection(&surface_size_rect).unwrap_or_default();
                        }

                        let batcher = AlphaBatchBuilder::new(
                            screen_size,
                            ctx.break_advanced_blend_batches,
                            ctx.batch_lookback_count,
                            task_id,
                            task_id.into(),
                            &ctx.frame_memory,
                        );

                        let mut batch_builder = BatchBuilder::new(batcher);

                        cmd_buffer.iter_prims(&mut |cmd, spatial_node_index, segments| {
                            batch_builder.add_prim_to_batch(
                                cmd,
                                spatial_node_index,
                                ctx,
                                gpu_cache,
                                render_tasks,
                                prim_headers,
                                transforms,
                                pic_task.raster_spatial_node_index,
                                pic_task.surface_spatial_node_index,
                                z_generator,
                                prim_instances,
                                gpu_buffer_builder,
                                segments,
                            );
                        });

                        let batcher = batch_builder.finalize();

                        let mut batch_containers = ctx.frame_memory.new_vec();
                        let mut alpha_batch_container = AlphaBatchContainer::new(
                            Some(dirty_rect),
                            &ctx.frame_memory
                        );

                        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: pic_task.clear_color,
                            kind: PictureCacheTargetKind::Draw {
                                alpha_batch_container,
                            },
                            dirty_rect,
                            valid_rect,
                        };

                        pass.picture_cache.push(target);
                    }
                    RenderTaskKind::TileComposite(ref tile_task) => {
                        let mut dirty_rect = tile_task.scissor_rect;
                        let mut valid_rect = tile_task.valid_rect;
                        // If we have a surface size, clip the dirty and vaild rects
                        // to that size. This ensures that native compositors will
                        // pass sanity checks (Bug 1971296).
                        if let ResolvedSurfaceTexture::Native { size, .. } = surface {
                            let surface_size_rect = <DeviceIntRect>::from_size(*size);
                            dirty_rect = dirty_rect.intersection(&surface_size_rect).unwrap_or_default();
                            valid_rect = valid_rect.intersection(&surface_size_rect).unwrap_or_default();
                        }

                        let target = PictureCacheTarget {
                            surface: surface.clone(),
                            clear_color: Some(tile_task.clear_color),
                            kind: PictureCacheTargetKind::Blit {
                                task_id: tile_task.task_id.expect("bug: no source task_id set"),
                                sub_rect_offset: tile_task.sub_rect_offset,
                            },
                            dirty_rect,
                            valid_rect,
                        };

                        pass.picture_cache.push(target);
                    }
                    _ => {
                        unreachable!();
                    }
                };
            }
            SubPassSurface::Persistent { surface: StaticRenderTaskSurface::TextureCache { target_kind, texture, .. } } => {
                let texture = pass.texture_cache
                    .entry(texture)
                    .or_insert_with(||
                        RenderTarget::new(
                            target_kind,
                            true,
                            texture,
                            screen_size,
                            gpu_supports_fast_clears,
                            None,
                            &ctx.frame_memory
                        )
                    );
                for task_id in &sub_pass.task_ids {
                    texture.add_task(
                        *task_id,
                        ctx,
                        gpu_cache,
                        gpu_buffer_builder,
                        render_tasks,
                        clip_store,
                        transforms,
                    );
                }
            }
            SubPassSurface::Persistent { surface: StaticRenderTaskSurface::ReadOnly { .. } } => {
                panic!("Should not create a render pass for read-only task locations.");
            }
        }
    }

    pass.color.build(
        ctx,
        gpu_cache,
        render_tasks,
        prim_headers,
        transforms,
        z_generator,
        prim_instances,
        cmd_buffers,
        gpu_buffer_builder,
    );
    pass.alpha.build(
        ctx,
        gpu_cache,
        render_tasks,
        prim_headers,
        transforms,
        z_generator,
        prim_instances,
        cmd_buffers,
        gpu_buffer_builder,
    );

    for target in &mut pass.texture_cache.values_mut() {
        target.build(
            ctx,
            gpu_cache,
            render_tasks,
            prim_headers,
            transforms,
            z_generator,
            prim_instances,
            cmd_buffers,
            gpu_buffer_builder,
        );
    }

    pass
}

/// A rendering-oriented representation of the frame built by the render backend
/// and presented to the renderer.
///
/// # Safety
///
/// The frame's allocator memory must be dropped after all of the frame's containers.
/// This is handled in the renderer and in `RenderedDocument`'s Drop implementation.
#[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 present: bool,
    pub passes: FrameVec<RenderPass>,

    pub transform_palette: FrameVec<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: FrameVec<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,

    /// Main GPU data buffer constructed (primarily) during the prepare
    /// pass for primitives that were visible and dirty.
    pub gpu_buffer_f: GpuBufferF,
    pub gpu_buffer_i: GpuBufferI,

    /// The backing store for the frame's allocator.
    ///
    /// # Safety
    ///
    /// Must not be dropped while frame allocations are alive.
    ///
    /// Rust has deterministic drop order [1]. We rely on `allocator_memory`
    /// being the last member of the `Frame` struct so that it is dropped
    /// after the frame's containers.
    ///
    /// [1]: https://doc.rust-lang.org/reference/destructors.html
    pub allocator_memory: FrameMemory,
}

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()
    }
}
