From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- gfx/wr/webrender/src/render_task_graph.rs | 1234 +++++++++++++++++++++++++++++ 1 file changed, 1234 insertions(+) create mode 100644 gfx/wr/webrender/src/render_task_graph.rs (limited to 'gfx/wr/webrender/src/render_task_graph.rs') 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..29ecf66a2e --- /dev/null +++ b/gfx/wr/webrender/src/render_task_graph.rs @@ -0,0 +1,1234 @@ +// 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/. + +//! This module contains the render task graph. +//! +//! Code associated with creating specific render tasks is in the render_task +//! module. + +use api::units::*; +use api::ImageFormat; +use crate::gpu_cache::{GpuCache, GpuCacheAddress}; +use crate::internal_types::{TextureSource, CacheTextureId, FastHashMap, FastHashSet, FrameId}; +use crate::render_task::{StaticRenderTaskSurface, RenderTaskLocation, RenderTask}; +use crate::render_target::RenderTargetKind; +use crate::render_task::{RenderTaskData, RenderTaskKind}; +use crate::resource_cache::ResourceCache; +use crate::texture_pack::GuillotineAllocator; +use crate::prim_store::DeferredResolve; +use crate::image_source::{resolve_image, resolve_cached_render_task}; +use crate::util::VecHelper; +use smallvec::SmallVec; +use std::mem; +use topological_sort::TopologicalSort; + +use crate::render_target::{RenderTargetList, ColorRenderTarget}; +use crate::render_target::{PictureCacheTarget, TextureCacheRenderTarget, AlphaRenderTarget}; +use crate::util::Allocation; +use std::{usize, f32}; + +/// 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; + +/// 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 expensive 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)] +#[derive(MallocSizeOf)] +#[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, + }; +} + +#[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 - 1); + pub const INVALID: PassId = PassId(!0 - 2); +} + +/// 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, + /// The pass that we can free this surface after (guaranteed + /// to be the same for all tasks assigned to this surface) + free_after: PassId, +} + +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, + free_after: PassId, + ) -> Option { + if self.kind == kind && self.is_shared == is_shared && self.free_after == free_after { + 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, +} + +/// 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, + /// The subpasses that make up this dependency pass + pub sub_passes: Vec, + /// A list of intermediate surfaces that can be invalidated after + /// this pass completes. + pub textures_to_invalidate: Vec, +} + +/// The RenderTaskGraph is the immutable representation of the render task graph. It is +/// built by the RenderTaskGraphBuilder, and is constructed once per frame. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct RenderTaskGraph { + /// List of tasks added to the graph + pub tasks: Vec, + + /// The passes that were created, based on dependencies between tasks + pub passes: Vec, + + /// 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, + + /// 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, +} + +/// The persistent interface that is used during frame building to construct the +/// frame graph. +pub struct RenderTaskGraphBuilder { + /// List of tasks added to the builder + tasks: Vec, + + /// List of task roots + roots: FastHashSet, + + /// 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, + + // Keep a map of `texture_id` to metadata about surfaces that are currently + // borrowed from the render target pool. + active_surfaces: FastHashMap, +} + +impl RenderTaskGraphBuilder { + /// Construct a new graph builder. Typically constructed once and maintained + /// over many frames, to avoid extra heap allocations where possible. + pub fn new() -> Self { + RenderTaskGraphBuilder { + tasks: Vec::new(), + roots: FastHashSet::default(), + frame_id: FrameId::INVALID, + textures_to_free: FastHashSet::default(), + active_surfaces: FastHashMap::default(), + } + } + + 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); + } + + /// 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, + deferred_resolves: &mut Vec, + max_shared_surface_size: i32, + ) -> RenderTaskGraph { + // 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 = RenderTaskGraph { + 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(), + }; + + // First, use a topological sort of the dependency graph to split the task set in to + // a list of passes. This is necessary because when we have a complex graph (e.g. due + // to a large number of sibling backdrop-filter primitives) traversing it via a simple + // recursion can be too slow. 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 + // or reused during subsequent passes). + + let mut pass_count = 0; + let mut passes = Vec::new(); + let mut task_sorter = TopologicalSort::::new(); + + // Iterate the task list, and add all the dependencies to the topo sort + for (parent_id, task) in graph.tasks.iter().enumerate() { + let parent_id = RenderTaskId { index: parent_id as u32 }; + + for child_id in &task.children { + task_sorter.add_dependency( + parent_id, + *child_id, + ); + } + } + + // Pop the sorted passes off the topological sort + loop { + // Get the next set of tasks that can be drawn + let tasks = task_sorter.pop_all(); + + // If there are no tasks left, we're done + if tasks.is_empty() { + // If the task sorter itself isn't empty but we couldn't pop off any + // tasks, that implies a circular dependency in the task graph + assert!(task_sorter.is_empty()); + break; + } else { + // Assign the `render_on` field to the task + for task_id in &tasks { + graph.tasks[task_id.index as usize].render_on = PassId(pass_count); + } + + // Store the task list for this pass, used later for `assign_free_pass`. + passes.push(tasks); + pass_count += 1; + } + } + + // Always create at least one pass for root tasks + pass_count = pass_count.max(1); + + // Determine which pass each task can be freed on, which depends on which is + // the last task that has this as an input. This must be done in top-down + // pass order to ensure that RenderTaskLocation::Existing references are + // visited in the correct order + for pass in passes { + for task_id in pass { + assign_free_pass( + task_id, + &mut graph, + ); + } + } + + // Construct passes array for tasks to be assigned to below + for _ in 0 .. pass_count { + 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() { + if task.kind.is_a_rendering_operation() { + 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_location = graph.tasks[task_id.index as usize].location.clone(); + + match task_location { + RenderTaskLocation::Unallocated { size } => { + let task = &mut graph.tasks[task_id.index as usize]; + + let mut location = None; + let kind = task.kind.target_kind(); + + // If a task is used as part of an existing-chain then we can't + // safely share it (nor would we want to). + let can_use_shared_surface = + task.kind.can_use_shared_surface() && + task.free_after != PassId::INVALID; + + 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, task.free_after) { + location = Some((texture_id, p)); + *used_rect = used_rect.union(&DeviceIntRect::from_origin_and_size(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, + free_after: task.free_after, + }; + + // Allocation of the task must fit in this new surface! + let p = surface.alloc_rect( + size, + kind, + can_use_shared_surface, + task.free_after, + ).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::from_origin_and_size(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::from_origin_and_size(location.unwrap().1, size), + }; + } + RenderTaskLocation::Existing { parent_task_id, size: existing_size, .. } => { + let parent_task_location = graph.tasks[parent_task_id.index as usize].location.clone(); + + match parent_task_location { + RenderTaskLocation::Unallocated { .. } | + RenderTaskLocation::CacheRequest { .. } | + RenderTaskLocation::Existing { .. } => { + panic!("bug: reference to existing task must be allocated by now"); + } + RenderTaskLocation::Dynamic { texture_id, rect, .. } => { + assert_eq!(existing_size, rect.size()); + + let kind = graph.tasks[parent_task_id.index as usize].kind.target_kind(); + + // A sub-pass is always created in this case, as existing tasks by definition can't be shared. + pass.sub_passes.push(SubPass { + surface: SubPassSurface::Dynamic { + texture_id, + target_kind: kind, + used_rect: rect, // clear will be skipped due to no-op check anyway + }, + task_ids: vec![*task_id], + }); + + let task = &mut graph.tasks[task_id.index as usize]; + task.location = parent_task_location; + } + RenderTaskLocation::Static { .. } => { + unreachable!("bug: not possible since we don't dup static locations"); + } + } + } + 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::CacheRequest { .. } => { + // No need to allocate nor to create a sub-path for read-only locations. + } + 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 { .. } | + RenderTaskLocation::Existing { .. } => 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 { .. } => {} + RenderTaskLocation::CacheRequest { .. } => {} + } + } + } + + // 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 { + // First check whether the render task texture and uv rects are managed + // externally. This is the case for image tasks and cached tasks. In both + // cases it results in a finding the information in the texture cache. + let cache_item = if let Some(ref cache_handle) = task.cache_handle { + Some(resolve_cached_render_task( + cache_handle, + resource_cache, + )) + } else if let RenderTaskKind::Image(request) = &task.kind { + Some(resolve_image( + *request, + resource_cache, + gpu_cache, + deferred_resolves, + )) + } else { + // General case (non-cached non-image tasks). + None + }; + + if let Some(cache_item) = cache_item { + // Update the render task even if the item is invalid. + // We'll handle it later and it's easier to not have to + // deal with unexpected location variants like + // RenderTaskLocation::CacheRequest when we do. + let source = cache_item.texture_id; + task.uv_rect_handle = cache_item.uv_rect_handle; + task.location = RenderTaskLocation::Static { + surface: StaticRenderTaskSurface::ReadOnly { source }, + rect: cache_item.uv_rect, + }; + } + // Give the render task an opportunity to add any + // information to the GPU cache, if appropriate. + let target_rect = task.get_target_rect(); + + task.write_gpu_blocks( + target_rect, + gpu_cache, + ); + + graph.task_data.push( + task.kind.write_task_data(target_rect) + ); + } + + graph + } +} + +impl RenderTaskGraph { + /// Print the render task graph to console + #[allow(dead_code)] + pub fn print( + &self, + ) { + debug!("-- RenderTaskGraph --"); + + for (i, task) in self.tasks.iter().enumerate() { + debug!("Task {} [{}]: render_on={} free_after={} children={:?}", + i, + task.kind.as_str(), + task.render_on.0, + task.free_after.0, + task.children, + ); + } + + for (p, pass) in self.passes.iter().enumerate() { + debug!("Pass {}:", p); + + for (s, sub_pass) in pass.sub_passes.iter().enumerate() { + debug!("\tSubPass {}: {:?}", + s, + sub_pass.surface, + ); + + for task_id in &sub_pass.task_ids { + debug!("\t\tTask {:?}", task_id.index); + } + } + } + } + + pub fn resolve_texture( + &self, + task_id: impl Into>, + ) -> Option { + let task_id = task_id.into()?; + let task = &self[task_id]; + + match task.get_texture_source() { + TextureSource::Invalid => None, + source => Some(source), + } + } + + pub fn resolve_location( + &self, + task_id: impl Into>, + gpu_cache: &GpuCache, + ) -> Option<(GpuCacheAddress, TextureSource)> { + self.resolve_impl(task_id.into()?, gpu_cache) + } + + fn resolve_impl( + &self, + task_id: RenderTaskId, + gpu_cache: &GpuCache, + ) -> Option<(GpuCacheAddress, TextureSource)> { + let task = &self[task_id]; + let texture_source = task.get_texture_source(); + + if let TextureSource::Invalid = texture_source { + return None; + } + + let uv_address = task.get_texture_address(gpu_cache); + + Some((uv_address, texture_source)) + } + + + #[cfg(test)] + pub fn new_for_testing() -> Self { + RenderTaskGraph { + tasks: Vec::new(), + passes: Vec::new(), + frame_id: FrameId::INVALID, + task_data: Vec::new(), + surface_count: 0, + unique_surfaces: FastHashSet::default(), + } + } + + /// 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 for RenderTaskGraph { + type Output = RenderTask; + fn index(&self, id: RenderTaskId) -> &RenderTask { + &self.tasks[id.index as usize] + } +} + +fn assign_free_pass( + id: RenderTaskId, + graph: &mut RenderTaskGraph, +) { + let task = &mut graph.tasks[id.index as usize]; + let render_on = task.render_on; + + let mut child_task_ids: SmallVec<[RenderTaskId; 8]> = SmallVec::new(); + child_task_ids.extend_from_slice(&task.children); + + for child_id in child_task_ids { + let child_location = graph.tasks[child_id.index as usize].location.clone(); + + // 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_location { + RenderTaskLocation::CacheRequest { .. } => {} + RenderTaskLocation::Static { .. } => { + // never get freed anyway, so can leave untouched + // (could validate that they remain at PassId::MIN) + } + RenderTaskLocation::Dynamic { .. } => { + panic!("bug: should not be allocated yet"); + } + RenderTaskLocation::Unallocated { .. } => { + let child_task = &mut graph.tasks[child_id.index as usize]; + + if child_task.free_after != PassId::INVALID { + child_task.free_after = child_task.free_after.min(render_on); + } + } + RenderTaskLocation::Existing { parent_task_id, .. } => { + let parent_task = &mut graph.tasks[parent_task_id.index as usize]; + parent_task.free_after = PassId::INVALID; + + let child_task = &mut graph.tasks[child_id.index as usize]; + + if child_task.free_after != PassId::INVALID { + child_task.free_after = child_task.free_after.min(render_on); + } + } + } + } +} + +/// 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, + pub color: RenderTargetList, + pub texture_cache: FastHashMap, + pub picture_cache: Vec, + pub textures_to_invalidate: Vec, +} + +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(), + textures_to_invalidate: src.textures_to_invalidate.clone(), + } + } +} + +// Dump an SVG visualization of the render graph for debugging purposes +#[cfg(feature = "capture")] +pub fn dump_render_tasks_as_svg( + render_tasks: &RenderTaskGraph, + 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 render_tasks.passes.iter().rev() { + let mut layout = VerticalLayout::new(x, margin, node_width); + + for task_id in &pass.task_ids { + 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(); + } +} + +/// 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 RenderTaskGraphBuilder { + fn test_expect( + mut self, + pass_count: usize, + total_surface_count: usize, + unique_surfaces: &[(i32, i32, ImageFormat)], + ) { + use crate::internal_types::FrameStamp; + use api::{DocumentId, IdNamespace}; + + let mut rc = ResourceCache::new_for_testing(); + let mut gc = GpuCache::new(); + + let mut frame_stamp = FrameStamp::first(DocumentId::new(IdNamespace(1), 1)); + frame_stamp.advance(); + gc.prepare_for_frames(); + gc.begin_frame(frame_stamp); + + let g = self.end_frame(&mut rc, &mut gc, &mut Vec::new(), 2048); + 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 = RenderTaskGraphBuilder::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_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 = RenderTaskGraphBuilder::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 = RenderTaskGraphBuilder::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 = RenderTaskGraphBuilder::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, &[ + (2048, 2048, 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 = RenderTaskGraphBuilder::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 = RenderTaskGraphBuilder::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, &[ + (2048, 2048, ImageFormat::RGBA8), + (2048, 2048, ImageFormat::RGBA8), + (2048, 2048, ImageFormat::RGBA8), + ]); +} -- cgit v1.2.3