pub mod pages;
pub use pages::{PageConfig, PageParity, PageSize, filter_page_size, page_ceiling, page_size};
use core::ops::Range;
use frust_gpu::TierCaps;
use vello_common::geometry::{RectU16, SizeU16};
use vello_common::peniko::BlendMode;
use vello_common::record::{CommandRecorder, Node, RecordedLayer, RecordedLayerKind};
use crate::compile::EngineDraw;
use crate::error::EngineError;
use crate::filters::{FilterStep, ServedFilter, blur, drop_shadow, served_filter};
pub const MAX_CHAIN_DEPTH: usize = 4;
pub const PING_PONG_GROUPS: usize = 2;
pub const MAX_LIVE_PAGES: usize = PING_PONG_GROUPS + 1;
#[cfg(feature = "full-scheduler")]
pub mod full {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageTarget {
pub layer: u32,
pub depth: usize,
pub parity: PageParity,
pub size: PageSize,
pub bounds: RectU16,
pub continued: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoundTarget {
Root,
Page(PageTarget),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Composite {
pub layer: u32,
pub parity: PageParity,
pub bounds: RectU16,
pub opacity: f32,
}
impl Composite {
#[must_use]
pub fn source(&self) -> RectU16 {
RectU16::new(0, 0, self.bounds.width(), self.bounds.height())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FilterPass {
pub layer: u32,
pub step: FilterStep,
pub source: PageParity,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RoundOp {
Draws(Range<u32>),
Composite(Composite),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Round {
pub target: RoundTarget,
pub ops: Vec<RoundOp>,
pub released: Vec<PageParity>,
pub filter: Option<FilterPass>,
}
impl Round {
#[must_use]
pub fn page(&self) -> Option<&PageTarget> {
match &self.target {
RoundTarget::Root => None,
RoundTarget::Page(page) => Some(page),
}
}
#[must_use]
pub fn filter_pass(&self) -> Option<&FilterPass> {
self.filter.as_ref()
}
#[must_use]
pub fn is_root(&self) -> bool {
matches!(self.target, RoundTarget::Root)
}
pub fn composites(&self) -> impl Iterator<Item = &Composite> {
self.ops.iter().filter_map(|op| match op {
RoundOp::Composite(composite) => Some(composite),
RoundOp::Draws(_) => None,
})
}
#[must_use]
pub fn draw_count(&self) -> u32 {
self.ops
.iter()
.map(|op| match op {
RoundOp::Draws(range) => range.end.saturating_sub(range.start),
RoundOp::Composite(_) => 0,
})
.sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Schedule;
impl Schedule {
pub fn build(
recorder: &CommandRecorder<EngineDraw>,
caps: &TierCaps,
config: &PageConfig,
) -> Result<Vec<Round>, EngineError> {
let mut rounds: Vec<Round> = Vec::new();
let mut pages = LivePages::default();
let mut entered = vec![false; recorder.layers.len()];
let mut stack = vec![Stream::root(&recorder.nodes)];
while !stack.is_empty() {
let step = match stack.last_mut() {
None => break,
Some(stream) => match stream.nodes.get(stream.index) {
None => Step::Done,
Some(node) => {
stream.index = stream.index.saturating_add(1);
if node.draws.start < node.draws.end {
stream.ops.push(RoundOp::Draws(node.draws.clone()));
}
match node.layer {
None => Step::Next,
Some(id) => Step::Enter {
id,
depth: stream.depth,
},
}
}
},
};
match step {
Step::Next => {}
Step::Done => {
if let Some(stream) = stack.pop() {
let carry =
finish(stream, &mut stack, &mut rounds, &mut pages, caps, config)?;
if let Some(parent) = stack.last_mut() {
parent.ops.extend(carry);
}
}
}
Step::Enter { id, depth } => {
let index = id as usize;
let (Some(layer), Some(seen)) =
(recorder.layers.get(index), entered.get_mut(index))
else {
return Err(escalate(format!(
"a node enters layer {id}, which the recording does not hold"
)));
};
if *seen {
return Err(escalate(format!(
"layer {id} is entered a second time; the simple scheduler walks \
each recorded layer once, and a recording whose nodes re-enter one \
describes no tree it could render"
)));
}
*seen = true;
let inherited = stack.last().is_some_and(|stream| stream.suppressed);
let role = layer_role(id, layer)?;
if matches!(role, LayerRole::Filtered) && stack.len() > 1 {
return Err(escalate(format!(
"filter layer {id} is recorded inside another layer, whose own bounds \
would be taken from a placement that undoes a source shift this \
engine's compiler never applied; a filter layer is served directly \
under the frame's own surface"
)));
}
match role {
LayerRole::Dropped => {}
LayerRole::Inline => stack.push(Stream::inline(layer, depth, inherited)),
LayerRole::Isolated | LayerRole::Filtered => {
let depth = depth.saturating_add(1);
if depth > MAX_CHAIN_DEPTH {
return Err(escalate(format!(
"{depth} nested isolated layers, deeper than the \
{MAX_CHAIN_DEPTH}-deep chain the simple scheduler serves"
)));
}
let suppressed = inherited || layer.bbox.is_empty();
stack.push(Stream::isolated(id, layer, depth, suppressed));
}
}
}
}
}
Ok(rounds)
}
}
fn finish<'a>(
stream: Stream<'a>,
stack: &mut [Stream<'a>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<Vec<RoundOp>, EngineError> {
let (id, layer) = match stream.owner {
StreamOwner::Inline => return Ok(stream.ops),
StreamOwner::Root => {
let released = released_pages(&stream.ops);
for parity in &released {
pages.release(*parity);
}
if !stream.ops.is_empty() || !stream.emitted {
rounds.push(Round {
target: RoundTarget::Root,
ops: stream.ops,
released,
filter: None,
});
}
return Ok(Vec::new());
}
StreamOwner::Isolated { id, layer } => (id, layer),
};
if stream.suppressed {
return Ok(Vec::new());
}
let bounds = layer.bbox;
let depth = stream.depth;
if matches!(layer.kind, RecordedLayerKind::Regular)
&& stream.page.is_none()
&& u32::from(bounds.width()) > page_ceiling(config, caps)
&& !holds_composite(&stream.ops)
{
return band_rounds(
BandedLayer {
id,
layer,
bounds,
depth,
},
stream.ops,
stack,
rounds,
pages,
caps,
config,
);
}
let size = match &layer.kind {
RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
_ => filter_page_size(bounds, config, caps)?,
};
let page = |parity, continued| {
RoundTarget::Page(PageTarget {
layer: id,
depth,
parity,
size,
bounds,
continued,
})
};
let contents = match stream.page {
Some(parity) => {
if !stream.ops.is_empty() {
let released = released_pages(&stream.ops);
for parity in &released {
pages.release(*parity);
}
rounds.push(Round {
target: page(parity, true),
ops: stream.ops,
released,
filter: None,
});
}
parity
}
None => {
make_room(stack, rounds, pages, caps, config)?;
let spills = matches!(layer.kind, RecordedLayerKind::Regular);
let parity = pages
.acquire(PageParity::from_depth(depth))
.or_else(|| spills.then(|| pages.acquire_spill()).flatten())
.ok_or_else(|| escalate(out_of_pages(id, spills)))?;
let released = released_pages(&stream.ops);
for parity in &released {
pages.release(*parity);
}
rounds.push(Round {
target: page(parity, false),
ops: stream.ops,
released,
filter: None,
});
parity
}
};
let composited = match &layer.kind {
RecordedLayerKind::Regular => contents,
kind => filter_rounds(
&FilterLayer {
id,
kind,
depth,
bounds,
size,
contents,
},
stack,
rounds,
pages,
caps,
config,
)?,
};
Ok(vec![RoundOp::Composite(Composite {
layer: id,
parity: composited,
bounds,
opacity: layer.props.opacity,
})])
}
struct FilterLayer<'a> {
id: u32,
kind: &'a RecordedLayerKind,
depth: usize,
bounds: RectU16,
size: PageSize,
contents: PageParity,
}
fn filter_rounds(
filtered: &FilterLayer<'_>,
stack: &mut [Stream<'_>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<PageParity, EngineError> {
let id = filtered.id;
let steps = match served_filter(id, filtered.kind).map_err(escalate)? {
ServedFilter::Blur(blur) => blur::blur_passes(&blur, SizeU16::from(filtered.bounds)),
ServedFilter::DropShadow(shadow) => {
drop_shadow::drop_shadow_passes(&shadow, SizeU16::from(filtered.bounds))
}
};
let Some(last) = steps.len().checked_sub(1) else {
return Ok(filtered.contents);
};
let scratch = filtered.contents.opposite();
for index in (0..stack.len()).rev() {
if !pages.holds(scratch) {
break;
}
cut_at(stack, index, rounds, pages, caps, config)?;
}
let scratch = pages.acquire(scratch).ok_or_else(|| {
escalate(format!(
"filter layer {id} would need both of the {PING_PONG_GROUPS} page groups at once — \
one for its contents and one for its passes to write — and the second is holding a \
page a later round composites that no open round could be cut to hand back"
))
})?;
let mut source = filtered.contents;
let mut dest = scratch;
for (index, step) in steps.iter().enumerate() {
let released = if index == last {
vec![source]
} else {
Vec::new()
};
for parity in &released {
pages.release(*parity);
}
rounds.push(Round {
target: RoundTarget::Page(PageTarget {
layer: id,
depth: filtered.depth,
parity: dest,
size: filtered.size,
bounds: filtered.bounds,
continued: false,
}),
ops: Vec::new(),
released,
filter: Some(FilterPass {
layer: id,
step: *step,
source,
}),
});
if index < last {
core::mem::swap(&mut source, &mut dest);
}
}
Ok(dest)
}
struct BandedLayer<'a> {
id: u32,
layer: &'a RecordedLayer,
bounds: RectU16,
depth: usize,
}
fn band_rounds(
banded: BandedLayer<'_>,
ops: Vec<RoundOp>,
stack: &mut [Stream<'_>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<Vec<RoundOp>, EngineError> {
let bands = pages::page_bands(banded.bounds, config, caps)?;
for band in bands {
make_room(stack, rounds, pages, caps, config)?;
let parity = pages
.acquire(PageParity::from_depth(banded.depth))
.ok_or_else(|| {
escalate(format!(
"layer {} would need a third live intermediate page to render one of its own \
column bands: both of the {PING_PONG_GROUPS} groups the simple scheduler \
ping-pongs between are already holding a page a later round composites, and \
no open round could be cut to hand one back",
banded.id
))
})?;
rounds.push(Round {
target: RoundTarget::Page(PageTarget {
layer: banded.id,
depth: banded.depth,
parity,
size: band.size,
bounds: band.bounds,
continued: false,
}),
ops: ops.clone(),
released: Vec::new(),
filter: None,
});
if let Some(parent) = stack.last_mut() {
parent.ops.push(RoundOp::Composite(Composite {
layer: banded.id,
parity,
bounds: band.bounds,
opacity: banded.layer.props.opacity,
}));
}
}
Ok(Vec::new())
}
fn make_room(
stack: &mut [Stream<'_>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<(), EngineError> {
if pages.free() > 1 {
return Ok(());
}
for index in 0..stack.len() {
if pages.free() == 0 {
break;
}
if stack[index].wants_a_page() {
cut_at(stack, index, rounds, pages, caps, config)?;
}
}
if pages.free() == 0 {
for index in (0..stack.len()).rev() {
if cut_at(stack, index, rounds, pages, caps, config)? {
break;
}
}
}
Ok(())
}
fn cut_at(
stack: &mut [Stream<'_>],
index: usize,
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<bool, EngineError> {
let Some(stream) = stack.get(index) else {
return Ok(false);
};
if stream.suppressed || matches!(stream.owner, StreamOwner::Inline) {
return Ok(false);
}
let end = stack
.iter()
.enumerate()
.skip(index.saturating_add(1))
.find(|(_, stream)| !matches!(stream.owner, StreamOwner::Inline))
.map_or(stack.len(), |(at, _)| at);
let batch = stack.get(index..end).unwrap_or(&[]);
if !batch.iter().any(|stream| holds_composite(&stream.ops)) {
return Ok(false);
}
let continued = stack[index].emitted;
let target = match stack[index].owner {
StreamOwner::Inline => return Ok(false),
StreamOwner::Root => RoundTarget::Root,
StreamOwner::Isolated { id, layer } => {
let bounds = layer.bbox;
let size = match &layer.kind {
RecordedLayerKind::Regular => page_size(bounds, config, caps)?,
_ => filter_page_size(bounds, config, caps)?,
};
let parity = match stack[index].page {
Some(parity) => parity,
None => {
let Some(parity) = pages.acquire(PageParity::from_depth(stack[index].depth))
else {
return Ok(false);
};
stack[index].page = Some(parity);
parity
}
};
RoundTarget::Page(PageTarget {
layer: id,
depth: stack[index].depth,
parity,
size,
bounds,
continued,
})
}
};
let mut ops: Vec<RoundOp> = Vec::new();
for stream in stack.get_mut(index..end).unwrap_or(&mut []) {
ops.append(&mut stream.ops);
}
let released = released_pages(&ops);
for parity in &released {
pages.release(*parity);
}
stack[index].emitted = true;
rounds.push(Round {
target,
ops,
released,
filter: None,
});
Ok(true)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LayerRole {
Inline,
Isolated,
Filtered,
Dropped,
}
enum Step {
Enter {
id: u32,
depth: usize,
},
Next,
Done,
}
struct Stream<'a> {
owner: StreamOwner<'a>,
nodes: &'a [Node],
index: usize,
depth: usize,
ops: Vec<RoundOp>,
emitted: bool,
page: Option<PageParity>,
suppressed: bool,
}
impl<'a> Stream<'a> {
fn root(nodes: &'a [Node]) -> Self {
Self {
owner: StreamOwner::Root,
nodes,
index: 0,
depth: 0,
ops: Vec::new(),
emitted: false,
page: None,
suppressed: false,
}
}
fn inline(layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
Self {
owner: StreamOwner::Inline,
nodes: &layer.nodes,
index: 0,
depth,
ops: Vec::new(),
emitted: false,
page: None,
suppressed,
}
}
fn isolated(id: u32, layer: &'a RecordedLayer, depth: usize, suppressed: bool) -> Self {
Self {
owner: StreamOwner::Isolated { id, layer },
nodes: &layer.nodes,
index: 0,
depth,
ops: Vec::new(),
emitted: false,
page: None,
suppressed,
}
}
fn wants_a_page(&self) -> bool {
matches!(self.owner, StreamOwner::Isolated { .. })
&& !self.suppressed
&& self.page.is_none()
}
}
enum StreamOwner<'a> {
Root,
Inline,
Isolated {
id: u32,
layer: &'a RecordedLayer,
},
}
#[derive(Debug, Default)]
struct LivePages {
held: [bool; PING_PONG_GROUPS],
spill: bool,
}
impl LivePages {
fn acquire(&mut self, preferred: PageParity) -> Option<PageParity> {
let parity = if !self.holds(preferred) {
preferred
} else if !self.holds(preferred.opposite()) {
preferred.opposite()
} else {
return None;
};
self.set(parity, true);
Some(parity)
}
fn acquire_spill(&mut self) -> Option<PageParity> {
if self.spill {
return None;
}
self.set(PageParity::Spill, true);
Some(PageParity::Spill)
}
fn release(&mut self, parity: PageParity) {
self.set(parity, false);
}
fn free(&self) -> usize {
self.held.iter().filter(|held| !**held).count()
}
fn holds(&self, parity: PageParity) -> bool {
match parity {
PageParity::Spill => self.spill,
group => self.held.get(group.index()).copied().unwrap_or(false),
}
}
fn set(&mut self, parity: PageParity, held: bool) {
match parity {
PageParity::Spill => self.spill = held,
group => {
if let Some(slot) = self.held.get_mut(group.index()) {
*slot = held;
}
}
}
}
}
fn layer_role(id: u32, layer: &RecordedLayer) -> Result<LayerRole, EngineError> {
if layer.props.blend_mode != BlendMode::default() {
return Err(escalate(format!(
"layer {id} composites with a non-default blend mode ({:?}), which has to read the \
target it blends into",
layer.props.blend_mode
)));
}
if layer.props.mask.is_some() {
return Err(escalate(format!(
"layer {id} carries a layer mask, which needs an intermediate of its own"
)));
}
if layer.props.clip_path.is_some() {
return Err(escalate(format!(
"layer {id} carries a layer clip path; frust lowers clips through the clip stack, not \
through a layer"
)));
}
let opacity = layer.props.opacity;
if !opacity.is_finite() {
return Err(escalate(format!(
"layer {id} carries a non-finite opacity ({opacity})"
)));
}
if opacity <= 0.0 {
return Ok(LayerRole::Dropped);
}
match &layer.kind {
RecordedLayerKind::Regular => Ok(if opacity >= 1.0 {
LayerRole::Inline
} else {
LayerRole::Isolated
}),
kind => {
served_filter(id, kind).map_err(escalate)?;
Ok(LayerRole::Filtered)
}
}
}
fn holds_composite(ops: &[RoundOp]) -> bool {
ops.iter().any(|op| matches!(op, RoundOp::Composite(_)))
}
fn released_pages(ops: &[RoundOp]) -> Vec<PageParity> {
let mut pages: Vec<PageParity> = Vec::new();
for op in ops {
if let RoundOp::Composite(composite) = op
&& !pages.contains(&composite.parity)
{
pages.push(composite.parity);
}
}
pages
}
fn out_of_pages(id: u32, spills: bool) -> String {
if spills {
format!(
"layer {id} would need a fourth live intermediate page: both of the \
{PING_PONG_GROUPS} groups the simple scheduler ping-pongs between and the one spill \
page beside them are already holding a page a later round composites, and no open \
round could be cut to hand one back. A nested chain of any depth fits, a fan of \
siblings of any width fits, and so does a chain hanging off an isolated ancestor's \
later child; what does not is a layer whose own round samples two live pages while a \
third is still owed to a round above it"
)
} else {
format!(
"filter layer {id} would need a third live intermediate page for its contents: both \
of the {PING_PONG_GROUPS} groups the simple scheduler ping-pongs between are already \
holding a page a later round composites, and no open round could be cut to hand one \
back. The spill page a regular layer falls back on is not offered here — a filter \
layer's passes ping-pong between the two groups themselves, so it is held to them"
)
}
}
fn escalate(reason: impl Into<String>) -> EngineError {
EngineError::SchedulerEscalation {
reason: reason.into(),
}
}