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::{Backdrop, 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),
Retained(RetainedTarget),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Composite {
pub layer: u32,
pub parity: PageParity,
pub bounds: RectU16,
pub opacity: f32,
pub retained: Option<RetainedSource>,
}
impl Composite {
#[must_use]
pub fn source(&self) -> RectU16 {
let (x, y) = self.retained.map_or((0, 0), |source| source.origin);
RectU16::new(
x,
y,
x.saturating_add(self.bounds.width()),
y.saturating_add(self.bounds.height()),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetainedSource {
pub bracket: u32,
pub origin: (u16, u16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetainedTarget {
pub layer: u32,
pub bracket: u32,
pub bounds: RectU16,
pub continued: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetainedRole {
Record {
bracket: u32,
},
Draw {
bracket: u32,
dest: RectU16,
origin: (u16, u16),
},
Hidden,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RetainedLayers {
roles: Vec<Option<RetainedRole>>,
}
impl RetainedLayers {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.roles.clear();
}
pub fn set(&mut self, layer: u32, role: RetainedRole) {
let index = layer as usize;
if self.roles.len() <= index {
self.roles.resize(index.saturating_add(1), None);
}
if let Some(slot) = self.roles.get_mut(index) {
*slot = Some(role);
}
}
#[must_use]
pub fn role(&self, layer: u32) -> Option<RetainedRole> {
self.roles.get(layer as usize).copied().flatten()
}
}
#[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>,
pub copy: Option<RegionCopy>,
pub samples: Vec<BackdropSample>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RegionCopy {
pub source: RectU16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackdropSample {
pub layer: u32,
pub parity: PageParity,
}
impl Round {
#[must_use]
pub fn page(&self) -> Option<&PageTarget> {
match &self.target {
RoundTarget::Root | RoundTarget::Retained(_) => None,
RoundTarget::Page(page) => Some(page),
}
}
#[must_use]
pub fn retained(&self) -> Option<&RetainedTarget> {
match &self.target {
RoundTarget::Retained(target) => Some(target),
RoundTarget::Root | RoundTarget::Page(_) => None,
}
}
#[must_use]
pub fn filter_pass(&self) -> Option<&FilterPass> {
self.filter.as_ref()
}
#[must_use]
pub fn region_copy(&self) -> Option<&RegionCopy> {
self.copy.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> {
Self::build_retained(recorder, caps, config, &RetainedLayers::new())
}
pub fn build_retained(
recorder: &CommandRecorder<EngineDraw>,
caps: &TierCaps,
config: &PageConfig,
retained: &RetainedLayers,
) -> Result<Vec<Round>, EngineError> {
Self::build_frame(recorder, caps, config, retained, &[])
}
pub fn build_frame(
recorder: &CommandRecorder<EngineDraw>,
caps: &TierCaps,
config: &PageConfig,
retained: &RetainedLayers,
backdrops: &[Backdrop],
) -> 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);
if let Some(backdrop) = backdrops.iter().find(|backdrop| backdrop.layer == id) {
layer_role(id, layer)?;
let at_root = stack
.iter()
.rev()
.find(|stream| !matches!(stream.owner, StreamOwner::Inline))
.is_some_and(|stream| matches!(stream.owner, StreamOwner::Root));
if at_root && !inherited && !layer.bbox.is_empty() {
backdrop_rounds(
backdrop,
&mut stack,
&mut rounds,
&mut pages,
caps,
config,
)?;
stack.push(Stream::inline(layer, depth, false));
}
continue;
}
if let Some(role) = retained.role(id) {
layer_role(id, layer)?;
match role {
RetainedRole::Record { bracket } => {
stack.push(Stream::retained(id, layer, bracket, depth, inherited))
}
RetainedRole::Draw {
bracket,
dest,
origin,
} => {
if !inherited
&& layer.props.opacity > 0.0
&& let Some(stream) = stack.last_mut()
{
stream.ops.push(RoundOp::Composite(Composite {
layer: id,
parity: PageParity::Even,
bounds: dest,
opacity: layer.props.opacity,
retained: Some(RetainedSource { bracket, origin }),
}));
}
}
RetainedRole::Hidden => {}
}
continue;
}
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 mut released = released_pages(&stream.ops);
release_samples(&stream.samples, &mut released);
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,
copy: None,
samples: stream.samples,
});
}
return Ok(Vec::new());
}
StreamOwner::Retained { id, layer, bracket } => {
return Ok(finish_retained(stream, id, layer, bracket, rounds, pages));
}
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,
copy: None,
samples: Vec::new(),
});
}
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,
copy: None,
samples: Vec::new(),
});
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,
retained: None,
})])
}
fn finish_retained(
stream: Stream<'_>,
id: u32,
layer: &RecordedLayer,
bracket: u32,
rounds: &mut Vec<Round>,
pages: &mut LivePages,
) -> Vec<RoundOp> {
if stream.suppressed {
return Vec::new();
}
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::Retained(RetainedTarget {
layer: id,
bracket,
bounds: layer.bbox,
continued: stream.emitted,
}),
ops: stream.ops,
released,
filter: None,
copy: None,
samples: Vec::new(),
});
}
let opacity = layer.props.opacity;
if opacity > 0.0 {
vec![RoundOp::Composite(Composite {
layer: id,
parity: PageParity::Even,
bounds: layer.bbox,
opacity,
retained: Some(RetainedSource {
bracket,
origin: (0, 0),
}),
})]
} else {
Vec::new()
}
}
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))
}
};
pass_rounds(filtered, &steps, stack, rounds, pages, caps, config)
}
fn pass_rounds(
filtered: &FilterLayer<'_>,
steps: &[FilterStep],
stack: &mut [Stream<'_>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<PageParity, EngineError> {
let id = filtered.id;
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,
}),
copy: None,
samples: Vec::new(),
});
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,
copy: None,
samples: Vec::new(),
});
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,
retained: None,
}));
}
}
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) || !stream.samples.is_empty())
{
return Ok(false);
}
let continued = stack[index].emitted;
let target = match stack[index].owner {
StreamOwner::Inline => return Ok(false),
StreamOwner::Root => RoundTarget::Root,
StreamOwner::Retained { id, layer, bracket } => RoundTarget::Retained(RetainedTarget {
layer: id,
bracket,
bounds: layer.bbox,
continued,
}),
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();
let mut samples: Vec<BackdropSample> = Vec::new();
for stream in stack.get_mut(index..end).unwrap_or(&mut []) {
ops.append(&mut stream.ops);
samples.append(&mut stream.samples);
}
let mut released = released_pages(&ops);
release_samples(&samples, &mut released);
for parity in &released {
pages.release(*parity);
}
stack[index].emitted = true;
rounds.push(Round {
target,
ops,
released,
filter: None,
copy: None,
samples,
});
Ok(true)
}
fn backdrop_rounds(
backdrop: &Backdrop,
stack: &mut [Stream<'_>],
rounds: &mut Vec<Round>,
pages: &mut LivePages,
caps: &TierCaps,
config: &PageConfig,
) -> Result<(), EngineError> {
flush_surface(stack, rounds, pages);
let id = backdrop.layer;
let region = backdrop.region;
let size = filter_page_size(region, config, caps)?;
let contents = pages.acquire(PageParity::Even).ok_or_else(|| {
escalate(format!(
"backdrop layer {id} found no free page group for its region copy after the surface \
round ahead of it was cut"
))
})?;
rounds.push(Round {
target: RoundTarget::Page(PageTarget {
layer: id,
depth: 1,
parity: contents,
size,
bounds: region,
continued: false,
}),
ops: Vec::new(),
released: Vec::new(),
filter: None,
copy: Some(RegionCopy { source: region }),
samples: Vec::new(),
});
let steps = blur::blur_passes(&backdrop.blur(), SizeU16::from(region));
let regular = RecordedLayerKind::Regular;
let blurred = pass_rounds(
&FilterLayer {
id,
kind: ®ular,
depth: 1,
bounds: region,
size,
contents,
},
&steps,
stack,
rounds,
pages,
caps,
config,
)?;
if let Some(root) = stack.first_mut() {
root.samples.push(BackdropSample {
layer: id,
parity: blurred,
});
}
Ok(())
}
fn flush_surface(stack: &mut [Stream<'_>], rounds: &mut Vec<Round>, pages: &mut LivePages) {
let mut ops: Vec<RoundOp> = Vec::new();
let mut samples: Vec<BackdropSample> = Vec::new();
for stream in stack.iter_mut() {
ops.append(&mut stream.ops);
samples.append(&mut stream.samples);
}
if ops.is_empty() && samples.is_empty() {
return;
}
let mut released = released_pages(&ops);
release_samples(&samples, &mut released);
for parity in &released {
pages.release(*parity);
}
if let Some(root) = stack.first_mut() {
root.emitted = true;
}
rounds.push(Round {
target: RoundTarget::Root,
ops,
released,
filter: None,
copy: None,
samples,
});
}
fn release_samples(samples: &[BackdropSample], released: &mut Vec<PageParity>) {
for sample in samples {
if !released.contains(&sample.parity) {
released.push(sample.parity);
}
}
}
#[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,
samples: Vec<BackdropSample>,
}
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,
samples: Vec::new(),
}
}
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,
samples: Vec::new(),
}
}
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,
samples: Vec::new(),
}
}
fn retained(
id: u32,
layer: &'a RecordedLayer,
bracket: u32,
depth: usize,
suppressed: bool,
) -> Self {
Self {
owner: StreamOwner::Retained { id, layer, bracket },
nodes: &layer.nodes,
index: 0,
depth,
ops: Vec::new(),
emitted: false,
page: None,
suppressed,
samples: Vec::new(),
}
}
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,
},
Retained {
id: u32,
layer: &'a RecordedLayer,
bracket: u32,
},
}
#[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
&& composite.retained.is_none()
&& !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(),
}
}