vello_gpu: Split the wgpu resources from the pipelines
diff --git a/vello_gpu/src/render/wgpu/mod.rs b/vello_gpu/src/render/wgpu/mod.rs
index 22213d3..d2e9f85 100644
--- a/vello_gpu/src/render/wgpu/mod.rs
+++ b/vello_gpu/src/render/wgpu/mod.rs
@@ -33,6 +33,7 @@
 use glifo::PendingClearRect;
 use hashbrown::HashMap;
 use passes::{Programs, RendererContext};
+use resources::GpuResources;
 use vello_common::geometry::{RectU16, SizeU16};
 use vello_common::image_cache::ImageCache;
 use vello_common::multi_atlas::{AtlasConfig, AtlasId};
@@ -112,8 +113,10 @@
     device: Device,
     /// The queue of `device`.
     queue: Queue,
-    /// Programs for rendering.
+    /// Pipelines and bind group layouts of the render passes.
     programs: Programs,
+    /// GPU resources for rendering.
+    resources: GpuResources,
     /// Encodes scenes and schedules their rendering.
     scene_preparer: ScenePreparer,
     /// Format of the render target that the pipelines are created for.
@@ -159,17 +162,24 @@
         let (scene_preparer, resources) = ScenePreparer::new(&mut settings, &device_limits);
         let resource_texture_dimension_2d = device_limits.resource_texture_dimension_2d();
 
+        let programs = Programs::new(
+            device,
+            render_target_config,
+            settings.pipeline_cache.as_ref(),
+        );
+        let gpu_resources = GpuResources::new(
+            device,
+            &programs,
+            &resources.image_cache,
+            render_target_config,
+            scene_preparer.layers_config,
+            resource_texture_dimension_2d,
+        );
         let renderer = Self {
             device: device.clone(),
             queue: queue.clone(),
-            programs: Programs::new(
-                device,
-                &resources.image_cache,
-                render_target_config,
-                scene_preparer.layers_config,
-                resource_texture_dimension_2d,
-                settings.pipeline_cache.as_ref(),
-            ),
+            programs,
+            resources: gpu_resources,
             scene_preparer,
             target_format: render_target_config.format,
             #[cfg(feature = "text")]
@@ -305,11 +315,8 @@
                 label: Some("Render to Atlas Encoder"),
             });
 
-        Programs::maybe_create_atlas_textures(
-            &self.device,
-            &mut self.programs.resources,
-            atlas_count,
-        );
+        self.resources
+            .maybe_create_atlas_textures(&self.device, atlas_count);
 
         let (atlas_width, atlas_height) = atlas_config.atlas_size;
         let atlas_render_size = RenderSize {
@@ -317,8 +324,7 @@
             height: atlas_height,
         };
 
-        let layer_view =
-            self.programs.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
+        let layer_view = self.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
 
         let dummy_image_cache = self
             .scene_preparer
@@ -348,13 +354,8 @@
 
     fn current_allocations(&self) -> IntermediateTextureAllocations {
         IntermediateTextureAllocations {
-            layer_pages: self
-                .programs
-                .resources
-                .layer_textures
-                .each_ref()
-                .map(Vec::len),
-            scratch: self.programs.resources.scratch_texture.is_some(),
+            layer_pages: self.resources.layer_textures.each_ref().map(Vec::len),
+            scratch: self.resources.scratch_texture.is_some(),
         }
     }
 
@@ -373,7 +374,7 @@
         texture_bindings: &TextureBindings,
         scene_offset: (i32, i32),
     ) -> Result<(), RenderError> {
-        self.programs.depth_cleared_this_frame = false;
+        self.resources.depth_cleared_this_frame = false;
         let current_allocations = self.current_allocations();
         let schedule = self.scene_preparer.prepare(
             scene,
@@ -385,20 +386,21 @@
                 scene_offset,
             ),
             depth_view.is_some(),
-            self.programs.resources.texture_size,
+            self.resources.texture_size,
             current_allocations,
             |id, image, region| texture_bindings.encode_paint(id, image, region, view.texture()),
         )?;
-        self.programs
-            .prepare_intermediate_textures(&self.device, &schedule);
+        self.resources
+            .prepare_intermediate_textures(&self.device, &self.programs, &schedule);
         // TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future
         // refinement, we could have a bounded alpha buffer, and break draws when the alpha
         // buffer fills.
         let draw_buffers = &self.scene_preparer.schedule_storage.buffers.draw_buffers;
         let strip_count = draw_buffers.opaque.strips().len() + draw_buffers.strips.len();
         let strip_bytes = (strip_count * size_of::<GpuStrip>()) as u64;
-        self.programs.prepare(
+        self.resources.prepare(
             &self.device,
+            &self.programs,
             encoder,
             &mut self.scene_preparer.gradient_cache,
             &self.scene_preparer.encoded_paints,
@@ -409,12 +411,13 @@
             &self.scene_preparer.schedule_storage.filter_context,
         );
 
-        self.programs
+        self.resources
             .strips_arena
             .begin_frame(&self.device, strip_bytes);
 
         let mut ctx = RendererContext {
-            programs: &mut self.programs,
+            programs: &self.programs,
+            resources: &mut self.resources,
             device: &self.device,
             encoder,
             view,
@@ -439,7 +442,7 @@
 
         self.scene_preparer.gradient_cache.maintain();
 
-        self.programs
+        self.resources
             .staging_belt
             .belt
             .finish_and_recall_on_submit(encoder);
@@ -503,14 +506,11 @@
     ) {
         let image_resource = image_cache.get(image_id).expect("Image resource not found");
 
-        Programs::maybe_create_atlas_textures(
-            &self.device,
-            &mut self.programs.resources,
-            image_cache.atlas_count() as u32,
-        );
+        self.resources
+            .maybe_create_atlas_textures(&self.device, image_cache.atlas_count() as u32);
         let offset = offset_override.unwrap_or(image_resource.offset);
         let atlas_texture =
-            &self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize];
+            &self.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize];
         writer.write_to_atlas(
             &self.device,
             &self.queue,
@@ -547,8 +547,7 @@
 
     /// Returns an individual image atlas texture.
     pub fn atlas_texture(&self, atlas_id: AtlasId) -> &Texture {
-        self.programs
-            .resources
+        self.resources
             .atlas_textures
             .get(atlas_id.as_u32() as usize)
             .unwrap()
@@ -563,8 +562,7 @@
         width: u16,
         height: u16,
     ) {
-        let layer_view =
-            self.programs.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
+        let layer_view = self.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone();
 
         let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
             label: Some("Clear Atlas Region"),
diff --git a/vello_gpu/src/render/wgpu/passes/blend.rs b/vello_gpu/src/render/wgpu/passes/blend.rs
index e2597f5..0bc0e31 100644
--- a/vello_gpu/src/render/wgpu/passes/blend.rs
+++ b/vello_gpu/src/render/wgpu/passes/blend.rs
@@ -12,7 +12,6 @@
     schedule::round::BlendOp,
     target::{BlendPassBindings, TextureParity},
 };
-use hashbrown::hash_map::Entry;
 use wgpu::{
     BindGroup, BindGroupLayout, Device, RenderPassColorAttachment, RenderPassDescriptor,
     RenderPipeline, TextureView, TextureViewDescriptor, util::DeviceExt,
@@ -141,24 +140,29 @@
         bindings: BlendPassBindings,
     ) {
         let texture_size = self.texture_size();
-        let resources = &self.programs.resources;
-        let blend_layer_bind_group = match self.programs.blend_layer_bind_groups.entry(bindings) {
-            Entry::Occupied(entry) => entry.into_mut(),
-            Entry::Vacant(entry) => {
-                let alphas_texture_view = resources
-                    .alphas_texture
-                    .create_view(&TextureViewDescriptor::default());
-                entry.insert(Programs::create_blend_layer_bind_group(
-                    self.device,
-                    &self.programs.blend.bind_group_layout,
-                    resources.layer_views([
-                        bindings.layer_id(TextureParity::Even),
-                        bindings.layer_id(TextureParity::Odd),
-                    ]),
-                    &alphas_texture_view,
-                ))
-            }
-        };
+        if !self
+            .resources
+            .blend_layer_bind_groups
+            .contains_key(&bindings)
+        {
+            let alphas_texture_view = self
+                .resources
+                .alphas_texture
+                .create_view(&TextureViewDescriptor::default());
+            let bind_group = Programs::create_blend_layer_bind_group(
+                self.device,
+                &self.programs.blend.bind_group_layout,
+                self.resources.layer_views([
+                    bindings.layer_id(TextureParity::Even),
+                    bindings.layer_id(TextureParity::Odd),
+                ]),
+                &alphas_texture_view,
+            );
+            self.resources
+                .blend_layer_bind_groups
+                .insert(bindings, bind_group);
+        }
+        let blend_layer_bind_group = &self.resources.blend_layer_bind_groups[&bindings];
 
         self.scratch_buffers.blend_instances.clear();
 
@@ -191,7 +195,7 @@
             let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
                 label: Some("Blend To Scratch"),
                 color_attachments: &[Some(RenderPassColorAttachment {
-                    view: self.programs.resources.scratch_view(),
+                    view: self.resources.scratch_view(),
                     depth_slice: None,
                     resolve_target: None,
                     ops: wgpu::Operations {
@@ -205,7 +209,7 @@
                 multiview_mask: None,
             });
             render_pass.set_pipeline(&self.programs.blend.pipeline);
-            render_pass.set_bind_group(0, &*blend_layer_bind_group, &[]);
+            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);
         }
@@ -223,8 +227,8 @@
             self.encoder,
             &self.programs.copy.pipeline,
             &self.scratch_buffers.copy_instances,
-            &self.programs.resources.scratch_copy_bind_group,
-            self.programs.resources.layer_view(bindings.target()),
+            &self.resources.scratch_copy_bind_group,
+            self.resources.layer_view(bindings.target()),
             "Copy Blend Scratch To Layer",
         );
     }
diff --git a/vello_gpu/src/render/wgpu/passes/clear.rs b/vello_gpu/src/render/wgpu/passes/clear.rs
index 0999fa3..0824b56 100644
--- a/vello_gpu/src/render/wgpu/passes/clear.rs
+++ b/vello_gpu/src/render/wgpu/passes/clear.rs
@@ -162,7 +162,7 @@
 
         let view = match target {
             DrawPassTarget::Root(_) => self.view,
-            DrawPassTarget::Layer(target) => self.programs.resources.layer_view(target),
+            DrawPassTarget::Layer(target) => self.resources.layer_view(target),
         };
         let rects = match settings {
             ClearSettings::Viewport { .. } => {
@@ -176,8 +176,8 @@
         let (target_size, pipeline) = match target {
             DrawPassTarget::Root(_) => (
                 [
-                    self.programs.render_size.width,
-                    self.programs.render_size.height,
+                    self.resources.render_size.width,
+                    self.resources.render_size.height,
                 ],
                 &self.programs.clear.root,
             ),
diff --git a/vello_gpu/src/render/wgpu/passes/filter.rs b/vello_gpu/src/render/wgpu/passes/filter.rs
index dbfe112..f2670ae 100644
--- a/vello_gpu/src/render/wgpu/passes/filter.rs
+++ b/vello_gpu/src/render/wgpu/passes/filter.rs
@@ -155,7 +155,7 @@
     copy_source: BindGroup,
 }
 
-impl Programs {
+impl GpuResources {
     pub(crate) fn create_filter_data_texture(device: &Device, width: u32, height: u32) -> Texture {
         device.create_texture(&wgpu::TextureDescriptor {
             label: Some("Filter Data Texture"),
@@ -191,6 +191,7 @@
     pub(crate) fn maybe_resize_filter_tex(
         &mut self,
         device: &Device,
+        programs: &Programs,
         resource_texture_dimension_2d: u32,
         filter_context: &FilterContext,
     ) {
@@ -200,10 +201,10 @@
             return;
         };
         debug_assert!(
-            self.resources.filter_data_texture.width() == resource_texture_dimension_2d,
+            self.filter_data_texture.width() == resource_texture_dimension_2d,
             "Filter texture width must match resource texture dimensions"
         );
-        let current_filter_height = self.resources.filter_data_texture.height();
+        let current_filter_height = self.filter_data_texture.height();
         if required_filter_height > current_filter_height {
             let required_filter_size =
                 (resource_texture_dimension_2d * required_filter_height) << 4;
@@ -214,12 +215,11 @@
                 resource_texture_dimension_2d,
                 required_filter_height,
             );
-            self.resources.filter_data_texture = filter_texture;
-            self.resources.filter_base_bind_group = Self::create_filter_base_bind_group(
+            self.filter_data_texture = filter_texture;
+            self.filter_base_bind_group = Self::create_filter_base_bind_group(
                 device,
-                &self.filter.bind_group_layout,
+                &programs.filter.bind_group_layout,
                 &self
-                    .resources
                     .filter_data_texture
                     .create_view(&TextureViewDescriptor::default()),
             );
@@ -234,7 +234,7 @@
         if filter_context.is_empty() {
             return;
         }
-        let texture = &self.resources.filter_data_texture;
+        let texture = &self.filter_data_texture;
         let rows = filter_context.total_texels().div_ceil(texture.width());
         let data = &mut self.filter_data[..(rows * texture.width() * 16) as usize];
 
@@ -250,9 +250,8 @@
         plan: &FilterPassPlan,
         bindings: FilterPassBindings,
     ) {
-        let resources = &self.programs.resources;
         if !self
-            .programs
+            .resources
             .filter_pair_bind_groups
             .contains_key(&bindings)
         {
@@ -262,7 +261,7 @@
                 TextureParity::Even => [target, temporary],
                 TextureParity::Odd => [temporary, target],
             };
-            let layer_views = resources.layer_views(layer_ids);
+            let layer_views = self.resources.layer_views(layer_ids);
             let inputs = layer_views.map(|view| {
                 create_filter_input_bind_group(
                     self.device,
@@ -274,9 +273,9 @@
             let copy_source = Programs::create_copy_bind_group(
                 self.device,
                 &self.programs.copy.bind_group_layout,
-                resources.layer_view(target),
+                self.resources.layer_view(target),
             );
-            self.programs.filter_pair_bind_groups.insert(
+            self.resources.filter_pair_bind_groups.insert(
                 bindings,
                 FilterPairBindGroups {
                     inputs,
@@ -284,7 +283,8 @@
                 },
             );
         }
-        let filter_pair_bind_groups = &self.programs.filter_pair_bind_groups[&bindings];
+        let resources = &*self.resources;
+        let filter_pair_bind_groups = &resources.filter_pair_bind_groups[&bindings];
 
         if let Some(copy_pass) = plan.copy_pass() {
             encode_copy_pass(
diff --git a/vello_gpu/src/render/wgpu/passes/mod.rs b/vello_gpu/src/render/wgpu/passes/mod.rs
index 08e1e55..d1c351f 100644
--- a/vello_gpu/src/render/wgpu/passes/mod.rs
+++ b/vello_gpu/src/render/wgpu/passes/mod.rs
@@ -9,23 +9,20 @@
 pub(crate) mod filter;
 pub(crate) mod strip;
 
-use crate::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
+use crate::draw::{ExternalTextureBindings, ExternalTextureRun};
 use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::bindings::TextureBindings;
 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, FilterPipeline, create_filter_original_texture_bind_group,
-};
-use crate::render::wgpu::passes::strip::{StripPipelines, StripTargetKind};
-use crate::render::wgpu::resources::{GpuResources, StripBufferArena, UploadStagingBelt};
+use crate::render::wgpu::passes::filter::FilterPipeline;
+use crate::render::wgpu::passes::strip::StripPipelines;
+use crate::render::wgpu::resources::GpuResources;
 use crate::util::RangedSlice;
 use crate::{
-    ClearSettings, GpuStrip, LayersConfig, RenderSize, TargetInit,
+    ClearSettings, GpuStrip, TargetInit,
     blend::BlendStrip,
     filter::FilterPassPlan,
-    paint::ExternalSampler,
     render::common::ScratchBuffers,
     schedule::{Backend, round::BlendOp},
     target::{
@@ -33,20 +30,16 @@
         RootTarget,
     },
 };
-use alloc::vec;
-use alloc::vec::Vec;
 use core::fmt::Debug;
 use hashbrown::HashMap;
 use vello_common::color::AlphaColor;
 use vello_common::geometry::{RectU16, SizeU16};
-use vello_common::image_cache::ImageCache;
-use vello_common::multi_atlas::AtlasConfig;
 use wgpu::{
     BindGroup, BindGroupLayout, ColorTargetState, ColorWrites, CommandEncoder, Device,
-    PipelineCompilationOptions, RenderPipeline, Sampler, TextureView, TextureViewDescriptor,
+    PipelineCompilationOptions, RenderPipeline, TextureView,
 };
 
-/// Defines the GPU resources and pipelines for rendering.
+/// The pipelines and bind group layouts of the render passes.
 #[derive(Debug)]
 pub(crate) struct Programs {
     /// Bind group layouts and pipelines of the strip pass.
@@ -59,44 +52,16 @@
     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 encoded paints
     pub(crate) encoded_paints_bind_group_layout: BindGroupLayout,
     /// Bind group layout for gradient texture
     pub(crate) gradient_bind_group_layout: BindGroupLayout,
-    /// Native image samplers shared by external texture slots.
-    external_samplers: HashMap<ExternalSampler, Sampler>,
-    /// Strip bind group cache.
-    pub(crate) strip_layer_bind_groups:
-        HashMap<(StripTargetKind, Option<LayerTextureId>), BindGroup>,
-    /// Blend bind group cache.
-    pub(crate) blend_layer_bind_groups: HashMap<BlendPassBindings, BindGroup>,
-    /// Filter bind group cache.
-    pub(crate) filter_pair_bind_groups: HashMap<FilterPassBindings, FilterPairBindGroups>,
-    /// GPU resources for rendering (created during prepare)
-    pub(crate) resources: GpuResources,
-    /// Arena holding all [`GpuStrip`] data.
-    pub(crate) strips_arena: StripBufferArena,
-    /// Shared staging memory for buffer and texture uploads.
-    pub(crate) staging_belt: UploadStagingBelt,
-    /// Dimensions of the rendering target
-    pub(crate) render_size: RenderSize,
-    /// Offset of the scene within the rendering target.
-    pub(crate) scene_offset: (i32, i32),
-    /// Scratch buffer for staging encoded paints texture data.
-    pub(crate) encoded_paints_data: Vec<u8>,
-    /// Scratch buffer for staging filter data texture data.
-    filter_data: Vec<u8>,
 }
 
 impl Programs {
     pub(crate) fn new(
         device: &Device,
-        image_cache: &ImageCache,
         render_target_config: &RenderTargetConfig,
-        layer_config: LayersConfig,
-        resource_texture_dimension_2d: u32,
         pipeline_cache: Option<&wgpu::PipelineCache>,
     ) -> Self {
         let encoded_paints_bind_group_layout =
@@ -109,173 +74,49 @@
             &gradient_bind_group_layout,
             pipeline_cache,
         );
-        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;
-
-        // The empty bind group needs a sampler even before any external image is drawn.
-        let mut external_samplers = HashMap::new();
-        external_samplers.insert(
-            ExternalSampler::DEFAULT,
-            Self::create_external_sampler(device, ExternalSampler::DEFAULT),
-        );
-
-        let layer_textures: [Vec<TextureView>; 2] = core::array::from_fn(|_| Vec::new());
-        let scratch_texture = None;
-        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],
-            &placeholder_external_texture_view,
-        );
-        let layer_config_buffer = Self::create_config_buffer(
-            device,
-            u32::from(texture_size.width()),
-            u32::from(texture_size.height()),
-            resource_texture_dimension_2d,
-        );
-
-        const INITIAL_ALPHA_TEXTURE_HEIGHT: u32 = 1;
-        let alphas_texture = Self::create_alphas_texture(
-            device,
-            resource_texture_dimension_2d,
-            INITIAL_ALPHA_TEXTURE_HEIGHT,
-        );
-        let view_config_buffer = Self::create_config_buffer(
-            device,
-            u32::from(render_target_config.width),
-            u32::from(render_target_config.height),
-            resource_texture_dimension_2d,
-        );
-
-        let AtlasConfig {
-            atlas_size: (atlas_width, atlas_height),
-            initial_atlas_count,
-            ..
-        } = image_cache.atlas_manager().config();
-        let atlas_size = (*atlas_width, *atlas_height);
-        let atlas_textures: Vec<_> = (0..*initial_atlas_count)
-            .map(|_| Self::create_atlas_texture(device, *atlas_width, *atlas_height))
-            .collect();
-        let atlas_texture_views = atlas_textures
-            .iter()
-            .map(|texture| texture.create_view(&TextureViewDescriptor::default()))
-            .collect();
-        let empty_external_texture_bind_group = Self::create_external_texture_bind_group(
-            device,
-            &strip.external_texture_bind_group_layout,
-            [&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT],
-            [&external_samplers[&ExternalSampler::DEFAULT]; EXTERNAL_TEXTURE_SLOT_COUNT],
-            strip
-                .wgpu_external_textures
-                .then_some(wgpu::BindingResource::TextureView(
-                    &placeholder_external_texture_view,
-                )),
-        );
-
-        const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
-        let encoded_paints_data = vec![
-            0;
-            ((resource_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4)
-                as usize
-        ];
-        let encoded_paints_texture = Self::create_encoded_paints_texture(
-            device,
-            resource_texture_dimension_2d,
-            INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT,
-        );
-        let encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
-            device,
-            &encoded_paints_bind_group_layout,
-            &encoded_paints_texture.create_view(&TextureViewDescriptor::default()),
-        );
-
-        const INITIAL_GRADIENT_TEXTURE_HEIGHT: u32 = 1;
-        let gradient_texture = Self::create_gradient_texture(
-            device,
-            resource_texture_dimension_2d,
-            INITIAL_GRADIENT_TEXTURE_HEIGHT,
-        );
-        let gradient_bind_group = Self::create_gradient_bind_group(
-            device,
-            &gradient_bind_group_layout,
-            &gradient_texture.create_view(&TextureViewDescriptor::default()),
-        );
-
-        // TODO: We really should deduplicate handling of this this with encoded paints texture.
-        const INITIAL_FILTER_TEXTURE_HEIGHT: u32 = 1;
-        let filter_data = vec![
-            0_u8;
-            ((resource_texture_dimension_2d * INITIAL_FILTER_TEXTURE_HEIGHT) << 4)
-                as usize
-        ];
-        let filter_data_texture = Self::create_filter_data_texture(
-            device,
-            resource_texture_dimension_2d,
-            INITIAL_FILTER_TEXTURE_HEIGHT,
-        );
-        let filter_base_bind_group = Self::create_filter_base_bind_group(
-            device,
-            &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,
-            &placeholder_external_texture_view,
-        );
-
-        let resources = GpuResources {
-            layer_textures,
-            scratch_texture,
-            filter_original_bind_group,
-            scratch_copy_bind_group,
-            layer_config_buffer,
-            alphas_texture,
-            resource_texture_dimension_2d,
-            atlas_textures,
-            atlas_texture_views,
-            atlas_size,
-            placeholder_external_texture_view,
-            empty_external_texture_bind_group,
-            encoded_paints_texture,
-            encoded_paints_bind_group,
-            gradient_texture,
-            gradient_bind_group,
-            filter_data_texture,
-            filter_base_bind_group,
-            texture_size,
-            view_config_buffer,
-        };
 
         Self {
             strip,
-            clear,
-            filter,
-            blend,
-            copy,
-            depth_cleared_this_frame: false,
+            clear: ClearPipelines::new(device, render_target_config, pipeline_cache),
+            filter: FilterPipeline::new(device, pipeline_cache),
+            blend: BlendPipeline::new(device, pipeline_cache),
+            copy: CopyPipeline::new(device, pipeline_cache),
             encoded_paints_bind_group_layout,
             gradient_bind_group_layout,
-            resources,
-            strips_arena: StripBufferArena::new(device),
-            staging_belt: UploadStagingBelt::new(device),
-            encoded_paints_data,
-            filter_data,
-            render_size: RenderSize {
-                width: render_target_config.width,
-                height: render_target_config.height,
-            },
-            scene_offset: (0, 0),
-            external_samplers,
-            strip_layer_bind_groups: HashMap::new(),
-            blend_layer_bind_groups: HashMap::new(),
-            filter_pair_bind_groups: HashMap::new(),
         }
     }
+
+    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,
+            }],
+        })
+    }
 }
 
 /// Create a pipeline that draws instanced quads into an intermediate texture without blending.
@@ -320,7 +161,8 @@
 /// A struct containing references to the many objects needed to get work
 /// scheduled onto the GPU.
 pub(crate) struct RendererContext<'a> {
-    pub(crate) programs: &'a mut Programs,
+    pub(crate) programs: &'a Programs,
+    pub(crate) resources: &'a mut GpuResources,
     pub(crate) device: &'a Device,
     pub(crate) encoder: &'a mut CommandEncoder,
     pub(crate) view: &'a TextureView,
@@ -359,7 +201,7 @@
     }
 
     fn texture_size(&self) -> SizeU16 {
-        self.programs.resources.texture_size
+        self.resources.texture_size
     }
 }
 
diff --git a/vello_gpu/src/render/wgpu/passes/strip.rs b/vello_gpu/src/render/wgpu/passes/strip.rs
index 9e34af9..c79d614 100644
--- a/vello_gpu/src/render/wgpu/passes/strip.rs
+++ b/vello_gpu/src/render/wgpu/passes/strip.rs
@@ -7,6 +7,7 @@
 use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::bindings::TextureBinding;
 use crate::render::wgpu::passes::{Programs, RendererContext};
+use crate::render::wgpu::resources::GpuResources;
 use crate::util::RangedSlice;
 use crate::{
     GpuStrip,
@@ -322,10 +323,10 @@
     fn create_run_external_texture_bind_group(
         &self,
         device: &Device,
+        placeholder: &TextureView,
         bindings: &[TextureBinding; EXTERNAL_TEXTURE_SLOT_COUNT],
         samplers: [&Sampler; EXTERNAL_TEXTURE_SLOT_COUNT],
     ) -> BindGroup {
-        let placeholder = &self.resources.placeholder_external_texture_view;
         let [binding] = bindings;
         let (texture_view, wgpu_external_texture) = match binding {
             TextureBinding::TextureView(view) => (
@@ -348,7 +349,9 @@
             wgpu_external_texture,
         )
     }
+}
 
+impl GpuResources {
     /// Uploads two strip slices (opaque then alpha) into the frame arena and
     /// returns the byte range of the uploaded data.
     fn upload_strip_pair(
@@ -384,13 +387,14 @@
         offset..offset + total_len
     }
 }
+
 impl RendererContext<'_> {
     fn external_texture_bind_group_for_textures(
         &mut self,
         bindings: ExternalTextureBindings,
     ) -> &BindGroup {
-        let atlas_texture_views = &self.programs.resources.atlas_texture_views;
-        let placeholder = &self.programs.resources.placeholder_external_texture_view;
+        let atlas_texture_views = &self.resources.atlas_texture_views;
+        let placeholder = &self.resources.placeholder_external_texture_view;
         let texture_bindings = self.texture_bindings;
         match self.external_texture_bind_groups.entry(bindings) {
             Entry::Occupied(entry) => entry.into_mut(),
@@ -411,14 +415,15 @@
                     _ => ExternalSampler::DEFAULT,
                 });
                 let samplers = sampler_keys.map(|key| {
-                    self.programs
+                    self.resources
                         .external_samplers
                         .entry(key)
-                        .or_insert_with(|| Programs::create_external_sampler(self.device, key))
+                        .or_insert_with(|| GpuResources::create_external_sampler(self.device, key))
                         .clone()
                 });
                 let bind_group = self.programs.create_run_external_texture_bind_group(
                     self.device,
+                    placeholder,
                     &slot_bindings,
                     samplers.each_ref(),
                 );
@@ -448,7 +453,7 @@
         }
 
         let strips_range =
-            self.programs
+            self.resources
                 .upload_strip_pair(self.encoder, opaque_strips, alpha_strips);
         let opaque_count = opaque_count as u32;
         let alpha_count = alpha_count as u32;
@@ -459,54 +464,13 @@
             // For layer targets we always want to load.
             wgpu::LoadOp::Load
         };
-        let (view, config_buffer, bind_group_target) = match target {
-            DrawPassTarget::Root(_) => (
-                self.view,
-                &self.programs.resources.view_config_buffer,
-                StripTargetKind::Root,
-            ),
-            DrawPassTarget::Layer(id) => (
-                self.programs.resources.layer_view(id),
-                &self.programs.resources.layer_config_buffer,
-                StripTargetKind::Layer,
-            ),
-        };
-        let bind_group_key = (bind_group_target, child_layer_texture);
-        if !self
-            .programs
-            .strip_layer_bind_groups
-            .contains_key(&bind_group_key)
-        {
-            let child_layer_view = child_layer_texture.map_or(
-                &self.programs.resources.placeholder_external_texture_view,
-                |id| self.programs.resources.layer_view(id),
-            );
-            let alphas_texture_view = self
-                .programs
-                .resources
-                .alphas_texture
-                .create_view(&TextureViewDescriptor::default());
-            let bind_group = Programs::create_strip_bind_group(
-                self.device,
-                &self.programs.strip.bind_group_layout,
-                &alphas_texture_view,
-                config_buffer,
-                child_layer_view,
-            );
-            self.programs
-                .strip_layer_bind_groups
-                .insert(bind_group_key, bind_group);
-        }
-
-        let bind_group = &self.programs.strip_layer_bind_groups[&bind_group_key];
-
         let enable_opaque = self.depth_view.is_some() && target.enable_opaque();
 
         let depth_stencil_attachment = if enable_opaque {
-            let depth_load = if self.programs.depth_cleared_this_frame {
+            let depth_load = if self.resources.depth_cleared_this_frame {
                 wgpu::LoadOp::Load
             } else {
-                self.programs.depth_cleared_this_frame = true;
+                self.resources.depth_cleared_this_frame = true;
                 wgpu::LoadOp::Clear(1.0)
             };
             Some(wgpu::RenderPassDepthStencilAttachment {
@@ -523,6 +487,47 @@
             None
         };
 
+        let bind_group_target = match target {
+            DrawPassTarget::Root(_) => StripTargetKind::Root,
+            DrawPassTarget::Layer(_) => StripTargetKind::Layer,
+        };
+        let bind_group_key = (bind_group_target, child_layer_texture);
+        if !self
+            .resources
+            .strip_layer_bind_groups
+            .contains_key(&bind_group_key)
+        {
+            let resources = &*self.resources;
+            let config_buffer = match target {
+                DrawPassTarget::Root(_) => &resources.view_config_buffer,
+                DrawPassTarget::Layer(_) => &resources.layer_config_buffer,
+            };
+            let child_layer_view = child_layer_texture
+                .map_or(&resources.placeholder_external_texture_view, |id| {
+                    resources.layer_view(id)
+                });
+            let alphas_texture_view = resources
+                .alphas_texture
+                .create_view(&TextureViewDescriptor::default());
+            let bind_group = Programs::create_strip_bind_group(
+                self.device,
+                &self.programs.strip.bind_group_layout,
+                &alphas_texture_view,
+                config_buffer,
+                child_layer_view,
+            );
+            self.resources
+                .strip_layer_bind_groups
+                .insert(bind_group_key, bind_group);
+        }
+
+        let resources = &*self.resources;
+        let view = match target {
+            DrawPassTarget::Root(_) => self.view,
+            DrawPassTarget::Layer(id) => resources.layer_view(id),
+        };
+        let bind_group = &resources.strip_layer_bind_groups[&bind_group_key];
+
         let mut render_pass = self.encoder.begin_render_pass(&RenderPassDescriptor {
             label: Some("Render to Texture Pass"),
             color_attachments: &[Some(RenderPassColorAttachment {
@@ -540,23 +545,13 @@
             multiview_mask: None,
         });
         render_pass.set_bind_group(0, bind_group, &[]);
-        render_pass.set_bind_group(2, &self.programs.resources.encoded_paints_bind_group, &[]);
-        render_pass.set_bind_group(3, &self.programs.resources.gradient_bind_group, &[]);
-        render_pass.set_vertex_buffer(
-            0,
-            self.programs
-                .strips_arena
-                .buffer
-                .slice(strips_range.clone()),
-        );
+        render_pass.set_bind_group(2, &resources.encoded_paints_bind_group, &[]);
+        render_pass.set_bind_group(3, &resources.gradient_bind_group, &[]);
+        render_pass.set_vertex_buffer(0, resources.strips_arena.buffer.slice(strips_range.clone()));
 
         let draw_strip_runs = |render_pass: &mut wgpu::RenderPass<'_>, first_instance, count| {
             if external_texture_runs.is_empty() {
-                render_pass.set_bind_group(
-                    1,
-                    &self.programs.resources.empty_external_texture_bind_group,
-                    &[],
-                );
+                render_pass.set_bind_group(1, &resources.empty_external_texture_bind_group, &[]);
                 render_pass.draw(0..4, first_instance..first_instance + count);
 
                 return;
diff --git a/vello_gpu/src/render/wgpu/resources.rs b/vello_gpu/src/render/wgpu/resources.rs
index 37b8e35..631a56f 100644
--- a/vello_gpu/src/render/wgpu/resources.rs
+++ b/vello_gpu/src/render/wgpu/resources.rs
@@ -3,10 +3,15 @@
 
 //! Creation and growth of the renderer's GPU resources.
 
+use crate::draw::EXTERNAL_TEXTURE_SLOT_COUNT;
+use crate::render::wgpu::RenderTargetConfig;
 use crate::render::wgpu::passes::Programs;
-use crate::render::wgpu::passes::filter::create_filter_original_texture_bind_group;
+use crate::render::wgpu::passes::filter::{
+    FilterPairBindGroups, create_filter_original_texture_bind_group,
+};
+use crate::render::wgpu::passes::strip::StripTargetKind;
 use crate::{
-    RenderSize,
+    LayersConfig, RenderSize,
     filter::FilterContext,
     gradient_cache::GradientRampCache,
     paint::{ExternalSampler, SamplerAddress, SamplerFilter},
@@ -15,10 +20,14 @@
         common::{GpuEncodedPaint, ScratchTexture},
     },
     schedule::Schedule,
-    target::LayerTextureId,
+    target::{BlendPassBindings, FilterPassBindings, LayerTextureId},
 };
+use alloc::vec;
 use alloc::vec::Vec;
 use core::{fmt::Debug, num::NonZeroU64};
+use hashbrown::HashMap;
+use vello_common::image_cache::ImageCache;
+use vello_common::multi_atlas::AtlasConfig;
 use vello_common::{geometry::SizeU16, tile::Tile};
 use wgpu::{
     BindGroup, BindGroupLayout, Buffer, BufferDescriptor, BufferViewMut, COPY_BUFFER_ALIGNMENT,
@@ -196,9 +205,191 @@
     pub(crate) filter_original_bind_group: BindGroup,
     /// Copy bind group.
     pub(crate) scratch_copy_bind_group: BindGroup,
+    /// Native image samplers shared by external texture slots.
+    pub(crate) external_samplers: HashMap<ExternalSampler, Sampler>,
+    /// Strip bind group cache.
+    pub(crate) strip_layer_bind_groups:
+        HashMap<(StripTargetKind, Option<LayerTextureId>), BindGroup>,
+    /// Blend bind group cache.
+    pub(crate) blend_layer_bind_groups: HashMap<BlendPassBindings, BindGroup>,
+    /// Filter bind group cache.
+    pub(crate) filter_pair_bind_groups: HashMap<FilterPassBindings, FilterPairBindGroups>,
+    /// Arena holding all [`GpuStrip`](crate::GpuStrip) data.
+    pub(crate) strips_arena: StripBufferArena,
+    /// Shared staging memory for buffer and texture uploads.
+    pub(crate) staging_belt: UploadStagingBelt,
+    /// Dimensions of the rendering target
+    pub(crate) render_size: RenderSize,
+    /// Offset of the scene within the rendering target.
+    pub(crate) scene_offset: (i32, i32),
+    /// Scratch buffer for staging encoded paints texture data.
+    pub(crate) encoded_paints_data: Vec<u8>,
+    /// Scratch buffer for staging filter data texture data.
+    pub(crate) filter_data: Vec<u8>,
+    /// Whether the depth buffer has been cleared this frame.
+    pub(crate) depth_cleared_this_frame: bool,
 }
 
 impl GpuResources {
+    pub(crate) fn new(
+        device: &Device,
+        programs: &Programs,
+        image_cache: &ImageCache,
+        render_target_config: &RenderTargetConfig,
+        layer_config: LayersConfig,
+        resource_texture_dimension_2d: u32,
+    ) -> Self {
+        let texture_size = layer_config.min_texture_size;
+
+        // The empty bind group needs a sampler even before any external image is drawn.
+        let mut external_samplers = HashMap::new();
+        external_samplers.insert(
+            ExternalSampler::DEFAULT,
+            Self::create_external_sampler(device, ExternalSampler::DEFAULT),
+        );
+
+        let layer_textures: [Vec<TextureView>; 2] = core::array::from_fn(|_| Vec::new());
+        let scratch_texture = None;
+        let placeholder_external_texture_view = Self::create_placeholder_external_texture(device);
+        let filter_original_bind_group = create_filter_original_texture_bind_group(
+            device,
+            &programs.filter.input_bind_group_layouts[1],
+            &placeholder_external_texture_view,
+        );
+        let layer_config_buffer = Self::create_config_buffer(
+            device,
+            u32::from(texture_size.width()),
+            u32::from(texture_size.height()),
+            resource_texture_dimension_2d,
+        );
+
+        const INITIAL_ALPHA_TEXTURE_HEIGHT: u32 = 1;
+        let alphas_texture = Self::create_alphas_texture(
+            device,
+            resource_texture_dimension_2d,
+            INITIAL_ALPHA_TEXTURE_HEIGHT,
+        );
+        let view_config_buffer = Self::create_config_buffer(
+            device,
+            u32::from(render_target_config.width),
+            u32::from(render_target_config.height),
+            resource_texture_dimension_2d,
+        );
+
+        let AtlasConfig {
+            atlas_size: (atlas_width, atlas_height),
+            initial_atlas_count,
+            ..
+        } = image_cache.atlas_manager().config();
+        let atlas_size = (*atlas_width, *atlas_height);
+        let atlas_textures: Vec<_> = (0..*initial_atlas_count)
+            .map(|_| Self::create_atlas_texture(device, *atlas_width, *atlas_height))
+            .collect();
+        let atlas_texture_views = atlas_textures
+            .iter()
+            .map(|texture| texture.create_view(&TextureViewDescriptor::default()))
+            .collect();
+        let empty_external_texture_bind_group =
+            Programs::create_external_texture_bind_group(
+                device,
+                &programs.strip.external_texture_bind_group_layout,
+                [&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT],
+                [&external_samplers[&ExternalSampler::DEFAULT]; EXTERNAL_TEXTURE_SLOT_COUNT],
+                programs.strip.wgpu_external_textures.then_some(
+                    wgpu::BindingResource::TextureView(&placeholder_external_texture_view),
+                ),
+            );
+
+        const INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT: u32 = 1;
+        let encoded_paints_data = vec![
+            0;
+            ((resource_texture_dimension_2d * INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT) << 4)
+                as usize
+        ];
+        let encoded_paints_texture = Self::create_encoded_paints_texture(
+            device,
+            resource_texture_dimension_2d,
+            INITIAL_ENCODED_PAINTS_TEXTURE_HEIGHT,
+        );
+        let encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
+            device,
+            &programs.encoded_paints_bind_group_layout,
+            &encoded_paints_texture.create_view(&TextureViewDescriptor::default()),
+        );
+
+        const INITIAL_GRADIENT_TEXTURE_HEIGHT: u32 = 1;
+        let gradient_texture = Self::create_gradient_texture(
+            device,
+            resource_texture_dimension_2d,
+            INITIAL_GRADIENT_TEXTURE_HEIGHT,
+        );
+        let gradient_bind_group = Self::create_gradient_bind_group(
+            device,
+            &programs.gradient_bind_group_layout,
+            &gradient_texture.create_view(&TextureViewDescriptor::default()),
+        );
+
+        // TODO: We really should deduplicate handling of this this with encoded paints texture.
+        const INITIAL_FILTER_TEXTURE_HEIGHT: u32 = 1;
+        let filter_data = vec![
+            0_u8;
+            ((resource_texture_dimension_2d * INITIAL_FILTER_TEXTURE_HEIGHT) << 4)
+                as usize
+        ];
+        let filter_data_texture = Self::create_filter_data_texture(
+            device,
+            resource_texture_dimension_2d,
+            INITIAL_FILTER_TEXTURE_HEIGHT,
+        );
+        let filter_base_bind_group = Self::create_filter_base_bind_group(
+            device,
+            &programs.filter.bind_group_layout,
+            &filter_data_texture.create_view(&TextureViewDescriptor::default()),
+        );
+        let scratch_copy_bind_group = Programs::create_copy_bind_group(
+            device,
+            &programs.copy.bind_group_layout,
+            &placeholder_external_texture_view,
+        );
+
+        Self {
+            layer_textures,
+            scratch_texture,
+            filter_original_bind_group,
+            scratch_copy_bind_group,
+            layer_config_buffer,
+            alphas_texture,
+            resource_texture_dimension_2d,
+            atlas_textures,
+            atlas_texture_views,
+            atlas_size,
+            placeholder_external_texture_view,
+            empty_external_texture_bind_group,
+            encoded_paints_texture,
+            encoded_paints_bind_group,
+            gradient_texture,
+            gradient_bind_group,
+            filter_data_texture,
+            filter_base_bind_group,
+            texture_size,
+            view_config_buffer,
+            external_samplers,
+            strip_layer_bind_groups: HashMap::new(),
+            blend_layer_bind_groups: HashMap::new(),
+            filter_pair_bind_groups: HashMap::new(),
+            strips_arena: StripBufferArena::new(device),
+            staging_belt: UploadStagingBelt::new(device),
+            render_size: RenderSize {
+                width: render_target_config.width,
+                height: render_target_config.height,
+            },
+            scene_offset: (0, 0),
+            encoded_paints_data,
+            filter_data,
+            depth_cleared_this_frame: false,
+        }
+    }
+
     pub(crate) fn layer_views(&self, layer_ids: [LayerTextureId; 2]) -> [&TextureView; 2] {
         layer_ids.map(|id| self.layer_view(id))
     }
@@ -216,7 +407,7 @@
 
 pub(crate) const SIZE_OF_CONFIG: NonZeroU64 = NonZeroU64::new(size_of::<Config>() as u64).unwrap();
 
-impl Programs {
+impl GpuResources {
     fn create_intermediate_texture(
         device: &Device,
         size: SizeU16,
@@ -240,25 +431,30 @@
         texture.create_view(&TextureViewDescriptor::default())
     }
 
-    pub(crate) fn prepare_intermediate_textures(&mut self, device: &Device, schedule: &Schedule) {
+    pub(crate) fn prepare_intermediate_textures(
+        &mut self,
+        device: &Device,
+        programs: &Programs,
+        schedule: &Schedule,
+    ) {
         let requirements = schedule.intermediate_texture_requirements();
         let texture_size = requirements.size;
-        let current_size = self.resources.texture_size;
+        let current_size = self.texture_size;
         let allocations = requirements.allocations;
         let layer_pages = allocations.layer_pages;
         let scratch_required = allocations.scratch;
         let size_changed = current_size != texture_size;
 
         if size_changed {
-            self.resources.layer_config_buffer = Self::create_config_buffer(
+            self.layer_config_buffer = Self::create_config_buffer(
                 device,
                 u32::from(texture_size.width()),
                 u32::from(texture_size.height()),
-                self.resources.resource_texture_dimension_2d,
+                self.resource_texture_dimension_2d,
             );
         }
 
-        for (index, textures) in self.resources.layer_textures.iter_mut().enumerate() {
+        for (index, textures) in self.layer_textures.iter_mut().enumerate() {
             let required_page_count = layer_pages[index];
             // Note: Currently, `texture_size` only grows across frames, so if this condition
             // is true it means that the new required size is larger than what we currently have.
@@ -280,22 +476,24 @@
                 )
             }));
         }
-        let scratch_changed = (self.resources.scratch_texture.is_some() && size_changed)
-            || (self.resources.scratch_texture.is_none() && scratch_required);
+        let scratch_changed = (self.scratch_texture.is_some() && size_changed)
+            || (self.scratch_texture.is_none() && scratch_required);
         if scratch_changed {
-            self.resources.scratch_texture = Some(ScratchTexture::new(
-                Self::create_intermediate_texture(device, texture_size, "Scratch Texture"),
-            ));
+            self.scratch_texture = Some(ScratchTexture::new(Self::create_intermediate_texture(
+                device,
+                texture_size,
+                "Scratch Texture",
+            )));
         }
 
-        self.resources.texture_size = texture_size;
+        self.texture_size = texture_size;
 
         if size_changed {
             self.clear_layer_bind_group_caches();
         }
 
         if scratch_changed {
-            self.update_scratch_bind_groups(device);
+            self.update_scratch_bind_groups(device, programs);
         }
     }
 
@@ -305,18 +503,21 @@
         self.filter_pair_bind_groups.clear();
     }
 
-    fn update_scratch_bind_groups(&mut self, device: &Device) {
-        let scratch_view = self.resources.scratch_view();
+    fn update_scratch_bind_groups(&mut self, device: &Device, programs: &Programs) {
+        let scratch_view = self.scratch_view();
         let filter_original_bind_group = create_filter_original_texture_bind_group(
             device,
-            &self.filter.input_bind_group_layouts[1],
+            &programs.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);
+        let scratch_copy_bind_group = Programs::create_copy_bind_group(
+            device,
+            &programs.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;
+        self.filter_original_bind_group = filter_original_bind_group;
+        self.scratch_copy_bind_group = scratch_copy_bind_group;
     }
 
     pub(crate) fn create_config_buffer(
@@ -418,38 +619,6 @@
         })
     }
 
-    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,
@@ -525,6 +694,7 @@
     pub(crate) fn prepare(
         &mut self,
         device: &Device,
+        programs: &Programs,
         encoder: &mut CommandEncoder,
         gradient_cache: &mut GradientRampCache,
         encoded_paints: &[GpuEncodedPaint],
@@ -534,11 +704,21 @@
         paint_idxs: &[u32],
         filter_context: &FilterContext,
     ) {
-        let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
+        let resource_texture_dimension_2d = self.resource_texture_dimension_2d;
         self.staging_belt.prepare(device);
         self.maybe_resize_alphas_tex(device, resource_texture_dimension_2d, alphas.len());
-        self.maybe_resize_encoded_paints_tex(device, resource_texture_dimension_2d, paint_idxs);
-        self.maybe_resize_filter_tex(device, resource_texture_dimension_2d, filter_context);
+        self.maybe_resize_encoded_paints_tex(
+            device,
+            programs,
+            resource_texture_dimension_2d,
+            paint_idxs,
+        );
+        self.maybe_resize_filter_tex(
+            device,
+            programs,
+            resource_texture_dimension_2d,
+            filter_context,
+        );
         self.maybe_update_config_buffer(
             encoder,
             resource_texture_dimension_2d,
@@ -551,7 +731,12 @@
         self.upload_filter_texture(encoder, filter_context);
 
         if gradient_cache.has_changed() {
-            self.maybe_resize_gradient_tex(device, resource_texture_dimension_2d, gradient_cache);
+            self.maybe_resize_gradient_tex(
+                device,
+                programs,
+                resource_texture_dimension_2d,
+                gradient_cache,
+            );
             self.upload_gradient_texture(encoder, gradient_cache);
             gradient_cache.mark_synced();
         }
@@ -569,10 +754,10 @@
             // There are 16 1-byte alpha values per texel.
             .div_ceil(resource_texture_dimension_2d << 4);
         debug_assert!(
-            self.resources.alphas_texture.width() == resource_texture_dimension_2d,
+            self.alphas_texture.width() == resource_texture_dimension_2d,
             "Alpha texture width must match resource texture dimensions"
         );
-        let current_alpha_height = self.resources.alphas_texture.height();
+        let current_alpha_height = self.alphas_texture.height();
         if required_alpha_height > current_alpha_height {
             // We need to resize the alpha texture to fit the new alpha data.
             assert!(
@@ -586,7 +771,7 @@
                 resource_texture_dimension_2d,
                 required_alpha_height,
             );
-            self.resources.alphas_texture = alphas_texture;
+            self.alphas_texture = alphas_texture;
             self.strip_layer_bind_groups.clear();
             self.blend_layer_bind_groups.clear();
         }
@@ -596,6 +781,7 @@
     fn maybe_resize_encoded_paints_tex(
         &mut self,
         device: &Device,
+        programs: &Programs,
         resource_texture_dimension_2d: u32,
         paint_idxs: &[u32],
     ) {
@@ -603,10 +789,10 @@
         let required_encoded_paints_height =
             required_texels.div_ceil(resource_texture_dimension_2d);
         debug_assert!(
-            self.resources.encoded_paints_texture.width() == resource_texture_dimension_2d,
+            self.encoded_paints_texture.width() == resource_texture_dimension_2d,
             "Encoded paints texture width must match resource texture dimensions"
         );
-        let current_encoded_paints_height = self.resources.encoded_paints_texture.height();
+        let current_encoded_paints_height = self.encoded_paints_texture.height();
         if required_encoded_paints_height > current_encoded_paints_height {
             assert!(
                 required_encoded_paints_height <= resource_texture_dimension_2d,
@@ -621,14 +807,13 @@
                 resource_texture_dimension_2d,
                 required_encoded_paints_height,
             );
-            self.resources.encoded_paints_texture = encoded_paints_texture;
+            self.encoded_paints_texture = encoded_paints_texture;
 
             // Since the encoded paints texture has changed, we need to update the strip bind groups.
-            self.resources.encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
+            self.encoded_paints_bind_group = Self::create_encoded_paints_bind_group(
                 device,
-                &self.encoded_paints_bind_group_layout,
+                &programs.encoded_paints_bind_group_layout,
                 &self
-                    .resources
                     .encoded_paints_texture
                     .create_view(&TextureViewDescriptor::default()),
             );
@@ -639,16 +824,17 @@
     fn maybe_resize_gradient_tex(
         &mut self,
         device: &Device,
+        programs: &Programs,
         resource_texture_dimension_2d: u32,
         gradient_cache: &GradientRampCache,
     ) {
         let gradient_pixels = (gradient_cache.luts_size() / 4) as u32; // 4 bytes per RGBA8 pixel
         let required_gradient_height = gradient_pixels.div_ceil(resource_texture_dimension_2d);
         debug_assert!(
-            self.resources.gradient_texture.width() == resource_texture_dimension_2d,
+            self.gradient_texture.width() == resource_texture_dimension_2d,
             "Gradient texture width must match resource texture dimensions"
         );
-        let current_gradient_height = self.resources.gradient_texture.height();
+        let current_gradient_height = self.gradient_texture.height();
         if required_gradient_height > current_gradient_height {
             assert!(
                 required_gradient_height <= resource_texture_dimension_2d,
@@ -659,14 +845,13 @@
                 resource_texture_dimension_2d,
                 required_gradient_height,
             );
-            self.resources.gradient_texture = gradient_texture;
+            self.gradient_texture = gradient_texture;
 
             // Since the gradient texture has changed, we need to update the gradient bind group.
-            self.resources.gradient_bind_group = Self::create_gradient_bind_group(
+            self.gradient_bind_group = Self::create_gradient_bind_group(
                 device,
-                &self.gradient_bind_group_layout,
+                &programs.gradient_bind_group_layout,
                 &self
-                    .resources
                     .gradient_texture
                     .create_view(&TextureViewDescriptor::default()),
             );
@@ -694,7 +879,7 @@
             };
             let mut buffer = self.staging_belt.write_buffer(
                 encoder,
-                &self.resources.view_config_buffer,
+                &self.view_config_buffer,
                 0,
                 SIZE_OF_CONFIG,
             );
@@ -707,16 +892,16 @@
 
     /// Create any newly allocated atlas textures.
     pub(crate) fn maybe_create_atlas_textures(
+        &mut self,
         device: &Device,
-        resources: &mut GpuResources,
         required_atlas_count: u32,
     ) {
-        let (width, height) = resources.atlas_size;
-        while resources.atlas_textures.len() < required_atlas_count as usize {
+        let (width, height) = self.atlas_size;
+        while self.atlas_textures.len() < required_atlas_count as usize {
             let texture = Self::create_atlas_texture(device, width, height);
             let texture_view = texture.create_view(&TextureViewDescriptor::default());
-            resources.atlas_textures.push(texture);
-            resources.atlas_texture_views.push(texture_view);
+            self.atlas_textures.push(texture);
+            self.atlas_texture_views.push(texture_view);
         }
     }
 
@@ -726,7 +911,7 @@
             return;
         }
 
-        let texture = &self.resources.alphas_texture;
+        let texture = &self.alphas_texture;
         self.staging_belt
             .upload_texture(encoder, texture, alphas, 16);
     }
@@ -742,7 +927,7 @@
             return;
         }
 
-        let texture = &self.resources.encoded_paints_texture;
+        let texture = &self.encoded_paints_texture;
         let rows = paint_idxs.last().unwrap().div_ceil(texture.width());
         let data = &mut self.encoded_paints_data[..(rows * texture.width() * 16) as usize];
 
@@ -761,11 +946,7 @@
             return;
         }
 
-        self.staging_belt.upload_texture(
-            encoder,
-            &self.resources.gradient_texture,
-            gradient_cache.luts(),
-            4,
-        );
+        self.staging_belt
+            .upload_texture(encoder, &self.gradient_texture, gradient_cache.luts(), 4);
     }
 }