use rustc_hash::FxHashMap;
use valo_dl::{Bounds, DisplayList, MaskKind, Op, Paint};
use valo_geometry::{Matrix, Rect};
use crate::pipelines::{Blend, PipelineBlend};
use crate::raster::FillTarget;
use super::backdrop::{BackdropRequest, SharedBackdrop};
use super::draw_context::{DepthRange, DrawContext, PixelArea};
use super::draw_state::{DrawState, ONE_DRAW_LAYER};
use super::drawing::Drawing;
use super::emit::{Cover, Entity, Role};
use super::filter_tree::FilterTree;
use super::gaussian::extract_scale;
use super::layer_coverage::{compute_save_layer_coverage, SourceCoverage};
use super::replay::ReplayState;
use super::shading::Shading;
use super::snapshot::Snapshot;
use super::source::DrawSource;
use super::Planner;
const SNAPSHOT_DEPTH: DepthRange = DepthRange::new(0, 3);
#[derive(Clone, Copy, Debug)]
pub(super) struct SnapshotDepths {
pub first: f32,
pub second: f32,
}
pub(super) struct LayerComposite {
pub blend: Blend,
pub mask: Option<MaskKind>,
pub filter: Option<FilterTree<'static>>,
pub at: DrawState,
}
pub(super) struct ResolvedLayer<'a> {
pub paint: &'a Paint,
pub mask: Option<MaskKind>,
pub content: Bounds,
pub clip: Bounds,
pub depth: DepthRange,
pub floods_clip: bool,
pub at: DrawState,
pub can_elide: bool,
pub backdrop: Option<BackdropRequest>,
}
impl<'a> ResolvedLayer<'a> {
pub fn of(op: &'a Op, list: &DisplayList, state: &ReplayState, at: DrawState) -> Self {
let Op::SaveLayer {
paint,
mask_composite,
scope_bounds,
clip_bounds,
base_slot,
composite_slot,
floods_clip,
can_elide,
backdrop,
..
} = op
else {
unreachable!("replay resolves only SaveLayer ops as layers, not {op:?}");
};
Self {
paint,
mask: *mask_composite,
content: scope_bounds.map(state.base()),
clip: clip_bounds.map(state.base()),
depth: DepthRange::new(state.absolute(*base_slot), composite_slot - base_slot),
floods_clip: *floods_clip,
at,
can_elide: *can_elide,
backdrop: backdrop
.as_ref()
.map(|backdrop| BackdropRequest::of(backdrop, list, state.base())),
}
}
fn coverage(&self, target: &PixelArea, filter: Option<&FilterTree<'_>>) -> Option<Rect> {
let limit = self.clip.intersect(&Bounds::of(target.rect())).rect()?;
let coverage = compute_save_layer_coverage(
&self.content,
&Matrix::IDENTITY,
&limit,
filter.map(|filter| filter as &dyn SourceCoverage),
self.floods_clip,
)?;
let reads_past_its_edge = filter.is_some_and(FilterTree::reads_past_its_edge);
let coverage = if reads_past_its_edge && !self.floods_clip {
limit
} else {
coverage
};
(!coverage.is_empty()).then(|| round_out(&coverage))
}
}
pub(super) enum LayerPlan {
Skip { erase: bool },
Elide(f32),
Open {
area: PixelArea,
composite: Box<LayerComposite>,
},
}
impl LayerPlan {
pub fn of(layer: &ResolvedLayer<'_>, target: &PixelArea) -> Self {
let filter = FilterTree::for_save_layer(layer.paint, layer.at.transform);
let Some(rect) = layer.coverage(target, filter.as_ref()) else {
return LayerPlan::Skip {
erase: layer.mask.is_some(),
};
};
if layer.can_elide {
return LayerPlan::Elide(layer.paint.color.a);
}
LayerPlan::Open {
area: PixelArea::over(rect),
composite: Box::new(LayerComposite {
blend: Blend::of(layer.paint.blend_mode),
mask: layer.mask,
filter,
at: layer
.at
.with_transform(Matrix::IDENTITY)
.faded(layer.paint.color.a),
}),
}
}
}
pub(super) enum Opened {
Skip,
Elided(f32),
Layer(LayerComposite),
}
impl Planner<'_> {
pub(super) fn open_layer(
&mut self,
layer: ResolvedLayer<'_>,
shared_backdrops: &mut FxHashMap<u64, SharedBackdrop>,
) -> Opened {
match LayerPlan::of(&layer, &self.contexts.top().area) {
LayerPlan::Skip { erase } => {
if erase {
self.drawing().erase_alpha(layer.at.z);
}
Opened::Skip
}
LayerPlan::Elide(alpha) => {
self.plan.stats.layers_elided += 1;
Opened::Elided(alpha)
}
LayerPlan::Open { area, composite } => {
let seed = layer.backdrop.and_then(|request| {
self.render_backdrop_seed(&layer.at, &area.rect(), request, shared_backdrops)
});
self.open_target(area, layer.depth);
if let Some(seed) = seed {
self.draw_backdrop_seed(&seed);
}
Opened::Layer(*composite)
}
}
}
pub(super) fn close_layer(&mut self, composite: LayerComposite) {
let picture = self.close_target().placed_where_drawn(true);
match composite.mask {
Some(kind) => self
.drawing()
.draw_mask_composite(&picture, kind, &composite.at),
None => self.composite_picture(
picture,
composite.filter.as_ref(),
composite.blend,
&composite.at,
),
}
}
pub(super) fn render_to_snapshot(
&mut self,
region: &Rect,
draw: impl FnOnce(&mut Drawing, SnapshotDepths),
) -> Snapshot {
self.open_target(PixelArea::over(round_out(region)), SNAPSHOT_DEPTH);
let depths = SnapshotDepths {
first: SNAPSHOT_DEPTH.z(1),
second: SNAPSHOT_DEPTH.z(2),
};
draw(&mut self.drawing(), depths);
self.close_target()
}
pub(super) fn plan_via_implicit_layer(
&mut self,
device_bounds: Rect,
at: &DrawState,
blend: Blend,
inner: impl FnOnce(&mut Drawing, &DrawState),
) -> Option<()> {
let rect = device_bounds.intersect(&self.contexts.top().area.rect())?;
self.open_target(PixelArea::over(rect), ONE_DRAW_LAYER);
inner(
&mut self.drawing(),
&DrawState::alone_in_layer(at.transform),
);
let picture = self.close_target();
self.composite_picture(picture, None, blend, &at.with_transform(Matrix::IDENTITY));
Some(())
}
pub(super) fn render_effect_layer(
&mut self,
content: DrawSource<'_>,
paint: &Paint,
local_bounds: Option<&Rect>,
filter: &FilterTree<'_>,
space: &SourceSpace,
) -> Option<Snapshot> {
let target = self.contexts.top().area.rect();
let limit = filter.source_coverage(&space.to_source(&target));
let region = match local_bounds {
Some(local_bounds) => {
let ink = space.source.map_rect(local_bounds).expand(1.0);
ink.intersect(&limit)?
}
None => limit,
};
self.open_target(PixelArea::over(round_out(®ion)), ONE_DRAW_LAYER);
self.drawing()
.draw_alone(content, paint, &DrawState::alone_in_layer(space.source));
let placed = space.remainder == Matrix::IDENTITY;
Some(self.close_target().placed_where_drawn(placed))
}
fn open_target(&mut self, area: PixelArea, depth: DepthRange) {
self.plan.stats.layers_rendered += 1;
let layer = self.tools.pool.take_layer(area.size(), self.tools.format);
self.contexts
.push(DrawContext::offscreen(layer, area, depth));
}
pub(super) fn open_raster_target(&mut self, fill: &FillTarget, list: &DisplayList) {
let attachments = self
.tools
.pool
.take_attachments(fill.size, self.tools.format);
self.contexts.push(DrawContext::raster(
attachments,
fill,
DepthRange::of_list(list),
));
}
pub(super) fn close_target(&mut self) -> Snapshot {
self.emit_segment();
self.contexts.pop().picture()
}
}
impl Drawing<'_, '_> {
fn draw_mask_composite(&mut self, mask: &Snapshot, kind: MaskKind, at: &DrawState) {
let shading = self.emit.mask_composite_shading(mask, kind, at.alpha);
self.erase_by(shading, at.z);
}
fn erase_alpha(&mut self, z: f32) {
self.erase_by(Shading::solid([0.0; 4]), z);
}
fn erase_by(&mut self, shading: Shading, z: f32) {
let entity = Entity {
stencil: None,
cover: Cover::Quad {
transform: Matrix::IDENTITY,
rect: self.context.area.rect(),
},
role: Role::Fill,
shading,
blend: PipelineBlend::DstIn,
z,
};
self.emit.push(self.context, entity);
}
}
#[derive(Clone, Debug, PartialEq)]
pub(super) struct SourceSpace {
pub source: Matrix,
pub remainder: Matrix,
}
impl SourceSpace {
pub fn of(transform: &Matrix) -> Option<Self> {
let [scale_x, scale_y] = extract_scale(transform);
let [.., offset_x, offset_y] = transform.to_affine();
let source = Matrix::translation(offset_x, offset_y).then(&Matrix::scale(scale_x, scale_y));
let remainder = transform.then(&source.invert()?);
Some(Self { source, remainder })
}
pub fn to_source(&self, region: &Rect) -> Rect {
self.remainder
.invert()
.map_or(Rect::EVERYTHING, |inverse| inverse.map_rect(region))
}
}
pub(super) fn round_out(rect: &Rect) -> Rect {
Rect::from_ltrb(
rect.x.floor(),
rect.y.floor(),
rect.right().ceil(),
rect.bottom().ceil(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn layer(paint: &Paint, content: Rect) -> ResolvedLayer<'_> {
ResolvedLayer {
paint,
mask: None,
content: Bounds::of(content),
clip: Bounds::Unbounded,
depth: DepthRange::new(4, 4),
floods_clip: false,
at: DrawState::in_layer(Matrix::IDENTITY, 0.5),
can_elide: false,
backdrop: None,
}
}
fn target() -> PixelArea {
PixelArea::of_size([100, 100])
}
fn opened_area(plan: LayerPlan) -> Rect {
match plan {
LayerPlan::Open { area, .. } => area.rect(),
_ => panic!("the layer opens"),
}
}
#[test]
fn a_layer_opens_over_its_content_or_is_skipped() {
let paint = Paint::default();
let content = Rect::new(10.5, 20.0, 30.0, 10.25);
assert_eq!(
opened_area(LayerPlan::of(&layer(&paint, content), &target())),
Rect::from_ltrb(10.0, 20.0, 41.0, 31.0)
);
let outside = layer(&paint, Rect::new(200.0, 0.0, 10.0, 10.0));
assert!(matches!(
LayerPlan::of(&outside, &target()),
LayerPlan::Skip { erase: false }
));
let mask = ResolvedLayer {
mask: Some(MaskKind::Alpha),
..layer(&paint, Rect::new(200.0, 0.0, 10.0, 10.0))
};
assert!(matches!(
LayerPlan::of(&mask, &target()),
LayerPlan::Skip { erase: true }
));
}
#[test]
fn an_elidable_layer_hands_its_alpha_on() {
let paint = Paint::from_color(valo_geometry::Color::rgba(0.0, 0.0, 0.0, 0.25));
let elidable = ResolvedLayer {
can_elide: true,
..layer(&paint, Rect::new(10.0, 10.0, 10.0, 10.0))
};
assert!(matches!(
LayerPlan::of(&elidable, &target()),
LayerPlan::Elide(alpha) if alpha == 0.25
));
}
#[test]
fn a_flooding_layer_covers_its_clip() {
let paint = Paint::from_color(valo_geometry::Color::rgba(0.0, 0.0, 0.0, 0.5));
let flooding = ResolvedLayer {
floods_clip: true,
clip: Bounds::Bounded(Rect::new(-10.0, 30.0, 50.0, 20.0)),
at: DrawState {
transform: Matrix::translation(3.0, 4.0),
z: 0.5,
alpha: 0.5,
},
..layer(&paint, Rect::new(12.0, 34.0, 2.0, 2.0))
};
let LayerPlan::Open { area, composite } = LayerPlan::of(&flooding, &target()) else {
panic!("the layer opens");
};
assert_eq!(area.rect(), Rect::new(0.0, 30.0, 40.0, 20.0));
assert_eq!(composite.at.transform, Matrix::IDENTITY);
assert_eq!(composite.at.alpha, 0.25);
assert_eq!(composite.at.z, 0.5);
}
#[test]
fn a_blur_that_reads_past_its_edge_covers_the_clip_and_a_decal_one_its_content() {
use valo_dl::{ImageFilter, TileMode};
let blurred = |tile_mode: Option<TileMode>| {
let blur = ImageFilter::blur(4.0, 4.0);
Paint {
image_filter: Some(match tile_mode {
Some(tile_mode) => blur.with_tile_mode(tile_mode),
None => blur,
}),
..Paint::default()
}
};
let content = Rect::new(40.0, 40.0, 20.0, 20.0);
let clip = Bounds::Bounded(Rect::new(10.0, 10.0, 80.0, 80.0));
let area = |paint: &Paint, clip: Bounds| {
let layer = ResolvedLayer {
clip,
..layer(paint, content)
};
opened_area(LayerPlan::of(&layer, &target()))
};
let decal = blurred(None);
assert_eq!(area(&decal, clip), content);
assert_eq!(area(&decal, Bounds::Unbounded), content);
let clamp = blurred(Some(TileMode::Clamp));
assert_eq!(area(&clamp, clip), Rect::new(10.0, 10.0, 80.0, 80.0));
assert_eq!(
area(&clamp, Bounds::Unbounded),
Rect::new(0.0, 0.0, 100.0, 100.0),
"unclipped, the target is the limit"
);
}
use crate::planner::filter_tree::FilterInput;
use crate::planner::gaussian::sigma_for_blur_radius;
use valo_dl::{ImageFilter, TileMode};
fn blurred_draw_coverage(entity: &Matrix, local: &Rect, sigma: f32) -> Rect {
let space = SourceSpace::of(entity).expect("an invertible transform");
let tree = FilterInput::Source
.with_image_filter(Some(&ImageFilter::blur(sigma, sigma)), TileMode::Decal);
let source = space.source.map_rect(local);
space
.remainder
.map_rect(&tree.coverage(&source, &space.source))
}
fn assert_rect_near(actual: Rect, expected: Rect) {
let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
assert!(
near(actual.x, expected.x)
&& near(actual.y, expected.y)
&& near(actual.right(), expected.right())
&& near(actual.bottom(), expected.bottom()),
"{actual:?} != {expected:?}"
);
}
#[test]
fn render_coverage_matches_get_coverage_rotated() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let entity =
Matrix::translation(400.0, 100.0).then(&Matrix::rotation(std::f32::consts::FRAC_PI_2));
let coverage = blurred_draw_coverage(&entity, &Rect::new(0.0, 0.0, 400.0, 300.0), sigma);
assert_rect_near(coverage, Rect::from_ltrb(99.0, 99.0, 401.0, 501.0));
}
#[test]
fn texture_contents_with_destination_rect() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let coverage = blurred_draw_coverage(
&Matrix::IDENTITY,
&Rect::new(50.0, 40.0, 100.0, 100.0),
sigma,
);
assert_rect_near(coverage, Rect::from_ltrb(49.0, 39.0, 151.0, 141.0));
}
#[test]
fn texture_contents_with_destination_rect_scaled() {
let sigma = sigma_for_blur_radius(1.0, &Matrix::IDENTITY);
let coverage = blurred_draw_coverage(
&Matrix::scale(2.0, 2.0),
&Rect::new(50.0, 40.0, 100.0, 100.0),
sigma,
);
assert_rect_near(coverage, Rect::from_ltrb(97.0, 77.0, 303.0, 283.0));
}
#[test]
fn texture_contents_with_effect_transform() {
let effect_transform = Matrix::scale(2.0, 2.0);
let sigma = sigma_for_blur_radius(1.0, &effect_transform);
let tree = FilterInput::Source
.with_image_filter(Some(&ImageFilter::blur(sigma, sigma)), TileMode::Decal);
let coverage = tree.coverage(&Rect::new(50.0, 40.0, 100.0, 100.0), &effect_transform);
assert_rect_near(coverage, Rect::from_ltrb(49.0, 39.0, 151.0, 141.0));
}
#[test]
fn source_space_leaves_a_mirror_to_the_remainder() {
let entity = Matrix::translation(100.0, 0.0).then(&Matrix::scale(-2.0, 3.0));
let space = SourceSpace::of(&entity).expect("an invertible transform");
assert_eq!(extract_scale(&space.source), [2.0, 3.0]);
let point = valo_geometry::Point::new(5.0, 7.0);
let placed = space.remainder.map_point(space.source.map_point(point));
assert_eq!(placed, entity.map_point(point));
}
}