vello_gpu: keep culled destructive layers so they still clear their parent
diff --git a/vello_gpu/src/schedule/mod.rs b/vello_gpu/src/schedule/mod.rs
index 103abd8..e8d7968 100644
--- a/vello_gpu/src/schedule/mod.rs
+++ b/vello_gpu/src/schedule/mod.rs
@@ -494,18 +494,9 @@
 
         let layer = &self.recorder.layers[layer_id as usize];
 
-        // TODO: Change recording so layers with empty bboxes are not emitted in the first place.
-        let mut bbox = if layer.bbox.is_empty() {
-            if layer.props.blend_mode.is_destructive() {
-                // Unlike in the non-destructive case, empty *destructive* layers are
-                // not a no-op, and we need to clear the whole parent region.
-                parent_bbox
-            } else {
-                return Ok(None);
-            }
-        } else {
-            layer.bbox
-        };
+        let clip_bbox = layer.props.clip_path.as_ref().map(|clip| clip.bbox);
+        let is_destructive = layer.props.blend_mode.is_destructive();
+        let mut bbox = layer.bbox;
 
         // We cannot do this for filter layers, because the filter needs to be applied to the whole
         // region _BEFORE_ applying clips. Instead, the clip is simply applied when sampling from
@@ -513,22 +504,26 @@
         // bbox and the part of the parent that is drawn to.
         match &layer.kind {
             RecordedLayerKind::Regular => {
-                bbox = layer
-                    .props
-                    .clip_path
-                    .as_ref()
-                    .map_or(bbox, |clip| bbox.intersect(clip.bbox))
+                bbox = clip_bbox
+                    .map_or(bbox, |clip| bbox.intersect(clip))
                     .intersect(parent_bbox);
             }
             RecordedLayerKind::Filter { placement, .. } => {
                 // TODO: Also constrain filter layers to the input region needed for the visible
                 // part of the parent.
-                if placement.dest_bbox().intersect(parent_bbox).is_empty() {
+                if !is_destructive && placement.dest_bbox().intersect(parent_bbox).is_empty() {
                     return Ok(None);
                 }
             }
         }
 
+        // Unlike in the non-destructive case, empty *destructive* layers are not a no-op, and we
+        // need to clear the parent region within the clip.
+        if bbox.is_empty() && is_destructive {
+            bbox = clip_bbox.map_or(parent_bbox, |clip| parent_bbox.intersect(clip));
+        }
+
+        // TODO: Change recording so layers with empty bboxes are not emitted in the first place.
         if bbox.is_empty() {
             return Ok(None);
         }
diff --git a/vello_tests/snapshots/compose_clear_empty_filter_layer.png b/vello_tests/snapshots/compose_clear_empty_filter_layer.png
new file mode 100644
index 0000000..ac3451d
--- /dev/null
+++ b/vello_tests/snapshots/compose_clear_empty_filter_layer.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:56f57423e0ff37d0284ae02f3dfa6ceffad893302f86b5aa4d17dabb5ba1b25d
+size 82
diff --git a/vello_tests/snapshots/compose_copy_outside_parent_clip_clears_parent.png b/vello_tests/snapshots/compose_copy_outside_parent_clip_clears_parent.png
new file mode 100644
index 0000000..ac3451d
--- /dev/null
+++ b/vello_tests/snapshots/compose_copy_outside_parent_clip_clears_parent.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:56f57423e0ff37d0284ae02f3dfa6ceffad893302f86b5aa4d17dabb5ba1b25d
+size 82
diff --git a/vello_tests/snapshots/offset_destructive_filter_layer_outside_target.png b/vello_tests/snapshots/offset_destructive_filter_layer_outside_target.png
new file mode 100644
index 0000000..a351311
--- /dev/null
+++ b/vello_tests/snapshots/offset_destructive_filter_layer_outside_target.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5df0da52aaba923344202c1e30e377e11cce5ceef303d4dc1c460078637e908c
+size 98
diff --git a/vello_tests/tests/compose.rs b/vello_tests/tests/compose.rs
index f9a9164..9f69811 100644
--- a/vello_tests/tests/compose.rs
+++ b/vello_tests/tests/compose.rs
@@ -3,6 +3,7 @@
 
 use crate::renderer::Renderer;
 use vello_common::color::palette::css::{BLUE, GREEN, LIME, PURPLE, RED, YELLOW};
+use vello_common::filter_effects::{Filter, FilterPrimitive};
 use vello_common::kurbo::{Affine, Circle, Point, Rect, Shape};
 use vello_common::peniko::{BlendMode, Color, Compose, Mix};
 use vello_dev_macros::vello_test;
@@ -467,3 +468,34 @@
 fn compose_non_isolated_plus(ctx: &mut impl Renderer) {
     compose_non_isolated(ctx, Compose::Plus);
 }
+
+#[vello_test]
+fn compose_copy_outside_parent_clip_clears_parent(ctx: &mut impl Renderer) {
+    ctx.push_clip_layer(&Rect::new(0.0, 0.0, 50.0, 50.0).to_path(0.1));
+    ctx.set_paint(BLUE);
+    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
+    ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Copy));
+    ctx.set_paint(RED);
+    ctx.fill_rect(&Rect::new(60.0, 60.0, 90.0, 90.0));
+    ctx.pop_layer();
+    ctx.pop_layer();
+}
+
+#[vello_test(skip_multithreaded)]
+fn compose_clear_empty_filter_layer(ctx: &mut impl Renderer) {
+    ctx.push_layer(None, None, None, None, None);
+    ctx.set_paint(BLUE);
+    ctx.fill_rect(&Rect::new(10.0, 10.0, 90.0, 90.0));
+    ctx.push_layer(
+        None,
+        Some(BlendMode::new(Mix::Normal, Compose::Clear)),
+        None,
+        None,
+        Some(Filter::from_primitive(FilterPrimitive::Offset {
+            dx: 0.0,
+            dy: 0.0,
+        })),
+    );
+    ctx.pop_layer();
+    ctx.pop_layer();
+}
diff --git a/vello_tests/tests/offset.rs b/vello_tests/tests/offset.rs
index e173abb..d545cf3 100644
--- a/vello_tests/tests/offset.rs
+++ b/vello_tests/tests/offset.rs
@@ -183,3 +183,23 @@
     ctx.fill_path(&hole);
     ctx.pop_layer();
 }
+
+#[vello_test(skip_multithreaded)]
+fn offset_destructive_filter_layer_outside_target(ctx: &mut impl Renderer) {
+    ctx.set_offset((-50, 0));
+    ctx.set_paint(BLUE);
+    ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
+    ctx.push_layer(
+        None,
+        Some(BlendMode::new(Mix::Normal, Compose::Copy)),
+        None,
+        None,
+        Some(Filter::from_primitive(FilterPrimitive::GaussianBlur {
+            std_deviation: 2.0,
+            edge_mode: EdgeMode::None,
+        })),
+    );
+    ctx.set_paint(RED);
+    ctx.fill_rect(&Rect::new(5.0, 5.0, 25.0, 25.0));
+    ctx.pop_layer();
+}