Skip to main content

euv_chart/backend/webgpu/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`WebGpuChartBackend`].
4impl WebGpuChartBackend {
5    /// Returns `true` when the host browser exposes WebGPU.
6    ///
7    /// The check is synchronous and cheap: it only looks for
8    /// `navigator.gpu`. It says nothing about whether an adapter can
9    /// actually be acquired — headless browsers, GPU-blacklisted devices,
10    /// and sandboxed frames all expose the entry point while failing to
11    /// hand out a device. For the definitive answer, create the backend:
12    /// [`WebGpuChartBackend::create`] asks for an adapter and a device for
13    /// real, and returns `None` when either fails.
14    ///
15    /// # Returns
16    ///
17    /// - `bool` - A boolean.
18    pub fn is_supported() -> bool {
19        let Some(window) = web_sys::window() else {
20            return false;
21        };
22        let navigator: Navigator = window.navigator();
23        match js_sys::Reflect::get(navigator.as_ref(), &JsValue::from_str(WEBGPU_PROPERTY_GPU)) {
24            Ok(value) => !value.is_undefined() && !value.is_null(),
25            Err(_) => false,
26        }
27    }
28
29    /// Creates a backend around the given canvas.
30    ///
31    /// The whole acquisition is asynchronous: `requestAdapter` and
32    /// `requestDevice` return JavaScript promises. Every failure mode —
33    /// no `navigator.gpu`, no adapter, no device, a canvas that already
34    /// has a context of another kind — returns `None`, so the backend
35    /// factory can fall back to WebGL2 and then to Canvas2D.
36    ///
37    /// The GPU buffers are not allocated here. They are created on the
38    /// first render, once the size of the glyph atlas is known, and then
39    /// kept until the display list changes.
40    ///
41    /// # Arguments
42    ///
43    /// - `&HtmlCanvasElement` - The `&HtmlCanvasElement` value.
44    ///
45    /// # Returns
46    ///
47    /// - `Option<Self>` - The `Option<Self>` value.
48    pub async fn create(canvas: &HtmlCanvasElement) -> Option<Self> {
49        let gpu: JsValue = Self::gpu_object()?;
50        let adapter: JsValue = Self::request_adapter(&gpu).await?;
51        let device: JsValue = Self::request_device(&adapter).await?;
52        let queue: JsValue = gpu_queue(&device)?;
53        let context: JsValue = Self::canvas_context(canvas)?;
54        let format: String = Self::preferred_format(&gpu)?;
55        Self::configure_context(&context, &device, &format)?;
56        let api: WebGpuApi = WebGpuApi::new(&device, &queue, &context)?;
57        let uniform_layout: JsValue = Self::create_uniform_layout(&device)?;
58        let texture_layout: JsValue = Self::create_texture_layout(&device)?;
59        let solid_layout: JsValue =
60            Self::create_pipeline_layout(&device, from_ref(&uniform_layout))?;
61        let textured_layout: JsValue = Self::create_pipeline_layout(
62            &device,
63            &[uniform_layout.clone(), texture_layout.clone()],
64        )?;
65        let solid_shader: JsValue = Self::create_shader_module(&device, WEBGPU_SOLID_SHADER)?;
66        let textured_shader: JsValue = Self::create_shader_module(&device, WEBGPU_TEXTURED_SHADER)?;
67        let solid_pipeline: JsValue =
68            Self::create_render_pipeline(&device, &solid_layout, &solid_shader, &format, false)?;
69        let textured_pipeline: JsValue = Self::create_render_pipeline(
70            &device,
71            &textured_layout,
72            &textured_shader,
73            &format,
74            true,
75        )?;
76        let uniform_buffer: JsValue = Self::create_buffer(
77            &device,
78            WEBGPU_UNIFORM_SIZE,
79            WEBGPU_BUFFER_USAGE_UNIFORM | WEBGPU_BUFFER_USAGE_COPY_DST,
80        )?;
81        let uniform_bind_group: JsValue = Self::create_bind_group(
82            &device,
83            &uniform_layout,
84            &[gpu_object(&[
85                (WEBGPU_KEY_BINDING, &JsValue::from_f64(0.0)),
86                (
87                    WEBGPU_KEY_RESOURCE,
88                    &gpu_object(&[(WEBGPU_KEY_BUFFER, &uniform_buffer)]),
89                ),
90            ])],
91        )?;
92        let sampler: JsValue = Self::create_sampler(&device)?;
93        let (pass_descriptor, pass_attachment) = Self::create_pass_descriptor();
94        Some(Self {
95            canvas: canvas.clone(),
96            device,
97            queue,
98            context,
99            format,
100            api,
101            encoder_api: None,
102            pass_api: None,
103            solid_pipeline,
104            textured_pipeline,
105            texture_layout,
106            sampler,
107            uniform_buffer,
108            uniform_bind_group,
109            pass_descriptor,
110            pass_attachment,
111            atlas: WebGpuAtlas {
112                texture: None,
113                bind_group: None,
114                width: 0,
115                height: 0,
116            },
117            solid_buffer: WebGpuBuffer {
118                buffer: None,
119                capacity: 0,
120            },
121            solid_index_buffer: WebGpuBuffer {
122                buffer: None,
123                capacity: 0,
124            },
125            textured_buffer: WebGpuBuffer {
126                buffer: None,
127                capacity: 0,
128            },
129            textured_index_buffer: WebGpuBuffer {
130                buffer: None,
131                capacity: 0,
132            },
133            batches: Vec::new(),
134            images: Vec::new(),
135            next_image_id: 1,
136            atlas_glyphs: 0,
137            fingerprint: None,
138            tessellator: Tessellator::new(),
139            width: 0.0,
140            height: 0.0,
141            dpr: 1.0,
142            solid_vertices: Vec::new(),
143            solid_indices: Vec::new(),
144            textured_vertices: Vec::new(),
145            textured_indices: Vec::new(),
146            clips: Vec::new(),
147        })
148    }
149
150    /// Returns the canvas the backend draws into.
151    ///
152    /// # Returns
153    ///
154    /// - `&HtmlCanvasElement` - A `HtmlCanvasElement` value.
155    pub fn canvas(&self) -> &HtmlCanvasElement {
156        self.get_canvas()
157    }
158
159    /// Returns the device pixel ratio the backend renders at.
160    ///
161    /// # Returns
162    ///
163    /// - `f64` - A 64-bit float.
164    pub fn dpr(&self) -> f64 {
165        self.get_dpr()
166    }
167
168    /// Returns the swap-chain texture format the backend renders into.
169    ///
170    /// # Returns
171    ///
172    /// - `&str` - A `str` value.
173    pub fn format(&self) -> &str {
174        self.get_format()
175    }
176
177    /// Returns the number of draw calls the last frame used.
178    ///
179    /// One batch is one draw call, so this is the number the chart pays
180    /// per frame. A chart that has not changed keeps the same number.
181    ///
182    /// # Returns
183    ///
184    /// - `usize` - A non-negative integer.
185    pub fn batch_count(&self) -> usize {
186        self.get_batches()
187            .iter()
188            .filter(|batch: &&WebGpuBatch| batch.index_count > 0)
189            .count()
190    }
191
192    /// Forces the backend to rebuild its geometry on the next render.
193    ///
194    /// The backend notices a changed display list on its own, by
195    /// fingerprinting it. This is for the cases the fingerprint cannot
196    /// see — a web font that finished loading, for instance, changes the
197    /// metrics of the text without changing the display list.
198    pub fn invalidate(&mut self) {
199        self.set_fingerprint(None);
200    }
201
202    /// Returns the `navigator.gpu` object, if the host has one.
203    ///
204    /// # Returns
205    ///
206    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
207    fn gpu_object() -> Option<JsValue> {
208        let window: Window = web_sys::window()?;
209        let navigator: Navigator = window.navigator();
210        let gpu: JsValue =
211            js_sys::Reflect::get(navigator.as_ref(), &JsValue::from_str(WEBGPU_PROPERTY_GPU))
212                .ok()?;
213        if gpu.is_undefined() || gpu.is_null() {
214            return None;
215        }
216        Some(gpu)
217    }
218
219    /// Requests an adapter from the `GPU` object.
220    ///
221    /// # Arguments
222    ///
223    /// - `&JsValue` - Shared reference to a `JsValue`.
224    ///
225    /// # Returns
226    ///
227    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
228    async fn request_adapter(gpu: &JsValue) -> Option<JsValue> {
229        let method: Function = gpu_method(gpu, WEBGPU_METHOD_REQUEST_ADAPTER)?;
230        let options: JsValue = gpu_object(&[(
231            "powerPreference",
232            &JsValue::from_str(WEBGPU_POWER_PREFERENCE),
233        )]);
234        let promise: js_sys::Promise = method.call1(gpu, &options).ok()?.unchecked_into();
235        let adapter: JsValue = JsFuture::from(promise).await.ok()?;
236        if adapter.is_undefined() || adapter.is_null() {
237            return None;
238        }
239        Some(adapter)
240    }
241
242    /// Requests a device from an adapter.
243    ///
244    /// # Arguments
245    ///
246    /// - `&JsValue` - Shared reference to a `JsValue`.
247    ///
248    /// # Returns
249    ///
250    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
251    async fn request_device(adapter: &JsValue) -> Option<JsValue> {
252        let method: Function = gpu_method(adapter, WEBGPU_METHOD_REQUEST_DEVICE)?;
253        let promise: js_sys::Promise = method.call0(adapter).ok()?.unchecked_into();
254        let device: JsValue = JsFuture::from(promise).await.ok()?;
255        if device.is_undefined() || device.is_null() {
256            return None;
257        }
258        Some(device)
259    }
260
261    /// Asks the canvas for a WebGPU context.
262    ///
263    /// # Arguments
264    ///
265    /// - `&HtmlCanvasElement` - Shared reference to a `HtmlCanvasElement`.
266    ///
267    /// # Returns
268    ///
269    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
270    fn canvas_context(canvas: &HtmlCanvasElement) -> Option<JsValue> {
271        let context: Object = canvas.get_context(WEBGPU_CONTEXT_ID).ok()??;
272        Some(context.into())
273    }
274
275    /// Asks the `GPU` object for the preferred swap-chain format.
276    ///
277    /// # Arguments
278    ///
279    /// - `&JsValue` - Shared reference to a `JsValue`.
280    ///
281    /// # Returns
282    ///
283    /// - `Option<String>` - `Some(...)` on success, `None` otherwise.
284    fn preferred_format(gpu: &JsValue) -> Option<String> {
285        let method: Function = gpu_method(gpu, WEBGPU_METHOD_GET_PREFERRED_CANVAS_FORMAT)?;
286        let format: JsValue = method.call0(gpu).ok()?;
287        format.as_string()
288    }
289
290    /// Configures the canvas context for the given device and format.
291    ///
292    /// # Arguments
293    ///
294    /// - `&JsValue` - Shared reference to a `JsValue`.
295    /// - `&str` - Shared reference to a `str`.
296    ///
297    /// # Returns
298    ///
299    /// - `Option<()>` - `Some(...)` on success, `None` otherwise.
300    fn configure_context(context: &JsValue, device: &JsValue, format: &str) -> Option<()> {
301        let method: Function = gpu_method(context, WEBGPU_METHOD_CONFIGURE)?;
302        let descriptor: JsValue = gpu_object(&[
303            ("device", device),
304            (WEBGPU_KEY_FORMAT, &JsValue::from_str(format)),
305            ("alphaMode", &JsValue::from_str(WEBGPU_ALPHA_MODE)),
306        ]);
307        method.call1(context, &descriptor).ok()?;
308        Some(())
309    }
310
311    /// Creates a shader module.
312    ///
313    /// # Arguments
314    ///
315    /// - `&JsValue` - Shared reference to a `JsValue`.
316    /// - `&str` - Shared reference to a `str`.
317    ///
318    /// # Returns
319    ///
320    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
321    fn create_shader_module(device: &JsValue, code: &str) -> Option<JsValue> {
322        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_SHADER_MODULE)?;
323        let descriptor = gpu_object(&[("code", &JsValue::from_str(code))]);
324        method.call1(device, &descriptor).ok()
325    }
326
327    /// Creates a buffer.
328    ///
329    /// # Arguments
330    ///
331    /// - `&JsValue` - Shared reference to a `JsValue`.
332    /// - `u32` - A 32-bit unsigned integer (`u32`).
333    ///
334    /// # Returns
335    ///
336    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
337    fn create_buffer(device: &JsValue, size: u32, usage: u32) -> Option<JsValue> {
338        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_BUFFER)?;
339        let descriptor: JsValue = gpu_object(&[
340            (WEBGPU_KEY_SIZE, &JsValue::from_f64(f64::from(size))),
341            (WEBGPU_KEY_USAGE, &JsValue::from_f64(f64::from(usage))),
342        ]);
343        method.call1(device, &descriptor).ok()
344    }
345
346    /// Creates a texture.
347    ///
348    /// # Arguments
349    ///
350    /// - `&JsValue` - Shared reference to a `JsValue`.
351    /// - `u32` - A 32-bit unsigned integer (`u32`).
352    /// - `&str` - Shared reference to a `str`.
353    ///
354    /// # Returns
355    ///
356    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
357    fn create_texture(
358        device: &JsValue,
359        width: u32,
360        height: u32,
361        format: &str,
362        usage: u32,
363    ) -> Option<JsValue> {
364        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_TEXTURE)?;
365        let descriptor: JsValue = gpu_object(&[
366            (
367                WEBGPU_KEY_SIZE,
368                &gpu_object(&[
369                    (WEBGPU_KEY_WIDTH, &JsValue::from_f64(f64::from(width))),
370                    (WEBGPU_KEY_HEIGHT, &JsValue::from_f64(f64::from(height))),
371                    (WEBGPU_KEY_DEPTH_OR_ARRAY_LAYERS, &JsValue::from_f64(1.0)),
372                ]),
373            ),
374            (WEBGPU_KEY_FORMAT, &JsValue::from_str(format)),
375            (WEBGPU_KEY_USAGE, &JsValue::from_f64(f64::from(usage))),
376        ]);
377        method.call1(device, &descriptor).ok()
378    }
379
380    /// Creates a view of a texture.
381    ///
382    /// # Arguments
383    ///
384    /// - `&JsValue` - Shared reference to a `JsValue`.
385    ///
386    /// # Returns
387    ///
388    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
389    fn create_view(texture: &JsValue) -> Option<JsValue> {
390        gpu_method(texture, WEBGPU_METHOD_CREATE_VIEW)?
391            .call0(texture)
392            .ok()
393    }
394
395    /// Destroys a GPU resource.
396    ///
397    /// # Arguments
398    ///
399    /// - `&JsValue` - Shared reference to a `JsValue`.
400    fn destroy(resource: &JsValue) {
401        if let Some(method) = gpu_method(resource, WEBGPU_METHOD_DESTROY) {
402            let _: Result<JsValue, JsValue> = method.call0(resource);
403        }
404    }
405
406    /// Creates a linear sampler with clamped addressing.
407    ///
408    /// # Arguments
409    ///
410    /// - `&JsValue` - Shared reference to a `JsValue`.
411    ///
412    /// # Returns
413    ///
414    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
415    fn create_sampler(device: &JsValue) -> Option<JsValue> {
416        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_SAMPLER)?;
417        let descriptor: JsValue = gpu_object(&[
418            ("magFilter", &JsValue::from_str(WEBGPU_FILTER_LINEAR)),
419            ("minFilter", &JsValue::from_str(WEBGPU_FILTER_LINEAR)),
420            (
421                "addressModeU",
422                &JsValue::from_str(WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE),
423            ),
424            (
425                "addressModeV",
426                &JsValue::from_str(WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE),
427            ),
428        ]);
429        method.call1(device, &descriptor).ok()
430    }
431
432    /// Creates the bind group layout that holds the clip-space transform.
433    ///
434    /// # Arguments
435    ///
436    /// - `&JsValue` - Shared reference to a `JsValue`.
437    ///
438    /// # Returns
439    ///
440    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
441    fn create_uniform_layout(device: &JsValue) -> Option<JsValue> {
442        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_BIND_GROUP_LAYOUT)?;
443        let entries: JsValue = gpu_array(&[gpu_object(&[
444            (WEBGPU_KEY_BINDING, &JsValue::from_f64(0.0)),
445            (
446                WEBGPU_KEY_VISIBILITY,
447                &JsValue::from_f64(f64::from(WEBGPU_SHADER_STAGE_VERTEX)),
448            ),
449            (
450                WEBGPU_KEY_BUFFER,
451                &gpu_object(&[(WEBGPU_KEY_TYPE, &JsValue::from_str("uniform"))]),
452            ),
453        ])]);
454        let descriptor: JsValue = gpu_object(&[(WEBGPU_KEY_ENTRIES, &entries)]);
455        method.call1(device, &descriptor).ok()
456    }
457
458    /// Creates the bind group layout that holds the texture and the sampler.
459    ///
460    /// # Arguments
461    ///
462    /// - `&JsValue` - Shared reference to a `JsValue`.
463    ///
464    /// # Returns
465    ///
466    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
467    fn create_texture_layout(device: &JsValue) -> Option<JsValue> {
468        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_BIND_GROUP_LAYOUT)?;
469        let entries: JsValue = gpu_array(&[
470            gpu_object(&[
471                (WEBGPU_KEY_BINDING, &JsValue::from_f64(0.0)),
472                (
473                    WEBGPU_KEY_VISIBILITY,
474                    &JsValue::from_f64(f64::from(WEBGPU_SHADER_STAGE_FRAGMENT)),
475                ),
476                (
477                    "sampler",
478                    &gpu_object(&[(WEBGPU_KEY_TYPE, &JsValue::from_str("filtering"))]),
479                ),
480            ]),
481            gpu_object(&[
482                (WEBGPU_KEY_BINDING, &JsValue::from_f64(1.0)),
483                (
484                    WEBGPU_KEY_VISIBILITY,
485                    &JsValue::from_f64(f64::from(WEBGPU_SHADER_STAGE_FRAGMENT)),
486                ),
487                (
488                    WEBGPU_KEY_TEXTURE,
489                    &gpu_object(&[("sampleType", &JsValue::from_str("float"))]),
490                ),
491            ]),
492        ]);
493        let descriptor: JsValue = gpu_object(&[(WEBGPU_KEY_ENTRIES, &entries)]);
494        method.call1(device, &descriptor).ok()
495    }
496
497    /// Creates a pipeline layout out of bind group layouts.
498    ///
499    /// # Arguments
500    ///
501    /// - `&JsValue` - Shared reference to a `JsValue`.
502    /// - `&[JsValue]` - Shared reference to a `[JsValue]`.
503    ///
504    /// # Returns
505    ///
506    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
507    fn create_pipeline_layout(device: &JsValue, layouts: &[JsValue]) -> Option<JsValue> {
508        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_PIPELINE_LAYOUT)?;
509        let descriptor = gpu_object(&[("bindGroupLayouts", &gpu_array(layouts))]);
510        method.call1(device, &descriptor).ok()
511    }
512
513    /// Creates a bind group.
514    ///
515    /// # Arguments
516    ///
517    /// - `&JsValue` - Shared reference to a `JsValue`.
518    /// - `&JsValue` - Shared reference to a `JsValue`.
519    /// - `&[JsValue]` - Shared reference to a `[JsValue]`.
520    ///
521    /// # Returns
522    ///
523    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
524    fn create_bind_group(
525        device: &JsValue,
526        layout: &JsValue,
527        entries: &[JsValue],
528    ) -> Option<JsValue> {
529        let method: Function = gpu_method(device, WEBGPU_METHOD_CREATE_BIND_GROUP)?;
530        let descriptor: JsValue = gpu_object(&[
531            (WEBGPU_KEY_LAYOUT, layout),
532            (WEBGPU_KEY_ENTRIES, &gpu_array(entries)),
533        ]);
534        method.call1(device, &descriptor).ok()
535    }
536
537    /// Creates a render pipeline.
538    ///
539    /// The two pipelines differ only in their vertex layout: the solid one
540    /// reads a position and a colour, the textured one reads a position,
541    /// a texture coordinate, and a colour.
542    ///
543    /// # Arguments
544    ///
545    /// - `&JsValue` - Shared reference to a `JsValue`.
546    /// - `&JsValue` - Shared reference to a `JsValue`.
547    /// - `&JsValue` - Shared reference to a `JsValue`.
548    /// - `&str` - Shared reference to a `str`.
549    /// - `bool` - A boolean (`bool`).
550    ///
551    /// # Returns
552    ///
553    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
554    fn create_render_pipeline(
555        device: &JsValue,
556        layout: &JsValue,
557        shader: &JsValue,
558        format: &str,
559        textured: bool,
560    ) -> Option<JsValue> {
561        let attributes: JsValue = if textured {
562            gpu_array(&[
563                gpu_object(&[
564                    (WEBGPU_KEY_SHADER_LOCATION, &JsValue::from_f64(0.0)),
565                    (WEBGPU_KEY_OFFSET, &JsValue::from_f64(0.0)),
566                    (
567                        WEBGPU_KEY_FORMAT,
568                        &JsValue::from_str(WEBGPU_VERTEX_FORMAT_FLOAT32X2),
569                    ),
570                ]),
571                gpu_object(&[
572                    (WEBGPU_KEY_SHADER_LOCATION, &JsValue::from_f64(1.0)),
573                    (WEBGPU_KEY_OFFSET, &JsValue::from_f64(8.0)),
574                    (
575                        WEBGPU_KEY_FORMAT,
576                        &JsValue::from_str(WEBGPU_VERTEX_FORMAT_FLOAT32X2),
577                    ),
578                ]),
579                gpu_object(&[
580                    (WEBGPU_KEY_SHADER_LOCATION, &JsValue::from_f64(2.0)),
581                    (WEBGPU_KEY_OFFSET, &JsValue::from_f64(16.0)),
582                    (
583                        WEBGPU_KEY_FORMAT,
584                        &JsValue::from_str(WEBGPU_VERTEX_FORMAT_UNORM8X4),
585                    ),
586                ]),
587            ])
588        } else {
589            gpu_array(&[
590                gpu_object(&[
591                    (WEBGPU_KEY_SHADER_LOCATION, &JsValue::from_f64(0.0)),
592                    (WEBGPU_KEY_OFFSET, &JsValue::from_f64(0.0)),
593                    (
594                        WEBGPU_KEY_FORMAT,
595                        &JsValue::from_str(WEBGPU_VERTEX_FORMAT_FLOAT32X2),
596                    ),
597                ]),
598                gpu_object(&[
599                    (WEBGPU_KEY_SHADER_LOCATION, &JsValue::from_f64(1.0)),
600                    (WEBGPU_KEY_OFFSET, &JsValue::from_f64(8.0)),
601                    (
602                        WEBGPU_KEY_FORMAT,
603                        &JsValue::from_str(WEBGPU_VERTEX_FORMAT_UNORM8X4),
604                    ),
605                ]),
606            ])
607        };
608        let stride: u32 = if textured {
609            WEBGPU_TEXTURED_VERTEX_STRIDE
610        } else {
611            WEBGPU_SOLID_VERTEX_STRIDE
612        };
613        let vertex_buffer: JsValue = gpu_object(&[
614            ("arrayStride", &JsValue::from_f64(f64::from(stride))),
615            ("stepMode", &JsValue::from_str(WEBGPU_STEP_MODE_VERTEX)),
616            ("attributes", &attributes),
617        ]);
618        let target: JsValue = gpu_object(&[
619            (WEBGPU_KEY_FORMAT, &JsValue::from_str(format)),
620            (
621                "blend",
622                &gpu_object(&[
623                    (
624                        "color",
625                        &gpu_object(&[
626                            (
627                                WEBGPU_KEY_SRC_FACTOR,
628                                &JsValue::from_str(WEBGPU_BLEND_FACTOR_ONE),
629                            ),
630                            (
631                                WEBGPU_KEY_DST_FACTOR,
632                                &JsValue::from_str(WEBGPU_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA),
633                            ),
634                            (
635                                WEBGPU_KEY_OPERATION,
636                                &JsValue::from_str(WEBGPU_BLEND_OPERATION_ADD),
637                            ),
638                        ]),
639                    ),
640                    (
641                        "alpha",
642                        &gpu_object(&[
643                            (
644                                WEBGPU_KEY_SRC_FACTOR,
645                                &JsValue::from_str(WEBGPU_BLEND_FACTOR_ONE),
646                            ),
647                            (
648                                WEBGPU_KEY_DST_FACTOR,
649                                &JsValue::from_str(WEBGPU_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA),
650                            ),
651                            (
652                                WEBGPU_KEY_OPERATION,
653                                &JsValue::from_str(WEBGPU_BLEND_OPERATION_ADD),
654                            ),
655                        ]),
656                    ),
657                ]),
658            ),
659        ]);
660        let descriptor: JsValue = gpu_object(&[
661            (WEBGPU_KEY_LAYOUT, layout),
662            (
663                WEBGPU_KEY_VERTEX,
664                &gpu_object(&[
665                    (WEBGPU_KEY_MODULE, shader),
666                    (
667                        WEBGPU_KEY_ENTRY_POINT,
668                        &JsValue::from_str(WEBGPU_VERTEX_ENTRY_POINT),
669                    ),
670                    ("buffers", &gpu_array(&[vertex_buffer])),
671                ]),
672            ),
673            (
674                "fragment",
675                &gpu_object(&[
676                    (WEBGPU_KEY_MODULE, shader),
677                    (
678                        WEBGPU_KEY_ENTRY_POINT,
679                        &JsValue::from_str(WEBGPU_FRAGMENT_ENTRY_POINT),
680                    ),
681                    ("targets", &gpu_array(&[target])),
682                ]),
683            ),
684            (
685                "primitive",
686                &gpu_object(&[
687                    ("topology", &JsValue::from_str("triangle-list")),
688                    ("cullMode", &JsValue::from_str("none")),
689                ]),
690            ),
691            (
692                "multisample",
693                &gpu_object(&[("count", &JsValue::from_f64(1.0))]),
694            ),
695        ]);
696        gpu_method(device, WEBGPU_METHOD_CREATE_RENDER_PIPELINE)?
697            .call1(device, &descriptor)
698            .ok()
699    }
700
701    /// Creates the render pass descriptor and the colour attachment inside
702    /// it.
703    ///
704    /// The descriptor is built once and reused: only the view of the
705    /// current swap-chain texture changes from frame to frame.
706    ///
707    /// # Returns
708    ///
709    /// - `(JsValue, JsValue)` - A `(JsValue, JsValue)` value.
710    fn create_pass_descriptor() -> (JsValue, JsValue) {
711        let attachment: JsValue = gpu_object(&[
712            ("view", &JsValue::UNDEFINED),
713            (
714                "clearValue",
715                &gpu_object(&[
716                    ("r", &JsValue::from_f64(0.0)),
717                    ("g", &JsValue::from_f64(0.0)),
718                    ("b", &JsValue::from_f64(0.0)),
719                    ("a", &JsValue::from_f64(0.0)),
720                ]),
721            ),
722            ("loadOp", &JsValue::from_str("clear")),
723            ("storeOp", &JsValue::from_str("store")),
724        ]);
725        let descriptor: JsValue =
726            gpu_object(&[("colorAttachments", &gpu_array(from_ref(&attachment)))]);
727        (descriptor, attachment)
728    }
729
730    /// Writes the clip-space transform into the uniform buffer.
731    ///
732    /// The chart works in CSS pixels with the origin in the top-left
733    /// corner; clip space wants the origin in the middle and the vertical
734    /// axis pointing up. The transform therefore scales the coordinates
735    /// and flips the vertical axis. The device pixel ratio cancels out:
736    /// the canvas backing store is `dpr` times the CSS size, so the two
737    /// factors meet and only the CSS size matters.
738    fn update_uniform(&mut self) {
739        let width: f64 = if self.get_width() > 0.0 {
740            self.get_width()
741        } else {
742            1.0
743        };
744        let height: f64 = if self.get_height() > 0.0 {
745            self.get_height()
746        } else {
747            1.0
748        };
749        let data: [f32; 4] = [
750            (2.0 / width) as f32,
751            (-2.0 / height) as f32,
752            -1.0_f32,
753            1.0_f32,
754        ];
755        let view: Float32Array = js_sys::Float32Array::from(data.as_slice());
756        let _: Result<JsValue, JsValue> = self.get_api().write_buffer.call3(
757            self.get_queue(),
758            self.get_uniform_buffer(),
759            &JsValue::from_f64(0.0),
760            &view,
761        );
762    }
763
764    /// Creates the glyph atlas texture, or re-creates it when the atlas
765    /// changed size.
766    ///
767    /// # Arguments
768    ///
769    /// - `&GlyphAtlas` - Shared reference to a `GlyphAtlas`.
770    fn sync_atlas(&mut self, atlas: &GlyphAtlas) {
771        let width: u32 = atlas.width();
772        let height: u32 = atlas.height();
773        if self.get_atlas().texture.is_some()
774            && self.get_atlas().width == width
775            && self.get_atlas().height == height
776        {
777            return;
778        }
779        if let Some(texture) = &self.get_atlas().texture {
780            Self::destroy(texture);
781        }
782        let usage: u32 = WEBGPU_TEXTURE_USAGE_COPY_DST
783            | WEBGPU_TEXTURE_USAGE_TEXTURE_BINDING
784            | WEBGPU_TEXTURE_USAGE_RENDER_ATTACHMENT;
785        let Some(texture) = Self::create_texture(
786            self.get_device(),
787            width,
788            height,
789            WEBGPU_TEXTURE_FORMAT,
790            usage,
791        ) else {
792            return;
793        };
794        let Some(view) = Self::create_view(&texture) else {
795            return;
796        };
797        let Some(bind_group) = Self::create_bind_group(
798            self.get_device(),
799            self.get_texture_layout(),
800            &[
801                gpu_object(&[
802                    (WEBGPU_KEY_BINDING, &JsValue::from_f64(0.0)),
803                    (WEBGPU_KEY_RESOURCE, self.get_sampler()),
804                ]),
805                gpu_object(&[
806                    (WEBGPU_KEY_BINDING, &JsValue::from_f64(1.0)),
807                    (WEBGPU_KEY_RESOURCE, &view),
808                ]),
809            ],
810        ) else {
811            return;
812        };
813        self.get_mut_atlas().texture = Some(texture);
814        self.get_mut_atlas().bind_group = Some(bind_group);
815        self.get_mut_atlas().width = width;
816        self.get_mut_atlas().height = height;
817        // The bind group changed, so the batches have to be built again.
818        self.set_fingerprint(None);
819    }
820
821    /// Uploads the glyph atlas into its texture.
822    ///
823    /// # Arguments
824    ///
825    /// - `&GlyphAtlas` - Shared reference to a `GlyphAtlas`.
826    fn upload_atlas(&mut self, atlas: &GlyphAtlas) {
827        let Some(texture) = self.get_atlas().texture.clone() else {
828            return;
829        };
830        let canvas: &HtmlCanvasElement = atlas.canvas();
831        let source: JsValue = gpu_object(&[
832            (WEBGPU_KEY_SOURCE, canvas.as_ref()),
833            (WEBGPU_KEY_FLIP_Y, &JsValue::FALSE),
834        ]);
835        let destination: JsValue = gpu_object(&[
836            (WEBGPU_KEY_TEXTURE, &texture),
837            (WEBGPU_KEY_PREMULTIPLIED_ALPHA, &JsValue::TRUE),
838        ]);
839        let size: JsValue = gpu_object(&[
840            (
841                WEBGPU_KEY_WIDTH,
842                &JsValue::from_f64(f64::from(atlas.width())),
843            ),
844            (
845                WEBGPU_KEY_HEIGHT,
846                &JsValue::from_f64(f64::from(atlas.height())),
847            ),
848            (WEBGPU_KEY_DEPTH_OR_ARRAY_LAYERS, &JsValue::from_f64(1.0)),
849        ]);
850        let _: Result<JsValue, JsValue> = self.get_api().copy_external_image.call3(
851            self.get_queue(),
852            &source,
853            &destination,
854            &size,
855        );
856    }
857
858    /// Computes the fingerprint of a display list.
859    ///
860    /// The fingerprint covers the shape of every item and the geometry
861    /// inside it, so a list that changed in any visible way produces a
862    /// different fingerprint. It is a pure Rust computation: it never
863    /// crosses into JavaScript, not even once per frame.
864    ///
865    /// # Arguments
866    ///
867    /// - `&DisplayList` - Shared reference to a `DisplayList`.
868    ///
869    /// # Returns
870    ///
871    /// - `u64` - A `u64` value.
872    fn list_fingerprint(&mut self, list: &DisplayList) -> u64 {
873        let mut hasher: GpuHasher = GpuHasher::new();
874        list.len().hash(&mut hasher);
875        for item in list.items() {
876            item.kind().hash(&mut hasher);
877            item.z.hash(&mut hasher);
878            item.element_id.hash(&mut hasher);
879            item.series_index.hash(&mut hasher);
880            item.data_index.hash(&mut hasher);
881            match &item.cmd {
882                DrawCmd::Fill {
883                    path,
884                    paint,
885                    opacity,
886                } => {
887                    hash_path(&mut hasher, path);
888                    hash_fill(&mut hasher, paint);
889                    hasher.write_f64(*opacity);
890                }
891                DrawCmd::Stroke {
892                    path,
893                    paint,
894                    style,
895                    opacity,
896                } => {
897                    hash_path(&mut hasher, path);
898                    hash_stroke(&mut hasher, paint);
899                    hash_line_style(&mut hasher, style);
900                    hasher.write_f64(*opacity);
901                }
902                DrawCmd::Strip {
903                    points,
904                    baseline,
905                    paint,
906                    opacity,
907                } => {
908                    hasher.write_u32(points.len() as u32);
909                    for point in points {
910                        hasher.write_f32(point.x);
911                        hasher.write_f32(point.y);
912                    }
913                    hasher.write_f32(*baseline);
914                    hash_fill(&mut hasher, paint);
915                    hasher.write_f64(*opacity);
916                }
917                DrawCmd::Text { text, x, y, style } => {
918                    text.hash(&mut hasher);
919                    hasher.write_f64(*x);
920                    hasher.write_f64(*y);
921                    hash_text_style(&mut hasher, style);
922                }
923                DrawCmd::Image {
924                    image,
925                    x,
926                    y,
927                    width,
928                    height,
929                } => {
930                    let id: u32 = self.image_id(image.as_ref());
931                    id.hash(&mut hasher);
932                    hasher.write_f64(*x);
933                    hasher.write_f64(*y);
934                    hasher.write_f64(*width);
935                    hasher.write_f64(*height);
936                }
937                DrawCmd::Clip { path } => hash_path(&mut hasher, path),
938                DrawCmd::PopClip => {}
939            }
940        }
941        hasher.finish()
942    }
943
944    /// Rebuilds the geometry of the display list.
945    ///
946    /// The rebuild is the expensive part of a frame: it tessellates every
947    /// path and shapes every text run. It only happens when the display
948    /// list actually changed, which is what keeps a steady-state frame
949    /// cheap.
950    ///
951    /// # Arguments
952    ///
953    /// - `&DisplayList` - Shared reference to a `DisplayList`.
954    /// - `&mut TextShaper` - Mutable reference to a `TextShaper` (mutated in place).
955    fn rebuild(&mut self, list: &DisplayList, shaper: &mut TextShaper) {
956        self.get_mut_solid_vertices().clear();
957        self.get_mut_solid_indices().clear();
958        self.get_mut_textured_vertices().clear();
959        self.get_mut_textured_indices().clear();
960        self.get_mut_batches().clear();
961        self.get_mut_clips().clear();
962        for item in list.items() {
963            match &item.cmd {
964                DrawCmd::Fill {
965                    path,
966                    paint,
967                    opacity,
968                } => self.push_fill(path, paint, *opacity),
969                DrawCmd::Stroke {
970                    path,
971                    paint,
972                    style,
973                    opacity,
974                } => self.push_stroke(path, paint, style, *opacity),
975                DrawCmd::Strip {
976                    points,
977                    baseline,
978                    paint,
979                    opacity,
980                } => self.push_strip(points, *baseline, paint, *opacity),
981                DrawCmd::Text { text, x, y, style } => {
982                    self.push_text(text, *x, *y, style, shaper);
983                }
984                DrawCmd::Image {
985                    image,
986                    x,
987                    y,
988                    width,
989                    height,
990                } => self.push_image(image, *x, *y, *width, *height),
991                DrawCmd::Clip { path } => self.push_clip(path),
992                DrawCmd::PopClip => self.pop_clip(),
993            }
994        }
995        self.upload_buffers();
996    }
997
998    /// Adds a filled path to the solid batch.
999    ///
1000    /// # Arguments
1001    ///
1002    /// - `&Path` - Shared reference to a `Path`.
1003    /// - `&Fill` - Shared reference to a `Fill`.
1004    /// - `f64` - A 64-bit float (`f64`).
1005    fn push_fill(&mut self, path: &Path, paint: &Fill, opacity: f64) {
1006        if matches!(paint, Fill::None) {
1007            return;
1008        }
1009        let mesh: SolidMesh = self.get_mut_tessellator().fill(path, 0xffff_ffff);
1010        if mesh.indices.is_empty() {
1011            return;
1012        }
1013        let base: u32 =
1014            (self.get_solid_vertices().len() / WEBGPU_SOLID_VERTEX_STRIDE as usize) as u32;
1015        let first_index: u32 = self.get_solid_indices().len() as u32;
1016        self.open_batch(WebGpuBatchKind::Solid, first_index, None);
1017        for vertex in &mesh.vertices {
1018            let color: Color = self.resolve_fill_color(paint, vertex.x, vertex.y);
1019            let alpha: u8 = scaled_alpha(color, opacity);
1020            push_solid_vertex(
1021                self.get_mut_solid_vertices(),
1022                vertex.x,
1023                vertex.y,
1024                color,
1025                alpha,
1026            );
1027        }
1028        for index in &mesh.indices {
1029            self.get_mut_solid_indices().push(base + index);
1030        }
1031        self.close_batch(mesh.indices.len() as u32);
1032    }
1033
1034    /// Adds a stroked path to the solid batch.
1035    ///
1036    /// # Arguments
1037    ///
1038    /// - `&Path` - Shared reference to a `Path`.
1039    /// - `&Stroke` - Shared reference to a `Stroke`.
1040    /// - `&LineStyle` - Shared reference to a `LineStyle`.
1041    /// - `f64` - A 64-bit float (`f64`).
1042    fn push_stroke(&mut self, path: &Path, paint: &Stroke, style: &LineStyle, opacity: f64) {
1043        if matches!(paint, Stroke::None) || style.width <= 0.0 {
1044            return;
1045        }
1046        let stroke_style: StrokeStyle = StrokeStyle {
1047            width: style.width as f32,
1048            cap: style.cap,
1049            join: style.join,
1050            dash: style.dash.clone(),
1051        };
1052        let mesh: SolidMesh = self
1053            .get_mut_tessellator()
1054            .stroke(path, &stroke_style, 0xffff_ffff);
1055        if mesh.indices.is_empty() {
1056            return;
1057        }
1058        let base: u32 =
1059            (self.get_solid_vertices().len() / WEBGPU_SOLID_VERTEX_STRIDE as usize) as u32;
1060        let first_index: u32 = self.get_solid_indices().len() as u32;
1061        self.open_batch(WebGpuBatchKind::Solid, first_index, None);
1062        for vertex in &mesh.vertices {
1063            let color: Color = self.resolve_stroke_color(paint, vertex.x, vertex.y);
1064            let alpha: u8 = scaled_alpha(color, opacity * style.opacity);
1065            push_solid_vertex(
1066                self.get_mut_solid_vertices(),
1067                vertex.x,
1068                vertex.y,
1069                color,
1070                alpha,
1071            );
1072        }
1073        for index in &mesh.indices {
1074            self.get_mut_solid_indices().push(base + index);
1075        }
1076        self.close_batch(mesh.indices.len() as u32);
1077    }
1078
1079    /// Adds an area strip to the solid batch.
1080    ///
1081    /// The strip is already a run of triangles, so it is not triangulated
1082    /// again: it goes straight into the vertex buffer.
1083    ///
1084    /// # Arguments
1085    ///
1086    /// - `&[Point]` - Shared reference to a `[Point]`.
1087    /// - `f32` - A 32-bit float (`f32`).
1088    /// - `&Fill` - Shared reference to a `Fill`.
1089    /// - `f64` - A 64-bit float (`f64`).
1090    fn push_strip(&mut self, points: &[Point], baseline: f32, paint: &Fill, opacity: f64) {
1091        if matches!(paint, Fill::None) || points.len() < 2 {
1092            return;
1093        }
1094        let mesh: SolidMesh = self
1095            .get_mut_tessellator()
1096            .fill_strip(points, baseline, 0xffff_ffff);
1097        if mesh.indices.is_empty() {
1098            return;
1099        }
1100        let base: u32 =
1101            (self.get_solid_vertices().len() / WEBGPU_SOLID_VERTEX_STRIDE as usize) as u32;
1102        let first_index: u32 = self.get_solid_indices().len() as u32;
1103        self.open_batch(WebGpuBatchKind::Solid, first_index, None);
1104        for vertex in &mesh.vertices {
1105            let color: Color = self.resolve_fill_color(paint, vertex.x, vertex.y);
1106            let alpha: u8 = scaled_alpha(color, opacity);
1107            push_solid_vertex(
1108                self.get_mut_solid_vertices(),
1109                vertex.x,
1110                vertex.y,
1111                color,
1112                alpha,
1113            );
1114        }
1115        for index in &mesh.indices {
1116            self.get_mut_solid_indices().push(base + index);
1117        }
1118        self.close_batch(mesh.indices.len() as u32);
1119    }
1120
1121    /// Returns the colour of a fill at the given position.
1122    ///
1123    /// # Arguments
1124    ///
1125    /// - `&Fill` - Shared reference to a `Fill`.
1126    /// - `f32` - A 32-bit float (`f32`).
1127    ///
1128    /// # Returns
1129    ///
1130    /// - `Color` - A `Color` value.
1131    fn resolve_fill_color(&self, paint: &Fill, x: f32, y: f32) -> Color {
1132        match paint {
1133            Fill::Solid(color) => *color,
1134            Fill::Linear(gradient) => linear_gradient_color(gradient, f64::from(x), f64::from(y)),
1135            Fill::Radial(gradient) => radial_gradient_color(gradient, f64::from(x), f64::from(y)),
1136            Fill::None => Color::default(),
1137        }
1138    }
1139
1140    /// Returns the colour of a stroke at the given position.
1141    ///
1142    /// # Arguments
1143    ///
1144    /// - `&Stroke` - Shared reference to a `Stroke`.
1145    /// - `f32` - A 32-bit float (`f32`).
1146    ///
1147    /// # Returns
1148    ///
1149    /// - `Color` - A `Color` value.
1150    fn resolve_stroke_color(&self, paint: &Stroke, x: f32, y: f32) -> Color {
1151        match paint {
1152            Stroke::Solid(color) => *color,
1153            Stroke::Linear(gradient) => linear_gradient_color(gradient, f64::from(x), f64::from(y)),
1154            Stroke::Radial(gradient) => radial_gradient_color(gradient, f64::from(x), f64::from(y)),
1155            Stroke::None => Color::default(),
1156        }
1157    }
1158
1159    /// Adds a text run to the batches.
1160    ///
1161    /// The label background goes into the solid batch and the glyphs into
1162    /// the textured one, in that order, so the background ends up behind
1163    /// the text exactly as it does in Canvas2D.
1164    ///
1165    /// # Arguments
1166    ///
1167    /// - `&str` - Shared reference to a `str`.
1168    /// - `f64` - A 64-bit float (`f64`).
1169    /// - `&TextStyle` - Shared reference to a `TextStyle`.
1170    /// - `&mut TextShaper` - Mutable reference to a `TextShaper` (mutated in place).
1171    fn push_text(
1172        &mut self,
1173        text: &str,
1174        x: f64,
1175        y: f64,
1176        style: &TextStyle,
1177        shaper: &mut TextShaper,
1178    ) {
1179        let run: TextRun = shaper.shape(text, style, x, y);
1180        if let Some(background) = style.background {
1181            for line in &run.lines {
1182                let left: f64 = line.left - style.padding.3;
1183                let top: f64 = line.baseline - run.ascent - style.padding.0;
1184                let width: f64 = line.width + style.padding.1 + style.padding.3;
1185                let height: f64 = run.ascent + run.descent + style.padding.0 + style.padding.2;
1186                self.push_solid_quad(
1187                    left as f32,
1188                    top as f32,
1189                    width as f32,
1190                    height as f32,
1191                    background,
1192                    1.0,
1193                );
1194            }
1195        }
1196        if run.quads.is_empty() {
1197            return;
1198        }
1199        let bind_group: Option<JsValue> = self.get_atlas().bind_group.clone();
1200        let base: u32 =
1201            (self.get_textured_vertices().len() / WEBGPU_TEXTURED_VERTEX_STRIDE as usize) as u32;
1202        let first_index: u32 = self.get_textured_indices().len() as u32;
1203        self.open_batch(WebGpuBatchKind::Textured, first_index, bind_group);
1204        let color: Color = style.color;
1205        let alpha: u8 = style.color.alpha;
1206        for quad in &run.quads {
1207            push_textured_vertex(
1208                self.get_mut_textured_vertices(),
1209                quad.x,
1210                quad.y,
1211                quad.u,
1212                quad.v,
1213                color,
1214                alpha,
1215            );
1216            push_textured_vertex(
1217                self.get_mut_textured_vertices(),
1218                quad.x + quad.width,
1219                quad.y,
1220                quad.u + quad.u_size,
1221                quad.v,
1222                color,
1223                alpha,
1224            );
1225            push_textured_vertex(
1226                self.get_mut_textured_vertices(),
1227                quad.x + quad.width,
1228                quad.y + quad.height,
1229                quad.u + quad.u_size,
1230                quad.v + quad.v_size,
1231                color,
1232                alpha,
1233            );
1234            push_textured_vertex(
1235                self.get_mut_textured_vertices(),
1236                quad.x,
1237                quad.y + quad.height,
1238                quad.u,
1239                quad.v + quad.v_size,
1240                color,
1241                alpha,
1242            );
1243        }
1244        for quad in 0..run.quads.len() as u32 {
1245            let vertex: u32 = base + quad * 4;
1246            self.get_mut_textured_indices().extend_from_slice(&[
1247                vertex,
1248                vertex + 1,
1249                vertex + 2,
1250                vertex,
1251                vertex + 2,
1252                vertex + 3,
1253            ]);
1254        }
1255        self.close_batch((run.quads.len() * 6) as u32);
1256    }
1257
1258    /// Adds an image to the textured batch.
1259    ///
1260    /// # Arguments
1261    ///
1262    /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1263    /// - `f64` - A 64-bit float (`f64`).
1264    fn push_image(&mut self, image: &HtmlImageElement, x: f64, y: f64, width: f64, height: f64) {
1265        if !image.complete() {
1266            return;
1267        }
1268        let natural_width: u32 = image.natural_width();
1269        let natural_height: u32 = image.natural_height();
1270        if natural_width == 0 || natural_height == 0 {
1271            return;
1272        }
1273        let source: String = image.src();
1274        let id: u32 = self.image_id(image.as_ref());
1275        let Some(bind_group) =
1276            self.prepare_image(id, image, &source, natural_width, natural_height)
1277        else {
1278            return;
1279        };
1280        let base: u32 =
1281            (self.get_textured_vertices().len() / WEBGPU_TEXTURED_VERTEX_STRIDE as usize) as u32;
1282        let first_index: u32 = self.get_textured_indices().len() as u32;
1283        self.open_batch(WebGpuBatchKind::Textured, first_index, Some(bind_group));
1284        let color: Color = Color::rgb(255, 255, 255);
1285        push_textured_vertex(
1286            self.get_mut_textured_vertices(),
1287            x as f32,
1288            y as f32,
1289            0.0,
1290            0.0,
1291            color,
1292            255,
1293        );
1294        push_textured_vertex(
1295            self.get_mut_textured_vertices(),
1296            (x + width) as f32,
1297            y as f32,
1298            1.0,
1299            0.0,
1300            color,
1301            255,
1302        );
1303        push_textured_vertex(
1304            self.get_mut_textured_vertices(),
1305            (x + width) as f32,
1306            (y + height) as f32,
1307            1.0,
1308            1.0,
1309            color,
1310            255,
1311        );
1312        push_textured_vertex(
1313            self.get_mut_textured_vertices(),
1314            x as f32,
1315            (y + height) as f32,
1316            0.0,
1317            1.0,
1318            color,
1319            255,
1320        );
1321        self.get_mut_textured_indices().extend_from_slice(&[
1322            base,
1323            base + 1,
1324            base + 2,
1325            base,
1326            base + 2,
1327            base + 3,
1328        ]);
1329        self.close_batch(6);
1330    }
1331
1332    /// Adds a flat-coloured rectangle to the solid batch.
1333    ///
1334    /// # Arguments
1335    ///
1336    /// - `f32` - A 32-bit float (`f32`).
1337    /// - `Color` - A `Color` parameter.
1338    /// - `f64` - A 64-bit float (`f64`).
1339    fn push_solid_quad(
1340        &mut self,
1341        x: f32,
1342        y: f32,
1343        width: f32,
1344        height: f32,
1345        color: Color,
1346        opacity: f64,
1347    ) {
1348        let base: u32 =
1349            (self.get_solid_vertices().len() / WEBGPU_SOLID_VERTEX_STRIDE as usize) as u32;
1350        let first_index: u32 = self.get_solid_indices().len() as u32;
1351        self.open_batch(WebGpuBatchKind::Solid, first_index, None);
1352        let alpha: u8 = scaled_alpha(color, opacity);
1353        push_solid_vertex(self.get_mut_solid_vertices(), x, y, color, alpha);
1354        push_solid_vertex(self.get_mut_solid_vertices(), x + width, y, color, alpha);
1355        push_solid_vertex(
1356            self.get_mut_solid_vertices(),
1357            x + width,
1358            y + height,
1359            color,
1360            alpha,
1361        );
1362        push_solid_vertex(self.get_mut_solid_vertices(), x, y + height, color, alpha);
1363        self.get_mut_solid_indices().extend_from_slice(&[
1364            base,
1365            base + 1,
1366            base + 2,
1367            base,
1368            base + 2,
1369            base + 3,
1370        ]);
1371        self.close_batch(6);
1372    }
1373
1374    /// Pushes a clip rectangle.
1375    ///
1376    /// The clip is approximated by the bounding box of the path, which is
1377    /// exact for the rectangular clips a chart uses to keep a series
1378    /// inside the grid.
1379    ///
1380    /// # Arguments
1381    ///
1382    /// - `&Path` - Shared reference to a `Path`.
1383    fn push_clip(&mut self, path: &Path) {
1384        let bounds: BBox = path.bounds();
1385        let scissor: WebGpuScissor = WebGpuScissor {
1386            x: (bounds.min_x * self.get_dpr()).floor().max(0.0) as u32,
1387            y: (bounds.min_y * self.get_dpr()).floor().max(0.0) as u32,
1388            width: ((bounds.max_x - bounds.min_x) * self.get_dpr())
1389                .ceil()
1390                .max(0.0) as u32,
1391            height: ((bounds.max_y - bounds.min_y) * self.get_dpr())
1392                .ceil()
1393                .max(0.0) as u32,
1394        };
1395        self.get_mut_clips().push(scissor);
1396    }
1397
1398    /// Pops a clip rectangle.
1399    fn pop_clip(&mut self) {
1400        self.get_mut_clips().pop();
1401    }
1402
1403    /// Returns the scissor rectangle in effect, if any.
1404    ///
1405    /// Nested clips intersect, exactly as they do in Canvas2D.
1406    ///
1407    /// # Returns
1408    ///
1409    /// - `Option<WebGpuScissor>` - `Some(...)` on success, `None` otherwise.
1410    fn current_scissor(&self) -> Option<WebGpuScissor> {
1411        let mut result: Option<WebGpuScissor> = None;
1412        for clip in self.get_clips() {
1413            result = Some(match result {
1414                None => *clip,
1415                Some(current) => {
1416                    let left: u32 = current.x.max(clip.x);
1417                    let top: u32 = current.y.max(clip.y);
1418                    let right: u32 = (current.x + current.width).min(clip.x + clip.width);
1419                    let bottom: u32 = (current.y + current.height).min(clip.y + clip.height);
1420                    WebGpuScissor {
1421                        x: left,
1422                        y: top,
1423                        width: right.saturating_sub(left),
1424                        height: bottom.saturating_sub(top),
1425                    }
1426                }
1427            });
1428        }
1429        result
1430    }
1431
1432    /// Opens a batch, or extends the previous one when it is compatible.
1433    ///
1434    /// Consecutive shapes of the same kind that share a bind group end up
1435    /// in one batch, and therefore in one draw call. The paint order is
1436    /// preserved either way, because the batches are drawn in the order
1437    /// they were opened.
1438    ///
1439    /// # Arguments
1440    ///
1441    /// - `WebGpuBatchKind` - A `WebGpuBatchKind` parameter.
1442    /// - `u32` - A 32-bit unsigned integer (`u32`).
1443    /// - `Option<JsValue>` - A `Option<JsValue>` parameter.
1444    fn open_batch(&mut self, kind: WebGpuBatchKind, first_index: u32, bind_group: Option<JsValue>) {
1445        let compatible: bool = match self.get_batches().last() {
1446            Some(last) => last.kind == kind && last.bind_group == bind_group,
1447            None => false,
1448        };
1449        if compatible {
1450            return;
1451        }
1452        let scissor: Option<WebGpuScissor> = self.current_scissor();
1453        self.get_mut_batches().push(WebGpuBatch {
1454            kind,
1455            first_index,
1456            index_count: 0,
1457            bind_group,
1458            scissor,
1459        });
1460    }
1461
1462    /// Closes the batch that is currently open.
1463    ///
1464    /// # Arguments
1465    ///
1466    /// - `u32` - A 32-bit unsigned integer (`u32`).
1467    fn close_batch(&mut self, index_count: u32) {
1468        if let Some(last) = self.get_mut_batches().last_mut() {
1469            last.index_count += index_count;
1470        }
1471    }
1472
1473    /// Returns the identifier of an image element, assigning one on first
1474    /// sight.
1475    ///
1476    /// # Arguments
1477    ///
1478    /// - `&JsValue` - Shared reference to a `JsValue`.
1479    ///
1480    /// # Returns
1481    ///
1482    /// - `u32` - A 32-bit unsigned integer.
1483    fn image_id(&mut self, element: &JsValue) -> u32 {
1484        for image in self.get_images() {
1485            if image.element == *element {
1486                return image.id;
1487            }
1488        }
1489        let id: u32 = self.get_next_image_id();
1490        self.set_next_image_id(self.get_next_image_id() + 1);
1491        self.get_mut_images().push(WebGpuImage {
1492            element: element.clone(),
1493            id,
1494            texture: None,
1495            bind_group: None,
1496            width: 0,
1497            height: 0,
1498            source: String::new(),
1499        });
1500        id
1501    }
1502
1503    /// Makes sure the image has a texture, and returns its bind group.
1504    ///
1505    /// # Arguments
1506    ///
1507    /// - `u32` - A 32-bit unsigned integer (`u32`).
1508    /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1509    /// - `&str` - Shared reference to a `str`.
1510    ///
1511    /// # Returns
1512    ///
1513    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
1514    fn prepare_image(
1515        &mut self,
1516        id: u32,
1517        image: &HtmlImageElement,
1518        source: &str,
1519        width: u32,
1520        height: u32,
1521    ) -> Option<JsValue> {
1522        let position: usize = self
1523            .get_images()
1524            .iter()
1525            .position(|entry: &WebGpuImage| entry.id == id)?;
1526        let stale: bool = {
1527            let entry: &_ = &self.get_images()[position];
1528            entry.width != width || entry.height != height || entry.source != source
1529        };
1530        if stale {
1531            let usage: u32 = WEBGPU_TEXTURE_USAGE_COPY_DST
1532                | WEBGPU_TEXTURE_USAGE_TEXTURE_BINDING
1533                | WEBGPU_TEXTURE_USAGE_RENDER_ATTACHMENT;
1534            let texture: JsValue = Self::create_texture(
1535                self.get_device(),
1536                width,
1537                height,
1538                WEBGPU_TEXTURE_FORMAT,
1539                usage,
1540            )?;
1541            let view: JsValue = Self::create_view(&texture)?;
1542            let bind_group: JsValue = Self::create_bind_group(
1543                self.get_device(),
1544                self.get_texture_layout(),
1545                &[
1546                    gpu_object(&[
1547                        (WEBGPU_KEY_BINDING, &JsValue::from_f64(0.0)),
1548                        (WEBGPU_KEY_RESOURCE, self.get_sampler()),
1549                    ]),
1550                    gpu_object(&[
1551                        (WEBGPU_KEY_BINDING, &JsValue::from_f64(1.0)),
1552                        (WEBGPU_KEY_RESOURCE, &view),
1553                    ]),
1554                ],
1555            )?;
1556            let images: &mut Vec<WebGpuImage> = self.get_mut_images();
1557            let entry: &mut WebGpuImage = &mut images[position];
1558            entry.texture = Some(texture);
1559            entry.bind_group = Some(bind_group);
1560            entry.width = width;
1561            entry.height = height;
1562            entry.source = source.to_string();
1563        }
1564        let texture: JsValue = self.get_images()[position].texture.clone()?;
1565        let copy_source: JsValue = gpu_object(&[
1566            (WEBGPU_KEY_SOURCE, image.as_ref()),
1567            (WEBGPU_KEY_FLIP_Y, &JsValue::FALSE),
1568        ]);
1569        let copy_destination: JsValue = gpu_object(&[
1570            (WEBGPU_KEY_TEXTURE, &texture),
1571            (WEBGPU_KEY_PREMULTIPLIED_ALPHA, &JsValue::TRUE),
1572        ]);
1573        let size: JsValue = gpu_object(&[
1574            (WEBGPU_KEY_WIDTH, &JsValue::from_f64(f64::from(width))),
1575            (WEBGPU_KEY_HEIGHT, &JsValue::from_f64(f64::from(height))),
1576            (WEBGPU_KEY_DEPTH_OR_ARRAY_LAYERS, &JsValue::from_f64(1.0)),
1577        ]);
1578        let _: Result<JsValue, JsValue> = self.get_api().copy_external_image.call3(
1579            self.get_queue(),
1580            &copy_source,
1581            &copy_destination,
1582            &size,
1583        );
1584        self.get_images()[position].bind_group.clone()
1585    }
1586
1587    /// Uploads the vertex and index data to the GPU.
1588    ///
1589    /// The data is uploaded in one call per buffer, never one call per
1590    /// vertex, and only when the display list changed.
1591    fn upload_buffers(&mut self) {
1592        let device: JsValue = self.get_device().clone();
1593        let queue: JsValue = self.get_queue().clone();
1594        let write_buffer: Function = self.get_api().write_buffer.clone();
1595        let Self {
1596            solid_buffer,
1597            solid_index_buffer,
1598            solid_vertices,
1599            solid_indices,
1600            textured_buffer,
1601            textured_index_buffer,
1602            textured_vertices,
1603            textured_indices,
1604            ..
1605        } = &mut *self;
1606        WebGpuBuffer::upload_bytes(
1607            &device,
1608            &queue,
1609            &write_buffer,
1610            solid_buffer,
1611            solid_vertices,
1612            WEBGPU_BUFFER_USAGE_VERTEX | WEBGPU_BUFFER_USAGE_COPY_DST,
1613        );
1614        WebGpuBuffer::upload_indices(
1615            &device,
1616            &queue,
1617            &write_buffer,
1618            solid_index_buffer,
1619            solid_indices,
1620            WEBGPU_BUFFER_USAGE_INDEX | WEBGPU_BUFFER_USAGE_COPY_DST,
1621        );
1622        WebGpuBuffer::upload_bytes(
1623            &device,
1624            &queue,
1625            &write_buffer,
1626            textured_buffer,
1627            textured_vertices,
1628            WEBGPU_BUFFER_USAGE_VERTEX | WEBGPU_BUFFER_USAGE_COPY_DST,
1629        );
1630        WebGpuBuffer::upload_indices(
1631            &device,
1632            &queue,
1633            &write_buffer,
1634            textured_index_buffer,
1635            textured_indices,
1636            WEBGPU_BUFFER_USAGE_INDEX | WEBGPU_BUFFER_USAGE_COPY_DST,
1637        );
1638    }
1639
1640    /// Encodes one frame and submits it to the queue.
1641    fn encode(&mut self) {
1642        let Some(texture) = self
1643            .get_api()
1644            .get_current_texture
1645            .call0(self.get_context())
1646            .ok()
1647        else {
1648            return;
1649        };
1650        let Some(view) = Self::create_view(&texture) else {
1651            return;
1652        };
1653        gpu_set(self.get_pass_attachment(), WEBGPU_PROPERTY_VIEW, &view);
1654        let Some(encoder) = self
1655            .get_api()
1656            .create_command_encoder
1657            .call0(self.get_device())
1658            .ok()
1659        else {
1660            return;
1661        };
1662        if self.try_get_encoder_api().is_none() {
1663            self.set_encoder_api(WebGpuEncoderApi::new(&encoder));
1664        }
1665        let Some(pass) =
1666            self.try_get_encoder_api()
1667                .as_ref()
1668                .and_then(|encoder_api: &WebGpuEncoderApi| {
1669                    encoder_api
1670                        .begin_render_pass
1671                        .call1(&encoder, self.get_pass_descriptor())
1672                        .ok()
1673                })
1674        else {
1675            return;
1676        };
1677        if self.try_get_pass_api().is_none() {
1678            self.set_pass_api(WebGpuPassApi::new(&pass));
1679        }
1680        let Some(pass_api) = self.try_get_pass_api().as_ref() else {
1681            return;
1682        };
1683        let _: Result<JsValue, JsValue> = pass_api.set_bind_group.call2(
1684            &pass,
1685            &JsValue::from_f64(0.0),
1686            self.get_uniform_bind_group(),
1687        );
1688        self.draw_batches(&pass, pass_api);
1689        let _: Result<JsValue, JsValue> = pass_api.end_pass.call0(&pass);
1690        let Some(command) =
1691            self.try_get_encoder_api()
1692                .as_ref()
1693                .and_then(|encoder_api: &WebGpuEncoderApi| {
1694                    encoder_api.finish_encoder.call0(&encoder).ok()
1695                })
1696        else {
1697            return;
1698        };
1699        let commands: JsValue = gpu_array(&[command]);
1700        let _: Result<JsValue, JsValue> = self.get_api().submit.call1(self.get_queue(), &commands);
1701    }
1702
1703    /// Draws the batches.
1704    ///
1705    /// The pipeline, the vertex buffer, the index buffer, and the bind
1706    /// group are only rebound when they actually change, so a frame with
1707    /// a handful of batches costs a handful of JavaScript calls.
1708    ///
1709    /// # Arguments
1710    ///
1711    /// - `&JsValue` - Shared reference to a `JsValue`.
1712    /// - `&WebGpuPassApi` - Shared reference to a `WebGpuPassApi`.
1713    fn draw_batches(&self, pass: &JsValue, pass_api: &WebGpuPassApi) {
1714        let mut current_kind: Option<WebGpuBatchKind> = None;
1715        let mut current_bind_group: Option<JsValue> = None;
1716        let mut current_scissor: Option<WebGpuScissor> = None;
1717        for batch in self.get_batches() {
1718            if batch.index_count == 0 {
1719                continue;
1720            }
1721            if current_kind != Some(batch.kind) {
1722                let (pipeline, vertex_buffer, index_buffer) = match batch.kind {
1723                    WebGpuBatchKind::Solid => (
1724                        self.get_solid_pipeline(),
1725                        self.get_solid_buffer(),
1726                        self.get_solid_index_buffer(),
1727                    ),
1728                    WebGpuBatchKind::Textured => (
1729                        self.get_textured_pipeline(),
1730                        self.get_textured_buffer(),
1731                        self.get_textured_index_buffer(),
1732                    ),
1733                };
1734                let _: Result<JsValue, JsValue> = pass_api.set_pipeline.call1(pass, pipeline);
1735                if let Some(buffer) = &vertex_buffer.buffer {
1736                    let _: Result<JsValue, JsValue> =
1737                        pass_api
1738                            .set_vertex_buffer
1739                            .call2(pass, &JsValue::from_f64(0.0), buffer);
1740                }
1741                if let Some(buffer) = &index_buffer.buffer {
1742                    let _: Result<JsValue, JsValue> = pass_api.set_index_buffer.call3(
1743                        pass,
1744                        buffer,
1745                        &JsValue::from_str("uint32"),
1746                        &JsValue::from_f64(0.0),
1747                    );
1748                }
1749                current_kind = Some(batch.kind);
1750                current_bind_group = None;
1751            }
1752            if batch.kind == WebGpuBatchKind::Textured {
1753                let group: Option<JsValue> = batch
1754                    .bind_group
1755                    .clone()
1756                    .or_else(|| self.get_atlas().bind_group.clone());
1757                if let Some(group) = group
1758                    && current_bind_group.as_ref() != Some(&group)
1759                {
1760                    let _: Result<JsValue, JsValue> =
1761                        pass_api
1762                            .set_bind_group
1763                            .call2(pass, &JsValue::from_f64(1.0), &group);
1764                    current_bind_group = Some(group);
1765                }
1766            }
1767            if batch.scissor != current_scissor {
1768                match &batch.scissor {
1769                    Some(scissor) => {
1770                        let _: Result<JsValue, JsValue> = pass_api.set_scissor_rect.call4(
1771                            pass,
1772                            &JsValue::from_f64(f64::from(scissor.x)),
1773                            &JsValue::from_f64(f64::from(scissor.y)),
1774                            &JsValue::from_f64(f64::from(scissor.width)),
1775                            &JsValue::from_f64(f64::from(scissor.height)),
1776                        );
1777                    }
1778                    None => {
1779                        let _: Result<JsValue, JsValue> = pass_api.set_scissor_rect.call4(
1780                            pass,
1781                            &JsValue::from_f64(0.0),
1782                            &JsValue::from_f64(0.0),
1783                            &JsValue::from_f64(f64::from(self.get_canvas().width())),
1784                            &JsValue::from_f64(f64::from(self.get_canvas().height())),
1785                        );
1786                    }
1787                }
1788                current_scissor = batch.scissor;
1789            }
1790            let _: Result<JsValue, JsValue> = pass_api.draw_indexed.call5(
1791                pass,
1792                &JsValue::from_f64(f64::from(batch.index_count)),
1793                &JsValue::from_f64(1.0),
1794                &JsValue::from_f64(f64::from(batch.first_index)),
1795                &JsValue::from_f64(0.0),
1796                &JsValue::from_f64(0.0),
1797            );
1798        }
1799    }
1800}
1801
1802/// Implements [`ChartBackend`] for [`WebGpuChartBackend`].
1803impl ChartBackend for WebGpuChartBackend {
1804    /// Implements the `kind` operation on [`WebGpuChartBackend`].
1805    ///
1806    /// # Returns
1807    ///
1808    /// - `BackendKind` - A `BackendKind` value.
1809    fn kind(&self) -> BackendKind {
1810        BackendKind::WebGpu
1811    }
1812
1813    /// Implements the `resize` operation on [`WebGpuChartBackend`].
1814    ///
1815    /// # Arguments
1816    ///
1817    /// - `f64` - A 64-bit float (`f64`).
1818    fn resize(&mut self, width: f64, height: f64, dpr: f64) {
1819        // The clip rectangles are kept in physical pixels, so a change of
1820        // the device pixel ratio moves them and the geometry has to be
1821        // built again.
1822        if (dpr - self.get_dpr()).abs() > f64::EPSILON {
1823            self.set_fingerprint(None);
1824        }
1825        self.set_dpr(dpr);
1826        self.set_width(width);
1827        self.set_height(height);
1828        let physical_width: u32 = (width * dpr).round().max(1.0) as u32;
1829        let physical_height: u32 = (height * dpr).round().max(1.0) as u32;
1830        if self.get_canvas().width() != physical_width {
1831            self.get_canvas().set_width(physical_width);
1832        }
1833        if self.get_canvas().height() != physical_height {
1834            self.get_canvas().set_height(physical_height);
1835        }
1836        self.update_uniform();
1837    }
1838
1839    /// Implements the `render` operation on [`WebGpuChartBackend`].
1840    ///
1841    /// # Arguments
1842    ///
1843    /// - `&DisplayList` - Shared reference to a `DisplayList`.
1844    /// - `&mut TextShaper` - Mutable reference to a `TextShaper` (mutated in place).
1845    fn render(&mut self, list: &DisplayList, shaper: &mut TextShaper) {
1846        self.sync_atlas(shaper.atlas());
1847        // The atlas starts over when it runs out of room, which moves the
1848        // glyphs to other places in the texture. The text geometry holds
1849        // the texture coordinates of the glyphs, so it has to be built
1850        // again when the atlas was reset.
1851        let glyphs: usize = shaper.atlas().len();
1852        if glyphs < self.get_atlas_glyphs() {
1853            self.set_fingerprint(None);
1854        }
1855        self.set_atlas_glyphs(glyphs);
1856        let fingerprint: u64 = self.list_fingerprint(list);
1857        if *self.try_get_fingerprint() != Some(fingerprint) {
1858            self.rebuild(list, shaper);
1859            self.set_fingerprint(Some(fingerprint));
1860        }
1861        if shaper.atlas().is_dirty() {
1862            self.upload_atlas(shaper.atlas());
1863            shaper.atlas_mut().mark_clean();
1864        }
1865        self.encode();
1866    }
1867
1868    /// Implements the `dispose` operation on [`WebGpuChartBackend`].
1869    fn dispose(&mut self) {
1870        let Self {
1871            solid_buffer,
1872            solid_index_buffer,
1873            textured_buffer,
1874            textured_index_buffer,
1875            ..
1876        } = &mut *self;
1877        for buffer in [
1878            solid_buffer,
1879            solid_index_buffer,
1880            textured_buffer,
1881            textured_index_buffer,
1882        ] {
1883            if let Some(handle) = &buffer.buffer {
1884                Self::destroy(handle);
1885            }
1886            buffer.buffer = None;
1887            buffer.capacity = 0;
1888        }
1889        if let Some(texture) = &self.get_atlas().texture {
1890            Self::destroy(texture);
1891        }
1892        self.get_mut_atlas().texture = None;
1893        self.get_mut_atlas().bind_group = None;
1894        self.get_mut_atlas().width = 0;
1895        self.get_mut_atlas().height = 0;
1896        for image in self.get_mut_images().iter_mut() {
1897            if let Some(texture) = &image.texture {
1898                Self::destroy(texture);
1899            }
1900            image.texture = None;
1901            image.bind_group = None;
1902        }
1903        self.get_mut_batches().clear();
1904        self.set_fingerprint(None);
1905    }
1906}
1907
1908/// Implements [`Drop`] for [`WebGpuChartBackend`].
1909impl Drop for WebGpuChartBackend {
1910    /// Releases the GPU resources when the backend is dropped.
1911    ///
1912    /// A chart that is dropped without being disposed would hold on to
1913    /// its buffers until the browser collects them, and a chart over a
1914    /// million points holds on to a lot of memory.
1915    fn drop(&mut self) {
1916        <Self as ChartBackend>::dispose(self);
1917    }
1918}
1919
1920/// Inherent implementation of [`WebGpuApi`].
1921impl WebGpuApi {
1922    /// Fetches the JavaScript methods of the device, the queue, and the
1923    /// canvas context.
1924    ///
1925    /// # Arguments
1926    ///
1927    /// - `&JsValue` - Shared reference to a `JsValue`.
1928    ///
1929    /// # Returns
1930    ///
1931    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
1932    fn new(device: &JsValue, queue: &JsValue, context: &JsValue) -> Option<Self> {
1933        Some(Self {
1934            create_command_encoder: gpu_method(device, WEBGPU_METHOD_CREATE_COMMAND_ENCODER)?,
1935            submit: gpu_method(queue, WEBGPU_METHOD_SUBMIT)?,
1936            write_buffer: gpu_method(queue, WEBGPU_METHOD_WRITE_BUFFER)?,
1937            copy_external_image: gpu_method(queue, WEBGPU_METHOD_COPY_EXTERNAL_IMAGE)?,
1938            get_current_texture: gpu_method(context, WEBGPU_METHOD_GET_CURRENT_TEXTURE)?,
1939        })
1940    }
1941}
1942
1943/// Inherent implementation of [`WebGpuEncoderApi`].
1944impl WebGpuEncoderApi {
1945    /// Fetches the JavaScript methods of a command encoder.
1946    ///
1947    /// # Arguments
1948    ///
1949    /// - `&JsValue` - Shared reference to a `JsValue`.
1950    ///
1951    /// # Returns
1952    ///
1953    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
1954    fn new(encoder: &JsValue) -> Option<Self> {
1955        Some(Self {
1956            begin_render_pass: gpu_method(encoder, WEBGPU_METHOD_BEGIN_RENDER_PASS)?,
1957            finish_encoder: gpu_method(encoder, WEBGPU_METHOD_FINISH)?,
1958        })
1959    }
1960}
1961
1962/// Inherent implementation of [`WebGpuPassApi`].
1963impl WebGpuPassApi {
1964    /// Fetches the JavaScript methods of a render pass.
1965    ///
1966    /// # Arguments
1967    ///
1968    /// - `&JsValue` - Shared reference to a `JsValue`.
1969    ///
1970    /// # Returns
1971    ///
1972    /// - `Option<Self>` - `Some(...)` on success, `None` otherwise.
1973    fn new(pass: &JsValue) -> Option<Self> {
1974        Some(Self {
1975            set_pipeline: gpu_method(pass, WEBGPU_METHOD_SET_PIPELINE)?,
1976            set_vertex_buffer: gpu_method(pass, WEBGPU_METHOD_SET_VERTEX_BUFFER)?,
1977            set_index_buffer: gpu_method(pass, WEBGPU_METHOD_SET_INDEX_BUFFER)?,
1978            set_bind_group: gpu_method(pass, WEBGPU_METHOD_SET_BIND_GROUP)?,
1979            set_scissor_rect: gpu_method(pass, WEBGPU_METHOD_SET_SCISSOR_RECT)?,
1980            draw_indexed: gpu_method(pass, WEBGPU_METHOD_DRAW_INDEXED)?,
1981            end_pass: gpu_method(pass, WEBGPU_METHOD_END)?,
1982        })
1983    }
1984}
1985
1986/// Inherent implementation of [`WebGpuBuffer`].
1987impl WebGpuBuffer {
1988    /// Makes sure the buffer can hold the given number of bytes.
1989    ///
1990    /// The capacity is rounded up to a power of two, so a chart whose data
1991    /// grows a little at a time does not re-create the buffer every time.
1992    ///
1993    /// # Arguments
1994    ///
1995    /// - `&JsValue` - Shared reference to a `JsValue`.
1996    /// - `u32` - A 32-bit unsigned integer (`u32`).
1997    ///
1998    /// # Returns
1999    ///
2000    /// - `Option<JsValue>` - `Some(...)` on success, `None` otherwise.
2001    fn ensure(&mut self, device: &JsValue, size: u32, usage: u32) -> Option<JsValue> {
2002        if size == 0 {
2003            return None;
2004        }
2005        if self.get_capacity() < size {
2006            let capacity: u32 = size
2007                .checked_next_power_of_two()
2008                .unwrap_or(u32::MAX)
2009                .max(1024);
2010            let buffer: JsValue = WebGpuChartBackend::create_buffer(device, capacity, usage)?;
2011            self.set_buffer(Some(buffer));
2012            self.set_capacity(capacity);
2013        }
2014        self.try_get_buffer().clone()
2015    }
2016
2017    /// Uploads raw bytes into the buffer.
2018    ///
2019    /// The bytes are handed to the GPU in one call, never one call per
2020    /// vertex, and the typed array is only alive for the duration of the
2021    /// call — `writeBuffer` copies the data before it returns.
2022    ///
2023    /// # Arguments
2024    ///
2025    /// - `&JsValue` - Shared reference to a `JsValue`.
2026    /// - `&Function` - Shared reference to a `Function`.
2027    /// - `&mut Self` - Mutable reference to a `Self` (mutated in place).
2028    /// - `&[u8]` - Shared reference to a `[u8]`.
2029    /// - `u32` - A 32-bit unsigned integer (`u32`).
2030    fn upload_bytes(
2031        device: &JsValue,
2032        queue: &JsValue,
2033        write_buffer: &Function,
2034        buffer: &mut Self,
2035        bytes: &[u8],
2036        usage: u32,
2037    ) {
2038        if bytes.is_empty() {
2039            return;
2040        }
2041        let Some(handle) = buffer.ensure(device, bytes.len() as u32, usage) else {
2042            return;
2043        };
2044        let data: Uint8Array = js_sys::Uint8Array::from(bytes);
2045        let _: Result<JsValue, JsValue> =
2046            write_buffer.call3(queue, &handle, &JsValue::from_f64(0.0), &data);
2047    }
2048
2049    /// Uploads indices into the buffer.
2050    ///
2051    /// # Arguments
2052    ///
2053    /// - `&JsValue` - Shared reference to a `JsValue`.
2054    /// - `&Function` - Shared reference to a `Function`.
2055    /// - `&mut Self` - Mutable reference to a `Self` (mutated in place).
2056    /// - `&[u32]` - Shared reference to a `[u32]`.
2057    /// - `u32` - A 32-bit unsigned integer (`u32`).
2058    fn upload_indices(
2059        device: &JsValue,
2060        queue: &JsValue,
2061        write_buffer: &Function,
2062        buffer: &mut Self,
2063        indices: &[u32],
2064        usage: u32,
2065    ) {
2066        if indices.is_empty() {
2067            return;
2068        }
2069        let Some(handle) = buffer.ensure(device, (indices.len() * 4) as u32, usage) else {
2070            return;
2071        };
2072        let data: Uint32Array = js_sys::Uint32Array::from(indices);
2073        let _: Result<JsValue, JsValue> =
2074            write_buffer.call3(queue, &handle, &JsValue::from_f64(0.0), &data);
2075    }
2076}
2077
2078/// Inherent implementation of [`GpuHasher`].
2079impl GpuHasher {
2080    /// The FNV-1a offset basis.
2081    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
2082
2083    /// The FNV-1a prime.
2084    const PRIME: u64 = 0x0000_0100_0000_01b3;
2085
2086    /// Returns a hasher primed with the offset basis.
2087    pub(crate) fn new() -> Self {
2088        Self {
2089            state: Self::OFFSET,
2090        }
2091    }
2092
2093    /// Mixes a floating-point value into the hash.
2094    ///
2095    /// Floating-point values do not implement `Hash`, so their bits are
2096    /// mixed in instead.
2097    ///
2098    /// # Arguments
2099    ///
2100    /// - `f32` - A 32-bit float (`f32`).
2101    pub(crate) fn write_f32(&mut self, value: f32) {
2102        self.write_u32(value.to_bits());
2103    }
2104
2105    /// Mixes a double into the hash.
2106    ///
2107    /// # Arguments
2108    ///
2109    /// - `f64` - A 64-bit float (`f64`).
2110    pub(crate) fn write_f64(&mut self, value: f64) {
2111        self.write_u64(value.to_bits());
2112    }
2113}
2114
2115/// Implements `Hasher` for [`GpuHasher`].
2116impl Hasher for GpuHasher {
2117    /// Implements the `finish` operation on [`GpuHasher`].
2118    ///
2119    /// # Returns
2120    ///
2121    /// - `u64` - A `u64` value.
2122    fn finish(&self) -> u64 {
2123        self.get_state()
2124    }
2125
2126    /// Implements the `write` operation on [`GpuHasher`].
2127    ///
2128    /// # Arguments
2129    ///
2130    /// - `&[u8]` - Shared reference to a `[u8]`.
2131    fn write(&mut self, bytes: &[u8]) {
2132        for byte in bytes {
2133            self.write_u8(*byte);
2134        }
2135    }
2136
2137    /// Implements the `write_u8` operation on [`GpuHasher`].
2138    ///
2139    /// # Arguments
2140    ///
2141    /// - `u8` - A `u8` parameter.
2142    fn write_u8(&mut self, value: u8) {
2143        self.set_state(self.get_state() ^ u64::from(value));
2144        self.set_state(self.get_state().wrapping_mul(Self::PRIME));
2145    }
2146
2147    /// Implements the `write_u32` operation on [`GpuHasher`].
2148    ///
2149    /// # Arguments
2150    ///
2151    /// - `u32` - A 32-bit unsigned integer (`u32`).
2152    fn write_u32(&mut self, value: u32) {
2153        for byte in value.to_le_bytes() {
2154            self.write_u8(byte);
2155        }
2156    }
2157
2158    /// Implements the `write_u64` operation on [`GpuHasher`].
2159    ///
2160    /// # Arguments
2161    ///
2162    /// - `u64` - A `u64` parameter.
2163    fn write_u64(&mut self, value: u64) {
2164        for byte in value.to_le_bytes() {
2165            self.write_u8(byte);
2166        }
2167    }
2168
2169    /// Implements the `write_i32` operation on [`GpuHasher`].
2170    ///
2171    /// # Arguments
2172    ///
2173    /// - `i32` - A 32-bit signed integer (`i32`).
2174    fn write_i32(&mut self, value: i32) {
2175        self.write_u32(value as u32);
2176    }
2177}