Skip to main content

vello_cpu/dispatch/
single_threaded.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::coarse::CommandBucketer;
5use crate::coarse::depth::DepthBuffer;
6use crate::dispatch::Dispatcher;
7use crate::filter::context::FilterContext;
8use crate::fine::{Fine, FineKernel, FineRenderParams, FineResources, rasterize_region};
9use crate::kurbo::{Affine, BezPath, Rect, Stroke};
10use crate::peniko::{BlendMode, Fill};
11use crate::record::RecordedFill;
12use crate::region::Regions;
13use crate::{CompositeMode, RasterizerSettings};
14use core::cell::RefCell;
15use vello_common::encode::EncodedPaint;
16use vello_common::fearless_simd::{Level, Simd};
17use vello_common::filter::FilterData;
18use vello_common::geometry::RectU16;
19use vello_common::mask::Mask;
20use vello_common::paint::{ImageResolver, Paint};
21use vello_common::pixmap::{Pixmap, PixmapMut};
22use vello_common::record::{
23    CommandRecorder, LayerClip, LayerProps, Node, PoppedLayer, RecordedLayerKind,
24};
25use vello_common::strip_generator::{GenerationMode, StripStorage};
26use vello_common::util::strip_bbox;
27use vello_common::viewport::ViewportState;
28
29/// Single-threaded implementation of the rendering dispatcher.
30#[derive(Debug)]
31pub(crate) struct SingleThreadedDispatcher {
32    /// Reusable coarse bucketer that converts recorded commands into per-row render commands.
33    bucketer: RefCell<CommandBucketer>,
34    /// Viewport state.
35    viewport: ViewportState,
36    /// Recorder for root and filter-layer command streams, plus layer metadata.
37    recorder: CommandRecorder<RecordedFill>,
38    /// Storage for generated strips and alpha coverage data.
39    strip_storage: StripStorage,
40    /// SIMD level for fearless SIMD dispatch.
41    level: Level,
42}
43
44impl SingleThreadedDispatcher {
45    /// Creates a new single-threaded dispatcher for the given dimensions.
46    ///
47    /// # Arguments
48    /// * `width` - Width of the rendering surface in pixels.
49    /// * `height` - Height of the rendering surface in pixels.
50    /// * `level` - SIMD level to use for rasterization.
51    pub(crate) fn new(width: u16, height: u16, level: Level) -> Self {
52        Self {
53            bucketer: RefCell::new(CommandBucketer::from_wh(width, height)),
54            viewport: ViewportState::new(width, height, level),
55            recorder: CommandRecorder::new(width, height),
56            strip_storage: StripStorage::new(GenerationMode::Append),
57            level,
58        }
59    }
60
61    /// Rasterizes the scene using f32 precision (high quality).
62    ///
63    /// This dispatches to the appropriate SIMD implementation based on the
64    /// configured level, using f32 for intermediate calculations.
65    #[cfg(feature = "f32_pipeline")]
66    fn rasterize_f32(
67        &self,
68        target: PixmapMut<'_>,
69        scene_width: u16,
70        scene_height: u16,
71        settings: RasterizerSettings,
72        encoded_paints: &[EncodedPaint],
73        image_resolver: &dyn ImageResolver,
74    ) {
75        use crate::fine::F32Kernel;
76        use vello_common::fearless_simd::dispatch;
77        dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, target, scene_width, scene_height, settings, encoded_paints, image_resolver));
78    }
79
80    /// Rasterizes the scene using u8 precision (fast).
81    ///
82    /// This dispatches to the appropriate SIMD implementation based on the
83    /// configured level, using u8 for intermediate calculations to maximize speed.
84    #[cfg(feature = "u8_pipeline")]
85    fn rasterize_u8(
86        &self,
87        target: PixmapMut<'_>,
88        scene_width: u16,
89        scene_height: u16,
90        settings: RasterizerSettings,
91        encoded_paints: &[EncodedPaint],
92        image_resolver: &dyn ImageResolver,
93    ) {
94        use crate::fine::U8Kernel;
95        use vello_common::fearless_simd::dispatch;
96        dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, target, scene_width, scene_height, settings, encoded_paints, image_resolver));
97    }
98
99    // Note: We purposefully don't add `vectorize` to each of these helpers,
100    // since vectorization is applied wherever necessary in child functions.
101    fn rasterize_with<S: Simd, F: FineKernel<S>>(
102        &self,
103        simd: S,
104        target: PixmapMut<'_>,
105        scene_width: u16,
106        scene_height: u16,
107        settings: RasterizerSettings,
108        encoded_paints: &[EncodedPaint],
109        image_resolver: &dyn ImageResolver,
110    ) {
111        let filters = self.rasterize_filter_layers::<S, F>(simd, encoded_paints, image_resolver);
112        let use_src_over = settings.composite_mode == CompositeMode::SrcOver;
113        let params = FineRenderParams {
114            scene_size: (scene_width, scene_height),
115            target_offset: settings.offset,
116        };
117
118        self.bucket_and_rasterize::<S, F>(
119            simd,
120            &self.recorder.nodes,
121            RectU16::new(0, 0, scene_width, scene_height),
122            &filters,
123            target,
124            params,
125            use_src_over,
126            encoded_paints,
127            image_resolver,
128        );
129    }
130
131    fn bucket_and_rasterize<S: Simd, F: FineKernel<S>>(
132        &self,
133        simd: S,
134        cmds: &[Node],
135        viewport: RectU16,
136        filter_ctx: &FilterContext,
137        mut target: PixmapMut<'_>,
138        params: FineRenderParams,
139        use_src_over: bool,
140        encoded_paints: &[EncodedPaint],
141        image_resolver: &dyn ImageResolver,
142    ) {
143        let mut bucketer = self.bucketer.borrow_mut();
144        bucketer.reset(viewport);
145        bucketer.bucket_commands(
146            cmds,
147            &self.recorder.draws,
148            &self.recorder.layers,
149            &self.strip_storage.strips,
150            encoded_paints,
151            filter_ctx,
152        );
153
154        let alpha_buffers = &[self.strip_storage.alphas.as_slice()];
155        let resources = FineResources {
156            alpha_buffers,
157            encoded_paints,
158            filter_paints: &bucketer.filter_paints,
159            image_resolver,
160        };
161        let mut regions = Regions::new(
162            &mut target,
163            params.scene_size,
164            params.target_offset,
165            bucketer.rows().len(),
166        );
167        Self::rasterize_target::<S, F>(simd, &bucketer, resources, &mut regions, use_src_over);
168    }
169
170    fn rasterize_target<S: Simd, F: FineKernel<S>>(
171        simd: S,
172        bucketer: &CommandBucketer,
173        resources: FineResources<'_>,
174        regions: &mut Regions<'_>,
175        use_src_over: bool,
176    ) {
177        // TODO: Reuse fine and depth buffer across targets?
178        let mut fine = Fine::<S, F>::new(simd, bucketer.width());
179        let mut depth = DepthBuffer::new(bucketer.width());
180        regions.update(|region| {
181            rasterize_region::<S, F>(
182                &mut fine,
183                &mut depth,
184                region,
185                bucketer,
186                resources,
187                use_src_over,
188            );
189        });
190    }
191
192    fn record_fill(
193        &mut self,
194        strip_start: usize,
195        paint: Paint,
196        blend_mode: BlendMode,
197        mask: Option<Mask>,
198    ) {
199        let strip_end = self.strip_storage.strips.len();
200        let strip_range = strip_start..strip_end;
201        let strips = &self.strip_storage.strips[strip_range.start..strip_range.end];
202        let draw = RecordedFill::new(0, strip_range, paint, blend_mode, mask);
203
204        self.recorder.push_draw(draw, strips);
205    }
206
207    fn rasterize_filter_layers<S: Simd, F: FineKernel<S>>(
208        &self,
209        simd: S,
210        encoded_paints: &[EncodedPaint],
211        image_resolver: &dyn ImageResolver,
212    ) -> FilterContext {
213        // TODO: Reuse across frames so that pixmaps can be reused.
214        let mut filter_ctx = FilterContext::new(self.recorder.layers.len());
215        // We record filter layers upon "push", so nested filter layers get added after their
216        // parents. Subsequent sibling filter layers also get added after the previous layer they
217        // are composited into. Therefore, iterating in reverse order is enough to ensure that all
218        // dependencies have been rendered before they are invoked.
219        for id in self.recorder.filter_layers.iter().rev().copied() {
220            let RecordedLayerKind::Filter {
221                filter_data: filter_plan,
222                placement,
223            } = &self.recorder.layers[id as usize].kind
224            else {
225                unreachable!("filter_layers only contains filter layers");
226            };
227            let pixmap_bbox = placement.pixmap_bbox;
228            if pixmap_bbox.is_empty() {
229                continue;
230            }
231
232            let width = pixmap_bbox.width();
233            let height = pixmap_bbox.height();
234            // TODO: See https://github.com/linebender/vello/pull/1701#discussion_r3400709986, explore
235            // using pools for more resources.
236            let mut pixmap = Pixmap::new(width, height);
237            let params = FineRenderParams {
238                scene_size: (width, height),
239                target_offset: (0, 0),
240            };
241
242            self.bucket_and_rasterize::<S, F>(
243                simd,
244                &self.recorder.layers[id as usize].nodes,
245                pixmap_bbox,
246                &filter_ctx,
247                (&mut pixmap).into(),
248                params,
249                false,
250                encoded_paints,
251                image_resolver,
252            );
253
254            F::filter_layer(
255                &mut pixmap,
256                &filter_plan.filter,
257                filter_ctx.scratch(),
258                filter_plan.transform,
259            );
260
261            // Save the filtered pixmap to disk for debugging.
262            // #[cfg(all(debug_assertions, feature = "std", feature = "png"))]
263            // save_filtered_layer_debug(&pixmap, id);
264
265            filter_ctx.set_layer(id as usize, pixmap);
266        }
267
268        filter_ctx
269    }
270}
271
272impl Dispatcher for SingleThreadedDispatcher {
273    fn has_layers(&self) -> bool {
274        self.recorder.has_layers()
275    }
276
277    fn fill_path(
278        &mut self,
279        path: &BezPath,
280        fill_rule: Fill,
281        transform: Affine,
282        paint: Paint,
283        blend_mode: BlendMode,
284        aliasing_threshold: Option<u8>,
285        mask: Option<Mask>,
286    ) {
287        let strip_start = self.strip_storage.strips.len();
288        let strip_storage = &mut self.strip_storage;
289        self.viewport
290            .with_generator_and_clip(|strip_generator, clip_path| {
291                strip_generator.generate_filled_path(
292                    path,
293                    fill_rule,
294                    transform,
295                    aliasing_threshold,
296                    strip_storage,
297                    clip_path,
298                );
299            });
300        self.record_fill(strip_start, paint, blend_mode, mask);
301    }
302
303    fn stroke_path(
304        &mut self,
305        path: &BezPath,
306        stroke: &Stroke,
307        transform: Affine,
308        paint: Paint,
309        blend_mode: BlendMode,
310        aliasing_threshold: Option<u8>,
311        mask: Option<Mask>,
312    ) {
313        let strip_start = self.strip_storage.strips.len();
314        let strip_storage = &mut self.strip_storage;
315        self.viewport
316            .with_generator_and_clip(|strip_generator, clip_path| {
317                strip_generator.generate_stroked_path(
318                    path,
319                    stroke,
320                    transform,
321                    aliasing_threshold,
322                    strip_storage,
323                    clip_path,
324                );
325            });
326        self.record_fill(strip_start, paint, blend_mode, mask);
327    }
328
329    fn fill_rect_fast(
330        &mut self,
331        rect: &Rect,
332        paint: Paint,
333        blend_mode: BlendMode,
334        mask: Option<Mask>,
335    ) {
336        let strip_start = self.strip_storage.strips.len();
337        let strip_storage = &mut self.strip_storage;
338        self.viewport
339            .with_generator_and_clip(|strip_generator, clip_path| {
340                strip_generator.generate_filled_rect_fast(rect, strip_storage, clip_path);
341            });
342        self.record_fill(strip_start, paint, blend_mode, mask);
343    }
344
345    fn push_layer(
346        &mut self,
347        clip_path: Option<&BezPath>,
348        fill_rule: Fill,
349        clip_transform: Affine,
350        blend_mode: BlendMode,
351        opacity: f32,
352        aliasing_threshold: Option<u8>,
353        mask: Option<Mask>,
354        filter_data: Option<FilterData>,
355    ) {
356        if let Some(filter_data) = &filter_data {
357            self.viewport.push_root_viewport(filter_data);
358        }
359
360        let clip_path = clip_path.map(|clip_path| {
361            let strip_start = self.strip_storage.strips.len();
362            let strip_storage = &mut self.strip_storage;
363            self.viewport
364                .with_generator_and_clip(|strip_generator, existing_clip| {
365                    strip_generator.generate_filled_path(
366                        clip_path,
367                        fill_rule,
368                        clip_transform,
369                        aliasing_threshold,
370                        strip_storage,
371                        existing_clip,
372                    );
373
374                    let strip_range = strip_start..strip_storage.strips.len();
375                    LayerClip {
376                        bbox: strip_bbox(&strip_storage.strips[strip_range.clone()])
377                            .unwrap_or(RectU16::ZERO),
378                        strip_range,
379                        thread_idx: 0,
380                    }
381                })
382        });
383
384        self.recorder.push_layer(
385            LayerProps {
386                blend_mode,
387                opacity,
388                mask,
389                clip_path,
390            },
391            filter_data,
392        );
393    }
394
395    fn pop_layer(&mut self) {
396        match self.recorder.pop_layer() {
397            PoppedLayer::Regular => {}
398            PoppedLayer::Filter => {
399                self.viewport.pop_root_viewport();
400            }
401        }
402    }
403
404    fn reset(&mut self, width: u16, height: u16) {
405        // Bucketer will be reset on demand, so no need to reset it here.
406        self.recorder.reset(width, height);
407        self.strip_storage.clear();
408        self.viewport.reset(width, height);
409    }
410
411    fn flush(&mut self) {
412        // No-op for single-threaded dispatcher (no work queue to flush).
413    }
414
415    fn rasterize(
416        &self,
417        target: PixmapMut<'_>,
418        scene_width: u16,
419        scene_height: u16,
420        settings: RasterizerSettings,
421        encoded_paints: &[EncodedPaint],
422        image_resolver: &dyn ImageResolver,
423    ) {
424        // If only the u8 pipeline is enabled, then use it.
425        #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
426        {
427            self.rasterize_u8(
428                target,
429                scene_width,
430                scene_height,
431                settings,
432                encoded_paints,
433                image_resolver,
434            );
435        }
436
437        // If only the f32 pipeline is enabled, then use it.
438        #[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
439        {
440            self.rasterize_f32(
441                target,
442                scene_width,
443                scene_height,
444                settings,
445                encoded_paints,
446                image_resolver,
447            );
448        }
449
450        // If both pipelines are enabled, select precision based on render mode parameter.
451        #[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
452        match settings.render_mode {
453            crate::RenderMode::OptimizeSpeed => {
454                // Use u8 precision for faster rendering.
455                self.rasterize_u8(
456                    target,
457                    scene_width,
458                    scene_height,
459                    settings,
460                    encoded_paints,
461                    image_resolver,
462                );
463            }
464            crate::RenderMode::OptimizeQuality => {
465                // Use f32 precision for higher quality.
466                self.rasterize_f32(
467                    target,
468                    scene_width,
469                    scene_height,
470                    settings,
471                    encoded_paints,
472                    image_resolver,
473                );
474            }
475        }
476
477        #[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
478        {
479            // This case never gets hit because there is a compile_error in the root.
480            // But have this code disables some warnings and makes the compile error easier to read
481            let _ = (
482                target,
483                scene_width,
484                scene_height,
485                settings,
486                encoded_paints,
487                image_resolver,
488            );
489        }
490    }
491
492    fn push_clip_path(
493        &mut self,
494        path: &BezPath,
495        fill_rule: Fill,
496        transform: Affine,
497        aliasing_threshold: Option<u8>,
498    ) {
499        self.viewport
500            .push_clip(path, fill_rule, transform, aliasing_threshold);
501    }
502
503    fn pop_clip_path(&mut self) {
504        self.viewport.pop_clip();
505    }
506
507    fn is_multi_threaded(&self) -> bool {
508        false
509    }
510}
511
512/// Saves a filtered pixmap to disk for debugging purposes.
513/// Only available in debug builds with `std` and `png` features enabled.
514#[allow(
515    dead_code,
516    reason = "useful debug utility, can be enabled by uncommenting the call site"
517)]
518#[cfg(all(debug_assertions, feature = "std", feature = "png"))]
519fn save_filtered_layer_debug(pixmap: &Pixmap, layer_id: usize) {
520    use std::path::PathBuf;
521
522    let diffs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/diffs");
523    let _ = std::fs::create_dir_all(&diffs_path);
524    let filename = diffs_path.join(alloc::format!("filtered_layer_{layer_id}.png"));
525
526    if let Ok(png_data) = pixmap.clone().into_png() {
527        let _ = std::fs::write(&filename, &png_data);
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use crate::kurbo::Shape;
535    use vello_common::color::palette::css::BLUE;
536    use vello_common::paint::PremulColor;
537
538    /// Verifies that `reset()` properly clears all internal buffers and state.
539    ///
540    /// This is important to ensure that a dispatcher can be reused for multiple
541    /// rendering passes without accumulating stale data from previous frames.
542    #[test]
543    fn buffers_cleared_on_reset() {
544        let mut dispatcher = SingleThreadedDispatcher::new(100, 100, Level::new());
545
546        // Render a simple shape to populate internal buffers.
547        dispatcher.fill_path(
548            &Rect::new(0.0, 0.0, 50.0, 50.0).to_path(0.1),
549            Fill::NonZero,
550            Affine::IDENTITY,
551            Paint::Solid(PremulColor::from_alpha_color(BLUE)),
552            BlendMode::default(),
553            None,
554            None,
555        );
556
557        // Ensure there is data to clear.
558        assert!(!dispatcher.strip_storage.strips.is_empty());
559        assert!(!dispatcher.recorder.nodes.is_empty());
560
561        dispatcher.reset(100, 100);
562
563        // Verify all buffers are cleared.
564        assert!(dispatcher.strip_storage.strips.is_empty());
565        assert!(dispatcher.strip_storage.alphas.is_empty());
566        assert!(dispatcher.recorder.nodes.is_empty());
567        assert!(dispatcher.recorder.layers.is_empty());
568        assert!(!dispatcher.viewport.has_root_viewports());
569    }
570}