vello_hybrid: Create a reusable state for tiles and buffers (#1395)
Before (1110ms):
<img width="1483" height="418" alt="image"
src="https://github.com/user-attachments/assets/9a24bda0-d946-4a75-b709-a4db9a7fdd4e"
/>
After (863ms):
<img width="1471" height="503" alt="image"
src="https://github.com/user-attachments/assets/9a3bcae5-363e-47eb-a288-2c0196ef26bd"
/>
diff --git a/sparse_strips/vello_hybrid/src/render/webgl.rs b/sparse_strips/vello_hybrid/src/render/webgl.rs
index 89564e0..f350b43 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl.rs
@@ -36,7 +36,7 @@
},
},
scene::Scene,
- schedule::{LoadOp, RendererBackend, Scheduler},
+ schedule::{LoadOp, RendererBackend, Scheduler, SchedulerState},
};
use alloc::sync::Arc;
@@ -82,6 +82,8 @@
programs: WebGlPrograms,
/// Scheduler for scheduling draws.
scheduler: Scheduler,
+ /// The state used by the scheduler.
+ scheduler_state: SchedulerState,
/// WebGL context.
gl: WebGl2RenderingContext,
/// Image cache for storing images atlas allocations.
@@ -152,6 +154,7 @@
Self {
programs: WebGlPrograms::new(gl.clone(), &image_cache, total_slots),
scheduler: Scheduler::new(total_slots),
+ scheduler_state: SchedulerState::default(),
gl,
image_cache,
encoded_paints: Vec::new(),
@@ -189,7 +192,8 @@
programs: &mut self.programs,
gl: &self.gl,
};
- self.scheduler.do_scene(&mut ctx, scene, &self.paint_idxs)?;
+ self.scheduler
+ .do_scene(&mut self.scheduler_state, &mut ctx, scene, &self.paint_idxs)?;
self.gradient_cache.maintain();
// Blit the view framebuffer to the default framebuffer (canvas element), reflecting the
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu.rs b/sparse_strips/vello_hybrid/src/render/wgpu.rs
index 5f64c06..2a90143 100644
--- a/sparse_strips/vello_hybrid/src/render/wgpu.rs
+++ b/sparse_strips/vello_hybrid/src/render/wgpu.rs
@@ -39,7 +39,7 @@
},
},
scene::Scene,
- schedule::{LoadOp, RendererBackend, Scheduler},
+ schedule::{LoadOp, RendererBackend, Scheduler, SchedulerState},
};
use bytemuck::{Pod, Zeroable};
use vello_common::{
@@ -83,6 +83,8 @@
programs: Programs,
/// Scheduler for scheduling draws.
scheduler: Scheduler,
+ /// The state used by the scheduler.
+ scheduler_state: SchedulerState,
/// Image cache for storing images atlas allocations.
image_cache: ImageCache,
/// Encoded paints for storing encoded paints.
@@ -119,6 +121,7 @@
Self {
programs: Programs::new(device, &image_cache, render_target_config, total_slots),
scheduler: Scheduler::new(total_slots),
+ scheduler_state: SchedulerState::default(),
image_cache,
gradient_cache,
encoded_paints: Vec::new(),
@@ -160,7 +163,12 @@
view,
};
- let result = self.scheduler.do_scene(&mut junk, scene, &self.paint_idxs);
+ let result = self.scheduler.do_scene(
+ &mut self.scheduler_state,
+ &mut junk,
+ scene,
+ &self.paint_idxs,
+ );
self.gradient_cache.maintain();
result
diff --git a/sparse_strips/vello_hybrid/src/schedule.rs b/sparse_strips/vello_hybrid/src/schedule.rs
index ebc18d8..cedd44c 100644
--- a/sparse_strips/vello_hybrid/src/schedule.rs
+++ b/sparse_strips/vello_hybrid/src/schedule.rs
@@ -247,12 +247,38 @@
clear: [Vec<u32>; 2],
}
+/// Reusable state used by the scheduler. We are holding this separately instead of integrating
+/// it into `Scheduler` because it avoids some borrowing issues.
+#[derive(Debug, Default)]
+pub(crate) struct SchedulerState {
+ /// The state of the current wide tile that is being processed.
+ tile_state: TileState,
+ /// Annotated commands for the current wide tile.
+ annotated_commands: Vec<AnnotatedCmd>,
+ /// Pointers to `PushBuf` commands.
+ pointer_to_push_buf_stack: Vec<usize>,
+}
+
+impl SchedulerState {
+ fn clear(&mut self) {
+ self.tile_state.clear();
+ self.annotated_commands.clear();
+ self.pointer_to_push_buf_stack.clear();
+ }
+}
+
/// State for a single wide tile.
#[derive(Debug, Default)]
struct TileState {
stack: Vec<TileEl>,
}
+impl TileState {
+ fn clear(&mut self) {
+ self.stack.clear();
+ }
+}
+
/// A claimed slot into one of the two slot textures (0, 1).
#[derive(Debug, Copy, Clone)]
enum ClaimedSlot {
@@ -282,9 +308,9 @@
/// TODO: In the future these annotations could be optionally enabled in coarse.rs directly avoiding
/// the need to do linear scans.
#[derive(Debug)]
-enum AnnotatedCmd<'a> {
- /// A wrapped command - no semantic meaning added.
- IdentityBorrowed(&'a Cmd),
+enum AnnotatedCmd {
+ /// The index of a wrapped command - no semantic meaning added.
+ Identity(usize),
PushBuf,
Empty,
SrcOverNormalBlend,
@@ -294,18 +320,18 @@
PushBufWithTemporarySlot,
}
-impl<'a> AnnotatedCmd<'a> {
- fn as_cmd<'b: 'a>(&'b self) -> Option<&'a Cmd> {
+impl AnnotatedCmd {
+ fn as_cmd<'a>(&self, cmds: &'a [Cmd]) -> Option<&'a Cmd> {
match self {
- AnnotatedCmd::IdentityBorrowed(cmd) => Some(cmd),
- AnnotatedCmd::PushBufWithTemporarySlot => Some(&Cmd::PushBuf(LayerKind::Regular(0))),
- AnnotatedCmd::PushBuf => Some(&Cmd::PushBuf(LayerKind::Regular(0))),
- AnnotatedCmd::SrcOverNormalBlend => Some(&Cmd::Blend(BlendMode {
+ Self::Identity(idx) => Some(&cmds[*idx]),
+ Self::PushBufWithTemporarySlot => Some(&Cmd::PushBuf(LayerKind::Regular(0))),
+ Self::PushBuf => Some(&Cmd::PushBuf(LayerKind::Regular(0))),
+ Self::SrcOverNormalBlend => Some(&Cmd::Blend(BlendMode {
mix: Mix::Normal,
compose: Compose::SrcOver,
})),
- AnnotatedCmd::PopBuf => Some(&Cmd::PopBuf),
- AnnotatedCmd::Empty => None,
+ Self::PopBuf => Some(&Cmd::PopBuf),
+ Self::Empty => None,
}
}
}
@@ -403,6 +429,7 @@
pub(crate) fn do_scene<R: RendererBackend>(
&mut self,
+ state: &mut SchedulerState,
renderer: &mut R,
scene: &Scene,
paint_idxs: &[u32],
@@ -417,21 +444,24 @@
let wide_tile_x = wide_tile_col * WideTile::WIDTH;
let wide_tile_y = wide_tile_row * Tile::HEIGHT;
- let tile_state = self.initialize_tile_state(
+ state.clear();
+
+ self.initialize_tile_state(
+ &mut state.tile_state,
wide_tile,
wide_tile_x,
wide_tile_y,
scene,
paint_idxs,
);
- let annotated_cmds = prepare_cmds(&wide_tile.cmds);
+ prepare_cmds(&wide_tile.cmds, state);
self.do_tile(
+ state,
renderer,
scene,
wide_tile_x,
wide_tile_y,
- &annotated_cmds,
- tile_state,
+ &wide_tile.cmds,
paint_idxs,
&scene.wide.attrs,
)?;
@@ -526,16 +556,16 @@
fn initialize_tile_state(
&mut self,
+ tile_state: &mut TileState,
tile: &WideTile<MODE_HYBRID>,
wide_tile_x: u16,
wide_tile_y: u16,
scene: &Scene,
idxs: &[u32],
- ) -> TileState {
- let mut state = TileState::default();
+ ) {
// Sentinel `TileEl` to indicate the end of the stack where we draw all
// commands to the final target.
- state.stack.push(TileEl {
+ tile_state.stack.push(TileEl {
dest_slot: ClaimedSlot::Texture0(SENTINEL_SLOT_IDX),
temporary_slot: TemporarySlot::None,
round: self.round,
@@ -559,34 +589,33 @@
);
}
}
- state
}
/// Iterates over wide tile commands and schedules them for rendering.
///
/// Returns `Some(command_idx)` if there is more work to be done. Returns `None` if the wide
/// tile has been fully consumed.
- fn do_tile<'a, R: RendererBackend>(
+ fn do_tile<R: RendererBackend>(
&mut self,
+ state: &mut SchedulerState,
renderer: &mut R,
scene: &Scene,
wide_tile_x: u16,
wide_tile_y: u16,
- cmds: &'a [AnnotatedCmd<'a>],
- mut state: TileState,
+ wide_tile_cmds: &[Cmd],
paint_idxs: &[u32],
attrs: &CommandAttrs,
) -> Result<(), RenderError> {
- for annotated_cmd in cmds {
+ for annotated_cmd in &state.annotated_commands {
// Note: this starts at 1 (for the final target)
- let depth = state.stack.len();
- let Some(cmd) = annotated_cmd.as_cmd() else {
+ let depth = state.tile_state.stack.len();
+ let Some(cmd) = annotated_cmd.as_cmd(wide_tile_cmds) else {
continue;
};
match cmd {
Cmd::Fill(fill) => {
- let el = state.stack.last_mut().unwrap();
+ let el = state.tile_state.stack.last_mut().unwrap();
let draw = self.draw_mut(el.round, el.get_draw_texture(depth));
let fill_attrs = &attrs.fill[fill.attrs_idx as usize];
@@ -612,7 +641,7 @@
draw.push(gpu_strip_builder.paint(payload, paint));
}
Cmd::AlphaFill(alpha_fill) => {
- let el = state.stack.last_mut().unwrap();
+ let el = state.tile_state.stack.last_mut().unwrap();
let draw = self.draw_mut(el.round, el.get_draw_texture(depth));
let fill_attrs = &attrs.fill[alpha_fill.attrs_idx as usize];
@@ -660,7 +689,7 @@
// buffer is being pushed that will be blended back to `tos`, copy contents from
// `tos.dest_slot` to `tos.temporary_slot` ready for future blending.
{
- let tos: &mut TileEl = state.stack.last_mut().unwrap();
+ let tos: &mut TileEl = state.tile_state.stack.last_mut().unwrap();
if let TemporarySlot::Invalid(temp_slot) = tos.temporary_slot {
let next_round = depth.is_multiple_of(2);
let el_round = tos.round + usize::from(next_round);
@@ -711,7 +740,7 @@
} else {
TemporarySlot::None
};
- state.stack.push(TileEl {
+ state.tile_state.stack.push(TileEl {
dest_slot: slot,
temporary_slot,
round: self.round,
@@ -719,8 +748,8 @@
});
}
Cmd::PopBuf => {
- let tos = state.stack.pop().unwrap();
- let nos = state.stack.last_mut().unwrap();
+ let tos = state.tile_state.stack.pop().unwrap();
+ let nos = state.tile_state.stack.last_mut().unwrap();
let next_round = depth.is_multiple_of(2) && depth > 2;
let round = nos.round.max(tos.round + usize::from(next_round));
nos.round = round;
@@ -743,8 +772,8 @@
}
}
Cmd::ClipFill(clip_fill) => {
- let tos: &TileEl = &state.stack[depth - 1];
- let nos = &state.stack[depth - 2];
+ let tos: &TileEl = &state.tile_state.stack[depth - 1];
+ let nos = &state.tile_state.stack[depth - 2];
// Basically if we are writing onto the even texture, we need to go up a round
// to target it.
@@ -781,12 +810,12 @@
};
draw.push(gpu_strip_builder.copy_from_slot(tos.dest_slot.get_idx(), 0xFF));
- let nos_ptr = state.stack.len() - 2;
- state.stack[nos_ptr].temporary_slot.invalidate();
+ let nos_ptr = state.tile_state.stack.len() - 2;
+ state.tile_state.stack[nos_ptr].temporary_slot.invalidate();
}
Cmd::ClipStrip(clip_alpha_fill) => {
- let tos = &state.stack[depth - 1];
- let nos = &state.stack[depth - 2];
+ let tos = &state.tile_state.stack[depth - 1];
+ let nos = &state.tile_state.stack[depth - 2];
let next_round = depth.is_multiple_of(2) && depth > 2;
let round = nos.round.max(tos.round + usize::from(next_round));
@@ -831,15 +860,15 @@
.with_sparse(clip_alpha_fill.width, col_idx)
.copy_from_slot(tos.dest_slot.get_idx(), 0xFF),
);
- let nos_ptr = state.stack.len() - 2;
- state.stack[nos_ptr].temporary_slot.invalidate();
+ let nos_ptr = state.tile_state.stack.len() - 2;
+ state.tile_state.stack[nos_ptr].temporary_slot.invalidate();
}
Cmd::Opacity(opacity) => {
- state.stack.last_mut().unwrap().opacity = *opacity;
+ state.tile_state.stack.last_mut().unwrap().opacity = *opacity;
}
Cmd::Blend(mode) => {
- let tos = state.stack.last().unwrap();
- let nos = &state.stack[state.stack.len() - 2];
+ let tos = state.tile_state.stack.last().unwrap();
+ let nos = &state.tile_state.stack[state.tile_state.stack.len() - 2];
let next_round: bool = depth.is_multiple_of(2) && depth > 2;
let round = nos.round.max(tos.round + usize::from(next_round));
@@ -871,8 +900,8 @@
compose_mode,
));
// Invalidate the temporary slot after use
- let nos_ptr = state.stack.len() - 2;
- state.stack[nos_ptr].temporary_slot.invalidate();
+ let nos_ptr = state.tile_state.stack.len() - 2;
+ state.tile_state.stack[nos_ptr].temporary_slot.invalidate();
} else {
assert_eq!(
nos.dest_slot.get_idx(),
@@ -1054,15 +1083,19 @@
///
/// TODO: Can be triggered via a const generic on coarse draw cmd generation which will avoid
/// a linear scan.
-fn prepare_cmds<'a>(cmds: &'a [Cmd]) -> Vec<AnnotatedCmd<'a>> {
+fn prepare_cmds(cmds: &[Cmd], state: &mut SchedulerState) {
+ let annotated_commands = &mut state.annotated_commands;
+ let pointer_to_push_buf_stack = &mut state.pointer_to_push_buf_stack;
+
// Reserve room for three extra items such that we can prevent repeated blends into the surface.
- let mut annotated_commands: Vec<AnnotatedCmd<'a>> = Vec::with_capacity(cmds.len() + 3);
- let mut pointer_to_push_buf_stack: Vec<usize> = Vec::new();
+ annotated_commands.reserve(cmds.len() + 3);
+
// We pretend that the surface might be blended into. This will be removed if no blends occur to
// the surface.
pointer_to_push_buf_stack.push(0);
annotated_commands.push(AnnotatedCmd::PushBuf);
- for cmd in cmds {
+
+ for (cmd_idx, cmd) in cmds.iter().enumerate() {
match cmd {
Cmd::PushBuf(_layer_id) => {
// TODO: Handle layer_id for filter effects when implemented.
@@ -1084,7 +1117,7 @@
_ => {}
};
- annotated_commands.push(AnnotatedCmd::IdentityBorrowed(cmd));
+ annotated_commands.push(AnnotatedCmd::Identity(cmd_idx));
}
if matches!(
@@ -1099,5 +1132,4 @@
// This extra wrapping can be removed.
annotated_commands[0] = AnnotatedCmd::Empty;
}
- annotated_commands
}