feat: add GPU timing to WebGL browser examples (#1852)

Add asynchronous GPU execution timing to the native WebGL2 and
wgpu-WebGL browser examples using disjoint timer queries.
 
- Vello Hybrid WebGL backend - Tiger scene 1

<img width="418" height="340" alt="image"
src="https://github.com/user-attachments/assets/134fdfe5-d7ab-4428-b29a-c4c1c29733d9"
/>


- Vello Hybrid WebGL backend - Filters scene 7

<img width="415" height="340" alt="image"
src="https://github.com/user-attachments/assets/fb28fc68-6b5c-45fd-bf13-2476271276b8"
/>

Created with assistance from GPT-5.6 Sol.
diff --git a/sparse_strips/vello_example_scenes/Cargo.toml b/sparse_strips/vello_example_scenes/Cargo.toml
index df3940c..afb5d3c 100644
--- a/sparse_strips/vello_example_scenes/Cargo.toml
+++ b/sparse_strips/vello_example_scenes/Cargo.toml
@@ -12,6 +12,12 @@
 [features]
 default = []
 cpu = ["dep:vello_cpu"]
+webgl_gpu_timing = [
+    "web-sys/ExtDisjointTimerQuery",
+    "web-sys/HtmlCanvasElement",
+    "web-sys/WebGl2RenderingContext",
+    "web-sys/WebGlQuery",
+]
 
 [dependencies]
 bytemuck = { workspace = true, features = [] }
diff --git a/sparse_strips/vello_example_scenes/src/performance.rs b/sparse_strips/vello_example_scenes/src/performance.rs
index c2195a2..dca19ab 100644
--- a/sparse_strips/vello_example_scenes/src/performance.rs
+++ b/sparse_strips/vello_example_scenes/src/performance.rs
@@ -7,13 +7,123 @@
     reason = "Sample windows are bounded to 120 entries"
 )]
 
+#[cfg(feature = "webgl_gpu_timing")]
+use std::collections::VecDeque;
 use std::fmt::Write as _;
 use wasm_bindgen::JsCast;
 use web_sys::HtmlElement;
+#[cfg(feature = "webgl_gpu_timing")]
+use web_sys::{ExtDisjointTimerQuery, HtmlCanvasElement, WebGl2RenderingContext, WebGlQuery};
 
 const MAX_SAMPLES: usize = 120;
 const PANEL_UPDATE_INTERVAL_MS: f64 = 250.0;
 const SUSPENDED_FRAME_THRESHOLD_MS: f64 = 250.0;
+#[cfg(feature = "webgl_gpu_timing")]
+const MAX_PENDING_GPU_QUERIES: usize = 8;
+
+#[cfg(feature = "webgl_gpu_timing")]
+#[derive(Debug)]
+/// Measures GPU-clock elapsed time for WebGL command ranges without blocking for results.
+///
+/// [`begin`](Self::begin) marks the start of a range in the GPU command stream, and
+/// [`end`](Self::end) marks its end. The result is the time the GPU takes to process the
+/// commands submitted between those markers.
+///
+/// Measurements can include GPU stalls or idle gaps inside the range, but not CPU execution time.
+pub struct WebGlGpuTimer {
+    gl: WebGl2RenderingContext,
+    pending: VecDeque<WebGlQuery>,
+    active: Option<WebGlQuery>,
+}
+
+#[cfg(feature = "webgl_gpu_timing")]
+impl WebGlGpuTimer {
+    pub fn new(canvas: &HtmlCanvasElement) -> Option<Self> {
+        let gl = canvas
+            .get_context("webgl2")
+            .ok()
+            .flatten()?
+            .dyn_into::<WebGl2RenderingContext>()
+            .ok()?;
+        gl.get_extension("EXT_disjoint_timer_query_webgl2")
+            .ok()
+            .flatten()?;
+
+        Some(Self {
+            gl,
+            pending: VecDeque::with_capacity(MAX_PENDING_GPU_QUERIES),
+            active: None,
+        })
+    }
+
+    /// Starts a measurement unless one is active or the pending-result queue is full.
+    pub fn begin(&mut self) {
+        if self.active.is_some() || self.pending.len() == MAX_PENDING_GPU_QUERIES {
+            return;
+        }
+        let Some(query) = self.gl.create_query() else {
+            return;
+        };
+        self.gl
+            .begin_query(ExtDisjointTimerQuery::TIME_ELAPSED_EXT, &query);
+        self.active = Some(query);
+    }
+
+    /// Ends the active measurement and queues it for asynchronous retrieval.
+    pub fn end(&mut self) {
+        let Some(query) = self.active.take() else {
+            return;
+        };
+        self.gl.end_query(ExtDisjointTimerQuery::TIME_ELAPSED_EXT);
+        self.pending.push_back(query);
+    }
+
+    /// Returns the oldest available measurement in milliseconds without waiting for the GPU.
+    ///
+    /// If the GPU timer becomes unreliable, for example after a GPU reset or clock-speed
+    /// change, all pending measurements are discarded because their durations may be incorrect.
+    pub fn poll(&mut self) -> Option<f64> {
+        let disjoint = self
+            .gl
+            .get_parameter(ExtDisjointTimerQuery::GPU_DISJOINT_EXT)
+            .ok()
+            .and_then(|value| value.as_bool())
+            .unwrap_or(false);
+        if disjoint {
+            self.clear_pending();
+            return None;
+        }
+
+        let query = self.pending.front()?;
+        let available = self
+            .gl
+            .get_query_parameter(query, WebGl2RenderingContext::QUERY_RESULT_AVAILABLE)
+            .as_bool()
+            .unwrap_or(false);
+        if !available {
+            return None;
+        }
+
+        let query = self.pending.pop_front().unwrap();
+        let duration_ns = self
+            .gl
+            .get_query_parameter(&query, WebGl2RenderingContext::QUERY_RESULT)
+            .as_f64();
+        self.gl.delete_query(Some(&query));
+        duration_ns.map(|duration_ns| duration_ns / 1_000_000.0)
+    }
+
+    /// Discards pending measurements; this must be called outside an active measurement.
+    pub fn reset(&mut self) {
+        self.clear_pending();
+    }
+
+    fn clear_pending(&mut self) {
+        for query in self.pending.drain(..) {
+            self.gl.delete_query(Some(&query));
+        }
+    }
+}
 
 #[derive(Clone, Copy, Debug)]
 pub struct PerformanceStage {
@@ -39,6 +149,7 @@
     frame_intervals: Vec<f64>,
     stage_times: [Vec<f64>; N],
     total_times: Vec<f64>,
+    gpu_times: Vec<f64>,
 }
 
 impl<const N: usize> PerformancePanel<N> {
@@ -96,6 +207,16 @@
             frame_intervals: Vec::with_capacity(MAX_SAMPLES),
             stage_times: std::array::from_fn(|_| Vec::with_capacity(MAX_SAMPLES)),
             total_times: Vec::with_capacity(MAX_SAMPLES),
+            gpu_times: Vec::with_capacity(MAX_SAMPLES),
+        }
+    }
+
+    pub fn record_gpu_time(&mut self, duration_ms: Option<f64>) {
+        if let Some(duration_ms) = duration_ms
+            && duration_ms.is_finite()
+            && duration_ms >= 0.0
+        {
+            push_sample(&mut self.gpu_times, duration_ms);
         }
     }
 
@@ -149,6 +270,7 @@
             samples.clear();
         }
         self.total_times.clear();
+        self.gpu_times.clear();
     }
 
     fn update_text(&self, scene: usize, scene_count: usize, width: u32, height: u32) {
@@ -161,6 +283,7 @@
         let stage_stats = std::array::from_fn(|index| {
             SampleStats::from_samples(&self.stage_times[index]).unwrap()
         });
+        let gpu = SampleStats::from_samples(&self.gpu_times);
 
         let html = format_panel_html(&PanelSnapshot {
             backend: self.backend,
@@ -174,6 +297,8 @@
             total: &total,
             stage_stats: &stage_stats,
             sample_count: self.total_times.len(),
+            gpu: gpu.as_ref(),
+            gpu_sample_count: self.gpu_times.len(),
         });
         self.element.set_inner_html(&html);
     }
@@ -191,12 +316,20 @@
     total: &'a SampleStats,
     stage_stats: &'a [SampleStats; N],
     sample_count: usize,
+    gpu: Option<&'a SampleStats>,
+    gpu_sample_count: usize,
 }
 
 fn format_panel_html<const N: usize>(panel: &PanelSnapshot<'_, N>) -> String {
     let fps = 1_000.0 / panel.frame.average;
-    let timeline_duration = panel.frame.average.max(panel.total.average);
-    let other_percent = (100.0 - 100.0 * panel.total.average / timeline_duration).max(0.0);
+    let timeline_duration = panel
+        .gpu
+        .map_or(panel.frame.average, |gpu| {
+            panel.frame.average.max(gpu.average)
+        })
+        .max(panel.total.average);
+    let other_percent =
+        100.0 * (panel.frame.average - panel.total.average).max(0.0) / timeline_duration;
     let other_average = (panel.frame.average - panel.total.average).max(0.0);
 
     let mut html = String::with_capacity(4096);
@@ -223,6 +356,11 @@
         )
         .unwrap();
     }
+    if panel.gpu.is_some() {
+        html.push_str(
+            r#"<span class="vello-perf-tooltip-row"><strong>GPU execution</strong><span>GPU time for rendering, submission, and presentation commands. Results arrive asynchronously and overlap CPU work.</span></span>"#,
+        );
+    }
     html.push_str(
         r#"<span class="vello-perf-tooltip-row"><strong>Browser/rAF</strong><span>Browser work, scheduling, and display wait.</span></span>
   <span class="vello-perf-tooltip-row"><strong>Full render</strong><span>Sum of measured stages, on the CPU.</span></span>
@@ -251,14 +389,30 @@
     write!(
         html,
         r#"<div style="width:{other_percent:.3}%;background:rgba(255,255,255,.16)"></div>
+</div>"#,
+    )
+    .unwrap();
+    if let Some(gpu) = panel.gpu {
+        let gpu_percent = 100.0 * gpu.average / timeline_duration;
+        write!(
+            html,
+            r#"<div style="display:flex;justify-content:space-between;margin:6px 0 4px">
+  <span>Average GPU execution</span><strong>{:.2} ms</strong>
 </div>
-<div style="display:grid;grid-template-columns:10px 1fr 58px 58px 58px;column-gap:6px;align-items:center;margin-top:7px">
+<div style="height:10px;overflow:hidden;border:1px solid rgba(255,255,255,.45);border-radius:3px">
+  <div style="width:{gpu_percent:.3}%;height:100%;background:#a855f7"></div>
+</div>"#,
+            gpu.average,
+        )
+        .unwrap();
+    }
+    html.push_str(
+        r#"<div style="display:grid;grid-template-columns:10px 1fr 58px 58px 58px;column-gap:6px;align-items:center;margin-top:7px">
   <span></span><span style="opacity:.7">Stage (ms)</span>
   <span style="text-align:right;opacity:.7">avg</span>
   <span style="text-align:right;opacity:.7">p95</span>
   <span style="text-align:right;opacity:.7">max</span>"#,
-    )
-    .unwrap();
+    );
     for (stage, stats) in panel.stages.iter().zip(panel.stage_stats) {
         write!(
             html,
@@ -276,14 +430,54 @@
         r#"<span style="width:8px;height:8px;background:rgba(255,255,255,.35);border-radius:2px"></span>
   <span>Browser/rAF</span>
   <span style="text-align:right">{other_average:.2}</span>
-  <span></span><span></span>
-</div>
+  <span></span><span></span>"#,
+    )
+    .unwrap();
+    if let Some(gpu) = panel.gpu {
+        write!(
+            html,
+            r#"<span style="width:8px;height:8px;background:#a855f7;border-radius:2px"></span>
+  <span>GPU execution</span>
+  <span style="text-align:right">{:.2}</span>
+  <span style="text-align:right">{:.2}</span>
+  <span style="text-align:right">{:.2}</span>"#,
+            gpu.average, gpu.p95, gpu.max,
+        )
+        .unwrap();
+    }
+    write!(
+        html,
+        r#"</div>
 <div style="display:flex;justify-content:space-between;margin-top:8px;padding-top:7px;border-top:1px solid rgba(255,255,255,.25)">
   <strong>Full CPU render</strong><strong>{:.2} ms avg</strong>
-</div>
-<div style="margin-top:3px;opacity:.65">{} samples</div>
+</div>"#,
+        panel.total.average,
+    )
+    .unwrap();
+    if let Some(gpu) = panel.gpu {
+        write!(
+            html,
+            r#"<div style="display:flex;justify-content:space-between;margin-top:3px">
+  <strong>Full GPU execution</strong><strong>{:.2} ms avg</strong>
+</div>"#,
+            gpu.average,
+        )
+        .unwrap();
+    }
+    write!(
+        html,
+        r#"<div style="margin-top:3px;opacity:.65">{} CPU samples"#,
+        panel.sample_count,
+    )
+    .unwrap();
+    if panel.gpu.is_some() {
+        write!(html, " · {} GPU samples", panel.gpu_sample_count).unwrap();
+    }
+    write!(
+        html,
+        r#"</div>
 <div style="opacity:.65">{}</div>"#,
-        panel.total.average, panel.sample_count, panel.timing_note,
+        panel.timing_note,
     )
     .unwrap();
     html
diff --git a/sparse_strips/vello_hybrid/examples/native_webgl/Cargo.toml b/sparse_strips/vello_hybrid/examples/native_webgl/Cargo.toml
index f8227de..96df70d 100644
--- a/sparse_strips/vello_hybrid/examples/native_webgl/Cargo.toml
+++ b/sparse_strips/vello_hybrid/examples/native_webgl/Cargo.toml
@@ -20,7 +20,7 @@
 log = { workspace = true }
 vello_common = { workspace = true }
 vello_hybrid = { workspace = true, features = ["webgl"] }
-vello_example_scenes = { workspace = true }
+vello_example_scenes = { workspace = true, features = ["webgl_gpu_timing"] }
 wasm-bindgen = { workspace = true }
 wasm-bindgen-futures = { workspace = true }
 web-sys = { workspace = true, features = [
diff --git a/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs b/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
index fffd651..d6d650b 100644
--- a/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/native_webgl/src/lib.rs
@@ -19,7 +19,7 @@
 use vello_example_scenes::image::ImageScene;
 use vello_example_scenes::{
     AnyScene,
-    performance::{FrameTiming, PerformancePanel, PerformanceStage, now},
+    performance::{FrameTiming, PerformancePanel, PerformanceStage, WebGlGpuTimer, now},
 };
 use vello_hybrid::{RenderSettings, Scene};
 use wasm_bindgen::prelude::*;
@@ -28,6 +28,7 @@
 struct RendererWrapper {
     renderer: vello_hybrid::WebGlRenderer,
     resources: vello_hybrid::Resources,
+    gpu_timer: Option<WebGlGpuTimer>,
 }
 
 impl RendererWrapper {
@@ -38,6 +39,7 @@
         Self {
             renderer,
             resources,
+            gpu_timer: WebGlGpuTimer::new(&canvas),
         }
     }
 }
@@ -81,6 +83,11 @@
         let current_scene = initial_scene_index(scenes.len());
 
         let renderer_wrapper = RendererWrapper::new(canvas.clone());
+        let timing_note = if renderer_wrapper.gpu_timer.is_some() {
+            "GPU queries are asynchronous and may arrive several frames later"
+        } else {
+            "GPU timing unavailable: EXT_disjoint_timer_query_webgl2 is unsupported"
+        };
 
         let mut app_state = Self {
             scenes,
@@ -107,7 +114,7 @@
                         color: "#f59e0b",
                     },
                 ],
-                "GPU executes asynchronously and is not timed",
+                timing_note,
             ),
             canvas,
         };
@@ -119,7 +126,12 @@
         app_state
     }
 
-    fn render(&mut self) -> FrameTiming<2> {
+    fn render(&mut self) -> (FrameTiming<2>, Option<f64>) {
+        let gpu_time = self
+            .renderer_wrapper
+            .gpu_timer
+            .as_mut()
+            .and_then(WebGlGpuTimer::poll);
         let frame_start = now();
         self.scene.reset();
 
@@ -136,6 +148,9 @@
             height: self.height,
         };
 
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.begin();
+        }
         self.renderer_wrapper
             .renderer
             .render(
@@ -145,18 +160,25 @@
                 &vello_hybrid::WebGlTextureBindings::new(),
             )
             .unwrap();
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.end();
+        }
         let frame_end = now();
 
-        FrameTiming {
-            stages_ms: [scene_end - frame_start, frame_end - scene_end],
-            total_ms: frame_end - frame_start,
-        }
+        (
+            FrameTiming {
+                stages_ms: [scene_end - frame_start, frame_end - scene_end],
+                total_ms: frame_end - frame_start,
+            },
+            gpu_time,
+        )
     }
 
     fn frame(&mut self, timestamp: f64) {
-        let timing = self.render();
+        let (timing, gpu_time) = self.render();
         let scene = self.current_scene + 1;
         let scene_count = self.scenes.len();
+        self.performance.record_gpu_time(gpu_time);
         self.performance.record(
             timestamp,
             Some(timing),
@@ -174,6 +196,9 @@
         self.height = height;
 
         self.scene.reset_and_resize(width as u16, height as u16);
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.reset();
+        }
         self.performance.reset();
     }
 
@@ -182,6 +207,9 @@
         update_page_url(self.current_scene);
         self.update_title();
         self.transform = Affine::IDENTITY;
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.reset();
+        }
         self.performance.reset();
     }
 
@@ -194,6 +222,9 @@
         update_page_url(self.current_scene);
         self.update_title();
         self.transform = Affine::IDENTITY;
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.reset();
+        }
         self.performance.reset();
     }
 
@@ -568,6 +599,7 @@
     let RendererWrapper {
         mut renderer,
         mut resources,
+        ..
     } = RendererWrapper::new(canvas);
 
     let render_size = vello_hybrid::RenderSize {
diff --git a/sparse_strips/vello_hybrid/examples/wgpu_webgl/Cargo.toml b/sparse_strips/vello_hybrid/examples/wgpu_webgl/Cargo.toml
index 2662e5b..3908969 100644
--- a/sparse_strips/vello_hybrid/examples/wgpu_webgl/Cargo.toml
+++ b/sparse_strips/vello_hybrid/examples/wgpu_webgl/Cargo.toml
@@ -20,7 +20,7 @@
 log = { workspace = true }
 vello_common = { workspace = true }
 vello_hybrid = { workspace = true }
-vello_example_scenes = { workspace = true }
+vello_example_scenes = { workspace = true, features = ["webgl_gpu_timing"] }
 wasm-bindgen = { workspace = true }
 wasm-bindgen-futures = { workspace = true }
 web-sys = { workspace = true, features = [
diff --git a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
index 7438e62..45f24b0 100644
--- a/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
+++ b/sparse_strips/vello_hybrid/examples/wgpu_webgl/src/lib.rs
@@ -9,8 +9,7 @@
 )]
 #![cfg(target_arch = "wasm32")]
 
-use std::cell::RefCell;
-use std::rc::Rc;
+use std::{cell::RefCell, rc::Rc};
 use vello_common::{
     fearless_simd::Level,
     kurbo::{Affine, Point},
@@ -19,7 +18,7 @@
 use vello_example_scenes::{
     AnyScene,
     image::ImageScene,
-    performance::{FrameTiming, PerformancePanel, PerformanceStage, now},
+    performance::{FrameTiming, PerformancePanel, PerformanceStage, WebGlGpuTimer, now},
 };
 use vello_hybrid::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene};
 use wasm_bindgen::prelude::*;
@@ -44,6 +43,7 @@
     queue: wgpu::Queue,
     surface: wgpu::Surface<'static>,
     depth_texture_view: wgpu::TextureView,
+    gpu_timer: Option<WebGlGpuTimer>,
 }
 
 impl RendererWrapper {
@@ -119,6 +119,7 @@
             queue,
             surface,
             depth_texture_view,
+            gpu_timer: WebGlGpuTimer::new(&canvas),
         }
     }
 
@@ -138,6 +139,9 @@
             &self.device,
             &vello_hybrid::RenderSize { width, height },
         );
+        if let Some(gpu_timer) = &mut self.gpu_timer {
+            gpu_timer.reset();
+        }
     }
 }
 
@@ -163,6 +167,11 @@
         let current_scene = initial_scene_index(scenes.len());
 
         let renderer_wrapper = RendererWrapper::new(canvas.clone()).await;
+        let timing_note = if renderer_wrapper.gpu_timer.is_some() {
+            "GPU queries are asynchronous and may arrive several frames later"
+        } else {
+            "GPU timing unavailable: EXT_disjoint_timer_query_webgl2 is unsupported"
+        };
 
         let mut app_state = Self {
             scenes,
@@ -193,7 +202,7 @@
                         color: "#3b82f6",
                     },
                 ],
-                "GPU executes asynchronously and is not timed",
+                timing_note,
             ),
             canvas,
         };
@@ -206,7 +215,12 @@
         app_state
     }
 
-    fn render(&mut self) -> Option<FrameTiming<3>> {
+    fn render(&mut self) -> (Option<FrameTiming<3>>, Option<f64>) {
+        let gpu_time = self
+            .renderer_wrapper
+            .gpu_timer
+            .as_mut()
+            .and_then(WebGlGpuTimer::poll);
         let frame_start = now();
         self.scene.reset();
 
@@ -229,7 +243,7 @@
             | CurrentSurfaceTexture::Timeout
             | CurrentSurfaceTexture::Outdated
             | CurrentSurfaceTexture::Suboptimal(_) => {
-                return None;
+                return (None, gpu_time);
             }
             CurrentSurfaceTexture::Lost => panic!("Surface was lost"),
             CurrentSurfaceTexture::Validation => {
@@ -245,6 +259,9 @@
             .device
             .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
 
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.begin();
+        }
         self.renderer_wrapper
             .renderer
             .render(
@@ -263,22 +280,29 @@
 
         self.renderer_wrapper.queue.submit([encoder.finish()]);
         surface_texture.present();
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.end();
+        }
         let frame_end = now();
 
-        Some(FrameTiming {
-            stages_ms: [
-                scene_end - frame_start,
-                render_end - scene_end,
-                frame_end - render_end,
-            ],
-            total_ms: frame_end - frame_start,
-        })
+        (
+            Some(FrameTiming {
+                stages_ms: [
+                    scene_end - frame_start,
+                    render_end - scene_end,
+                    frame_end - render_end,
+                ],
+                total_ms: frame_end - frame_start,
+            }),
+            gpu_time,
+        )
     }
 
     fn frame(&mut self, timestamp: f64) {
-        let timing = self.render();
+        let (timing, gpu_time) = self.render();
         let scene = self.current_scene + 1;
         let scene_count = self.scenes.len();
+        self.performance.record_gpu_time(gpu_time);
         self.performance.record(
             timestamp,
             timing,
@@ -305,6 +329,9 @@
         update_page_url(self.current_scene);
         self.update_title();
         self.transform = Affine::IDENTITY;
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.reset();
+        }
         self.performance.reset();
     }
 
@@ -317,6 +344,9 @@
         update_page_url(self.current_scene);
         self.update_title();
         self.transform = Affine::IDENTITY;
+        if let Some(gpu_timer) = &mut self.renderer_wrapper.gpu_timer {
+            gpu_timer.reset();
+        }
         self.performance.reset();
     }
 
@@ -678,6 +708,7 @@
         queue,
         surface,
         depth_texture_view,
+        ..
     } = RendererWrapper::new(canvas).await;
 
     let render_size = vello_hybrid::RenderSize {