blob: 7f2ac68f662fddb290f31bda91aa763489072a3f [file] [edit]
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Executes planned rendering rounds through a backend.
use super::round::{BlendOp, Rounds};
use super::{Schedule, ScheduleBuffers, ScheduleStorage};
use crate::draw::ExternalTextureRun;
use crate::filter::FilterPassPlan;
use crate::target::{
BlendPassBindings, DrawPassBindings, DrawPassTarget, FilterPassBindings, LayerTextureId,
RootTarget,
};
use crate::util::RangedSlice;
use crate::{GpuStrip, blend::BlendStrip};
use vello_common::geometry::{RectU16, SizeU16};
/// A backend for executing GPU render passes.
pub(crate) trait Backend {
/// The error returned while executing a render pass.
type Error;
/// Execute the opaque pass against the root target with the given strips. If this method is
/// ever called, it's called before any of the other ones and only once.
///
/// The strips are guaranteed to be non-empty.
fn opaque_draw_pass(
&mut self,
strips: &[GpuStrip],
external_texture_runs: &[ExternalTextureRun],
) -> Result<(), Self::Error>;
/// Execute a draw pass against the given target.
///
/// The strips are guaranteed to be non-empty.
fn draw_pass(
&mut self,
strips: RangedSlice<'_, GpuStrip>,
external_texture_runs: &[ExternalTextureRun],
bindings: DrawPassBindings,
) -> Result<(), Self::Error>;
/// Execute a clear pass with the given rectangles against the target.
///
/// The clear rectangles are guaranteed to be non-empty.
fn clear_pass(&mut self, target: LayerTextureId, rects: &[RectU16]) -> Result<(), Self::Error>;
/// Execute a blend pass between a parent and child texture.
///
/// The blends are guaranteed to be non-empty.
fn blend_pass(
&mut self,
blends: RangedSlice<'_, BlendOp>,
blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) -> Result<(), Self::Error>;
/// Execute a filter pass according to the filter plan between two textures.
///
/// The filter pass plan is guaranteed to be non-empty.
fn filter_pass(
&mut self,
plan: &FilterPassPlan,
bindings: FilterPassBindings,
) -> Result<(), Self::Error>;
}
pub(crate) fn execute<R: Backend>(
renderer: &mut R,
storage: &mut ScheduleStorage,
schedule: Schedule,
root_output_target: RootTarget,
) -> Result<(), R::Error> {
let ScheduleStorage {
buffers,
filter_pass_plan,
..
} = storage;
schedule.execute(renderer, root_output_target, buffers, filter_pass_plan)
}
impl Schedule {
fn execute<R: Backend>(
&self,
renderer: &mut R,
root_output_target: RootTarget,
buffers: &ScheduleBuffers,
filter_plan: &mut FilterPassPlan,
) -> Result<(), R::Error> {
if DrawPassTarget::Root(root_output_target).enable_opaque()
&& !buffers.draw_buffers.opaque.is_empty()
{
renderer.opaque_draw_pass(
buffers.draw_buffers.opaque.strips(),
buffers.draw_buffers.opaque.external_texture_runs(),
)?;
}
self.rounds.execute(
renderer,
root_output_target,
buffers,
filter_plan,
self.intermediate_textures.size,
)
}
}
impl Rounds {
fn execute<R: Backend>(
&self,
backend: &mut R,
root_output_target: RootTarget,
buffers: &ScheduleBuffers,
filter_plan: &mut FilterPassPlan,
texture_size: SizeU16,
) -> Result<(), R::Error> {
// This is the core loop that ties everything together.
// TODO: Currently, we upload data for layers when executing each round. It might be
// worth exploring whether we can upload the data once in the beginning, and then
// reference it via slices.
// We iterate over each round separately.
for round in self.iter() {
// Draw the even layer texture before the odd one. A child layer always lives in the opposite
// group from its parent, and only odd-texture draws sample the even texture (never the reverse
// within a round), so this order guarantees those pixels are ready when we read them.
for layer_passes in round.layer_passes(buffers) {
// For each layer texture target, first perform the draws of all layers that are
// allocated in this texture.
if let Some(pass) = layer_passes.draw {
backend.draw_pass(pass.strips, pass.external_texture_runs, pass.bindings)?;
}
// Next, we apply all filters for layers in this texture.
if let Some(pass) = layer_passes.filter {
filter_plan.init(pass.filters.iter().copied(), texture_size);
backend.filter_pass(filter_plan, pass.bindings)?;
}
// Finally, we apply all blend operations of layers in the current texture
// that form the backdrop of some child texture.
if let Some(pass) = layer_passes.blend {
backend.blend_pass(pass.blends, pass.blend_strips, pass.bindings)?;
}
}
// Once layers are done, we perform draws against the root target.
if let Some(pass) = round.root_draw_pass(buffers, root_output_target) {
backend.draw_pass(pass.strips, pass.external_texture_runs, pass.bindings)?;
}
// And in the end, clear all layer regions that are deallocated in this round.
for props in round.clear_passes() {
backend.clear_pass(props.target, props.rects)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schedule::test_support::{SceneCase, ScheduledCase};
use crate::target::{LayerTextureId, TextureParity};
use alloc::vec::Vec;
use vello_common::filter_effects::{Filter, FilterPrimitive};
use vello_common::peniko::{BlendMode, Compose, Mix};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Call {
Opaque,
Draw(DrawPassTarget, Option<LayerTextureId>),
Filter(LayerTextureId),
Blend(TextureParity),
Clear(LayerTextureId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stage {
Opaque,
EvenDraw,
EvenFilter,
EvenBlend,
OddDraw,
OddFilter,
OddBlend,
RootDraw,
EvenClear,
OddClear,
}
impl Call {
fn stage(self) -> Stage {
match self {
Self::Opaque => Stage::Opaque,
Self::Draw(DrawPassTarget::Layer(id), _) => match id.texture_parity {
TextureParity::Even => Stage::EvenDraw,
TextureParity::Odd => Stage::OddDraw,
},
Self::Draw(DrawPassTarget::Root(_), _) => Stage::RootDraw,
Self::Filter(id) => match id.texture_parity {
TextureParity::Even => Stage::EvenFilter,
TextureParity::Odd => Stage::OddFilter,
},
Self::Blend(TextureParity::Even) => Stage::EvenBlend,
Self::Blend(TextureParity::Odd) => Stage::OddBlend,
Self::Clear(id) => match id.texture_parity {
TextureParity::Even => Stage::EvenClear,
TextureParity::Odd => Stage::OddClear,
},
}
}
}
#[derive(Default)]
struct Recorder {
calls: Vec<Call>,
}
impl Backend for Recorder {
type Error = core::convert::Infallible;
fn opaque_draw_pass(
&mut self,
_strips: &[GpuStrip],
_external_texture_runs: &[ExternalTextureRun],
) -> Result<(), Self::Error> {
self.calls.push(Call::Opaque);
Ok(())
}
fn draw_pass(
&mut self,
_strips: RangedSlice<'_, GpuStrip>,
_external_texture_runs: &[ExternalTextureRun],
bindings: DrawPassBindings,
) -> Result<(), Self::Error> {
self.calls.push(Call::Draw(bindings.target, bindings.child));
Ok(())
}
fn clear_pass(
&mut self,
target: LayerTextureId,
_rects: &[RectU16],
) -> Result<(), Self::Error> {
self.calls.push(Call::Clear(target));
Ok(())
}
fn blend_pass(
&mut self,
_blends: RangedSlice<'_, BlendOp>,
_blend_strips: &[BlendStrip],
bindings: BlendPassBindings,
) -> Result<(), Self::Error> {
self.calls
.push(Call::Blend(bindings.target().texture_parity));
Ok(())
}
fn filter_pass(
&mut self,
_plan: &FilterPassPlan,
bindings: FilterPassBindings,
) -> Result<(), Self::Error> {
self.calls.push(Call::Filter(bindings.target()));
Ok(())
}
}
fn layer_id(parity: TextureParity) -> LayerTextureId {
LayerTextureId::new(parity, 0)
}
fn execution_case(root_target: RootTarget, depth: usize) -> ScheduledCase {
fn chain(case: &mut SceneCase, depth: usize) {
case.layer(|case| {
if depth == 1 {
case.draw_at(4.0, 0.5);
} else {
chain(case, depth - 1);
}
});
}
let mut case = SceneCase::new(16, 8);
case.draw_at(0.0, 1.0);
chain(&mut case, depth);
case.schedule(root_target, SizeU16::new(64), 2, true)
.unwrap()
}
fn round_order_case() -> ScheduledCase {
let mut case = SceneCase::new(32, 8);
case.draw_at(0.0, 1.0);
case.layer(|case| {
case.draw_at(4.0, 0.5);
case.layer_with(
None,
Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)),
Some(Filter::from_primitive(FilterPrimitive::Offset {
dx: 0.0,
dy: 0.0,
})),
|case| case.draw_at(8.0, 0.5),
);
});
case.layer(|case| {
case.layer(|case| {
case.draw_at(16.0, 0.5);
case.layer_with(
None,
Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)),
Some(Filter::from_primitive(FilterPrimitive::Offset {
dx: 0.0,
dy: 0.0,
})),
|case| case.draw_at(20.0, 0.5),
);
});
});
case.schedule(RootTarget::UserSurface, SizeU16::new(128), 6, true)
.unwrap()
}
#[test]
fn round_order() {
let mut recorder = Recorder::default();
round_order_case().execute(&mut recorder);
assert_eq!(
recorder
.calls
.into_iter()
.map(Call::stage)
.collect::<Vec<_>>(),
[
// First round.
Stage::Opaque,
Stage::EvenDraw,
Stage::EvenFilter,
Stage::OddDraw,
Stage::OddFilter,
Stage::OddBlend,
Stage::RootDraw,
Stage::EvenClear,
Stage::OddClear,
// Second round.
Stage::EvenBlend,
Stage::OddDraw,
Stage::RootDraw,
Stage::EvenClear,
Stage::OddClear,
]
);
}
#[test]
fn child_bindings() {
let mut recorder = Recorder::default();
execution_case(RootTarget::UserSurface, 2).execute(&mut recorder);
let draws = recorder
.calls
.into_iter()
.filter_map(|call| match call {
Call::Draw(target, child) => Some((target, child)),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
draws,
[
(DrawPassTarget::Layer(layer_id(TextureParity::Even)), None,),
(
DrawPassTarget::Layer(layer_id(TextureParity::Odd)),
Some(layer_id(TextureParity::Even)),
),
(
DrawPassTarget::Root(RootTarget::UserSurface),
Some(layer_id(TextureParity::Odd)),
),
]
);
}
#[test]
fn atlas_skips_opaque() {
let mut recorder = Recorder::default();
execution_case(RootTarget::AtlasLayer, 2).execute(&mut recorder);
assert!(!recorder.calls.contains(&Call::Opaque));
}
#[test]
fn empty_passes_are_skipped() {
let mut recorder = Recorder::default();
SceneCase::new(16, 8)
.schedule_root(true)
.execute(&mut recorder);
assert!(recorder.calls.is_empty());
}
}