Repository: linebender/vello (2025-07-29T12:32:40Z)

Issue:
`vello_cpu` crashes if you keep painting after calling `flush` and not resetting
Example:

```rs
ctx.fill_rect(&Rect::new(0.0, 0.0, WIDTH as f64, HEIGHT as f64));
ctx.flush();
ctx.fill_rect(&Rect::new(0.0, 0.0, WIDTH as f64, HEIGHT as f64));
ctx.flush();
ctx.fill_rect(&Rect::new(0.0, 0.0, WIDTH as f64, HEIGHT as f64));
```


Discussion:
The problem is that flush clears worker, so we cannot continue to render, but we cannot reinit it because rasterizer needs empty threadpool.

Actually the thing bothering me is flush, render, flush, render. We could make it do init, but that would require it to take &mut self.
I think what we need is less invasive reset, so a way to again populate worker without doing full reset (sets flush to false).
Okay even better we can just lazily init thread workers.
Yes, the only reason why `flush` exists in the first place is that I wanted `render_to_pixmap` to avoid taking `&mut self` instead of `&self`. Maybe it's just me, but it doesn't feel right to me that a method used for rendering to a pixmap would need a mutable reference to the render context.

I'll try to think whether I can up with a better solution, but probably only once #1133 landed, to avoid packing too many changes into one PR.



Patch:
diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
index dfa97e9aa..48f6b1bfa 100644
--- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
+++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
@@ -136,6 +136,9 @@ impl MultiThreadedDispatcher {
 
     fn register_task(&mut self, task: RenderTask) {
         self.flushed = false;
+        if self.task_sender.is_none() {
+            self.init();
+        }
 
         let cost = task.estimate_render_time();
         self.task_batch.push(task);
@@ -344,6 +347,9 @@ impl Dispatcher for MultiThreadedDispatcher {
     }
 
     fn flush(&mut self) {
+        if self.flushed {
+            return;
+        }
         self.flush_tasks();
         let sender = core::mem::take(&mut self.task_sender);
         // Note that dropping the sender will signal to the workers that no more new paths
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs
index 7bbb83cd9..eef4d2721 100644
--- a/sparse_strips/vello_cpu/src/render.rs
+++ b/sparse_strips/vello_cpu/src/render.rs
@@ -613,5 +613,7 @@ mod tests {
         ctx.fill_path(&Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1));
         ctx.flush();
         ctx.render_to_pixmap(&mut pixmap, RenderMode::OptimizeQuality);
+        ctx.flush();
+        ctx.render_to_pixmap(&mut pixmap, RenderMode::OptimizeQuality);
     }
 }


Test patch:
diff --git a/sparse_strips/vello_sparse_tests/tests/issues.rs b/sparse_strips/vello_sparse_tests/tests/issues.rs
index d5f26b0d8..508d47272 100644
--- a/sparse_strips/vello_sparse_tests/tests/issues.rs
+++ b/sparse_strips/vello_sparse_tests/tests/issues.rs
@@ -330,3 +330,13 @@ fn gradient_radial_outside(ctx: &mut impl Renderer) {
     );
     ctx.fill_rect(&Rect::new(0., 0., 100., 50.));
 }
+
+#[vello_test(no_ref)]
+/// <https://github.com/linebender/vello/issues/1113>
+fn do_not_panic_on_multiple_flushes(ctx: &mut impl Renderer) {
+    ctx.fill_rect(&Rect::new(0.0, 0.0, 4.0, 4.0));
+    ctx.flush();
+    ctx.fill_rect(&Rect::new(0.0, 0.0, 4.0, 4.0));
+    ctx.flush();
+    ctx.fill_rect(&Rect::new(0.0, 0.0, 4.0, 4.0));
+}