Remove programs <-> resources cycle
diff --git a/vello_gpu/src/render/webgl/init.rs b/vello_gpu/src/render/webgl/init.rs
index 60a2eef..7d7e197 100644
--- a/vello_gpu/src/render/webgl/init.rs
+++ b/vello_gpu/src/render/webgl/init.rs
@@ -19,11 +19,9 @@
     WebGlError, WebGlOperation, WebGlResultExt, WebGlShaderProgramOperation, WebGlShaderStage,
 };
 use crate::{
-    RenderSettings, RenderSize, Resources,
+    RenderSettings, Resources,
     render::common::{DeviceLimits, SharedCore},
 };
-use alloc::vec;
-use alloc::vec::Vec;
 use core::cell::Cell;
 use core::time::Duration;
 use vello_gpu_shaders::{blend, copy, filter as filter_shader, render};
@@ -65,7 +63,6 @@
 pub struct WebGlRendererInit {
     shaders: WebGlShaders,
     resources: WebGlResources,
-    encoded_paints_data: Vec<u8>,
     core: SharedCore,
 }
 
@@ -203,12 +200,10 @@
         initialize_filter_vao(gl, &webgl_resources);
         initialize_blend_vao(gl, &webgl_resources);
         initialize_copy_vao(gl, &webgl_resources);
-        let encoded_paints_data = vec![0; (resource_texture_dimension_2d << 4) as usize];
 
         let init = WebGlRendererInit {
             shaders,
             resources: webgl_resources,
-            encoded_paints_data,
             core,
         };
 
@@ -394,7 +389,6 @@
         let Self {
             shaders,
             resources,
-            encoded_paints_data,
             core,
         } = self;
         let WebGlShaders {
@@ -439,18 +433,14 @@
             copy_program,
             copy_uniforms,
             shader_compilation_stats,
-            resources,
-            render_size: RenderSize {
-                width: 0,
-                height: 0,
-            },
-            negate_ndc: false,
-            scene_offset: (0, 0),
-            encoded_paints_data,
-            filter_data: Vec::new(),
         };
 
-        Ok(WebGlRenderer { programs, gl, core })
+        Ok(WebGlRenderer {
+            programs,
+            resources,
+            gl,
+            core,
+        })
     }
 }
 
diff --git a/vello_gpu/src/render/webgl/mod.rs b/vello_gpu/src/render/webgl/mod.rs
index e65cdc1..48c2b08 100644
--- a/vello_gpu/src/render/webgl/mod.rs
+++ b/vello_gpu/src/render/webgl/mod.rs
@@ -56,6 +56,7 @@
 #[cfg(feature = "text")]
 use glifo::PendingClearRect;
 use passes::{WebGlPrograms, WebGlRendererContext};
+use resources::WebGlResources;
 use vello_common::color::{AlphaColor, Srgb};
 use vello_common::image_cache::ImageCache;
 use vello_common::multi_atlas::{AtlasConfig, AtlasId};
@@ -80,6 +81,8 @@
 pub struct WebGlRenderer {
     /// Programs for rendering.
     pub(super) programs: WebGlPrograms,
+    /// GPU resources for rendering.
+    pub(super) resources: WebGlResources,
     /// WebGL context.
     pub(crate) gl: WebGl2RenderingContext,
     /// Backend-independent renderer state.
@@ -246,7 +249,7 @@
         atlas_id: AtlasId,
         texture_bindings: &WebGlTextureBindings,
     ) -> Result<(), WebGlError> {
-        self.programs
+        self.resources
             .maybe_create_atlas_textures(&self.gl, atlas_count)?;
 
         let (atlas_width, atlas_height) = atlas_config.atlas_size;
@@ -254,7 +257,7 @@
             width: atlas_width,
             height: atlas_height,
         };
-        let target = self.programs.resources.atlas_targets[atlas_id.as_u32() as usize].clone();
+        let target = self.resources.atlas_targets[atlas_id.as_u32() as usize].clone();
         // TODO: Explore using an option instead of a dummy image cache.
         let dummy_image_cache = self
             .core
@@ -286,21 +289,16 @@
         target: &WebGlTextureTarget,
         render: impl FnOnce(&mut Self) -> Result<(), WebGlError>,
     ) -> Result<(), WebGlError> {
-        let previous_target = self.programs.resources.root_target.replace(target.clone());
+        let previous_target = self.resources.root_target.replace(target.clone());
         let result = render(self);
-        self.programs.resources.root_target = previous_target;
+        self.resources.root_target = previous_target;
         result
     }
 
     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(),
         }
     }
 
@@ -318,7 +316,7 @@
         render_target_texture: Option<&WebGlTexture>,
         scene_offset: (i32, i32),
     ) -> Result<(), WebGlError> {
-        let use_depth_buffer = self.programs.resources.root_uses_depth_buffer();
+        let use_depth_buffer = self.resources.root_uses_depth_buffer();
 
         let current_allocations = self.current_allocations();
         let schedule = self.core.prepare(
@@ -331,19 +329,19 @@
                 scene_offset,
             ),
             use_depth_buffer,
-            self.programs.resources.texture_size,
+            self.resources.texture_size,
             current_allocations,
             |id, image, region| {
                 texture_bindings.encode_paint(id, image, region, render_target_texture)
             },
         )?;
-        self.programs
+        self.resources
             .prepare_intermediate_textures(&self.gl, &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.
-        self.programs.prepare(
+        self.resources.prepare(
             &self.gl,
             &mut self.core.gradient_cache,
             &self.core.encoded_paints,
@@ -354,9 +352,10 @@
             &self.core.schedule_storage.filter_context,
             root_output_target,
         )?;
-        self.programs.resources.depth_cleared_this_frame = false;
+        self.resources.depth_cleared_this_frame = false;
         let mut ctx = WebGlRendererContext {
-            programs: &mut self.programs,
+            programs: &self.programs,
+            resources: &mut self.resources,
             gl: &self.gl,
             texture_bindings,
             scratch_buffers: &mut self.core.scratch_buffers,
@@ -374,19 +373,19 @@
 
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            self.programs.resources.root_framebuffer(),
+            self.resources.root_framebuffer(),
         );
-        if self.programs.resources.depth_cleared_this_frame {
+        if self.resources.depth_cleared_this_frame {
             // See: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_invalidateframebuffer
             // We want to indicate to the GPU driver that we won't read the depth buffer again
             // until the next clear. This enables the GPU to avoid storing depth tiles back to VRAM.
             self.gl
                 .invalidate_framebuffer(
                     WebGl2RenderingContext::FRAMEBUFFER,
-                    if self.programs.resources.root_target.is_some() {
-                        &self.programs.resources.offscreen_depth_attachment_array
+                    if self.resources.root_target.is_some() {
+                        &self.resources.offscreen_depth_attachment_array
                     } else {
-                        &self.programs.resources.default_depth_attachment_array
+                        &self.resources.default_depth_attachment_array
                     },
                 )
                 .map_js_error(WebGlOperation::DataTransfer(
@@ -469,12 +468,12 @@
             .get(image_id)
             .ok_or(RenderError::MissingImage(image_id))?;
 
-        self.programs
+        self.resources
             .maybe_create_atlas_textures(&self.gl, image_cache.atlas_count() as u32)?;
         let offset = offset_override.unwrap_or(image_resource.offset);
         writer.write_to_atlas(
             &self.gl,
-            &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],
             offset,
             writer.width(),
             writer.height(),
@@ -509,8 +508,7 @@
 
     /// Returns an individual image atlas texture.
     pub fn atlas_texture(&self, atlas_id: AtlasId) -> &WebGlTexture {
-        self.programs
-            .resources
+        self.resources
             .atlas_textures
             .get(atlas_id.as_u32() as usize)
             .map(|texture| &**texture)
@@ -519,7 +517,7 @@
 
     /// Returns the current allocation state of the image/glyph atlas texture.
     pub fn atlas_info(&self) -> AtlasTextureInfo {
-        let resources = &self.programs.resources;
+        let resources = &self.resources;
         AtlasTextureInfo {
             width: resources.atlas_size.0,
             height: resources.atlas_size.1,
@@ -537,11 +535,11 @@
     ) -> Result<(), WebGlError> {
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            Some(&self.programs.resources.atlas_targets[atlas_id.as_u32() as usize].framebuffer),
+            Some(&self.resources.atlas_targets[atlas_id.as_u32() as usize].framebuffer),
         );
 
         // Set viewport to match the atlas texture dimensions
-        let (atlas_width, atlas_height) = self.programs.resources.atlas_size;
+        let (atlas_width, atlas_height) = self.resources.atlas_size;
         self.gl
             .viewport(0, 0, i32::from(atlas_width), i32::from(atlas_height));
 
diff --git a/vello_gpu/src/render/webgl/passes/blend.rs b/vello_gpu/src/render/webgl/passes/blend.rs
index 3a997c2..cacd240 100644
--- a/vello_gpu/src/render/webgl/passes/blend.rs
+++ b/vello_gpu/src/render/webgl/passes/blend.rs
@@ -6,7 +6,7 @@
 use crate::render::webgl::WebGlError;
 use crate::render::webgl::gl::resource::Program;
 use crate::render::webgl::passes::{
-    WebGlPrograms, WebGlRendererContext, checked_instance_count, initialize_packed_u32_attribs,
+    WebGlRendererContext, checked_instance_count, initialize_packed_u32_attribs,
     required_uniform_location,
 };
 use crate::render::webgl::resources::WebGlResources;
@@ -26,11 +26,11 @@
     alphas_texture: WebGlUniformLocation,
 }
 
-impl WebGlPrograms {
+impl WebGlResources {
     fn upload_blend_instances(&self, gl: &WebGl2RenderingContext, instances: &[GpuBlendInstance]) {
         gl.bind_buffer(
             WebGl2RenderingContext::ARRAY_BUFFER,
-            Some(&self.resources.blend_instance_buffer),
+            Some(&self.blend_instance_buffer),
         );
         gl.buffer_data_with_u8_array(
             WebGl2RenderingContext::ARRAY_BUFFER,
@@ -83,8 +83,7 @@
         self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
         self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
         self.gl.depth_mask(false);
-        self.gl
-            .bind_vertex_array(Some(&self.programs.resources.blend_vao));
+        self.gl.bind_vertex_array(Some(&self.resources.blend_vao));
 
         self.scratch_buffers.blend_instances.clear();
         for blend in blends.iter() {
@@ -102,13 +101,13 @@
                     .push(GpuBlendInstance::new(blend, None, texture_size));
             }
         }
-        self.programs
+        self.resources
             .upload_blend_instances(self.gl, &self.scratch_buffers.blend_instances);
         let instance_count = checked_instance_count(self.scratch_buffers.blend_instances.len())?;
 
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            Some(self.programs.resources.scratch_framebuffer()),
+            Some(self.resources.scratch_framebuffer()),
         );
         self.gl.viewport(
             0,
@@ -121,7 +120,7 @@
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.alphas_texture),
+            Some(&self.resources.alphas_texture),
         );
         self.gl
             .uniform1i(Some(&self.programs.blend_uniforms.alphas_texture), 2);
@@ -130,8 +129,7 @@
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(
-                self.programs
-                    .resources
+                self.resources
                     .layer_texture(bindings.layer_id(TextureParity::Even)),
             ),
         );
@@ -142,8 +140,7 @@
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(
-                self.programs
-                    .resources
+                self.resources
                     .layer_texture(bindings.layer_id(TextureParity::Odd)),
             ),
         );
@@ -160,7 +157,7 @@
                 .copied()
                 .map(GpuBlendInstance::copy_from_scratch),
         );
-        self.programs
+        self.resources
             .upload_copy_instances(self.gl, &self.scratch_buffers.copy_instances);
 
         self.gl.disable(WebGl2RenderingContext::BLEND);
@@ -169,7 +166,7 @@
         self.gl.depth_mask(false);
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            Some(self.programs.resources.layer_framebuffer(bindings.target())),
+            Some(self.resources.layer_framebuffer(bindings.target())),
         );
         self.gl.viewport(
             0,
@@ -177,14 +174,13 @@
             i32::from(texture_size.width()),
             i32::from(texture_size.height()),
         );
-        self.gl
-            .bind_vertex_array(Some(&self.programs.resources.copy_vao));
+        self.gl.bind_vertex_array(Some(&self.resources.copy_vao));
         self.gl.use_program(Some(&self.programs.copy_program));
 
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(self.programs.resources.scratch_binding_texture()),
+            Some(self.resources.scratch_binding_texture()),
         );
         self.gl
             .uniform1i(Some(&self.programs.copy_uniforms.source_texture), 0);
diff --git a/vello_gpu/src/render/webgl/passes/clear.rs b/vello_gpu/src/render/webgl/passes/clear.rs
index 0c1b85b..dfd31e0 100644
--- a/vello_gpu/src/render/webgl/passes/clear.rs
+++ b/vello_gpu/src/render/webgl/passes/clear.rs
@@ -17,16 +17,16 @@
             DrawPassTarget::Root(_) => {
                 self.gl.bind_framebuffer(
                     WebGl2RenderingContext::FRAMEBUFFER,
-                    self.programs.resources.root_framebuffer(),
+                    self.resources.root_framebuffer(),
                 );
                 (
-                    self.programs.render_size.width,
-                    self.programs.render_size.height,
+                    self.resources.render_size.width,
+                    self.resources.render_size.height,
                 )
             }
             DrawPassTarget::Layer(target) => {
                 let size = self.texture_size();
-                let framebuffer = self.programs.resources.layer_framebuffer(target);
+                let framebuffer = self.resources.layer_framebuffer(target);
                 self.gl
                     .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(framebuffer));
                 (size.width(), size.height())
diff --git a/vello_gpu/src/render/webgl/passes/copy.rs b/vello_gpu/src/render/webgl/passes/copy.rs
index 8436d18..b489048 100644
--- a/vello_gpu/src/render/webgl/passes/copy.rs
+++ b/vello_gpu/src/render/webgl/passes/copy.rs
@@ -6,9 +6,7 @@
 use crate::copy::GpuCopyInstance;
 use crate::render::webgl::WebGlError;
 use crate::render::webgl::gl::resource::Program;
-use crate::render::webgl::passes::{
-    WebGlPrograms, initialize_packed_u32_attribs, required_uniform_location,
-};
+use crate::render::webgl::passes::{initialize_packed_u32_attribs, required_uniform_location};
 use crate::render::webgl::resources::WebGlResources;
 use vello_gpu_shaders::copy;
 use web_sys::{WebGl2RenderingContext, WebGlUniformLocation};
@@ -18,7 +16,7 @@
     pub(crate) source_texture: WebGlUniformLocation,
 }
 
-impl WebGlPrograms {
+impl WebGlResources {
     pub(crate) fn upload_copy_instances(
         &self,
         gl: &WebGl2RenderingContext,
@@ -26,7 +24,7 @@
     ) {
         gl.bind_buffer(
             WebGl2RenderingContext::ARRAY_BUFFER,
-            Some(&self.resources.copy_instance_buffer),
+            Some(&self.copy_instance_buffer),
         );
         gl.buffer_data_with_u8_array(
             WebGl2RenderingContext::ARRAY_BUFFER,
diff --git a/vello_gpu/src/render/webgl/passes/filter.rs b/vello_gpu/src/render/webgl/passes/filter.rs
index 4053b7a..1e3cd63 100644
--- a/vello_gpu/src/render/webgl/passes/filter.rs
+++ b/vello_gpu/src/render/webgl/passes/filter.rs
@@ -6,7 +6,7 @@
 use crate::render::webgl::WebGlError;
 use crate::render::webgl::gl::resource::Program;
 use crate::render::webgl::passes::{
-    WebGlPrograms, WebGlRendererContext, checked_instance_count, initialize_packed_u32_attribs,
+    WebGlRendererContext, checked_instance_count, initialize_packed_u32_attribs,
     required_uniform_location,
 };
 use crate::render::webgl::resources::{
@@ -26,14 +26,14 @@
     original_texture: WebGlUniformLocation,
 }
 
-impl WebGlPrograms {
+impl WebGlResources {
     /// Grow the filter data texture if needed.
     pub(crate) fn maybe_resize_filter_data_tex(
         &mut self,
         gl: &WebGl2RenderingContext,
         filter_context: &FilterContext,
     ) -> Result<(), WebGlError> {
-        let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
+        let resource_texture_dimension_2d = self.resource_texture_dimension_2d;
 
         let Some(required_height) =
             filter_context.required_filter_data_height(resource_texture_dimension_2d)
@@ -41,16 +41,16 @@
             return Ok(());
         };
 
-        if required_height > self.resources.filter_data_texture_height {
+        if required_height > self.filter_data_texture_height {
             let required_size = (resource_texture_dimension_2d * required_height) << 4;
             self.filter_data.resize(required_size as usize, 0);
-            self.resources.filter_data_texture = create_data_texture_storage(
+            self.filter_data_texture = create_data_texture_storage(
                 gl,
                 WebGl2RenderingContext::RGBA32UI,
                 resource_texture_dimension_2d,
                 required_height,
             )?;
-            self.resources.filter_data_texture_height = required_height;
+            self.filter_data_texture_height = required_height;
         }
 
         Ok(())
@@ -66,7 +66,7 @@
             return Ok(());
         }
 
-        let width = self.resources.resource_texture_dimension_2d;
+        let width = self.resource_texture_dimension_2d;
         let used_height = filter_context
             .required_filter_data_height(width)
             .expect("non-empty filter context must have a data height");
@@ -74,7 +74,7 @@
         filter_context.serialize_to_buffer(&mut self.filter_data);
         upload_rgba32ui_rows(
             gl,
-            &self.resources.filter_data_texture,
+            &self.filter_data_texture,
             bytemuck::cast_slice::<u8, u32>(&self.filter_data[..used_size]),
             width,
             used_height,
@@ -90,7 +90,7 @@
     ) {
         gl.bind_buffer(
             WebGl2RenderingContext::ARRAY_BUFFER,
-            Some(&self.resources.filter_instance_buffer),
+            Some(&self.filter_instance_buffer),
         );
         gl.buffer_data_with_u8_array(
             WebGl2RenderingContext::ARRAY_BUFFER,
@@ -143,17 +143,16 @@
         bindings: FilterPassBindings,
     ) -> Result<(), WebGlError> {
         if let Some(copy_pass) = plan.copy_pass() {
-            self.programs.upload_copy_instances(self.gl, copy_pass);
+            self.resources.upload_copy_instances(self.gl, copy_pass);
             self.gl.disable(WebGl2RenderingContext::BLEND);
             self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
             self.gl.disable(WebGl2RenderingContext::DEPTH_TEST);
             self.gl.depth_mask(false);
-            self.gl
-                .bind_vertex_array(Some(&self.programs.resources.copy_vao));
+            self.gl.bind_vertex_array(Some(&self.resources.copy_vao));
             self.gl.use_program(Some(&self.programs.copy_program));
             self.gl.bind_framebuffer(
                 WebGl2RenderingContext::FRAMEBUFFER,
-                Some(self.programs.resources.scratch_framebuffer()),
+                Some(self.resources.scratch_framebuffer()),
             );
             let scratch_size = self.texture_size();
             self.gl.viewport(
@@ -165,7 +164,7 @@
             self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
             self.gl.bind_texture(
                 WebGl2RenderingContext::TEXTURE_2D,
-                Some(self.programs.resources.layer_texture(bindings.target())),
+                Some(self.resources.layer_texture(bindings.target())),
             );
             self.gl
                 .uniform1i(Some(&self.programs.copy_uniforms.source_texture), 0);
@@ -173,13 +172,12 @@
         }
 
         self.gl.use_program(Some(&self.programs.filter_program));
-        self.gl
-            .bind_vertex_array(Some(&self.programs.resources.filter_vao));
+        self.gl.bind_vertex_array(Some(&self.resources.filter_vao));
 
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.filter_data_texture),
+            Some(&self.resources.filter_data_texture),
         );
         self.gl
             .uniform1i(Some(&self.programs.filter_uniforms.filter_data), 0);
@@ -187,7 +185,7 @@
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(self.programs.resources.scratch_binding_texture()),
+            Some(self.resources.scratch_binding_texture()),
         );
         self.gl
             .uniform1i(Some(&self.programs.filter_uniforms.original_texture), 2);
@@ -217,7 +215,7 @@
         self.gl.depth_mask(false);
         self.gl.bind_framebuffer(
             WebGl2RenderingContext::FRAMEBUFFER,
-            Some(self.programs.resources.layer_framebuffer(output)),
+            Some(self.resources.layer_framebuffer(output)),
         );
         let target_texture_size = self.texture_size();
         self.gl.viewport(
@@ -230,14 +228,14 @@
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(self.programs.resources.layer_texture(input)),
+            Some(self.resources.layer_texture(input)),
         );
         self.gl
             .uniform1i(Some(&self.programs.filter_uniforms.source_texture), 1);
 
         // TODO: Filter instances ideally should be uploaded once for the whole round (or even once
         // globally), not per pass.
-        self.programs.upload_filter_instances(self.gl, instances);
+        self.resources.upload_filter_instances(self.gl, instances);
         self.draw_instanced_quads(checked_instance_count(instances.len())?);
 
         Ok(())
diff --git a/vello_gpu/src/render/webgl/passes/mod.rs b/vello_gpu/src/render/webgl/passes/mod.rs
index ae81fa8..120aa2d 100644
--- a/vello_gpu/src/render/webgl/passes/mod.rs
+++ b/vello_gpu/src/render/webgl/passes/mod.rs
@@ -21,7 +21,7 @@
 use crate::render::webgl::{WebGlError, WebGlOperation, WebGlShaderInterfaceOperation};
 use crate::util::RangedSlice;
 use crate::{
-    ClearSettings, GpuStrip, RenderSize,
+    ClearSettings, GpuStrip,
     blend::BlendStrip,
     filter::FilterPassPlan,
     render::common::ScratchBuffers,
@@ -31,14 +31,11 @@
         RootTarget,
     },
 };
-use alloc::vec::Vec;
 use vello_common::color::AlphaColor;
 use vello_common::geometry::{RectU16, SizeU16};
 use web_sys::{WebGl2RenderingContext, WebGlUniformLocation};
 
-/// Contains the WebGL programs and resources for rendering.
-// TODO: Split the compiled programs from the resource state, so that `resources` no longer
-// depends on `passes` and can sit underneath it.
+/// Contains the compiled WebGL programs.
 #[derive(Debug)]
 pub(crate) struct WebGlPrograms {
     /// Program for rendering strips.
@@ -59,18 +56,6 @@
     pub(crate) copy_uniforms: CopyUniforms,
     /// Shader compilation stats.
     pub(crate) shader_compilation_stats: ShaderCompilationStats,
-    /// WebGL resources for rendering.
-    pub(crate) resources: WebGlResources,
-    /// Dimensions of the rendering target.
-    pub(crate) render_size: RenderSize,
-    /// Whether the last config buffer upload had NDC Y negation enabled.
-    pub(crate) negate_ndc: bool,
-    /// 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>,
 }
 
 pub(crate) fn required_uniform_location(
@@ -104,7 +89,8 @@
 // the GPU must finish drawing before the next `upload_strip_pair` can be executed (effectively pausing
 // execution). Investigate a buffer pool or creating a new buffer per pass.
 pub(crate) struct WebGlRendererContext<'a> {
-    pub(crate) programs: &'a mut WebGlPrograms,
+    pub(crate) programs: &'a WebGlPrograms,
+    pub(crate) resources: &'a mut WebGlResources,
     pub(crate) gl: &'a WebGl2RenderingContext,
     pub(crate) texture_bindings: &'a WebGlTextureBindings,
     pub(crate) scratch_buffers: &'a mut ScratchBuffers,
@@ -131,7 +117,7 @@
     }
 
     fn texture_size(&self) -> SizeU16 {
-        self.programs.resources.texture_size
+        self.resources.texture_size
     }
 }
 
diff --git a/vello_gpu/src/render/webgl/passes/strip.rs b/vello_gpu/src/render/webgl/passes/strip.rs
index 31cc28e..132c7d7 100644
--- a/vello_gpu/src/render/webgl/passes/strip.rs
+++ b/vello_gpu/src/render/webgl/passes/strip.rs
@@ -6,7 +6,7 @@
 use crate::draw::{EXTERNAL_TEXTURE_SLOT_COUNT, ExternalTextureBindings, ExternalTextureRun};
 use crate::render::webgl::gl::resource::Program;
 use crate::render::webgl::passes::{
-    WebGlPrograms, WebGlRendererContext, checked_instance_count, required_uniform_location,
+    WebGlRendererContext, checked_instance_count, required_uniform_location,
 };
 use crate::render::webgl::resources::{WebGlResources, create_external_sampler};
 use crate::render::webgl::{WebGlError, WebGlOperation, WebGlShaderInterfaceOperation};
@@ -44,7 +44,7 @@
     external_textures: [WebGlUniformLocation; EXTERNAL_TEXTURE_SLOT_COUNT],
 }
 
-impl WebGlPrograms {
+impl WebGlResources {
     /// Uploads two strip slices (opaque then alpha) into a single GPU buffer.
     fn upload_strip_pair(
         &mut self,
@@ -58,7 +58,7 @@
 
         gl.bind_buffer(
             WebGl2RenderingContext::ARRAY_BUFFER,
-            Some(&self.resources.strips_buffer),
+            Some(&self.strips_buffer),
         );
 
         let opaque_len = size_of_val(opaque_strips);
@@ -262,7 +262,7 @@
                 .active_texture(WebGl2RenderingContext::TEXTURE0 + texture_unit);
             let (texture, sampler_key): (&WebGlTexture, ExternalSampler) = match texture_source {
                 Some(TextureSourceId::Atlas(atlas_id)) => (
-                    &self.programs.resources.atlas_textures[atlas_id.as_u32() as usize],
+                    &self.resources.atlas_textures[atlas_id.as_u32() as usize],
                     ExternalSampler::DEFAULT,
                 ),
                 Some(TextureSourceId::External(texture_id, sampler)) => (
@@ -274,13 +274,13 @@
                     sampler,
                 ),
                 None => (
-                    &self.programs.resources.placeholder_external_texture,
+                    &self.resources.placeholder_external_texture,
                     ExternalSampler::DEFAULT,
                 ),
             };
             self.gl
                 .bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(texture));
-            let sampler = match self.programs.resources.external_samplers.entry(sampler_key) {
+            let sampler = match self.resources.external_samplers.entry(sampler_key) {
                 Entry::Occupied(entry) => entry.into_mut(),
                 Entry::Vacant(entry) => {
                     entry.insert(create_external_sampler(self.gl, sampler_key)?)
@@ -325,33 +325,33 @@
             DrawPassTarget::Root(_) => {
                 self.gl.bind_framebuffer(
                     WebGl2RenderingContext::FRAMEBUFFER,
-                    self.programs.resources.root_framebuffer(),
+                    self.resources.root_framebuffer(),
                 );
-                let width = self.programs.render_size.width;
-                let height = self.programs.render_size.height;
+                let width = self.resources.render_size.width;
+                let height = self.resources.render_size.height;
                 self.gl.viewport(0, 0, i32::from(width), i32::from(height));
 
                 self.gl.bind_buffer_base(
                     WebGl2RenderingContext::UNIFORM_BUFFER,
                     self.programs.strip_uniforms.config_vs_block_index,
-                    Some(&self.programs.resources.view_config_buffer),
+                    Some(&self.resources.view_config_buffer),
                 );
                 self.gl.bind_buffer_base(
                     WebGl2RenderingContext::UNIFORM_BUFFER,
                     self.programs.strip_uniforms.config_fs_block_index,
-                    Some(&self.programs.resources.view_config_buffer),
+                    Some(&self.resources.view_config_buffer),
                 );
             }
             DrawPassTarget::Layer(id) => {
                 self.gl.bind_framebuffer(
                     WebGl2RenderingContext::FRAMEBUFFER,
-                    Some(self.programs.resources.layer_framebuffer(*id)),
+                    Some(self.resources.layer_framebuffer(*id)),
                 );
                 let size = self.texture_size();
                 self.gl
                     .viewport(0, 0, i32::from(size.width()), i32::from(size.height()));
 
-                let buf = &self.programs.resources.layer_config_buffer;
+                let buf = &self.resources.layer_config_buffer;
                 self.gl.bind_buffer_base(
                     WebGl2RenderingContext::UNIFORM_BUFFER,
                     self.programs.strip_uniforms.config_vs_block_index,
@@ -371,14 +371,13 @@
         self.gl.use_program(Some(&self.programs.strip_program));
 
         // Set up attributes.
-        self.gl
-            .bind_vertex_array(Some(&self.programs.resources.strip_vao));
+        self.gl.bind_vertex_array(Some(&self.resources.strip_vao));
 
         // Bind textures.
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.alphas_texture),
+            Some(&self.resources.alphas_texture),
         );
         self.gl
             .uniform1i(Some(&self.programs.strip_uniforms.alphas_texture), 0);
@@ -387,8 +386,8 @@
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
             Some(child_layer_texture.map_or_else(
-                || &self.programs.resources.placeholder_external_texture,
-                |id| self.programs.resources.layer_texture(id),
+                || &self.resources.placeholder_external_texture,
+                |id| self.resources.layer_texture(id),
             )),
         );
         self.gl
@@ -398,7 +397,7 @@
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE2);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.encoded_paints_texture),
+            Some(&self.resources.encoded_paints_texture),
         );
         self.gl.uniform1i(
             Some(&self.programs.strip_uniforms.encoded_paints_texture_fs),
@@ -413,7 +412,7 @@
         self.gl.active_texture(WebGl2RenderingContext::TEXTURE3);
         self.gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.gradient_texture),
+            Some(&self.resources.gradient_texture),
         );
         self.gl
             .uniform1i(Some(&self.programs.strip_uniforms.gradient_texture), 3);
@@ -439,7 +438,7 @@
         // that possibility in the future.
         let enable_opaque = self.use_depth_buffer && target.enable_opaque();
 
-        self.programs
+        self.resources
             .upload_strip_pair(self.gl, opaque_strips, alpha_strips);
         let opaque_count = checked_instance_count(opaque_count)?;
         let alpha_count = checked_instance_count(alpha_count)?;
@@ -450,8 +449,8 @@
             self.gl.depth_func(WebGl2RenderingContext::LEQUAL);
 
             // Clear depth buffer on first use per frame.
-            if !self.programs.resources.depth_cleared_this_frame {
-                self.programs.resources.depth_cleared_this_frame = true;
+            if !self.resources.depth_cleared_this_frame {
+                self.resources.depth_cleared_this_frame = true;
                 self.gl.clear_depth(1.0);
                 self.gl.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT);
             }
diff --git a/vello_gpu/src/render/webgl/probe.rs b/vello_gpu/src/render/webgl/probe.rs
index 2bf2e89..ea6a5b5 100644
--- a/vello_gpu/src/render/webgl/probe.rs
+++ b/vello_gpu/src/render/webgl/probe.rs
@@ -86,7 +86,7 @@
         let render_size = RenderSize { width, height };
         // Check whether the canvas framebuffer was configured by the user to
         // hold a depth buffer, and apply the same to the probe.
-        let use_depth_buffer = self.programs.resources.drawing_buffer_uses_depth_buffer;
+        let use_depth_buffer = self.resources.drawing_buffer_uses_depth_buffer;
 
         let probe_texture = create_texture_storage(
             &self.gl,
diff --git a/vello_gpu/src/render/webgl/resources.rs b/vello_gpu/src/render/webgl/resources.rs
index c6d3306..cf5235c 100644
--- a/vello_gpu/src/render/webgl/resources.rs
+++ b/vello_gpu/src/render/webgl/resources.rs
@@ -4,7 +4,6 @@
 //! Creation and growth of the renderer's GPU resources.
 
 use crate::render::webgl::gl::resource::{Buffer, Framebuffer, Sampler, Texture, VertexArray};
-use crate::render::webgl::passes::WebGlPrograms;
 use crate::render::webgl::target::{WebGlTextureTarget, create_framebuffer_for_texture};
 use crate::render::webgl::{
     WebGlDataTransferOperation, WebGlError, WebGlOperation, WebGlResultExt,
@@ -21,6 +20,7 @@
     schedule::Schedule,
     target::{DrawPassTarget, LayerTextureId, RootTarget},
 };
+use alloc::vec;
 use alloc::vec::Vec;
 use hashbrown::HashMap;
 use vello_common::image_cache::ImageCache;
@@ -101,6 +101,16 @@
     pub(crate) layer_config_buffer: Buffer,
     /// Layer texture pages grouped by parity.
     pub(crate) layer_textures: [Vec<WebGlIntermediateTexture>; 2],
+    /// Dimensions of the rendering target.
+    pub(crate) render_size: RenderSize,
+    /// Whether the last config buffer upload had NDC Y negation enabled.
+    pub(crate) negate_ndc: bool,
+    /// 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>,
 }
 
 impl WebGlResources {
@@ -157,7 +167,7 @@
     }
 }
 
-impl WebGlPrograms {
+impl WebGlResources {
     /// Prepare resources for rendering.
     pub(crate) fn prepare(
         &mut self,
@@ -171,7 +181,7 @@
         filter_context: &FilterContext,
         root_target: RootTarget,
     ) -> Result<(), WebGlError> {
-        let resource_texture_dimension_2d = self.resources.resource_texture_dimension_2d;
+        let resource_texture_dimension_2d = self.resource_texture_dimension_2d;
 
         self.maybe_resize_alphas_tex(gl, resource_texture_dimension_2d, alphas.len())?;
         self.maybe_resize_encoded_paints_tex(gl, resource_texture_dimension_2d, paint_idxs)?;
@@ -203,7 +213,7 @@
         gl: &WebGl2RenderingContext,
         schedule: &Schedule,
     ) -> Result<(), WebGlError> {
-        let current_size = self.resources.texture_size;
+        let current_size = self.texture_size;
         let requirements = schedule.intermediate_texture_requirements();
         let texture_size = requirements.size;
         let allocations = requirements.allocations;
@@ -214,13 +224,13 @@
         if size_changed {
             upload_layer_config_buffer(
                 gl,
-                &self.resources.layer_config_buffer,
+                &self.layer_config_buffer,
                 texture_size,
-                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];
 
             // When resizing textures, we explicitly first destroy the old textures before creating
@@ -244,19 +254,20 @@
             }
         }
 
-        let recreate_scratch = (self.resources.scratch_texture.is_some() && size_changed)
-            || (self.resources.scratch_texture.is_none() && scratch_required);
+        let recreate_scratch = (self.scratch_texture.is_some() && size_changed)
+            || (self.scratch_texture.is_none() && scratch_required);
 
         if recreate_scratch {
             // Make sure the old texture is destroyed first.
-            let _ = self.resources.scratch_texture.take();
+            let _ = self.scratch_texture.take();
 
-            self.resources.scratch_texture = Some(ScratchTexture::new(
-                create_intermediate_texture(gl, texture_size)?,
-            ));
+            self.scratch_texture = Some(ScratchTexture::new(create_intermediate_texture(
+                gl,
+                texture_size,
+            )?));
         }
 
-        self.resources.texture_size = texture_size;
+        self.texture_size = texture_size;
 
         Ok(())
     }
@@ -267,13 +278,12 @@
         gl: &WebGl2RenderingContext,
         required_atlas_count: u32,
     ) -> Result<(), WebGlError> {
-        let (width, height) = self.resources.atlas_size;
-        while self.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 = create_atlas_texture(gl, width, height)?;
-            self.resources
-                .atlas_targets
+            self.atlas_targets
                 .push(WebGlTextureTarget::for_atlas(gl, &texture, width, height)?);
-            self.resources.atlas_textures.push(texture);
+            self.atlas_textures.push(texture);
         }
 
         Ok(())
@@ -290,7 +300,7 @@
             // There are 16 1-byte alpha values per texel.
             .div_ceil(resource_texture_dimension_2d << 4);
 
-        let current_alpha_height = self.resources.alpha_texture_height;
+        let current_alpha_height = self.alpha_texture_height;
         if required_alpha_height > current_alpha_height {
             validate_resource_texture_size(
                 resource_texture_dimension_2d,
@@ -299,13 +309,13 @@
             )?;
 
             // We need to resize the alpha texture to fit the new alpha data.
-            self.resources.alphas_texture = create_data_texture_storage(
+            self.alphas_texture = create_data_texture_storage(
                 gl,
                 WebGl2RenderingContext::RGBA32UI,
                 resource_texture_dimension_2d,
                 required_alpha_height,
             )?;
-            self.resources.alpha_texture_height = required_alpha_height;
+            self.alpha_texture_height = required_alpha_height;
         }
 
         Ok(())
@@ -321,7 +331,7 @@
         let required_texels = paint_idxs.last().unwrap();
         let required_encoded_paints_height =
             required_texels.div_ceil(resource_texture_dimension_2d);
-        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 {
             validate_resource_texture_size(
                 resource_texture_dimension_2d,
@@ -333,13 +343,13 @@
                 (resource_texture_dimension_2d * required_encoded_paints_height) << 4;
             self.encoded_paints_data
                 .resize(required_encoded_paints_size as usize, 0);
-            self.resources.encoded_paints_texture = create_data_texture_storage(
+            self.encoded_paints_texture = create_data_texture_storage(
                 gl,
                 WebGl2RenderingContext::RGBA32UI,
                 resource_texture_dimension_2d,
                 required_encoded_paints_height,
             )?;
-            self.resources.encoded_paints_texture_height = required_encoded_paints_height;
+            self.encoded_paints_texture_height = required_encoded_paints_height;
         }
 
         Ok(())
@@ -361,7 +371,7 @@
         let required_gradient_height =
             (gradient_data_size as u32).div_ceil(resource_texture_dimension_2d * 4);
 
-        let current_gradient_height = self.resources.gradient_texture_height;
+        let current_gradient_height = self.gradient_texture_height;
         if required_gradient_height > current_gradient_height {
             validate_resource_texture_size(
                 resource_texture_dimension_2d,
@@ -369,13 +379,13 @@
                 resource_texture_dimension_2d,
             )?;
 
-            self.resources.gradient_texture = create_data_texture_storage(
+            self.gradient_texture = create_data_texture_storage(
                 gl,
                 WebGl2RenderingContext::RGBA8,
                 resource_texture_dimension_2d,
                 required_gradient_height,
             )?;
-            self.resources.gradient_texture_height = required_gradient_height;
+            self.gradient_texture_height = required_gradient_height;
         }
 
         Ok(())
@@ -410,7 +420,7 @@
 
             gl.bind_buffer(
                 WebGl2RenderingContext::UNIFORM_BUFFER,
-                Some(&self.resources.view_config_buffer),
+                Some(&self.view_config_buffer),
             );
             gl.buffer_data_with_u8_array(
                 WebGl2RenderingContext::UNIFORM_BUFFER,
@@ -434,7 +444,7 @@
             return Ok(());
         }
 
-        let alpha_texture_width = self.resources.resource_texture_dimension_2d;
+        let alpha_texture_width = self.resource_texture_dimension_2d;
         // There are 16 1-byte alpha values per texel.
         let used_height = (alphas.len() as u32).div_ceil(alpha_texture_width << 4);
         let used_size = (alpha_texture_width as usize) * (used_height as usize) * 16;
@@ -446,7 +456,7 @@
 
         let result = upload_rgba32ui_rows(
             gl,
-            &self.resources.alphas_texture,
+            &self.alphas_texture,
             bytemuck::cast_slice::<u8, u32>(alphas),
             alpha_texture_width,
             used_height,
@@ -466,7 +476,7 @@
         paint_idxs: &[u32],
     ) -> Result<(), WebGlError> {
         if !encoded_paints.is_empty() {
-            let encoded_paints_texture_width = self.resources.resource_texture_dimension_2d;
+            let encoded_paints_texture_width = self.resource_texture_dimension_2d;
             let used_texels = *paint_idxs.last().unwrap();
             let used_height = used_texels.div_ceil(encoded_paints_texture_width);
             let used_size = ((encoded_paints_texture_width * used_height) << 4) as usize;
@@ -475,7 +485,7 @@
 
             upload_rgba32ui_rows(
                 gl,
-                &self.resources.encoded_paints_texture,
+                &self.encoded_paints_texture,
                 bytemuck::cast_slice::<u8, u32>(&self.encoded_paints_data[..used_size]),
                 encoded_paints_texture_width,
                 used_height,
@@ -495,7 +505,7 @@
             return Ok(());
         }
 
-        let gradient_texture_width = self.resources.resource_texture_dimension_2d;
+        let gradient_texture_width = self.resource_texture_dimension_2d;
         // Each texel is RGBA8, so 4 bytes per texel. Only the rows covering the LUT cache are
         // uploaded; see `upload_alpha_texture`.
         let used_height = (gradient_cache.luts_size() as u32).div_ceil(gradient_texture_width * 4);
@@ -509,7 +519,7 @@
         gl.active_texture(WebGl2RenderingContext::TEXTURE0);
         gl.bind_texture(
             WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.resources.gradient_texture),
+            Some(&self.gradient_texture),
         );
 
         let result = gl
@@ -838,6 +848,14 @@
         scratch_texture,
         layer_config_buffer,
         layer_textures,
+        render_size: RenderSize {
+            width: 0,
+            height: 0,
+        },
+        negate_ndc: false,
+        scene_offset: (0, 0),
+        encoded_paints_data: vec![0; (resource_texture_dimension_2d << 4) as usize],
+        filter_data: Vec::new(),
     })
 }