Allow WebGL texture uploads during shader initialization
diff --git a/vello_gpu/src/render/webgl/mod.rs b/vello_gpu/src/render/webgl/mod.rs
index 12c5d99..9249e1c 100644
--- a/vello_gpu/src/render/webgl/mod.rs
+++ b/vello_gpu/src/render/webgl/mod.rs
@@ -503,7 +503,8 @@
                 )
             },
             |renderer, image_cache, upload, dst_x, dst_y| {
-                renderer.write_to_atlas(
+                renderer.programs.resources.write_to_atlas(
+                    &renderer.gl,
                     image_cache,
                     upload.image_id,
                     &upload.pixmap,
@@ -558,6 +559,7 @@
         texture_bindings: &WebGlTextureBindings,
     ) -> Result<(), WebGlError> {
         self.programs
+            .resources
             .maybe_create_atlas_textures(&self.gl, atlas_count)?;
 
         let (atlas_width, atlas_height) = atlas_config.atlas_size;
@@ -757,62 +759,12 @@
         resources: &mut Resources,
         writer: &T,
     ) -> Result<ImageId, WebGlError> {
-        self.upload_image_with(&mut resources.image_cache, writer, IMAGE_PADDING)
-    }
-
-    pub(crate) fn upload_image_with<T: WebGlAtlasWriter>(
-        &mut self,
-        image_cache: &mut ImageCache,
-        writer: &T,
-        padding: u16,
-    ) -> Result<ImageId, WebGlError> {
-        let width = writer.width();
-        let height = writer.height();
-        let image_id = image_cache
-            .allocate(width, height, padding)
-            .map_err(RenderError::AtlasError)?;
-
-        if let Err(err) = self.write_to_atlas(image_cache, image_id, writer, None) {
-            image_cache.deallocate(image_id);
-
-            return Err(err);
-        }
-
-        Ok(image_id)
-    }
-
-    /// Write pixel data to an existing atlas allocation.
-    ///
-    /// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image
-    /// cache. The `image_id` must have been previously allocated (e.g. via
-    /// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as
-    /// bitmap font glyphs) to a pre-allocated atlas region.
-    ///
-    /// If `offset_override` is `Some`, the provided offset is used instead of the
-    /// allocator-assigned position. Pass `None` to use the default atlas offset.
-    pub(crate) fn write_to_atlas<T: WebGlAtlasWriter>(
-        &mut self,
-        image_cache: &ImageCache,
-        image_id: ImageId,
-        writer: &T,
-        offset_override: Option<[u16; 2]>,
-    ) -> Result<(), WebGlError> {
-        let image_resource = image_cache
-            .get(image_id)
-            .ok_or(RenderError::MissingImage(image_id))?;
-
-        self.programs
-            .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.programs.resources.upload_image_with(
             &self.gl,
-            &self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize],
-            offset,
-            writer.width(),
-            writer.height(),
-        )?;
-
-        Ok(())
+            &mut resources.image_cache,
+            writer,
+            IMAGE_PADDING,
+        )
     }
 
     /// Destroy an image from the cache and clear the allocated slot in the atlas.
@@ -821,22 +773,9 @@
         resources: &mut Resources,
         image_id: ImageId,
     ) -> Result<(), WebGlError> {
-        if let Some(image_resource) = resources.image_cache.get(image_id) {
-            let padding = image_resource.padding;
-            self.clear_atlas_region(
-                image_resource.atlas_id,
-                [
-                    image_resource.offset[0] - padding,
-                    image_resource.offset[1] - padding,
-                ],
-                image_resource.width + padding * 2,
-                image_resource.height + padding * 2,
-            )?;
-
-            let _ = resources.image_cache.deallocate(image_id);
-        }
-
-        Ok(())
+        self.programs
+            .resources
+            .destroy_image(&self.gl, &mut resources.image_cache, image_id)
     }
 
     /// Returns an individual image atlas texture.
@@ -867,39 +806,9 @@
         width: u16,
         height: u16,
     ) -> Result<(), WebGlError> {
-        let temp_framebuffer = Framebuffer::new(&self.gl)?;
-
-        // Bind our temporary framebuffer
-        self.gl
-            .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer));
-
-        self.gl.framebuffer_texture_2d(
-            WebGl2RenderingContext::FRAMEBUFFER,
-            WebGl2RenderingContext::COLOR_ATTACHMENT0,
-            WebGl2RenderingContext::TEXTURE_2D,
-            Some(&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]),
-            0,
-        );
-
-        // Set viewport to match the atlas texture dimensions
-        let (atlas_width, atlas_height) = self.programs.resources.atlas_size;
-        self.gl
-            .viewport(0, 0, i32::from(atlas_width), i32::from(atlas_height));
-
-        // Enable scissor test and set scissor rectangle to our region
-        self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
-        self.gl.scissor(
-            offset[0] as i32,
-            offset[1] as i32,
-            width as i32,
-            height as i32,
-        );
-
-        // Clear the region to transparent (0, 0, 0, 0)
-        self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
-        self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
-
-        Ok(())
+        self.programs
+            .resources
+            .clear_atlas_region(&self.gl, atlas_id, offset, width, height)
     }
 
     fn prepare_gpu_encoded_paints(
@@ -1096,6 +1005,38 @@
 }
 
 impl WebGlRendererInit {
+    /// Get the WebGL context.
+    ///
+    /// This can be used to upload externally owned textures while the programs are compiling.
+    pub fn gl_context(&self) -> &WebGl2RenderingContext {
+        &self.gl
+    }
+
+    /// Allocate and upload an image to the atlas.
+    pub fn upload_image<T: WebGlAtlasWriter>(
+        &mut self,
+        resources: &mut Resources,
+        writer: &T,
+    ) -> Result<ImageId, WebGlError> {
+        self.programs.resources.upload_image_with(
+            &self.gl,
+            &mut resources.image_cache,
+            writer,
+            IMAGE_PADDING,
+        )
+    }
+
+    /// Destroy an uploaded image and clear its atlas allocation.
+    pub fn destroy_image(
+        &mut self,
+        resources: &mut Resources,
+        image_id: ImageId,
+    ) -> Result<(), WebGlError> {
+        self.programs
+            .resources
+            .destroy_image(&self.gl, &mut resources.image_cache, image_id)
+    }
+
     /// Polls shader compilation and program linking without blocking when
     /// `KHR_parallel_shader_compile` is available.
     ///
@@ -1353,6 +1294,144 @@
 }
 
 impl WebGlResources {
+    fn upload_image_with<T: WebGlAtlasWriter>(
+        &mut self,
+        gl: &WebGl2RenderingContext,
+        image_cache: &mut ImageCache,
+        writer: &T,
+        padding: u16,
+    ) -> Result<ImageId, WebGlError> {
+        let width = writer.width();
+        let height = writer.height();
+        let image_id = image_cache
+            .allocate(width, height, padding)
+            .map_err(RenderError::AtlasError)?;
+
+        if let Err(err) = self.write_to_atlas(gl, image_cache, image_id, writer, None) {
+            image_cache.deallocate(image_id);
+
+            return Err(err);
+        }
+
+        Ok(image_id)
+    }
+
+    /// Write pixel data to an existing atlas allocation.
+    ///
+    /// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image
+    /// cache. The `image_id` must have been previously allocated (e.g. via
+    /// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as
+    /// bitmap font glyphs) to a pre-allocated atlas region.
+    ///
+    /// If `offset_override` is `Some`, the provided offset is used instead of the
+    /// allocator-assigned position. Pass `None` to use the default atlas offset.
+    fn write_to_atlas<T: WebGlAtlasWriter>(
+        &mut self,
+        gl: &WebGl2RenderingContext,
+        image_cache: &ImageCache,
+        image_id: ImageId,
+        writer: &T,
+        offset_override: Option<[u16; 2]>,
+    ) -> Result<(), WebGlError> {
+        let image_resource = image_cache
+            .get(image_id)
+            .ok_or(RenderError::MissingImage(image_id))?;
+
+        self.maybe_create_atlas_textures(gl, image_cache.atlas_count() as u32)?;
+        let offset = offset_override.unwrap_or(image_resource.offset);
+        writer.write_to_atlas(
+            gl,
+            &self.atlas_textures[image_resource.atlas_id.as_u32() as usize],
+            offset,
+            writer.width(),
+            writer.height(),
+        )?;
+
+        Ok(())
+    }
+
+    /// Destroy an uploaded image and clear its atlas allocation.
+    fn destroy_image(
+        &mut self,
+        gl: &WebGl2RenderingContext,
+        image_cache: &mut ImageCache,
+        image_id: ImageId,
+    ) -> Result<(), WebGlError> {
+        if let Some(image_resource) = image_cache.get(image_id) {
+            let padding = image_resource.padding;
+            self.clear_atlas_region(
+                gl,
+                image_resource.atlas_id,
+                [
+                    image_resource.offset[0] - padding,
+                    image_resource.offset[1] - padding,
+                ],
+                image_resource.width + padding * 2,
+                image_resource.height + padding * 2,
+            )?;
+
+            let _ = image_cache.deallocate(image_id);
+        }
+
+        Ok(())
+    }
+
+    fn clear_atlas_region(
+        &mut self,
+        gl: &WebGl2RenderingContext,
+        atlas_id: AtlasId,
+        offset: [u16; 2],
+        width: u16,
+        height: u16,
+    ) -> Result<(), WebGlError> {
+        let temp_framebuffer = Framebuffer::new(gl)?;
+
+        // Bind our temporary framebuffer
+        gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer));
+
+        gl.framebuffer_texture_2d(
+            WebGl2RenderingContext::FRAMEBUFFER,
+            WebGl2RenderingContext::COLOR_ATTACHMENT0,
+            WebGl2RenderingContext::TEXTURE_2D,
+            Some(&self.atlas_textures[atlas_id.as_u32() as usize]),
+            0,
+        );
+
+        // Set viewport to match the atlas texture dimensions
+        let (atlas_width, atlas_height) = self.atlas_size;
+        gl.viewport(0, 0, i32::from(atlas_width), i32::from(atlas_height));
+
+        // Enable scissor test and set scissor rectangle to our region
+        gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
+        gl.scissor(
+            offset[0] as i32,
+            offset[1] as i32,
+            width as i32,
+            height as i32,
+        );
+
+        // Clear the region to transparent (0, 0, 0, 0)
+        gl.clear_color(0.0, 0.0, 0.0, 0.0);
+        gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
+
+        Ok(())
+    }
+
+    /// Create any newly allocated atlas textures.
+    fn maybe_create_atlas_textures(
+        &mut self,
+        gl: &WebGl2RenderingContext,
+        required_atlas_count: u32,
+    ) -> Result<(), WebGlError> {
+        let (width, height) = self.atlas_size;
+        while self.atlas_textures.len() < required_atlas_count as usize {
+            self.atlas_textures
+                .push(create_atlas_texture(gl, width, height)?);
+        }
+
+        Ok(())
+    }
+
     fn layer_texture(&self, id: LayerTextureId) -> &Texture {
         self.layer_textures[id.texture_parity.get_parity()][usize::from(id.page_index)]
             .binding_texture()
@@ -1722,22 +1801,6 @@
         Ok(())
     }
 
-    /// Create any newly allocated atlas textures.
-    fn maybe_create_atlas_textures(
-        &mut self,
-        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 {
-            self.resources
-                .atlas_textures
-                .push(create_atlas_texture(gl, width, height)?);
-        }
-
-        Ok(())
-    }
-
     /// Grow the filter data texture if needed.
     fn maybe_resize_filter_data_tex(
         &mut self,