vello_gpu: Move wgpu pipeline creation into the pass modules
diff --git a/vello_gpu/src/render/wgpu/mod.rs b/vello_gpu/src/render/wgpu/mod.rs
index 21eb8e2..70719ed 100644
--- a/vello_gpu/src/render/wgpu/mod.rs
+++ b/vello_gpu/src/render/wgpu/mod.rs
@@ -599,7 +599,7 @@
             u32::from(height),
         );
         // Use atlas clear pipeline to render transparent pixels
-        render_pass.set_pipeline(&self.programs.atlas_clear_pipeline);
+        render_pass.set_pipeline(&self.programs.clear.atlas);
         // Draw fullscreen quad
         render_pass.draw(0..4, 0..1);
     }
diff --git a/vello_gpu/src/render/wgpu/passes/blend.rs b/vello_gpu/src/render/wgpu/passes/blend.rs
index 0705bab..e2597f5 100644
--- a/vello_gpu/src/render/wgpu/passes/blend.rs
+++ b/vello_gpu/src/render/wgpu/passes/blend.rs
@@ -4,6 +4,7 @@
 //! The blend pass.
 
 use crate::render::wgpu::passes::copy::encode_copy_pass;
+use crate::render::wgpu::passes::create_texture_op_pipeline;
 use crate::render::wgpu::passes::{Programs, RendererContext};
 use crate::util::RangedSlice;
 use crate::{
@@ -14,9 +15,96 @@
 use hashbrown::hash_map::Entry;
 use wgpu::{
     BindGroup, BindGroupLayout, Device, RenderPassColorAttachment, RenderPassDescriptor,
-    TextureView, TextureViewDescriptor, util::DeviceExt,
+    RenderPipeline, TextureView, TextureViewDescriptor, util::DeviceExt,
 };
 
+/// Bind group layout and pipeline of the blend pass.
+#[derive(Debug)]
+pub(crate) struct BlendPipeline {
+    /// Blend bind group layout.
+    pub(crate) bind_group_layout: BindGroupLayout,
+    /// Blend pipeline.
+    pub(crate) pipeline: RenderPipeline,
+}
+
+impl BlendPipeline {
+    pub(crate) fn new(device: &Device, pipeline_cache: Option<&wgpu::PipelineCache>) -> Self {
+        let blend_layer_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Blend Layer Textures Bind Group Layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 2,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Uint,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                ],
+            });
+        let blend_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Blend Shader"),
+            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::BLEND.into()),
+        });
+        let blend_pipeline_layout =
+            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Blend Pipeline Layout"),
+                bind_group_layouts: &[Some(&blend_layer_bind_group_layout)],
+                immediate_size: 0,
+            });
+        let blend_vertex_state = wgpu::VertexBufferLayout {
+            array_stride: size_of::<GpuBlendInstance>() as u64,
+            step_mode: wgpu::VertexStepMode::Instance,
+            attributes: &wgpu::vertex_attr_array![
+                0 => Uint32,
+                1 => Uint32,
+                2 => Uint32,
+                3 => Uint32,
+                4 => Uint32,
+                5 => Uint32,
+                6 => Uint32,
+                7 => Uint32,
+            ],
+        };
+        let blend_pipeline = create_texture_op_pipeline(
+            device,
+            "Blend Pipeline",
+            &blend_shader,
+            &blend_pipeline_layout,
+            &blend_vertex_state,
+            pipeline_cache,
+        );
+
+        Self {
+            bind_group_layout: blend_layer_bind_group_layout,
+            pipeline: blend_pipeline,
+        }
+    }
+}
+
 impl Programs {
     fn create_blend_layer_bind_group(
         device: &Device,
@@ -62,7 +150,7 @@
                     .create_view(&TextureViewDescriptor::default());
                 entry.insert(Programs::create_blend_layer_bind_group(
                     self.device,
-                    &self.programs.blend_layer_bind_group_layout,
+                    &self.programs.blend.bind_group_layout,
                     resources.layer_views([
                         bindings.layer_id(TextureParity::Even),
                         bindings.layer_id(TextureParity::Odd),
@@ -116,7 +204,7 @@
                 timestamp_writes: None,
                 multiview_mask: None,
             });
-            render_pass.set_pipeline(&self.programs.blend_pipeline);
+            render_pass.set_pipeline(&self.programs.blend.pipeline);
             render_pass.set_bind_group(0, &*blend_layer_bind_group, &[]);
             render_pass.set_vertex_buffer(0, instance_buffer.slice(..));
             render_pass.draw(0..4, 0..instance_count);
@@ -133,7 +221,7 @@
         encode_copy_pass(
             self.device,
             self.encoder,
-            &self.programs.copy_pipeline,
+            &self.programs.copy.pipeline,
             &self.scratch_buffers.copy_instances,
             &self.programs.resources.scratch_copy_bind_group,
             self.programs.resources.layer_view(bindings.target()),
diff --git a/vello_gpu/src/render/wgpu/passes/clear.rs b/vello_gpu/src/render/wgpu/passes/clear.rs
index f3d34e2..0999fa3 100644
--- a/vello_gpu/src/render/wgpu/passes/clear.rs
+++ b/vello_gpu/src/render/wgpu/passes/clear.rs
@@ -3,14 +3,136 @@
 
 //! The clear pass.
 
+use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::passes::RendererContext;
 use crate::{ClearSettings, render::common::GpuClearInstance, target::DrawPassTarget};
 use vello_common::color::{AlphaColor, Srgb};
 use vello_common::geometry::RectU16;
 use wgpu::{
-    CommandEncoder, RenderPassColorAttachment, RenderPassDescriptor, TextureView, util::DeviceExt,
+    ColorTargetState, ColorWrites, CommandEncoder, Device, PipelineCompilationOptions,
+    RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, TextureView, util::DeviceExt,
 };
 
+/// Pipelines for clearing rectangles of layer textures, the root target and atlas regions.
+#[derive(Debug)]
+pub(crate) struct ClearPipelines {
+    /// Layer-clear pipeline.
+    pub(crate) layer: RenderPipeline,
+    /// User-target rectangle-clear pipeline.
+    pub(crate) root: RenderPipeline,
+    /// Pipeline for clearing atlas regions.
+    pub(crate) atlas: RenderPipeline,
+}
+
+impl ClearPipelines {
+    pub(crate) fn new(
+        device: &Device,
+        render_target_config: &RenderTargetConfig,
+        pipeline_cache: Option<&wgpu::PipelineCache>,
+    ) -> Self {
+        let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Clear Shader"),
+            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::CLEAR.into()),
+        });
+
+        let clear_pipeline_layout =
+            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Clear Pipeline Layout"),
+                bind_group_layouts: &[],
+                immediate_size: 0,
+            });
+
+        let clear_vertex_state = wgpu::VertexBufferLayout {
+            array_stride: size_of::<GpuClearInstance>() as u64,
+            step_mode: wgpu::VertexStepMode::Instance,
+            attributes: &wgpu::vertex_attr_array![
+                0 => Uint32x2,
+                1 => Uint32x2,
+                2 => Uint32x2,
+                3 => Uint32,
+            ],
+        };
+        let create_clear_pipeline = |label, format| {
+            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+                label: Some(label),
+                layout: Some(&clear_pipeline_layout),
+                vertex: wgpu::VertexState {
+                    module: &clear_shader,
+                    entry_point: Some("vs_main"),
+                    buffers: &[Some(clear_vertex_state.clone())],
+                    compilation_options: PipelineCompilationOptions::default(),
+                },
+                fragment: Some(wgpu::FragmentState {
+                    module: &clear_shader,
+                    entry_point: Some("fs_main"),
+                    targets: &[Some(ColorTargetState {
+                        format,
+                        blend: None,
+                        write_mask: ColorWrites::ALL,
+                    })],
+                    compilation_options: PipelineCompilationOptions::default(),
+                }),
+                primitive: wgpu::PrimitiveState {
+                    topology: wgpu::PrimitiveTopology::TriangleStrip,
+                    ..Default::default()
+                },
+                depth_stencil: None,
+                multisample: wgpu::MultisampleState::default(),
+                multiview_mask: None,
+                cache: pipeline_cache,
+            })
+        };
+        let clear_pipeline =
+            create_clear_pipeline("Clear Pipeline", wgpu::TextureFormat::Rgba8Unorm);
+        let root_clear_pipeline =
+            create_clear_pipeline("Root Clear Pipeline", render_target_config.format);
+
+        // Create atlas clear pipeline
+        let atlas_clear_pipeline_layout =
+            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Atlas Clear Pipeline Layout"),
+                bind_group_layouts: &[],
+                immediate_size: 0,
+            });
+        // TODO: Change the atlas clear pipeline to use the existing layer clear mechanism.
+        let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Atlas Clear Pipeline"),
+            layout: Some(&atlas_clear_pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &clear_shader,
+                // Use a different vertex shader entry point
+                entry_point: Some("vs_main_fullscreen"),
+                buffers: &[],
+                compilation_options: PipelineCompilationOptions::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &clear_shader,
+                entry_point: Some("fs_transparent"),
+                targets: &[Some(ColorTargetState {
+                    format: wgpu::TextureFormat::Rgba8Unorm,
+                    blend: None,
+                    write_mask: ColorWrites::ALL,
+                })],
+                compilation_options: PipelineCompilationOptions::default(),
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleStrip,
+                ..Default::default()
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState::default(),
+            multiview_mask: None,
+            cache: pipeline_cache,
+        });
+
+        Self {
+            layer: clear_pipeline,
+            root: root_clear_pipeline,
+            atlas: atlas_clear_pipeline,
+        }
+    }
+}
+
 impl RendererContext<'_> {
     pub(crate) fn clear_full_target(
         encoder: &mut CommandEncoder,
@@ -57,13 +179,13 @@
                     self.programs.render_size.width,
                     self.programs.render_size.height,
                 ],
-                &self.programs.root_clear_pipeline,
+                &self.programs.clear.root,
             ),
             DrawPassTarget::Layer(_) => {
                 let texture_size = self.texture_size();
                 (
                     [texture_size.width(), texture_size.height()],
-                    &self.programs.clear_pipeline,
+                    &self.programs.clear.layer,
                 )
             }
         };
diff --git a/vello_gpu/src/render/wgpu/passes/copy.rs b/vello_gpu/src/render/wgpu/passes/copy.rs
index c81f572..58c6dac 100644
--- a/vello_gpu/src/render/wgpu/passes/copy.rs
+++ b/vello_gpu/src/render/wgpu/passes/copy.rs
@@ -5,11 +5,72 @@
 
 use crate::copy::GpuCopyInstance;
 use crate::render::wgpu::passes::Programs;
+use crate::render::wgpu::passes::create_texture_op_pipeline;
 use wgpu::{
     BindGroup, BindGroupLayout, CommandEncoder, Device, RenderPassColorAttachment,
     RenderPassDescriptor, RenderPipeline, TextureView, util::DeviceExt,
 };
 
+/// Bind group layout and pipeline of the copy pass.
+#[derive(Debug)]
+pub(crate) struct CopyPipeline {
+    /// Copy bind group layout.
+    pub(crate) bind_group_layout: BindGroupLayout,
+    /// Copy pipeline.
+    pub(crate) pipeline: RenderPipeline,
+}
+
+impl CopyPipeline {
+    pub(crate) fn new(device: &Device, pipeline_cache: Option<&wgpu::PipelineCache>) -> Self {
+        let copy_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Copy Bind Group Layout"),
+                entries: &[wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                }],
+            });
+        let copy_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Copy Shader"),
+            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::COPY.into()),
+        });
+        let copy_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("Copy Pipeline Layout"),
+            bind_group_layouts: &[Some(&copy_bind_group_layout)],
+            immediate_size: 0,
+        });
+        let copy_vertex_state = wgpu::VertexBufferLayout {
+            array_stride: size_of::<GpuCopyInstance>() as u64,
+            step_mode: wgpu::VertexStepMode::Instance,
+            attributes: &wgpu::vertex_attr_array![
+                0 => Uint32,
+                1 => Uint32,
+                2 => Uint32,
+                3 => Uint32,
+            ],
+        };
+        let copy_pipeline = create_texture_op_pipeline(
+            device,
+            "Copy Pipeline",
+            &copy_shader,
+            &copy_pipeline_layout,
+            &copy_vertex_state,
+            pipeline_cache,
+        );
+
+        Self {
+            bind_group_layout: copy_bind_group_layout,
+            pipeline: copy_pipeline,
+        }
+    }
+}
+
 impl Programs {
     pub(crate) fn create_copy_bind_group(
         device: &Device,
diff --git a/vello_gpu/src/render/wgpu/passes/filter.rs b/vello_gpu/src/render/wgpu/passes/filter.rs
index a8f04e6..dbfe112 100644
--- a/vello_gpu/src/render/wgpu/passes/filter.rs
+++ b/vello_gpu/src/render/wgpu/passes/filter.rs
@@ -12,11 +12,143 @@
 };
 use core::fmt::Debug;
 use wgpu::{
-    BindGroup, BindGroupLayout, CommandEncoder, Device, Extent3d, RenderPassColorAttachment,
-    RenderPassDescriptor, RenderPipeline, Sampler, Texture, TextureView, TextureViewDescriptor,
-    util::DeviceExt,
+    BindGroup, BindGroupLayout, ColorTargetState, ColorWrites, CommandEncoder, Device, Extent3d,
+    PipelineCompilationOptions, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
+    Sampler, Texture, TextureView, TextureViewDescriptor, util::DeviceExt,
 };
 
+/// Bind group layouts, sampler and pipeline of the filter pass.
+#[derive(Debug)]
+pub(crate) struct FilterPipeline {
+    /// Bind group layout for filter data texture.
+    pub(crate) bind_group_layout: BindGroupLayout,
+    /// Filter input layouts.
+    pub(crate) input_bind_group_layouts: [BindGroupLayout; 2],
+    /// Filter sampler.
+    pub(crate) sampler: Sampler,
+    /// Pipeline for applying filter effects.
+    pub(crate) pipeline: RenderPipeline,
+}
+
+impl FilterPipeline {
+    pub(crate) fn new(device: &Device, pipeline_cache: Option<&wgpu::PipelineCache>) -> Self {
+        let filter_texture_entry = wgpu::BindGroupLayoutEntry {
+            binding: 0,
+            visibility: wgpu::ShaderStages::FRAGMENT,
+            ty: wgpu::BindingType::Texture {
+                sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                view_dimension: wgpu::TextureViewDimension::D2,
+                multisampled: false,
+            },
+            count: None,
+        };
+        let filter_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Filter Bind Group Layout"),
+                entries: &[wgpu::BindGroupLayoutEntry {
+                    binding: 0,
+                    visibility: wgpu::ShaderStages::FRAGMENT,
+                    ty: wgpu::BindingType::Texture {
+                        sample_type: wgpu::TextureSampleType::Uint,
+                        view_dimension: wgpu::TextureViewDimension::D2,
+                        multisampled: false,
+                    },
+                    count: None,
+                }],
+            });
+        let filter_input_bind_group_layouts = [
+            // Input texture and linear sampler.
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Filter Input Bind Group Layout"),
+                entries: &[
+                    filter_texture_entry,
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+                        count: None,
+                    },
+                ],
+            }),
+            // The original layer texture.
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Filter Original Texture Bind Group Layout"),
+                entries: &[filter_texture_entry],
+            }),
+        ];
+
+        let filter_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Filter Shader"),
+            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::FILTER.into()),
+        });
+        let filter_pipeline_layout =
+            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Filter Pipeline Layout"),
+                bind_group_layouts: &[
+                    Some(&filter_bind_group_layout),
+                    Some(&filter_input_bind_group_layouts[0]),
+                    Some(&filter_input_bind_group_layouts[1]),
+                ],
+                immediate_size: 0,
+            });
+        let filter_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("Filter Pipeline"),
+            layout: Some(&filter_pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &filter_shader,
+                entry_point: Some("vs_main"),
+                buffers: &[Some(wgpu::VertexBufferLayout {
+                    array_stride: size_of::<FilterInstanceData>() as u64,
+                    step_mode: wgpu::VertexStepMode::Instance,
+                    attributes: &wgpu::vertex_attr_array![
+                        0 => Uint32,
+                        1 => Uint32,
+                        2 => Uint32,
+                        3 => Uint32,
+                        4 => Uint32,
+                        5 => Uint32,
+                        6 => Uint32,
+                        7 => Uint32,
+                        8 => Uint32,
+                    ],
+                })],
+                compilation_options: PipelineCompilationOptions::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &filter_shader,
+                entry_point: Some("fs_main"),
+                targets: &[Some(ColorTargetState {
+                    format: wgpu::TextureFormat::Rgba8Unorm,
+                    blend: None,
+                    write_mask: ColorWrites::ALL,
+                })],
+                compilation_options: PipelineCompilationOptions::default(),
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleStrip,
+                ..Default::default()
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState::default(),
+            cache: pipeline_cache,
+            multiview_mask: None,
+        });
+        let filter_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+            label: Some("Filter Linear Sampler"),
+            mag_filter: wgpu::FilterMode::Linear,
+            min_filter: wgpu::FilterMode::Linear,
+            ..Default::default()
+        });
+
+        Self {
+            bind_group_layout: filter_bind_group_layout,
+            input_bind_group_layouts: filter_input_bind_group_layouts,
+            sampler: filter_sampler,
+            pipeline: filter_pipeline,
+        }
+    }
+}
+
 #[derive(Debug)]
 pub(crate) struct FilterPairBindGroups {
     inputs: [BindGroup; 2],
@@ -85,7 +217,7 @@
             self.resources.filter_data_texture = filter_texture;
             self.resources.filter_base_bind_group = Self::create_filter_base_bind_group(
                 device,
-                &self.filter_bind_group_layout,
+                &self.filter.bind_group_layout,
                 &self
                     .resources
                     .filter_data_texture
@@ -134,14 +266,14 @@
             let inputs = layer_views.map(|view| {
                 create_filter_input_bind_group(
                     self.device,
-                    &self.programs.filter_input_bind_group_layouts[0],
-                    &self.programs.filter_sampler,
+                    &self.programs.filter.input_bind_group_layouts[0],
+                    &self.programs.filter.sampler,
                     view,
                 )
             });
             let copy_source = Programs::create_copy_bind_group(
                 self.device,
-                &self.programs.copy_bind_group_layout,
+                &self.programs.copy.bind_group_layout,
                 resources.layer_view(target),
             );
             self.programs.filter_pair_bind_groups.insert(
@@ -158,7 +290,7 @@
             encode_copy_pass(
                 self.device,
                 self.encoder,
-                &self.programs.copy_pipeline,
+                &self.programs.copy.pipeline,
                 copy_pass,
                 &filter_pair_bind_groups.copy_source,
                 resources.scratch_view(),
@@ -173,7 +305,7 @@
                 self.device,
                 self.encoder,
                 resources,
-                &self.programs.filter_pipeline,
+                &self.programs.filter.pipeline,
                 instances,
                 &filter_pair_bind_groups.inputs[input.texture_parity.get_parity()],
                 &resources.filter_original_bind_group,
diff --git a/vello_gpu/src/render/wgpu/passes/mod.rs b/vello_gpu/src/render/wgpu/passes/mod.rs
index 3c12fc5..08e1e55 100644
--- a/vello_gpu/src/render/wgpu/passes/mod.rs
+++ b/vello_gpu/src/render/wgpu/passes/mod.rs
@@ -12,20 +12,21 @@
 use crate::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
 use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::bindings::TextureBindings;
-use crate::render::wgpu::passes::clear::clear_color;
+use crate::render::wgpu::passes::blend::BlendPipeline;
+use crate::render::wgpu::passes::clear::{ClearPipelines, clear_color};
+use crate::render::wgpu::passes::copy::CopyPipeline;
 use crate::render::wgpu::passes::filter::{
-    FilterPairBindGroups, create_filter_original_texture_bind_group,
+    FilterPairBindGroups, FilterPipeline, create_filter_original_texture_bind_group,
 };
-use crate::render::wgpu::passes::strip::StripTargetKind;
+use crate::render::wgpu::passes::strip::{StripPipelines, StripTargetKind};
 use crate::render::wgpu::resources::{GpuResources, StripBufferArena, UploadStagingBelt};
 use crate::util::RangedSlice;
 use crate::{
     ClearSettings, GpuStrip, LayersConfig, RenderSize, TargetInit,
-    blend::{BlendStrip, GpuBlendInstance},
-    copy::GpuCopyInstance,
-    filter::{FilterInstanceData, FilterPassPlan},
+    blend::BlendStrip,
+    filter::FilterPassPlan,
     paint::ExternalSampler,
-    render::common::{GpuClearInstance, ScratchBuffers},
+    render::common::ScratchBuffers,
     schedule::{Backend, round::BlendOp},
     target::{
         BlendPassBindings, DrawPassBindings, DrawPassTarget, FilterPassBindings, LayerTextureId,
@@ -41,43 +42,31 @@
 use vello_common::image_cache::ImageCache;
 use vello_common::multi_atlas::AtlasConfig;
 use wgpu::{
-    BindGroup, BindGroupLayout, BlendState, ColorTargetState, ColorWrites, CommandEncoder, Device,
+    BindGroup, BindGroupLayout, ColorTargetState, ColorWrites, CommandEncoder, Device,
     PipelineCompilationOptions, RenderPipeline, Sampler, TextureView, TextureViewDescriptor,
 };
 
 /// Defines the GPU resources and pipelines for rendering.
 #[derive(Debug)]
 pub(crate) struct Programs {
-    /// Intermediate strip pipeline.
-    intermediate_strip_pipeline: RenderPipeline,
-    /// Root alpha-strip pipeline.
-    alpha_strip_pipeline: RenderPipeline,
-    /// Root alpha-strip pipeline with depth testing.
-    depth_alpha_strip_pipeline: RenderPipeline,
-    /// Root opaque-strip pipeline.
-    opaque_strip_pipeline: RenderPipeline,
+    /// Bind group layouts and pipelines of the strip pass.
+    pub(crate) strip: StripPipelines,
+    /// Clear pipelines.
+    pub(crate) clear: ClearPipelines,
+    /// Bind group layouts, sampler and pipeline of the filter pass.
+    pub(crate) filter: FilterPipeline,
+    /// Bind group layout and pipeline of the blend pass.
+    pub(crate) blend: BlendPipeline,
+    /// Bind group layout and pipeline of the copy pass.
+    pub(crate) copy: CopyPipeline,
     /// Whether the depth buffer has been cleared this frame.
     pub(crate) depth_cleared_this_frame: bool,
-    /// Bind group layout for strip draws
-    strip_bind_group_layout: BindGroupLayout,
     /// Bind group layout for encoded paints
     pub(crate) encoded_paints_bind_group_layout: BindGroupLayout,
     /// Bind group layout for gradient texture
     pub(crate) gradient_bind_group_layout: BindGroupLayout,
-    /// Bind group layout for external textures.
-    external_texture_bind_group_layout: BindGroupLayout,
-    /// Bind group layout for filter data texture.
-    filter_bind_group_layout: BindGroupLayout,
-    /// Filter input layouts.
-    pub(crate) filter_input_bind_group_layouts: [BindGroupLayout; 2],
-    /// Filter sampler.
-    filter_sampler: Sampler,
     /// Native image samplers shared by external texture slots.
     external_samplers: HashMap<ExternalSampler, Sampler>,
-    /// Blend bind group layout.
-    blend_layer_bind_group_layout: BindGroupLayout,
-    /// Copy bind group layout.
-    pub(crate) copy_bind_group_layout: BindGroupLayout,
     /// Strip bind group cache.
     pub(crate) strip_layer_bind_groups:
         HashMap<(StripTargetKind, Option<LayerTextureId>), BindGroup>,
@@ -85,24 +74,10 @@
     pub(crate) blend_layer_bind_groups: HashMap<BlendPassBindings, BindGroup>,
     /// Filter bind group cache.
     pub(crate) filter_pair_bind_groups: HashMap<FilterPassBindings, FilterPairBindGroups>,
-    /// Pipeline for applying filter effects.
-    filter_pipeline: RenderPipeline,
-    /// Layer-clear pipeline.
-    clear_pipeline: RenderPipeline,
-    /// User-target rectangle-clear pipeline.
-    root_clear_pipeline: RenderPipeline,
-    /// Pipeline for clearing atlas regions.
-    pub(crate) atlas_clear_pipeline: RenderPipeline,
-    /// Blend pipeline.
-    blend_pipeline: RenderPipeline,
-    /// Copy pipeline.
-    copy_pipeline: RenderPipeline,
     /// GPU resources for rendering (created during prepare)
     pub(crate) resources: GpuResources,
     /// Arena holding all [`GpuStrip`] data.
     pub(crate) strips_arena: StripBufferArena,
-    /// Whether the external texture bind group has a `texture_external` binding.
-    wgpu_external_textures: bool,
     /// Shared staging memory for buffer and texture uploads.
     pub(crate) staging_belt: UploadStagingBelt,
     /// Dimensions of the rendering target
@@ -124,557 +99,22 @@
         resource_texture_dimension_2d: u32,
         pipeline_cache: Option<&wgpu::PipelineCache>,
     ) -> Self {
-        let strip_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Strip Bind Group Layout"),
-                entries: &[
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Uint,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Buffer {
-                            ty: wgpu::BufferBindingType::Uniform,
-                            has_dynamic_offset: false,
-                            min_binding_size: None,
-                        },
-                        count: None,
-                    },
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 2,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                ],
-            });
-
-        #[cfg(feature = "wgpu_external_texture")]
-        let wgpu_external_textures = device.features().contains(wgpu::Features::EXTERNAL_TEXTURE);
-        #[cfg(not(feature = "wgpu_external_texture"))]
-        let wgpu_external_textures = false;
-        let external_texture_layout_entry = |binding| wgpu::BindGroupLayoutEntry {
-            binding,
-            visibility: wgpu::ShaderStages::FRAGMENT,
-            ty: wgpu::BindingType::Texture {
-                sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                view_dimension: wgpu::TextureViewDimension::D2,
-                multisampled: false,
-            },
-            count: None,
-        };
-        let external_sampler_layout_entry = |binding| wgpu::BindGroupLayoutEntry {
-            binding,
-            visibility: wgpu::ShaderStages::FRAGMENT,
-            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-            count: None,
-        };
-        let mut external_texture_layout_entries = vec![
-            external_texture_layout_entry(0),
-            external_sampler_layout_entry(1),
-        ];
-        if wgpu_external_textures {
-            external_texture_layout_entries.push(wgpu::BindGroupLayoutEntry {
-                binding: 2,
-                visibility: wgpu::ShaderStages::FRAGMENT,
-                ty: wgpu::BindingType::ExternalTexture,
-                count: None,
-            });
-        }
-        let external_texture_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("External Texture Bind Group Layout"),
-                entries: &external_texture_layout_entries,
-            });
-
         let encoded_paints_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Encoded Paints Bind Group Layout"),
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
-                    ty: wgpu::BindingType::Texture {
-                        sample_type: wgpu::TextureSampleType::Uint,
-                        view_dimension: wgpu::TextureViewDimension::D2,
-                        multisampled: false,
-                    },
-                    count: None,
-                }],
-            });
-
-        let gradient_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Gradient Bind Group Layout"),
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::FRAGMENT,
-                    ty: wgpu::BindingType::Texture {
-                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                        view_dimension: wgpu::TextureViewDimension::D2,
-                        multisampled: false,
-                    },
-                    count: None,
-                }],
-            });
-
-        #[cfg(feature = "wgpu_external_texture")]
-        let strip_shader_source = if wgpu_external_textures {
-            vello_gpu_shaders::wgsl::RENDER_TEXTURE_EXTERNAL
-        } else {
-            vello_gpu_shaders::wgsl::RENDER
-        };
-        #[cfg(not(feature = "wgpu_external_texture"))]
-        let strip_shader_source = vello_gpu_shaders::wgsl::RENDER;
-        let strip_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Strip Shader"),
-            source: wgpu::ShaderSource::Wgsl(strip_shader_source.into()),
-        });
-
-        let clear_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Clear Shader"),
-            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::CLEAR.into()),
-        });
-
-        let strip_pipeline_layout =
-            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                label: Some("Strip Pipeline Layout"),
-                bind_group_layouts: &[
-                    Some(&strip_bind_group_layout),
-                    Some(&external_texture_bind_group_layout),
-                    Some(&encoded_paints_bind_group_layout),
-                    Some(&gradient_bind_group_layout),
-                ],
-                immediate_size: 0,
-            });
-
-        let clear_pipeline_layout =
-            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                label: Some("Clear Pipeline Layout"),
-                bind_group_layouts: &[],
-                immediate_size: 0,
-            });
-
-        let depth_format = wgpu::TextureFormat::Depth24Plus;
-
-        let strip_vertex_state = wgpu::VertexBufferLayout {
-            array_stride: size_of::<GpuStrip>() as u64,
-            step_mode: wgpu::VertexStepMode::Instance,
-            attributes: &GpuStrip::vertex_attributes(),
-        };
-
-        let create_strip_pipeline =
-            |label, format, blend, depth_stencil: Option<wgpu::DepthStencilState>| {
-                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-                    label: Some(label),
-                    layout: Some(&strip_pipeline_layout),
-                    vertex: wgpu::VertexState {
-                        module: &strip_shader,
-                        entry_point: Some("vs_main"),
-                        buffers: &[Some(strip_vertex_state.clone())],
-                        compilation_options: PipelineCompilationOptions::default(),
-                    },
-                    fragment: Some(wgpu::FragmentState {
-                        module: &strip_shader,
-                        entry_point: Some("fs_main"),
-                        targets: &[Some(ColorTargetState {
-                            format,
-                            blend,
-                            write_mask: ColorWrites::ALL,
-                        })],
-                        compilation_options: PipelineCompilationOptions::default(),
-                    }),
-                    primitive: wgpu::PrimitiveState {
-                        topology: wgpu::PrimitiveTopology::TriangleStrip,
-                        ..Default::default()
-                    },
-                    depth_stencil,
-                    multisample: wgpu::MultisampleState::default(),
-                    multiview_mask: None,
-                    cache: pipeline_cache,
-                })
-            };
-
-        let depth_stencil = |depth_write_enabled| wgpu::DepthStencilState {
-            format: depth_format,
-            depth_write_enabled: Some(depth_write_enabled),
-            depth_compare: Some(wgpu::CompareFunction::LessEqual),
-            stencil: wgpu::StencilState::default(),
-            bias: wgpu::DepthBiasState::default(),
-        };
-
-        let intermediate_strip_pipeline = create_strip_pipeline(
-            "Strip Intermediate Pipeline",
-            wgpu::TextureFormat::Rgba8Unorm,
-            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
-            None,
+            Self::create_encoded_paints_bind_group_layout(device);
+        let gradient_bind_group_layout = Self::create_gradient_bind_group_layout(device);
+        let strip = StripPipelines::new(
+            device,
+            render_target_config,
+            &encoded_paints_bind_group_layout,
+            &gradient_bind_group_layout,
+            pipeline_cache,
         );
-
-        let alpha_strip_pipeline = create_strip_pipeline(
-            "Strip Alpha Pipeline",
-            render_target_config.format,
-            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
-            None,
-        );
-
-        let depth_alpha_strip_pipeline = create_strip_pipeline(
-            "Strip Depth Alpha Pipeline",
-            render_target_config.format,
-            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
-            Some(depth_stencil(false)),
-        );
-
-        let opaque_strip_pipeline = create_strip_pipeline(
-            "Strip Opaque Pipeline",
-            render_target_config.format,
-            None,
-            Some(depth_stencil(true)),
-        );
-
-        let clear_vertex_state = wgpu::VertexBufferLayout {
-            array_stride: size_of::<GpuClearInstance>() as u64,
-            step_mode: wgpu::VertexStepMode::Instance,
-            attributes: &wgpu::vertex_attr_array![
-                0 => Uint32x2,
-                1 => Uint32x2,
-                2 => Uint32x2,
-                3 => Uint32,
-            ],
-        };
-        let create_clear_pipeline = |label, format| {
-            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-                label: Some(label),
-                layout: Some(&clear_pipeline_layout),
-                vertex: wgpu::VertexState {
-                    module: &clear_shader,
-                    entry_point: Some("vs_main"),
-                    buffers: &[Some(clear_vertex_state.clone())],
-                    compilation_options: PipelineCompilationOptions::default(),
-                },
-                fragment: Some(wgpu::FragmentState {
-                    module: &clear_shader,
-                    entry_point: Some("fs_main"),
-                    targets: &[Some(ColorTargetState {
-                        format,
-                        blend: None,
-                        write_mask: ColorWrites::ALL,
-                    })],
-                    compilation_options: PipelineCompilationOptions::default(),
-                }),
-                primitive: wgpu::PrimitiveState {
-                    topology: wgpu::PrimitiveTopology::TriangleStrip,
-                    ..Default::default()
-                },
-                depth_stencil: None,
-                multisample: wgpu::MultisampleState::default(),
-                multiview_mask: None,
-                cache: pipeline_cache,
-            })
-        };
-        let clear_pipeline =
-            create_clear_pipeline("Clear Pipeline", wgpu::TextureFormat::Rgba8Unorm);
-        let root_clear_pipeline =
-            create_clear_pipeline("Root Clear Pipeline", render_target_config.format);
-
-        // Create atlas clear pipeline
-        let atlas_clear_pipeline_layout =
-            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                label: Some("Atlas Clear Pipeline Layout"),
-                bind_group_layouts: &[],
-                immediate_size: 0,
-            });
-        // TODO: Change the atlas clear pipeline to use the existing layer clear mechanism.
-        let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-            label: Some("Atlas Clear Pipeline"),
-            layout: Some(&atlas_clear_pipeline_layout),
-            vertex: wgpu::VertexState {
-                module: &clear_shader,
-                // Use a different vertex shader entry point
-                entry_point: Some("vs_main_fullscreen"),
-                buffers: &[],
-                compilation_options: PipelineCompilationOptions::default(),
-            },
-            fragment: Some(wgpu::FragmentState {
-                module: &clear_shader,
-                entry_point: Some("fs_transparent"),
-                targets: &[Some(ColorTargetState {
-                    format: wgpu::TextureFormat::Rgba8Unorm,
-                    blend: None,
-                    write_mask: ColorWrites::ALL,
-                })],
-                compilation_options: PipelineCompilationOptions::default(),
-            }),
-            primitive: wgpu::PrimitiveState {
-                topology: wgpu::PrimitiveTopology::TriangleStrip,
-                ..Default::default()
-            },
-            depth_stencil: None,
-            multisample: wgpu::MultisampleState::default(),
-            multiview_mask: None,
-            cache: pipeline_cache,
-        });
-
-        let filter_texture_entry = wgpu::BindGroupLayoutEntry {
-            binding: 0,
-            visibility: wgpu::ShaderStages::FRAGMENT,
-            ty: wgpu::BindingType::Texture {
-                sample_type: wgpu::TextureSampleType::Float { filterable: true },
-                view_dimension: wgpu::TextureViewDimension::D2,
-                multisampled: false,
-            },
-            count: None,
-        };
-        let filter_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Filter Bind Group Layout"),
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::FRAGMENT,
-                    ty: wgpu::BindingType::Texture {
-                        sample_type: wgpu::TextureSampleType::Uint,
-                        view_dimension: wgpu::TextureViewDimension::D2,
-                        multisampled: false,
-                    },
-                    count: None,
-                }],
-            });
-        let filter_input_bind_group_layouts = [
-            // Input texture and linear sampler.
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Filter Input Bind Group Layout"),
-                entries: &[
-                    filter_texture_entry,
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
-                        count: None,
-                    },
-                ],
-            }),
-            // The original layer texture.
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Filter Original Texture Bind Group Layout"),
-                entries: &[filter_texture_entry],
-            }),
-        ];
-
-        let filter_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Filter Shader"),
-            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::FILTER.into()),
-        });
-        let filter_pipeline_layout =
-            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                label: Some("Filter Pipeline Layout"),
-                bind_group_layouts: &[
-                    Some(&filter_bind_group_layout),
-                    Some(&filter_input_bind_group_layouts[0]),
-                    Some(&filter_input_bind_group_layouts[1]),
-                ],
-                immediate_size: 0,
-            });
-        let filter_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-            label: Some("Filter Pipeline"),
-            layout: Some(&filter_pipeline_layout),
-            vertex: wgpu::VertexState {
-                module: &filter_shader,
-                entry_point: Some("vs_main"),
-                buffers: &[Some(wgpu::VertexBufferLayout {
-                    array_stride: size_of::<FilterInstanceData>() as u64,
-                    step_mode: wgpu::VertexStepMode::Instance,
-                    attributes: &wgpu::vertex_attr_array![
-                        0 => Uint32,
-                        1 => Uint32,
-                        2 => Uint32,
-                        3 => Uint32,
-                        4 => Uint32,
-                        5 => Uint32,
-                        6 => Uint32,
-                        7 => Uint32,
-                        8 => Uint32,
-                    ],
-                })],
-                compilation_options: PipelineCompilationOptions::default(),
-            },
-            fragment: Some(wgpu::FragmentState {
-                module: &filter_shader,
-                entry_point: Some("fs_main"),
-                targets: &[Some(ColorTargetState {
-                    format: wgpu::TextureFormat::Rgba8Unorm,
-                    blend: None,
-                    write_mask: ColorWrites::ALL,
-                })],
-                compilation_options: PipelineCompilationOptions::default(),
-            }),
-            primitive: wgpu::PrimitiveState {
-                topology: wgpu::PrimitiveTopology::TriangleStrip,
-                ..Default::default()
-            },
-            depth_stencil: None,
-            multisample: wgpu::MultisampleState::default(),
-            cache: pipeline_cache,
-            multiview_mask: None,
-        });
-
-        let blend_layer_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Blend Layer Textures Bind Group Layout"),
-                entries: &[
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 0,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 1,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                    wgpu::BindGroupLayoutEntry {
-                        binding: 2,
-                        visibility: wgpu::ShaderStages::FRAGMENT,
-                        ty: wgpu::BindingType::Texture {
-                            sample_type: wgpu::TextureSampleType::Uint,
-                            view_dimension: wgpu::TextureViewDimension::D2,
-                            multisampled: false,
-                        },
-                        count: None,
-                    },
-                ],
-            });
-
-        let copy_bind_group_layout =
-            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
-                label: Some("Copy Bind Group Layout"),
-                entries: &[wgpu::BindGroupLayoutEntry {
-                    binding: 0,
-                    visibility: wgpu::ShaderStages::FRAGMENT,
-                    ty: wgpu::BindingType::Texture {
-                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
-                        view_dimension: wgpu::TextureViewDimension::D2,
-                        multisampled: false,
-                    },
-                    count: None,
-                }],
-            });
-        let blend_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Blend Shader"),
-            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::BLEND.into()),
-        });
-        let copy_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("Copy Shader"),
-            source: wgpu::ShaderSource::Wgsl(vello_gpu_shaders::wgsl::COPY.into()),
-        });
-        let blend_pipeline_layout =
-            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-                label: Some("Blend Pipeline Layout"),
-                bind_group_layouts: &[Some(&blend_layer_bind_group_layout)],
-                immediate_size: 0,
-            });
-        let copy_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
-            label: Some("Copy Pipeline Layout"),
-            bind_group_layouts: &[Some(&copy_bind_group_layout)],
-            immediate_size: 0,
-        });
-        let blend_vertex_state = wgpu::VertexBufferLayout {
-            array_stride: size_of::<GpuBlendInstance>() as u64,
-            step_mode: wgpu::VertexStepMode::Instance,
-            attributes: &wgpu::vertex_attr_array![
-                0 => Uint32,
-                1 => Uint32,
-                2 => Uint32,
-                3 => Uint32,
-                4 => Uint32,
-                5 => Uint32,
-                6 => Uint32,
-                7 => Uint32,
-            ],
-        };
-        let copy_vertex_state = wgpu::VertexBufferLayout {
-            array_stride: size_of::<GpuCopyInstance>() as u64,
-            step_mode: wgpu::VertexStepMode::Instance,
-            attributes: &wgpu::vertex_attr_array![
-                0 => Uint32,
-                1 => Uint32,
-                2 => Uint32,
-                3 => Uint32,
-            ],
-        };
-        let create_texture_op_pipeline =
-            |label,
-             shader_module: &wgpu::ShaderModule,
-             layout,
-             vertex_state: &wgpu::VertexBufferLayout<'_>| {
-                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
-                    label: Some(label),
-                    layout: Some(layout),
-                    vertex: wgpu::VertexState {
-                        module: shader_module,
-                        entry_point: Some("vs_main"),
-                        buffers: &[Some(vertex_state.clone())],
-                        compilation_options: PipelineCompilationOptions::default(),
-                    },
-                    fragment: Some(wgpu::FragmentState {
-                        module: shader_module,
-                        entry_point: Some("fs_main"),
-                        targets: &[Some(ColorTargetState {
-                            format: wgpu::TextureFormat::Rgba8Unorm,
-                            blend: None,
-                            write_mask: ColorWrites::ALL,
-                        })],
-                        compilation_options: PipelineCompilationOptions::default(),
-                    }),
-                    primitive: wgpu::PrimitiveState {
-                        topology: wgpu::PrimitiveTopology::TriangleStrip,
-                        ..Default::default()
-                    },
-                    depth_stencil: None,
-                    multisample: wgpu::MultisampleState::default(),
-                    multiview_mask: None,
-                    cache: pipeline_cache,
-                })
-            };
-        let blend_pipeline = create_texture_op_pipeline(
-            "Blend Pipeline",
-            &blend_shader,
-            &blend_pipeline_layout,
-            &blend_vertex_state,
-        );
-        let copy_pipeline = create_texture_op_pipeline(
-            "Copy Pipeline",
-            &copy_shader,
-            &copy_pipeline_layout,
-            &copy_vertex_state,
-        );
+        let clear = ClearPipelines::new(device, render_target_config, pipeline_cache);
+        let filter = FilterPipeline::new(device, pipeline_cache);
+        let blend = BlendPipeline::new(device, pipeline_cache);
+        let copy = CopyPipeline::new(device, pipeline_cache);
 
         let texture_size = layer_config.min_texture_size;
-        let filter_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
-            label: Some("Filter Linear Sampler"),
-            mag_filter: wgpu::FilterMode::Linear,
-            min_filter: wgpu::FilterMode::Linear,
-            ..Default::default()
-        });
 
         // The empty bind group needs a sampler even before any external image is drawn.
         let mut external_samplers = HashMap::new();
@@ -688,7 +128,7 @@
         let placeholder_external_texture_view = Self::create_placeholder_external_texture(device);
         let filter_original_bind_group = create_filter_original_texture_bind_group(
             device,
-            &filter_input_bind_group_layouts[1],
+            &filter.input_bind_group_layouts[1],
             &placeholder_external_texture_view,
         );
         let layer_config_buffer = Self::create_config_buffer(
@@ -726,12 +166,14 @@
             .collect();
         let empty_external_texture_bind_group = Self::create_external_texture_bind_group(
             device,
-            &external_texture_bind_group_layout,
+            &strip.external_texture_bind_group_layout,
             [&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT],
             [&external_samplers[&ExternalSampler::DEFAULT]; EXTERNAL_TEXTURE_SLOT_COUNT],
-            wgpu_external_textures.then_some(wgpu::BindingResource::TextureView(
-                &placeholder_external_texture_view,
-            )),
+            strip
+                .wgpu_external_textures
+                .then_some(wgpu::BindingResource::TextureView(
+                    &placeholder_external_texture_view,
+                )),
         );
 
         const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
@@ -777,12 +219,12 @@
         );
         let filter_base_bind_group = Self::create_filter_base_bind_group(
             device,
-            &filter_bind_group_layout,
+            &filter.bind_group_layout,
             &filter_data_texture.create_view(&TextureViewDescriptor::default()),
         );
         let scratch_copy_bind_group = Self::create_copy_bind_group(
             device,
-            &copy_bind_group_layout,
+            &copy.bind_group_layout,
             &placeholder_external_texture_view,
         );
 
@@ -810,22 +252,16 @@
         };
 
         Self {
-            intermediate_strip_pipeline,
-            alpha_strip_pipeline,
-            depth_alpha_strip_pipeline,
-            opaque_strip_pipeline,
+            strip,
+            clear,
+            filter,
+            blend,
+            copy,
             depth_cleared_this_frame: false,
-            strip_bind_group_layout,
             encoded_paints_bind_group_layout,
             gradient_bind_group_layout,
-            external_texture_bind_group_layout,
-            filter_bind_group_layout,
-            filter_pipeline,
-            blend_pipeline,
-            copy_pipeline,
             resources,
             strips_arena: StripBufferArena::new(device),
-            wgpu_external_textures,
             staging_belt: UploadStagingBelt::new(device),
             encoded_paints_data,
             filter_data,
@@ -834,14 +270,7 @@
                 height: render_target_config.height,
             },
             scene_offset: (0, 0),
-            clear_pipeline,
-            root_clear_pipeline,
-            atlas_clear_pipeline,
-            filter_input_bind_group_layouts,
-            filter_sampler,
             external_samplers,
-            blend_layer_bind_group_layout,
-            copy_bind_group_layout,
             strip_layer_bind_groups: HashMap::new(),
             blend_layer_bind_groups: HashMap::new(),
             filter_pair_bind_groups: HashMap::new(),
@@ -849,6 +278,45 @@
     }
 }
 
+/// Create a pipeline that draws instanced quads into an intermediate texture without blending.
+fn create_texture_op_pipeline(
+    device: &Device,
+    label: &str,
+    shader_module: &wgpu::ShaderModule,
+    layout: &wgpu::PipelineLayout,
+    vertex_state: &wgpu::VertexBufferLayout<'_>,
+    pipeline_cache: Option<&wgpu::PipelineCache>,
+) -> RenderPipeline {
+    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+        label: Some(label),
+        layout: Some(layout),
+        vertex: wgpu::VertexState {
+            module: shader_module,
+            entry_point: Some("vs_main"),
+            buffers: &[Some(vertex_state.clone())],
+            compilation_options: PipelineCompilationOptions::default(),
+        },
+        fragment: Some(wgpu::FragmentState {
+            module: shader_module,
+            entry_point: Some("fs_main"),
+            targets: &[Some(ColorTargetState {
+                format: wgpu::TextureFormat::Rgba8Unorm,
+                blend: None,
+                write_mask: ColorWrites::ALL,
+            })],
+            compilation_options: PipelineCompilationOptions::default(),
+        }),
+        primitive: wgpu::PrimitiveState {
+            topology: wgpu::PrimitiveTopology::TriangleStrip,
+            ..Default::default()
+        },
+        depth_stencil: None,
+        multisample: wgpu::MultisampleState::default(),
+        multiview_mask: None,
+        cache: pipeline_cache,
+    })
+}
+
 /// A struct containing references to the many objects needed to get work
 /// scheduled onto the GPU.
 pub(crate) struct RendererContext<'a> {
diff --git a/vello_gpu/src/render/wgpu/passes/strip.rs b/vello_gpu/src/render/wgpu/passes/strip.rs
index a01f1e6..9e34af9 100644
--- a/vello_gpu/src/render/wgpu/passes/strip.rs
+++ b/vello_gpu/src/render/wgpu/passes/strip.rs
@@ -4,6 +4,7 @@
 //! The strip pass.
 
 use crate::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
+use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::bindings::TextureBinding;
 use crate::render::wgpu::passes::{Programs, RendererContext};
 use crate::util::RangedSlice;
@@ -16,10 +17,226 @@
 use core::{fmt::Debug, num::NonZeroU64, ops::Range};
 use hashbrown::hash_map::Entry;
 use wgpu::{
-    BindGroup, BindGroupLayout, Buffer, CommandEncoder, Device, RenderPassColorAttachment,
-    RenderPassDescriptor, Sampler, TextureView, TextureViewDescriptor,
+    BindGroup, BindGroupLayout, BlendState, Buffer, ColorTargetState, ColorWrites, CommandEncoder,
+    Device, PipelineCompilationOptions, RenderPassColorAttachment, RenderPassDescriptor,
+    RenderPipeline, Sampler, TextureView, TextureViewDescriptor,
 };
 
+/// Bind group layouts and pipelines of the strip pass.
+#[derive(Debug)]
+pub(crate) struct StripPipelines {
+    /// Bind group layout for strip draws
+    pub(crate) bind_group_layout: BindGroupLayout,
+    /// Bind group layout for external textures.
+    pub(crate) external_texture_bind_group_layout: BindGroupLayout,
+    /// Whether the external texture bind group has a `texture_external` binding.
+    pub(crate) wgpu_external_textures: bool,
+    /// Intermediate strip pipeline.
+    pub(crate) intermediate: RenderPipeline,
+    /// Root alpha-strip pipeline.
+    pub(crate) alpha: RenderPipeline,
+    /// Root alpha-strip pipeline with depth testing.
+    pub(crate) depth_alpha: RenderPipeline,
+    /// Root opaque-strip pipeline.
+    pub(crate) opaque: RenderPipeline,
+}
+
+impl StripPipelines {
+    pub(crate) fn new(
+        device: &Device,
+        render_target_config: &RenderTargetConfig,
+        encoded_paints_bind_group_layout: &BindGroupLayout,
+        gradient_bind_group_layout: &BindGroupLayout,
+        pipeline_cache: Option<&wgpu::PipelineCache>,
+    ) -> Self {
+        let strip_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Strip Bind Group Layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Uint,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Buffer {
+                            ty: wgpu::BufferBindingType::Uniform,
+                            has_dynamic_offset: false,
+                            min_binding_size: None,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 2,
+                        visibility: wgpu::ShaderStages::FRAGMENT,
+                        ty: wgpu::BindingType::Texture {
+                            sample_type: wgpu::TextureSampleType::Float { filterable: false },
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                            multisampled: false,
+                        },
+                        count: None,
+                    },
+                ],
+            });
+
+        #[cfg(feature = "wgpu_external_texture")]
+        let wgpu_external_textures = device.features().contains(wgpu::Features::EXTERNAL_TEXTURE);
+        #[cfg(not(feature = "wgpu_external_texture"))]
+        let wgpu_external_textures = false;
+        let external_texture_layout_entry = |binding| wgpu::BindGroupLayoutEntry {
+            binding,
+            visibility: wgpu::ShaderStages::FRAGMENT,
+            ty: wgpu::BindingType::Texture {
+                sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                view_dimension: wgpu::TextureViewDimension::D2,
+                multisampled: false,
+            },
+            count: None,
+        };
+        let external_sampler_layout_entry = |binding| wgpu::BindGroupLayoutEntry {
+            binding,
+            visibility: wgpu::ShaderStages::FRAGMENT,
+            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+            count: None,
+        };
+        let mut external_texture_layout_entries = vec![
+            external_texture_layout_entry(0),
+            external_sampler_layout_entry(1),
+        ];
+        if wgpu_external_textures {
+            external_texture_layout_entries.push(wgpu::BindGroupLayoutEntry {
+                binding: 2,
+                visibility: wgpu::ShaderStages::FRAGMENT,
+                ty: wgpu::BindingType::ExternalTexture,
+                count: None,
+            });
+        }
+        let external_texture_bind_group_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("External Texture Bind Group Layout"),
+                entries: &external_texture_layout_entries,
+            });
+
+        #[cfg(feature = "wgpu_external_texture")]
+        let strip_shader_source = if wgpu_external_textures {
+            vello_gpu_shaders::wgsl::RENDER_TEXTURE_EXTERNAL
+        } else {
+            vello_gpu_shaders::wgsl::RENDER
+        };
+        #[cfg(not(feature = "wgpu_external_texture"))]
+        let strip_shader_source = vello_gpu_shaders::wgsl::RENDER;
+        let strip_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("Strip Shader"),
+            source: wgpu::ShaderSource::Wgsl(strip_shader_source.into()),
+        });
+
+        let strip_pipeline_layout =
+            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+                label: Some("Strip Pipeline Layout"),
+                bind_group_layouts: &[
+                    Some(&strip_bind_group_layout),
+                    Some(&external_texture_bind_group_layout),
+                    Some(encoded_paints_bind_group_layout),
+                    Some(gradient_bind_group_layout),
+                ],
+                immediate_size: 0,
+            });
+
+        let depth_format = wgpu::TextureFormat::Depth24Plus;
+
+        let strip_vertex_state = wgpu::VertexBufferLayout {
+            array_stride: size_of::<GpuStrip>() as u64,
+            step_mode: wgpu::VertexStepMode::Instance,
+            attributes: &GpuStrip::vertex_attributes(),
+        };
+
+        let create_strip_pipeline =
+            |label, format, blend, depth_stencil: Option<wgpu::DepthStencilState>| {
+                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+                    label: Some(label),
+                    layout: Some(&strip_pipeline_layout),
+                    vertex: wgpu::VertexState {
+                        module: &strip_shader,
+                        entry_point: Some("vs_main"),
+                        buffers: &[Some(strip_vertex_state.clone())],
+                        compilation_options: PipelineCompilationOptions::default(),
+                    },
+                    fragment: Some(wgpu::FragmentState {
+                        module: &strip_shader,
+                        entry_point: Some("fs_main"),
+                        targets: &[Some(ColorTargetState {
+                            format,
+                            blend,
+                            write_mask: ColorWrites::ALL,
+                        })],
+                        compilation_options: PipelineCompilationOptions::default(),
+                    }),
+                    primitive: wgpu::PrimitiveState {
+                        topology: wgpu::PrimitiveTopology::TriangleStrip,
+                        ..Default::default()
+                    },
+                    depth_stencil,
+                    multisample: wgpu::MultisampleState::default(),
+                    multiview_mask: None,
+                    cache: pipeline_cache,
+                })
+            };
+
+        let depth_stencil = |depth_write_enabled| wgpu::DepthStencilState {
+            format: depth_format,
+            depth_write_enabled: Some(depth_write_enabled),
+            depth_compare: Some(wgpu::CompareFunction::LessEqual),
+            stencil: wgpu::StencilState::default(),
+            bias: wgpu::DepthBiasState::default(),
+        };
+
+        let intermediate_strip_pipeline = create_strip_pipeline(
+            "Strip Intermediate Pipeline",
+            wgpu::TextureFormat::Rgba8Unorm,
+            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
+            None,
+        );
+
+        let alpha_strip_pipeline = create_strip_pipeline(
+            "Strip Alpha Pipeline",
+            render_target_config.format,
+            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
+            None,
+        );
+
+        let depth_alpha_strip_pipeline = create_strip_pipeline(
+            "Strip Depth Alpha Pipeline",
+            render_target_config.format,
+            Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
+            Some(depth_stencil(false)),
+        );
+
+        let opaque_strip_pipeline = create_strip_pipeline(
+            "Strip Opaque Pipeline",
+            render_target_config.format,
+            None,
+            Some(depth_stencil(true)),
+        );
+
+        Self {
+            bind_group_layout: strip_bind_group_layout,
+            external_texture_bind_group_layout,
+            wgpu_external_textures,
+            intermediate: intermediate_strip_pipeline,
+            alpha: alpha_strip_pipeline,
+            depth_alpha: depth_alpha_strip_pipeline,
+            opaque: opaque_strip_pipeline,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub(crate) enum StripTargetKind {
     Root,
@@ -113,7 +330,8 @@
         let (texture_view, wgpu_external_texture) = match binding {
             TextureBinding::TextureView(view) => (
                 view,
-                self.wgpu_external_textures
+                self.strip
+                    .wgpu_external_textures
                     .then_some(wgpu::BindingResource::TextureView(placeholder)),
             ),
             #[cfg(feature = "wgpu_external_texture")]
@@ -124,7 +342,7 @@
         };
         Self::create_external_texture_bind_group(
             device,
-            &self.external_texture_bind_group_layout,
+            &self.strip.external_texture_bind_group_layout,
             [texture_view],
             samplers,
             wgpu_external_texture,
@@ -270,7 +488,7 @@
                 .create_view(&TextureViewDescriptor::default());
             let bind_group = Programs::create_strip_bind_group(
                 self.device,
-                &self.programs.strip_bind_group_layout,
+                &self.programs.strip.bind_group_layout,
                 &alphas_texture_view,
                 config_buffer,
                 child_layer_view,
@@ -367,7 +585,7 @@
                 enable_opaque,
                 "opaque strips require the final view depth attachment"
             );
-            render_pass.set_pipeline(&self.programs.opaque_strip_pipeline);
+            render_pass.set_pipeline(&self.programs.strip.opaque);
             draw_strip_runs(&mut render_pass, 0, opaque_count);
         }
 
@@ -375,13 +593,13 @@
             // Alpha pass
             if matches!(target, DrawPassTarget::Root(RootTarget::UserSurface)) {
                 let pipeline = if enable_opaque {
-                    &self.programs.depth_alpha_strip_pipeline
+                    &self.programs.strip.depth_alpha
                 } else {
-                    &self.programs.alpha_strip_pipeline
+                    &self.programs.strip.alpha
                 };
                 render_pass.set_pipeline(pipeline);
             } else {
-                render_pass.set_pipeline(&self.programs.intermediate_strip_pipeline);
+                render_pass.set_pipeline(&self.programs.strip.intermediate);
             }
 
             draw_strip_runs(&mut render_pass, opaque_count, alpha_count);
diff --git a/vello_gpu/src/render/wgpu/resources.rs b/vello_gpu/src/render/wgpu/resources.rs
index 83e4261..37b8e35 100644
--- a/vello_gpu/src/render/wgpu/resources.rs
+++ b/vello_gpu/src/render/wgpu/resources.rs
@@ -309,11 +309,11 @@
         let scratch_view = self.resources.scratch_view();
         let filter_original_bind_group = create_filter_original_texture_bind_group(
             device,
-            &self.filter_input_bind_group_layouts[1],
+            &self.filter.input_bind_group_layouts[1],
             scratch_view,
         );
         let scratch_copy_bind_group =
-            Self::create_copy_bind_group(device, &self.copy_bind_group_layout, scratch_view);
+            Self::create_copy_bind_group(device, &self.copy.bind_group_layout, scratch_view);
 
         self.resources.filter_original_bind_group = filter_original_bind_group;
         self.resources.scratch_copy_bind_group = scratch_copy_bind_group;
@@ -418,6 +418,38 @@
         })
     }
 
+    pub(crate) fn create_encoded_paints_bind_group_layout(device: &Device) -> BindGroupLayout {
+        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("Encoded Paints Bind Group Layout"),
+            entries: &[wgpu::BindGroupLayoutEntry {
+                binding: 0,
+                visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+                ty: wgpu::BindingType::Texture {
+                    sample_type: wgpu::TextureSampleType::Uint,
+                    view_dimension: wgpu::TextureViewDimension::D2,
+                    multisampled: false,
+                },
+                count: None,
+            }],
+        })
+    }
+
+    pub(crate) fn create_gradient_bind_group_layout(device: &Device) -> BindGroupLayout {
+        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("Gradient Bind Group Layout"),
+            entries: &[wgpu::BindGroupLayoutEntry {
+                binding: 0,
+                visibility: wgpu::ShaderStages::FRAGMENT,
+                ty: wgpu::BindingType::Texture {
+                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
+                    view_dimension: wgpu::TextureViewDimension::D2,
+                    multisampled: false,
+                },
+                count: None,
+            }],
+        })
+    }
+
     pub(crate) fn create_encoded_paints_texture(
         device: &Device,
         width: u32,