use super::ScheduleBuffers;
use crate::draw::{Draw, ExternalTextureRun};
use crate::filter::GpuFilterData;
use crate::target::{
BlendPassBindings, DrawPassBindings, DrawPassTarget, FilterPassBindings, LayerTextureId,
LayerTextureRegion, RootTarget, RoundBindings, TextureParity, TextureRegion,
};
use crate::util::{RangedSlice, Ranges, VecExt};
use crate::{GpuStrip, blend::BlendStrip};
use alloc::vec::Vec;
use core::ops::Range;
use vello_common::geometry::RectU16;
use vello_common::peniko::BlendMode;
#[derive(Debug, Default)]
pub(super) struct Rounds {
pub(super) rounds: Vec<Round>,
}
impl Rounds {
pub(super) fn iter(&self) -> core::slice::Iter<'_, Round> {
self.rounds.iter()
}
pub(super) fn required_layer_pages(&self) -> [usize; 2] {
let mut page_counts = [0; 2];
for texture in self
.rounds
.iter()
.flat_map(|round| round.texture_binding.required_textures())
.flatten()
{
let page_count = &mut page_counts[texture.texture_parity.get_parity()];
*page_count = (*page_count).max(usize::from(texture.page_index) + 1);
}
page_counts
}
pub(super) fn round_mut(&mut self, index: usize) -> &mut Round {
&mut self.rounds[index]
}
pub(super) fn push_layer_clear(
&mut self,
round_idx: usize,
texture_parity: TextureParity,
rect: RectU16,
) {
let parity = texture_parity.get_parity();
self.rounds[round_idx].layer_texture_clears[parity].push(rect);
}
pub(super) fn ensure_exists(&mut self, round_idx: usize) {
while self.rounds.len() <= round_idx {
self.rounds.push(Round::default());
}
}
pub(super) fn bind_at_or_after(
&mut self,
mut point: SchedulePoint,
requirement: RoundBindings,
) -> SchedulePoint {
const MAX_ROUNDS: usize = 40_000;
loop {
if point.round > MAX_ROUNDS {
panic!("possible deadlock in scheduler detected");
}
self.ensure_exists(point.round);
let round = &mut self.rounds[point.round];
if let Some(binding) = round.texture_binding.merge(requirement) {
round.texture_binding = binding;
return point;
}
point.round += 1;
}
}
}
#[cfg(any(test, debug_assertions))]
impl Rounds {
pub(super) fn validate(&self, buffers: &ScheduleBuffers) {
for (round_idx, round) in self.rounds.iter().enumerate() {
round.validate(round_idx, buffers);
}
}
}
#[derive(Debug, Default)]
pub(super) struct Round {
pub(super) texture_binding: RoundBindings,
layer_texture_passes: [LayerTexturePass; 2],
root_draw: Draw,
layer_texture_clears: [Vec<RectU16>; 2],
}
impl Round {
pub(super) fn layer_passes<'a>(
&'a self,
buffers: &'a ScheduleBuffers,
) -> impl Iterator<Item = LayerPass<'a>> + 'a {
self.layer_texture_passes
.iter()
.enumerate()
.filter_map(move |(index, pass)| {
let texture_parity = TextureParity::from_parity(index);
let target = self.texture_binding.layer_id(texture_parity);
let opposite = self.texture_binding.layer_id(texture_parity.opposite());
let draw = (pass.draw.strip_ranges.len() != 0).then(|| DrawPass {
strips: buffers.draw_buffers.strips.ranged(&pass.draw.strip_ranges),
external_texture_runs: &pass.draw.external_texture_runs,
bindings: DrawPassBindings::new(
DrawPassTarget::Layer(target.unwrap()),
pass.draw.has_child_layer.then(|| opposite.unwrap()),
),
});
let filter = (pass.filter_ranges.len() != 0).then(|| FilterPass {
filters: buffers.filter_ops.ranged(&pass.filter_ranges),
bindings: FilterPassBindings::new(target.unwrap(), opposite.unwrap()),
});
let blend = (pass.blend_ranges.len() != 0).then(|| BlendPass {
blends: buffers.blend_ops.ranged(&pass.blend_ranges),
blend_strips: &buffers.blend_strips,
bindings: BlendPassBindings::new(target.unwrap(), opposite.unwrap()),
});
(draw.is_some() || filter.is_some() || blend.is_some()).then_some(LayerPass {
texture_parity,
draw,
filter,
blend,
})
})
}
pub(super) fn root_draw_pass<'a>(
&'a self,
buffers: &'a ScheduleBuffers,
target: RootTarget,
) -> Option<DrawPass<'a>> {
if self.root_draw.strip_ranges.len() == 0 {
return None;
}
let child = self
.root_draw
.has_child_layer
.then(|| self.texture_binding.layer_id(TextureParity::Odd).unwrap());
Some(DrawPass {
strips: buffers
.draw_buffers
.strips
.ranged(&self.root_draw.strip_ranges),
external_texture_runs: &self.root_draw.external_texture_runs,
bindings: DrawPassBindings::new(DrawPassTarget::Root(target), child),
})
}
pub(super) fn clear_passes(&self) -> impl Iterator<Item = ClearPass<'_>> {
self.layer_texture_clears
.iter()
.enumerate()
.filter_map(|(index, rects)| {
if rects.is_empty() {
return None;
}
let texture_parity = TextureParity::from_parity(index);
Some(ClearPass {
target: self.texture_binding.layer_id(texture_parity).unwrap(),
rects,
})
})
}
pub(super) fn root_draw_mut(&mut self) -> &mut Draw {
&mut self.root_draw
}
pub(super) fn layer_draw_mut(&mut self, texture_parity: TextureParity) -> &mut Draw {
&mut self.layer_texture_passes[texture_parity.get_parity()].draw
}
pub(super) fn push_blend_op(
&mut self,
parent_texture_parity: TextureParity,
buffers: &mut ScheduleBuffers,
blend: BlendOp,
) {
buffers.blend_ops.push_ranged(
&mut self.layer_texture_passes[parent_texture_parity.get_parity()].blend_ranges,
blend,
);
}
pub(super) fn push_filter_op(
&mut self,
texture_parity: TextureParity,
buffers: &mut ScheduleBuffers,
filter: FilterOp,
) {
buffers.filter_ops.push_ranged(
&mut self.layer_texture_passes[texture_parity.get_parity()].filter_ranges,
filter,
);
}
}
#[cfg(any(test, debug_assertions))]
impl Round {
fn validate(&self, round_idx: usize, buffers: &ScheduleBuffers) {
for (index, pass) in self.layer_texture_passes.iter().enumerate() {
let texture_parity = TextureParity::from_parity(index);
let target = self.texture_binding.layer_id(texture_parity);
let opposite = self.texture_binding.layer_id(texture_parity.opposite());
let has_draw = pass.draw.strip_ranges.len() != 0;
let has_filter = pass.filter_ranges.len() != 0;
let has_blend = pass.blend_ranges.len() != 0;
assert!(
!pass.draw.has_child_layer || has_draw,
"round {round_idx} has a child-layer binding without a layer draw"
);
assert!(
!(has_draw || has_filter || has_blend) || target.is_some(),
"round {round_idx} has a layer pass with no bound target"
);
assert!(
!(pass.draw.has_child_layer || has_filter || has_blend) || opposite.is_some(),
"round {round_idx} has a layer pass with no bound sampled page"
);
for filter in buffers.filter_ops.ranged(&pass.filter_ranges).iter() {
assert_eq!(
target,
Some(filter.textures.original.target),
"round {round_idx} has a filter whose target is not bound"
);
assert_eq!(
opposite,
Some(filter.textures.temporary.target),
"round {round_idx} has a filter whose temporary texture is not bound"
);
}
for blend in buffers.blend_ops.ranged(&pass.blend_ranges).iter() {
assert_eq!(
target,
Some(blend.parent_region.texture.target),
"round {round_idx} has a blend whose parent texture is not bound"
);
assert_eq!(
opposite,
Some(blend.child_region.texture.target),
"round {round_idx} has a blend whose child texture is not bound"
);
}
assert!(
self.layer_texture_clears[index].is_empty() || target.is_some(),
"round {round_idx} has a clear with no bound target"
);
}
assert!(
!self.root_draw.has_child_layer
|| (self.root_draw.strip_ranges.len() != 0
&& self.texture_binding.layer_id(TextureParity::Odd).is_some()),
"round {round_idx} has a root draw with no bound child layer"
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum RoundStage {
Start,
Even(LayerStage),
Odd(LayerStage),
RootDraw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum LayerStage {
Draw,
Filter,
Blend,
}
impl RoundStage {
pub(super) const fn draw(texture_parity: TextureParity) -> Self {
match texture_parity {
TextureParity::Even => Self::Even(LayerStage::Draw),
TextureParity::Odd => Self::Odd(LayerStage::Draw),
}
}
pub(super) const fn filter(texture_parity: TextureParity) -> Self {
match texture_parity {
TextureParity::Even => Self::Even(LayerStage::Filter),
TextureParity::Odd => Self::Odd(LayerStage::Filter),
}
}
pub(super) const fn blend(texture_parity: TextureParity) -> Self {
match texture_parity {
TextureParity::Even => Self::Even(LayerStage::Blend),
TextureParity::Odd => Self::Odd(LayerStage::Blend),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct SchedulePoint {
pub(super) round: usize,
pub(super) stage: RoundStage,
}
impl SchedulePoint {
pub(super) const fn start(round: usize) -> Self {
Self {
round,
stage: RoundStage::Start,
}
}
pub(super) fn after(self, stage: RoundStage) -> Self {
if stage > self.stage {
Self {
round: self.round,
stage,
}
} else {
Self {
round: self.round + 1,
stage,
}
}
}
pub(super) fn after_or_at(self, stage: RoundStage) -> Self {
if stage >= self.stage {
Self {
round: self.round,
stage,
}
} else {
Self {
round: self.round + 1,
stage,
}
}
}
}
#[derive(Debug, Default)]
pub(super) struct LayerTexturePass {
draw: Draw,
filter_ranges: Ranges,
blend_ranges: Ranges,
}
#[derive(Debug)]
pub(super) struct DrawPass<'a> {
pub(super) strips: RangedSlice<'a, GpuStrip>,
pub(super) external_texture_runs: &'a [ExternalTextureRun],
pub(super) bindings: DrawPassBindings,
}
#[derive(Debug)]
pub(super) struct FilterPass<'a> {
pub(super) filters: RangedSlice<'a, FilterOp>,
pub(super) bindings: FilterPassBindings,
}
#[derive(Debug)]
pub(super) struct BlendPass<'a> {
pub(super) blends: RangedSlice<'a, BlendOp>,
pub(super) blend_strips: &'a [BlendStrip],
pub(super) bindings: BlendPassBindings,
}
#[derive(Debug)]
pub(super) struct ClearPass<'a> {
pub(super) target: LayerTextureId,
pub(super) rects: &'a [RectU16],
}
#[derive(Debug)]
pub(super) struct LayerPass<'a> {
pub(super) draw: Option<DrawPass<'a>>,
pub(super) filter: Option<FilterPass<'a>>,
pub(super) blend: Option<BlendPass<'a>>,
#[allow(dead_code, reason = "only used for a test.")]
pub(super) texture_parity: TextureParity,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct FilterTextureRegions {
pub(crate) original: TextureRegion,
pub(crate) temporary: TextureRegion,
}
impl FilterTextureRegions {
pub(crate) fn new(original: TextureRegion, temporary: TextureRegion) -> Self {
Self {
original,
temporary,
}
}
pub(crate) fn round_bindings(self) -> RoundBindings {
RoundBindings::new(self.original.target)
.merge(RoundBindings::new(self.temporary.target))
.unwrap()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct FilterOp {
pub(crate) textures: FilterTextureRegions,
pub(crate) filter_data_offset: u32,
pub(crate) gpu_filter: GpuFilterData,
}
#[derive(Debug, Clone)]
pub(crate) struct BlendOp {
pub(crate) parent_region: LayerTextureRegion,
pub(crate) child_region: LayerTextureRegion,
pub(crate) blend_bbox: RectU16,
pub(crate) blend_mode: BlendMode,
pub(crate) opacity: f32,
pub(crate) clip_strips: Option<Range<u32>>,
}
#[cfg(test)]
mod tests {
use super::{
BlendOp, FilterOp, FilterTextureRegions, LayerStage, Round, RoundStage, Rounds,
SchedulePoint,
};
use crate::filter::GpuFilterData;
use crate::schedule::ScheduleBuffers;
use crate::target::{
LayerTextureId, LayerTextureRegion, RoundBindings, TextureParity, TextureRegion,
};
use crate::util::VecExt;
use bytemuck::Zeroable;
use vello_common::geometry::RectU16;
use vello_common::peniko::BlendMode;
fn layer_id(texture_parity: TextureParity, page_index: u16) -> LayerTextureId {
LayerTextureId::new(texture_parity, page_index)
}
fn region(texture_parity: TextureParity, page_index: u16) -> TextureRegion {
TextureRegion {
target: layer_id(texture_parity, page_index),
rect: RectU16::new(0, 0, 16, 16),
}
}
fn layer_region(texture_parity: TextureParity, page_index: u16) -> LayerTextureRegion {
LayerTextureRegion {
texture: region(texture_parity, page_index),
layer_bbox: RectU16::new(0, 0, 16, 16),
}
}
fn filter_op(filter_data_offset: u32) -> FilterOp {
FilterOp {
textures: FilterTextureRegions::new(
region(TextureParity::Even, 0),
region(TextureParity::Odd, 0),
),
filter_data_offset,
gpu_filter: GpuFilterData::zeroed(),
}
}
#[test]
fn schedule_point_stage_occurrences() {
let draw = SchedulePoint {
round: 4,
stage: RoundStage::Even(LayerStage::Draw),
};
assert_eq!(
draw.after(RoundStage::Even(LayerStage::Filter)),
SchedulePoint {
round: 4,
stage: RoundStage::Even(LayerStage::Filter),
}
);
assert_eq!(
draw.after(RoundStage::Odd(LayerStage::Draw)),
SchedulePoint {
round: 4,
stage: RoundStage::Odd(LayerStage::Draw),
}
);
assert_eq!(
draw.after(RoundStage::Even(LayerStage::Draw)),
SchedulePoint {
round: 5,
stage: RoundStage::Even(LayerStage::Draw),
}
);
assert_eq!(draw.after_or_at(RoundStage::Even(LayerStage::Draw)), draw);
assert_eq!(
draw.after_or_at(RoundStage::Start),
SchedulePoint {
round: 5,
stage: RoundStage::Start,
}
);
}
#[test]
fn unused_bindings_do_not_resolve_passes() {
let round = Round {
texture_binding: RoundBindings::new(layer_id(TextureParity::Even, 2))
.merge(RoundBindings::new(layer_id(TextureParity::Odd, 5)))
.unwrap(),
..Round::default()
};
assert_eq!(round.layer_passes(&ScheduleBuffers::default()).count(), 0);
assert_eq!(round.clear_passes().count(), 0);
}
#[test]
fn pass_iterators_skip_empty_parities() {
let even = layer_id(TextureParity::Even, 2);
let odd = layer_id(TextureParity::Odd, 5);
let mut round = Round {
texture_binding: RoundBindings::new(even)
.merge(RoundBindings::new(odd))
.unwrap(),
..Round::default()
};
let mut buffers = ScheduleBuffers::default();
round.push_filter_op(TextureParity::Even, &mut buffers, filter_op(10));
round.layer_texture_clears[TextureParity::Odd.get_parity()].push(RectU16::new(0, 0, 8, 8));
let layer_passes = round.layer_passes(&buffers).collect::<alloc::vec::Vec<_>>();
assert_eq!(layer_passes.len(), 1);
assert!(layer_passes[0].draw.is_none());
assert_eq!(
layer_passes[0].filter.as_ref().unwrap().bindings.target(),
even
);
assert!(layer_passes[0].blend.is_none());
let clear_passes = round.clear_passes().collect::<alloc::vec::Vec<_>>();
assert_eq!(clear_passes.len(), 1);
assert_eq!(clear_passes[0].target, odd);
}
#[test]
fn validator_accepts_bound_work() {
let mut rounds = Rounds::default();
let mut buffers = ScheduleBuffers::default();
let bindings = filter_op(10).textures.round_bindings();
let point = rounds.bind_at_or_after(SchedulePoint::start(0), bindings);
rounds.round_mut(point.round).push_filter_op(
TextureParity::Even,
&mut buffers,
filter_op(10),
);
rounds.push_layer_clear(point.round, TextureParity::Odd, RectU16::new(0, 0, 8, 8));
rounds.validate(&buffers);
}
#[test]
#[should_panic(expected = "layer pass with no bound target")]
fn validator_rejects_unbound_layer_pass() {
let mut rounds = Rounds::default();
let mut buffers = ScheduleBuffers::default();
rounds.ensure_exists(0);
rounds
.round_mut(0)
.push_filter_op(TextureParity::Even, &mut buffers, filter_op(10));
rounds.validate(&buffers);
}
#[test]
#[should_panic(expected = "clear with no bound target")]
fn validator_rejects_unbound_clear() {
let mut rounds = Rounds::default();
rounds.ensure_exists(0);
rounds.push_layer_clear(0, TextureParity::Odd, RectU16::new(0, 0, 8, 8));
rounds.validate(&ScheduleBuffers::default());
}
#[test]
fn binding_conflicts_even() {
let mut rounds = Rounds::default();
let requested = SchedulePoint {
round: 0,
stage: RoundStage::Even(LayerStage::Draw),
};
let even_page_1 = RoundBindings::new(layer_id(TextureParity::Even, 1));
let odd_page_2 = RoundBindings::new(layer_id(TextureParity::Odd, 2));
let even_page_3 = RoundBindings::new(layer_id(TextureParity::Even, 3));
assert_eq!(rounds.bind_at_or_after(requested, even_page_1), requested);
assert_eq!(rounds.bind_at_or_after(requested, odd_page_2), requested);
assert_eq!(
rounds.bind_at_or_after(requested, even_page_3),
SchedulePoint {
round: 1,
..requested
}
);
assert_eq!(
rounds.rounds[0].texture_binding.page_indices(),
[Some(1), Some(2)]
);
assert_eq!(
rounds.rounds[1].texture_binding.page_indices(),
[Some(3), None]
);
assert_eq!(rounds.required_layer_pages(), [4, 3]);
}
#[test]
fn binding_conflicts_odd() {
let mut rounds = Rounds::default();
let requested = SchedulePoint {
round: 0,
stage: RoundStage::Odd(LayerStage::Filter),
};
let even_page_1 = RoundBindings::new(layer_id(TextureParity::Even, 1));
let odd_page_2 = RoundBindings::new(layer_id(TextureParity::Odd, 2));
let odd_page_4 = RoundBindings::new(layer_id(TextureParity::Odd, 4));
assert_eq!(rounds.bind_at_or_after(requested, even_page_1), requested);
assert_eq!(rounds.bind_at_or_after(requested, odd_page_2), requested);
assert_eq!(
rounds.bind_at_or_after(requested, odd_page_4),
SchedulePoint {
round: 1,
..requested
}
);
assert_eq!(
rounds.rounds[0].texture_binding.page_indices(),
[Some(1), Some(2)]
);
assert_eq!(
rounds.rounds[1].texture_binding.page_indices(),
[None, Some(4)]
);
assert_eq!(rounds.required_layer_pages(), [2, 5]);
}
#[test]
fn binding_conflicts_skip_rounds() {
let mut rounds = Rounds::default();
let point = |round| SchedulePoint {
round,
stage: RoundStage::RootDraw,
};
rounds.bind_at_or_after(
point(0),
RoundBindings::new(layer_id(TextureParity::Even, 0)),
);
rounds.bind_at_or_after(
point(1),
RoundBindings::new(layer_id(TextureParity::Even, 1)),
);
assert_eq!(
rounds.bind_at_or_after(
point(0),
RoundBindings::new(layer_id(TextureParity::Even, 2)),
),
point(2)
);
assert_eq!(rounds.rounds.len(), 3);
assert_eq!(rounds.required_layer_pages(), [3, 0]);
}
#[test]
fn operation_ranges() {
let mut rounds = Rounds::default();
let mut buffers = ScheduleBuffers::default();
rounds.ensure_exists(0);
let round = &mut rounds.rounds[0];
round.push_filter_op(TextureParity::Even, &mut buffers, filter_op(10));
round.push_filter_op(TextureParity::Odd, &mut buffers, filter_op(20));
round.push_filter_op(TextureParity::Even, &mut buffers, filter_op(30));
round.push_blend_op(
TextureParity::Odd,
&mut buffers,
BlendOp {
parent_region: layer_region(TextureParity::Odd, 0),
child_region: layer_region(TextureParity::Even, 0),
blend_bbox: RectU16::new(0, 0, 16, 16),
blend_mode: BlendMode::default(),
opacity: 1.0,
clip_strips: None,
},
);
let even_clear_1 = RectU16::new(0, 0, 8, 8);
let odd_clear = RectU16::new(8, 0, 16, 8);
let even_clear_2 = RectU16::new(0, 8, 8, 16);
rounds.push_layer_clear(0, TextureParity::Even, even_clear_1);
rounds.push_layer_clear(0, TextureParity::Odd, odd_clear);
rounds.push_layer_clear(0, TextureParity::Even, even_clear_2);
let round = &rounds.rounds[0];
let even_pass = &round.layer_texture_passes[TextureParity::Even.get_parity()];
let odd_pass = &round.layer_texture_passes[TextureParity::Odd.get_parity()];
let even_offsets: alloc::vec::Vec<_> = buffers
.filter_ops
.ranged(&even_pass.filter_ranges)
.iter()
.map(|op| op.filter_data_offset)
.collect();
let odd_offsets: alloc::vec::Vec<_> = buffers
.filter_ops
.ranged(&odd_pass.filter_ranges)
.iter()
.map(|op| op.filter_data_offset)
.collect();
assert_eq!(even_offsets, [10, 30]);
assert_eq!(odd_offsets, [20]);
assert_eq!(even_pass.blend_ranges.len(), 0);
assert_eq!(odd_pass.blend_ranges.len(), 1);
assert_eq!(
round.layer_texture_clears[TextureParity::Even.get_parity()],
[even_clear_1, even_clear_2]
);
assert_eq!(
round.layer_texture_clears[TextureParity::Odd.get_parity()],
[odd_clear]
);
}
}