/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{units::*, ClipMode, ColorF}; use euclid::{Scale, point2};
/// This type reflects the unfortunate situation with quad coordinates where we /// sometimes use layout and sometimes device coordinates. pubtype LayoutOrDeviceRect = api::euclid::default::Box2D<f32>;
const MIN_AA_SEGMENTS_SIZE: f32 = 4.0; const MIN_QUAD_SPLIT_SIZE: f32 = 256.0; // We merge compatible neighbor tiles in the same rows which means that allowing // more tiles on the x axis doesn't generally produce more tiles, but it allows // more precise segmentation. // The segmentation right now is still quite coarse. const MAX_TILES_PER_QUAD_X: usize = 8; const MAX_TILES_PER_QUAD_Y: usize = 4;
// The local to device transform as a scale+offset transform if it // can be represented as such. pubfn as_2d_scale_offset(&self) -> Option<&ScaleOffset> { self.as_scale_offset.as_ref()
}
// X and Y scale facotrs of the local to device transform. pubfn scale_factors(&self) -> (f32, f32) { let s = self.map_prim_to_raster.scale_factors();
/// Describes how clipping affects the rendering of a quad primitive. /// /// As a general rule, parts of the quad that require masking are prerendered in an /// intermediate target and the mask is applied using multiplicative blending to /// the intermediate result before compositing it into the destination target. /// /// Each segment can opt in or out of masking independently. #[derive(Debug, Copy, Clone)] pubenum QuadRenderStrategy { /// The quad is not affected by any mask and is drawn directly in the destination /// target.
Direct, /// The quad is drawn entirely in an intermediate target and a mask is applied /// before compositing in the destination target.
Indirect, /// A rounded rectangle clip is applied to the quad primitive via a nine-patch. /// The segments of the nine-patch that require a mask are rendered and masked in /// an intermediate target, while other segments are drawn directly in the destination /// target.
NinePatch {
radius: LayoutVector2D,
clip_rect: LayoutRect,
}, /// Split the primitive into coarse tiles so that each tile independently /// has the opportunity to be drawn directly in the destination target or /// via an intermediate target if it is affected by a mask.
Tiled,
}
// This could move back into preapre_quad_impl if it took the tile's // coverage rect into account rather than the whole primitive's, but // for now it does the latter so we might as well not do the work // multiple times. let strategy = match cache_key {
Some(_) => QuadRenderStrategy::Indirect,
None => get_prim_render_strategy(
transform.prim_spatial_node_index(),
clip_chain,
frame_state.clip_store,
interned_clips,
transform.is_2d_scale_offset(),
pattern_ctx.spatial_tree,
),
};
let needs_repetition = stretch_size.width < local_rect.width()
|| stretch_size.height < local_rect.height();
if !needs_repetition { // The stretch size may be larger than the local rect's size which // should resut in some stretching (without repetitions). However, // the non-repeated quad code paths don't take a stretch_size, so // we bake it into the local rect and make sure that the local clip // prevents the primitive from overflowing its initial bounds. let local_clip_rect = local_clip_rect.intersection_unchecked(&local_rect); let local_rect = LayoutRect::from_origin_and_size(
local_rect.min,
stretch_size,
);
// If the source pattern is an image, we can repeat it directly using the repeat // shader, without an extra render task. let src_task_id = pattern.as_render_task();
// If the number of repetitions is high, we are better off using the repeat shader, // but we want to avoid the extra render task if it is large. let num_repetitions = local_rect.area() / stretch_size.area(); let repeat_using_a_shader = src_task_id.is_some()
|| (num_repetitions > 16.0 && surface_rect.width() < 1024.0 && surface_rect.height() < 1024.0)
|| (num_repetitions > 64.0 && surface_rect.area() < 1024.0 * 1024.0);
if repeat_using_a_shader { let (src_task_id, base_color) = match src_task_id {
Some(task) => (task, pattern.base_color),
None => { // The source is not an image. Make it one by rendering // the pattern in a render task.
// Note: caching is disabled when using the repeating shader. // The cache key would need more information about the repetition.
prepare_quad_impl(
strategy,
&repeat_pattern,
local_rect,
local_clip_rect,
aligned_aa_edges,
transfomed_aa_edges,
prim_instance_index,
&None,
clip_chain,
transform,
&pattern_ctx,
pic_context,
targets,
interned_clips,
frame_state,
scratch,
);
return;
}
// Repeat by duplicating the primitive.
let visible_rect = compute_conservative_visible_rect(
clip_chain,
frame_state.current_dirty_region().combined,
frame_state.current_dirty_region().visibility_spatial_node,
transform.prim_spatial_node_index(),
frame_context.spatial_tree,
).intersection_unchecked(local_clip_rect);
let stride = stretch_size + tile_spacing; let repetitions = crate::image_tiling::repetitions(&local_rect, &visible_rect, stride); for tile in repetitions { let tile_rect = LayoutRect::from_origin_and_size(tile.origin, stretch_size); let clip_rect = local_clip_rect.intersection_unchecked(&tile_rect); let pattern_offset = tile.origin - local_rect.min; let pattern = pattern_builder.build(
None,
pattern_offset,
&pattern_ctx,
&mut PatternBuilderState {
frame_gpu_data: frame_state.frame_gpu_data,
transforms: frame_state.transforms,
},
);
// The indirect transform drives the resolution at which each segment is going // going to be rasterized in intermediate render tasks. let scales = transform.scale_factors(); let base_indirect_transform = ScaleOffset::from_scale(scales.into());
nine_patch.for_each_segment(local_rect, &mut|dst_rect, src_rect, side, _repeat_h, _repeat_v| { // First find the sub-rect of the source pattern that this segment is using. let min_x = local_rect.min.x + stretch_size.width * src_rect.uv0.x; let min_y = local_rect.min.y + stretch_size.height * src_rect.uv0.y; let max_x = local_rect.min.x + stretch_size.width * src_rect.uv1.x; let max_y = local_rect.min.y + stretch_size.height * src_rect.uv1.y; let pattern_rect = LayoutRect {
min: point2(min_x, min_y),
max: point2(max_x, max_y),
};
// Rasterize the source pattern into a render task.
// We could get away without the intermediate task in some cases, for example // if the segment does not repeat the pattern. However this is fiddly due to // how the nine-patch's source slicing distorts the local space of the pattern. // Always using an intermediate render task lets us easily handle the additional // stretching effect on the image instead of introducing an additional transform // for the pattern's coordinate space. On the other hand it means that we have // to handle large source patterns and potentially down-scale them.
frame_state: &mut FrameBuildingState,
scratch: &mut PrimitiveScratchBuffer,
) { // If the local-to-device transform can be expressed as a 2D scale-offset, // We'll apply the transformation on the CPU and submit geometry in device // space to the shaders. Otherwise, the geometry is sent to the shaders in // layout space along with a transform.
let transform_id = if transform.is_2d_scale_offset() {
GpuTransformId::IDENTITY
} else {
frame_state.transforms.gpu.get_id_with_post_scale(
transform.prim_spatial_node_index(),
transform.raster_spatial_node_index(),
transform.device_pixel_scale().get(),
ctx.spatial_tree,
)
};
let prim_is_2d_scale_translation = transform.is_2d_scale_offset(); let prim_is_2d_axis_aligned = transform.is_2d_axis_aligned();
letmut quad_flags = QuadFlags::empty();
// Only use AA edge instances if the primitive is large enough to require it let prim_size = local_rect.size(); if prim_size.width > MIN_AA_SEGMENTS_SIZE && prim_size.height > MIN_AA_SEGMENTS_SIZE {
quad_flags |= QuadFlags::USE_AA_SEGMENTS;
}
let needs_scissor = !prim_is_2d_scale_translation; if !needs_scissor {
quad_flags |= QuadFlags::APPLY_RENDER_TASK_CLIP;
}
let aa_flags = if prim_is_2d_axis_aligned {
aligned_aa_edges
} else {
transfomed_aa_edges
};
// We round the coordinates of non-antialiased edges of the primitive. // This allows us to ensure that indirect axis-aligned primitives cover the render // task exactly. Since we do this for indirect primitives, we have to also do it for // other rendering strategies to avoid cracks between side-by-side primitives. let round_edges = !aa_flags;
let main_prim_address = frame_state.frame_gpu_data.f32.push(&quad);
// Render the primitive as a single instance. Coordinates are provided to the // shader in layout space.
frame_state.push_prim(
&PrimitiveCommand::quad(
pattern.kind,
pattern.shader_input,
pattern.texture_input.task_id, crate::prim_store::storage::Index::from_u32(prim_instance_index.0),
main_prim_address,
transform_id,
quad_flags,
aa_flags,
pattern.blend_mode,
),
transform.prim_spatial_node_index(),
targets,
);
// If the pattern samples from a texture, add it as a dependency // of the surface we're drawing directly on to. if pattern.texture_input.task_id != RenderTaskId::INVALID {
frame_state
.surface_builder
.add_child_render_task(pattern.texture_input.task_id, frame_state.rg_builder);
}
return;
}
let surface = &mut frame_state.surfaces[pic_context.surface_index.0]; let clipped_pic_rect = clip_chain.pic_coverage_rect.intersection_unchecked(&surface.clipping_rect);
let pic_to_raster = SpaceMapper::new_with_target(
surface.raster_spatial_node_index,
surface.surface_spatial_node_index,
RasterRect::max_rect(),
ctx.spatial_tree,
); let Some(clipped_raster_rect) = pic_to_raster.map(&clipped_pic_rect) else { return; };
// TODO: we are making the assumption that raster space and world space have the same // scale. I think that it is the case, but it's not super clean. let device_scale: Scale<f32, RasterPixel, DevicePixel> = Scale::new(transform.device_pixel_scale.0);
// Rounding is important here because clipped_surface_rect.min may be used as the origin // of render tasks. Fractional values would introduce fractional offsets in the render tasks. letmut clipped_surface_rect = (clipped_raster_rect * device_scale).round();
let main_prim_address = frame_state.frame_gpu_data.f32.push(&quad);
letmut clipped_surface_rect = *clipped_surface_rect; if local_to_device_scale_offset.is_some() && aa_flags.is_empty() { // If the primitive has a simple transform, then quad.clip is in device space // and is a strict subset of clipped_surface_rect. If there is no anti-aliasing, // and the pattern is opaque, we want to ensure that the primitive covers the // entire render task so that we can safely skip clearing it. // In this situation, create_quad_primitive has rounded the edges of quad.clip // so we are not introducing a fractional offset in clipped_surface_rect.
clipped_surface_rect = quad.clip.cast_unit();
}
let task_size = clipped_surface_rect.size().to_i32(); if task_size.is_empty() { return None;
}
// TODO: re-land clip-out mode. let mode = ClipMode::Clip;
if pattern.is_opaque {
quad_flags |= QuadFlags::IS_OPAQUE;
}
fn should_create_task(mode: ClipMode, x: usize, y: usize) -> bool { match mode { // Only create render tasks for the corners.
ClipMode::Clip => x != 1 && y != 1, // Create render tasks for all segments (the // center will be skipped).
ClipMode::ClipOut => true,
}
}
fn prepare_tiles(
prim_instance_index: PrimitiveInstanceIndex,
local_rect: &LayoutRect,
local_clip_rect: &LayoutRect,
device_clip_rect: &DeviceRect,
pattern: &Pattern, mut quad_flags: QuadFlags,
aa_flags: EdgeMask,
clip_chain: &ClipChainInstance,
gpu_transform: GpuTransformId,
transform: &mut QuadTransformState,
pic_context: &PictureContext,
ctx: &PatternBuilderContext,
interned_clips: &DataStore<ClipIntern>,
frame_state: &mut FrameBuildingState,
scratch: &mut PrimitiveScratchBuffer,
targets: &[CommandBufferIndex],
) { // Render the primtive as a grid of tiles decomposed in device space. // Tiles that need it are drawn in a render task and then composited into the // destination picture. // The coordinates are provided to the shaders: // - in layout space for the render task, // - in device space for the instances that draw into the destination picture.
let surface = &mut frame_state.surfaces[pic_context.surface_index.0];
surface.map_local_to_picture.set_target_spatial_node(
transform.prim_spatial_node_index(),
ctx.spatial_tree,
);
let unclipped_surface_rect = device_clip_rect.round_out();
let force_masks = !transform.is_2d_scale_offset(); // Set up the tile classifier for the params of this quad
scratch.retained.quad_tile_classifier.reset(
unclipped_surface_rect,
force_masks,
);
// Walk each clip, extract the local mask regions and add them to the tile classifier. for i in0 .. clip_chain.clips_range.count { let clip_instance = frame_state.clip_store.get_instance_from_range(&clip_chain.clips_range, i); let clip_node = &interned_clips[clip_instance.handle];
let transform = match clip_to_raster.as_2d_scale_offset() {
Some(t) => t.then_scale(transform.device_pixel_scale.0),
None => { // If the clip transform is not axis-aligned, just assume the entire primitive // is affected by the clip, for now. // TODO: If we take this path, it means that we would have been better-off using // the indirect rendering strategy.
scratch.retained.quad_tile_classifier.add_mask_region(unclipped_surface_rect); continue;
}
};
// A rect clip in the same coordinate system as the primitive is folded // into the local clip rect and applied directly by the pattern shader, so // tiles straddling its boundary don't need a clip mask. let applied_as_local_clip = clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM);
// Add regions to the classifier depending on the clip kind match clip_node.item.kind {
ClipItemKind::Rectangle { mode } => { let rect = transform.map_rect(&clip_instance.clip_rect);
scratch.retained.quad_tile_classifier.add_clip_rect(rect, mode, applied_as_local_clip);
}
ClipItemKind::RoundedRectangle { mode: ClipMode::Clip, ref radius } => { // For rounded-rects with Clip mode, we need a mask for each corner, // and to add the clip rect itself (to cull tiles outside that rect)
// Map the local rect and radii let radius = clamped_radius(radius, clip_instance.clip_rect.size()); let clip_device_rect = transform.map_rect(&clip_instance.clip_rect); // If the transform has a negative scale, the rect will be correctly // flipped by the transform so that it isn't empty, but the sizes will // be negative. Make sure that the size stay positive. let r_tl = transform.map_size(&radius.top_left).abs(); let r_tr = transform.map_size(&radius.top_right).abs(); let r_br = transform.map_size(&radius.bottom_right).abs(); let r_bl = transform.map_size(&radius.bottom_left).abs();
// Construct the mask regions for each corner let c_tl = DeviceRect::from_origin_and_size(
clip_device_rect.min,
r_tl,
); let c_tr = DeviceRect::from_origin_and_size(
DevicePoint::new(
clip_device_rect.max.x - r_tr.width,
clip_device_rect.min.y,
),
r_tr,
); let c_br = DeviceRect::from_origin_and_size(
DevicePoint::new(
clip_device_rect.max.x - r_br.width,
clip_device_rect.max.y - r_br.height,
),
r_br,
); let c_bl = DeviceRect::from_origin_and_size(
DevicePoint::new(
clip_device_rect.min.x,
clip_device_rect.max.y - r_bl.height,
),
r_bl,
);
scratch.retained.quad_tile_classifier.add_clip_rect(clip_device_rect, ClipMode::Clip, applied_as_local_clip);
scratch.retained.quad_tile_classifier.add_mask_region(c_tl);
scratch.retained.quad_tile_classifier.add_mask_region(c_tr);
scratch.retained.quad_tile_classifier.add_mask_region(c_br);
scratch.retained.quad_tile_classifier.add_mask_region(c_bl);
}
ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, ref radius } => { let radius = clamped_radius(radius, clip_instance.clip_rect.size()); // Try to find an inner rect within the clip-out rounded rect that we can // use to cull inner tiles. If we can't, the entire rect needs to be masked match extract_inner_rect_k(&clip_instance.clip_rect, &radius, 0.5) {
Some(ref inner_rect) => { let rect = transform.map_rect(inner_rect);
scratch.retained.quad_tile_classifier.add_clip_rect(rect, ClipMode::ClipOut, false);
}
None => { let clip_device_rect = transform.map_rect(&clip_instance.clip_rect);
scratch.retained.quad_tile_classifier.add_mask_region(clip_device_rect);
}
}
}
ClipItemKind::Image { .. } => {
panic!("bug: image clips unexpected in this path");
}
}
}
// Classify each tile within the quad to be Pattern / Mask / Clipped
scratch.frame.quad_direct_segments.clear();
scratch.frame.quad_indirect_segments.clear();
let tiles = scratch.retained.quad_tile_classifier.classify(); for tile in tiles { // Check whether this tile requires a mask let is_direct = match tile.kind {
QuadTileKind::Clipped => { // Note: We shouldn't take this branch since clipped tiles are // filtered out by the iterator. continue;
}
QuadTileKind::Pattern { has_mask } => !has_mask,
};
// At extreme scales the rect can round to zero size due to // f32 precision, causing a panic in new_dynamic, so just // skip segments that would produce zero size tasks. // https://bugzilla.mozilla.org/show_bug.cgi?id=1941838#c13 let tile_size = tile.rect.size().to_i32(); if tile_size.is_empty() { continue;
}
if is_direct {
scratch.frame.quad_direct_segments.push(QuadSegment {
rect: tile.rect.cast_unit(),
task_id: RenderTaskId::INVALID
});
} else { if pattern.is_opaque {
quad_flags |= QuadFlags::IS_OPAQUE;
}
if !scratch.frame.quad_direct_segments.is_empty() { // Nine-patch segments are only allowed for axis-aligned primitives. let local_to_device = transform.as_2d_scale_offset().unwrap();
let device_prim_rect: DeviceRect = local_to_device.map_rect(&local_rect);
if pattern.texture_input.task_id != RenderTaskId::INVALID { for segment in &mut scratch.frame.quad_direct_segments {
segment.task_id = pattern.texture_input.task_id;
}
}
// Both the nine-patch and tiled paths rely on axis-aligned primitive for now. // In the case of nine-patch this is currently a hard requirement, while the // tiling path works with non-axis-aligned primitives but less efficiently than // the indirect path since all tiles end up treated as masks. let try_split_prim = if prim_is_scale_offset { // TODO: we should compute this based on the (tightest possible) // rect in device space instead of a rect in picture space. let size = clip_chain.pic_coverage_rect.size();
size.width > MIN_QUAD_SPLIT_SIZE || size.height > MIN_QUAD_SPLIT_SIZE
} else { false
};
if !try_split_prim { return QuadRenderStrategy::Indirect;
}
if prim_is_scale_offset && clip_chain.clips_range.count == 1 { let clip_instance = clip_store.get_instance_from_range(&clip_chain.clips_range, 0); let clip_node = &interned_clips[clip_instance.handle];
iflet ClipItemKind::RoundedRectangle { ref radius, mode: ClipMode::Clip, .. } = clip_node.item.kind { let size = clip_instance.clip_rect.size(); let radius = clamped_radius(radius, size); let max_corner_width = radius.top_left.width
.max(radius.bottom_left.width)
.max(radius.top_right.width)
.max(radius.bottom_right.width); let max_corner_height = radius.top_left.height
.max(radius.bottom_left.height)
.max(radius.top_right.height)
.max(radius.bottom_right.height);
/// Adjust the transform and device rect until the latter fits the provided /// maximum size. /// Also ensure that near-zero size tasks do are at least fn adjust_indirect_pattern_resolution(
local_rect: &LayoutRect,
max_device_size: f32,
device_rect: &mut DeviceRect,
indirect_transform: &mut ScaleOffset,
) { // This catches invalid cases such as NaNs or zeroes that would have caused us // to loop forever. let valid = local_rect.width() > 0.0
&& local_rect.height() > 0.0
&& indirect_transform.scale.x != 0.0
&& indirect_transform.scale.y != 0.0;
if !valid { return;
}
// Down-scale until the render task fits in the provided maximum size. while device_rect.width() > max_device_size {
indirect_transform.scale.x *= 0.5;
*device_rect = indirect_transform.map_rect(local_rect);
} while device_rect.height() > max_device_size {
indirect_transform.scale.y *= 0.5;
*device_rect = indirect_transform.map_rect(local_rect);
}
// Up-scale until the render task size rounds to at least one pixel. while device_rect.width() <= 0.5 {
indirect_transform.scale.x *= 2.0;
*device_rect = indirect_transform.map_rect(local_rect);
} while device_rect.height() <= 0.5 {
indirect_transform.scale.y *= 2.0;
*device_rect = indirect_transform.map_rect(local_rect);
}
}
if (clip_chain.clips_range.count as usize) >= CACHE_MAX_CLIPS { return None;
}
let prim_spatial_node_index = transform.prim_spatial_node_index(); // The assumption is here is that the vast majority of transforms // are 2d scale offsets and that 3d ones tend to be animated, so // in order to keep the key small, we only attempt to cache when // the transform is a 2d scale offset. // This will miss some caching opportunities, but they should // hopefully be rare. let Some(transform) = transform.as_2d_scale_offset() else { return None;
};
letmut clip_uids = [!0; CACHE_MAX_CLIPS];
for i in0 .. clip_chain.clips_range.count { let clip_instance = clip_store.get_instance_from_range(&clip_chain.clips_range, i);
clip_uids[i as usize] = clip_instance.handle.uid().get_uid(); if clip_instance.spatial_node_index != prim_spatial_node_index { return None;
}
}
// If the pattern samples from a texture, add it as a dependency // of the indirect render task that relies on it. if pattern.texture_input.task_id != RenderTaskId::INVALID {
rg_builder.add_dependency(task_id, pattern.texture_input.task_id);
}
if clips_range.count > 0 { let task_rect = DeviceRect::from_origin_and_size(
content_origin,
task_size.to_f32(),
);
if is_opaque {
quad_flags |= QuadFlags::IS_OPAQUE;
}
frame_state.push_cmd(
&PrimitiveCommand::quad(
pattern.kind,
pattern.shader_input,
pattern.texture_input.task_id, crate::prim_store::storage::Index::from_u32(prim_instance_index.0),
prim_address,
GpuTransformId::IDENTITY,
quad_flags, // TODO(gw): No AA on composite, unless we use it to apply 2d clips
EdgeMask::empty(),
pattern.blend_mode,
),
targets,
);
}
// Note: At the primitive level we specify an invalid task ID here, which // may look suspicious since we are using the textured shader. However each // segment comes with its own render task id, and that's what the batching // code uses.
let quad_flags = QuadFlags::APPLY_RENDER_TASK_CLIP;
frame_state.push_cmd(
&PrimitiveCommand::quad(
PatternKind::ColorOrTexture,
PatternShaderInput( crate::pattern::TEXTURED_SHADER_MODE_TEXTURE, crate::pattern::TEXTURED_SHADER_MAP_TO_SEGMENT,
),
RenderTaskId::INVALID, crate::prim_store::storage::Index::from_u32(prim_instance_index.0),
composite_prim_address,
GpuTransformId::IDENTITY,
quad_flags, // TODO(gw): No AA on composite, unless we use it to apply 2d clips
EdgeMask::empty(),
blend_mode,
),
targets,
);
}
for i in0 .. clips_range.count { let clip_instance = clip_store.get_instance_from_range(&clips_range, i); let clip_item = &interned_clips[clip_instance.handle].item;
// TODO(gw): For now, we skip the main mask prim below for image masks. Perhaps // we can better merge the logic together? // TODO(gw): How to efficiently handle if the image-mask rect doesn't cover local prim rect? return;
}
};
let clip_spatial_node = spatial_tree.get_spatial_node(clip_instance.spatial_node_index); let raster_spatial_node = spatial_tree.get_spatial_node(raster_spatial_node_index); let raster_clip = raster_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id;
// See the documentation of RectangleClipSubTask::clip_space. let (clip_space, clip_transform_id, quad_address, quad_transform_id, is_same_coord_system) = if raster_clip { let quad_transform_id = GpuTransformId::IDENTITY; let pattern = Pattern::color(ColorF::WHITE);
// TODO: This transform could be set to identity in favor of using the // pattern transform which serves the same purpose and is cheaper since // is a scale-offset. In this code path the raster-to-clip transform is // guaranteed to be representable by a scale and offset. let clip_transform_id = transforms.gpu.get_id_with_pre_scale(
device_pixel_scale.inverse().get(),
raster_spatial_node_index,
clip_instance.spatial_node_index,
spatial_tree,
); let pattern_transform = ScaleOffset::identity();
let quad_transform_id = transforms.gpu.get_id_with_post_scale(
prim_spatial_node_index,
raster_spatial_node_index,
device_pixel_scale.get(),
spatial_tree,
);
// Conservatively inflate the clip's primitive to ensure that it covers potential // anti-aliasing pixels of the original primitive. 2.0 matches AA_PIXEL_RADIUS in // quad.glsl. let rect = prim_local_coverage_rect.inflate(2.0, 2.0);
// See the corresponfing #defines in ps_quad.glsl #[repr(u8)] enum PartIndex {
Center = 0,
Left = 1,
Top = 2,
Right = 3,
Bottom = 4,
All = 5,
}
let texture = match src_task_id {
RenderTaskId::INVALID => TextureSource::Invalid,
_ => match render_tasks.resolve_texture(src_task_id) {
Some(texture) => texture,
None => { // If a valid render task does not yield a texture source, render // nothing. This can happen, for example when a stacking context // could not be snapshotted. return;
},
}
};
// See QuadHeader in ps_quad.glsl letmut writer = gpu_buffer_builder.i32.write_blocks(QuadHeader::NUM_BLOCKS);
writer.push(&QuadHeader {
transform_id,
z_id,
pattern_input,
}); let prim_address_i = writer.finish();
let textures = BatchTextures::prim_textured(
texture,
TextureSource::Invalid,
);
let prim_blend_mode = if quad_flags.contains(QuadFlags::IS_OPAQUE)
&& blend_mode == BlendMode::PremultipliedAlpha
{
BlendMode::None
} else {
blend_mode
};
if edge_flags.is_empty() { // No antialisaing.
f(prim_batch_key, instance.into());
} elseif quad_flags.contains(QuadFlags::USE_AA_SEGMENTS) { // Add instances for the antialisaing. This gives the center part // an opportunity to stay in the opaque pass. if edge_flags.contains(EdgeMask::LEFT) { let instance = QuadInstance {
part_index: PartIndex::Left as u8,
..instance
};
f(aa_batch_key, instance.into());
} if edge_flags.contains(EdgeMask::TOP) { let instance = QuadInstance {
part_index: PartIndex::Top as u8,
..instance
};
f(aa_batch_key, instance.into());
} if edge_flags.contains(EdgeMask::RIGHT) { let instance = QuadInstance {
part_index: PartIndex::Right as u8,
..instance
};
f(aa_batch_key, instance.into());
} if edge_flags.contains(EdgeMask::BOTTOM) { let instance = QuadInstance {
part_index: PartIndex::Bottom as u8,
..instance
};
f(aa_batch_key, instance.into());
}
instance = QuadInstance {
part_index: PartIndex::Center as u8,
..instance
};
f(prim_batch_key, instance.into());
} else { // Render the anti-aliased quad with a single primitive.
f(aa_batch_key, instance.into());
}
}
/// Classification result for a tile within a quad #[allow(dead_code)] #[cfg_attr(feature = "capture", derive(Serialize))] #[derive(Debug, Copy, Clone, PartialEq)] pubenum QuadTileKind { // Clipped out - can be skipped
Clipped, // Requires the pattern only, can draw directly
Pattern {
has_mask: bool,
},
}
/// A `ClipMode::Clip` region registered with the tile classifier. #[cfg_attr(feature = "capture", derive(Serialize))] #[derive(Copy, Clone, Debug)] struct ClipInRegion {
rect: DeviceRect, // Whether tiles straddling the region's boundary require a clip mask. This is // false when the clip is already applied via the primitive's local clip rect // (i.e. it is in the same coordinate system as the primitive), in which case // the pattern shader clips those tiles directly and no mask is needed. Tiles // fully outside the region are always culled regardless of this flag.
needs_mask: bool,
}
/// A helper struct for classifying a set of tiles within a quad depending on /// what strategy they can be used to draw them. #[cfg_attr(feature = "capture", derive(Serialize))] pubstruct QuadTileClassifier {
buffer: [QuadTileInfo; MAX_TILES_PER_QUAD_X * MAX_TILES_PER_QUAD_Y],
mask_regions: Vec<DeviceRect>,
clip_in_regions: Vec<ClipInRegion>,
clip_out_regions: Vec<DeviceRect>,
rect: DeviceRect,
x_tiles: usize,
y_tiles: usize, // Treat all tiles that have some coverage as masked.
force_masks: bool,
}
/// Add an area that needs a clip mask / indirect area pubfn add_mask_region(
&mutself,
mask_region: DeviceRect,
) { if !mask_region.is_empty() { self.mask_regions.push(mask_region);
}
}
// TODO(gw): Make use of this to skip tiles that are completely clipped out in a follow up! pubfn add_clip_rect(
&mutself,
clip_rect: DeviceRect,
clip_mode: ClipMode,
applied_as_local_clip: bool,
) { match clip_mode {
ClipMode::Clip => { self.clip_in_regions.push(ClipInRegion {
rect: clip_rect,
needs_mask: !applied_as_local_clip,
});
}
ClipMode::ClipOut => { self.clip_out_regions.push(clip_rect);
self.add_mask_region(self.rect);
}
}
}
/// Classify all the tiles in to categories, based on the provided masks and clip regions pubfn classify(
&mutself,
) -> QuadTileIterator {
assert_ne!(self.x_tiles, 0);
assert_ne!(self.y_tiles, 0);
let tile_count = self.x_tiles * self.y_tiles; let tiles = &mutself.buffer[0 .. tile_count];
for info in tiles.iter_mut() { // A clip-in region culls tiles that fall entirely outside it. Tiles // that straddle its boundary require a mask, unless the clip is // already applied via the primitive's local clip rect (in which case // the pattern shader clips them directly). Tiles fully contained by // the region are unaffected by it. for clip_region in &self.clip_in_regions { match info.kind {
QuadTileKind::Clipped => {},
QuadTileKind::Pattern { refmut has_mask } => { if !clip_region.rect.intersects(&info.rect) {
info.kind = QuadTileKind::Clipped;
} elseif clip_region.needs_mask && !clip_region.rect.contains_box(&info.rect) {
*has_mask = true;
}
}
}
}
// If a tile doesn't intersect with a clip-out region, it's clipped for clip_region in &self.clip_out_regions { match info.kind {
QuadTileKind::Clipped => {},
QuadTileKind::Pattern { .. } => { if clip_region.contains_box(&info.rect) {
info.kind = QuadTileKind::Clipped;
}
}
}
}
// If a tile intersects with a mask region, and isn't clipped, it needs a mask for mask_region in &self.mask_regions { match info.kind {
QuadTileKind::Clipped | QuadTileKind::Pattern { has_mask: true, .. } => {},
QuadTileKind::Pattern { refmut has_mask, .. } => { if mask_region.intersects(&info.rect) {
*has_mask = true;
}
}
}
}
}
// Skip over empty tiles while tile.kind == QuadTileKind::Clipped {
tile = *self.tiles.first()?; self.tiles = &self.tiles[1..];
}
// Merge consecutive compatible tiles. // This reduces some of the per-tile overhead both on CPU and GPU, especially // with SWGL which benefits enormously from working with long rows of pixels. whilelet Some(info) = self.tiles.first() { if tile.rect.min.y != info.rect.min.y || tile.kind != info.kind { // Different row or different kind, stop merging. break;
}
let max = match info.kind { // If a tile must be rendered into an intermediate target, don't make // wider than 1024 pixels so that it plays well with the texture atlas.
QuadTileKind::Pattern { has_mask: true } => 1024.0, // If the tile is rendered directly into the destination target let it // be as wide as possible.
QuadTileKind::Pattern { has_mask: false } => f32::MAX,
QuadTileKind::Clipped => { break; }
};
if info.rect.max.x - tile.rect.min.x > max { break;
}
// At this point we know that this tile on the same row, adjacent // and of the same kind as the previous one, so they can be merged.
tile.rect.max.x = info.rect.max.x; self.tiles = &self.tiles[1..];
}
let mask_rect = DeviceRect::new(DevicePoint::new(90.0, 180.0), DevicePoint::new(510.0, 710.0));
qc.add_mask_region(mask_rect);
let clip_rect = DeviceRect::new(DevicePoint::new(120.0, 220.0), DevicePoint::new(714.0, 1015.0));
qc.add_clip_rect(clip_rect, ClipMode::Clip, false);
let clip_out_rect = DeviceRect::new(DevicePoint::new(130.0, 200.0), DevicePoint::new(714.0, 609.0));
qc.add_clip_rect(clip_out_rect, ClipMode::ClipOut, false);
qc_verify(qc, &[
M,
M,
M,
M,
]);
}
// A straddling clip that is not applied as the local clip rect masks the // boundary tiles. #[test] fn quad_classify_13() { letmut qc = qc_new(0.0, 0.0, 768.0, 768.0);
let rect = DeviceRect::new(DevicePoint::new(128.0, 128.0), DevicePoint::new(640.0, 640.0));
qc.add_clip_rect(rect, ClipMode::Clip, false);
qc_verify(qc, &[
M,
M, P, M,
M,
]);
}
// The same straddling clip, but applied as the local clip rect: the boundary // tiles are clipped by the shader and don't need a mask. #[test] fn quad_classify_14() { letmut qc = qc_new(0.0, 0.0, 768.0, 768.0);
let rect = DeviceRect::new(DevicePoint::new(128.0, 128.0), DevicePoint::new(640.0, 640.0));
qc.add_clip_rect(rect, ClipMode::Clip, true);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.