Skip to main content

euv_engine/renderer/
impl.rs

1use super::*;
2
3/// OPT 2: cache `JsValue::from_str(...)` results in a thread-local map so we
4/// don't pay a fresh wasm-linear-memory string allocation for every
5/// `Reflect::get(obj, &JsValue::from_str(METHOD_NAME))` or
6/// `Reflect::set(obj, &JsValue::from_str(PROPERTY_NAME), value)` call.
7/// WebGPU render paths use 79 `Reflect::get` calls and ~50
8/// `Reflect::set` calls in this file; each previously allocated a 1-N
9/// byte JS string in linear memory. We only cache the constant
10/// `&'static str` keys here — dynamic string lookups (e.g. uniform
11/// names) are unaffected. The map is created once per thread, lazily,
12/// and grows monotonically for the lifetime of the wasm instance.
13fn cached_method_name(name: &'static str) -> JsValue {
14    use std::cell::RefCell;
15    use std::collections::HashMap;
16    thread_local! {
17        static CACHE: RefCell<Option<HashMap<&'static str, JsValue>>> =
18            const { RefCell::new(None) };
19    }
20    CACHE.with(|slot| {
21        let mut borrow: std::cell::RefMut<'_, Option<HashMap<&'static str, JsValue>>> =
22            slot.borrow_mut();
23        let map: &mut HashMap<&'static str, JsValue> = borrow.get_or_insert_with(HashMap::new);
24        if let Some(value) = map.get(name) {
25            return value.clone();
26        }
27        let value: JsValue = JsValue::from_str(name);
28        map.insert(name, value.clone());
29        value
30    })
31}
32
33/// Implements camera transformation methods for `Camera2D`.
34impl Camera2D {
35    /// Creates a new camera centered at the origin with default zoom and no rotation.
36    ///
37    /// # Arguments
38    ///
39    /// - `f64` - The viewport width in pixels.
40    /// - `f64` - The viewport height in pixels.
41    ///
42    /// # Returns
43    ///
44    /// - `Camera2D` - The new camera.
45    pub fn create(viewport_width: f64, viewport_height: f64) -> Camera2D {
46        Camera2D::new(
47            Vector2D::zero(),
48            RENDERER_DEFAULT_CAMERA_ZOOM,
49            RENDERER_DEFAULT_CAMERA_ROTATION,
50            viewport_width,
51            viewport_height,
52        )
53    }
54
55    /// Converts a world-space point to screen-space coordinates.
56    ///
57    /// # Arguments
58    ///
59    /// - `Vector2D` - The world-space point.
60    ///
61    /// # Returns
62    ///
63    /// - `Vector2D` - The screen-space point.
64    pub fn world_to_screen(&self, world: Vector2D) -> Vector2D {
65        let relative: Vector2D = world - self.get_position();
66        let rotated: Vector2D = relative.rotated(-self.get_rotation());
67        Vector2D::new(
68            rotated.get_x() * self.get_zoom() + self.get_viewport_width() * 0.5,
69            rotated.get_y() * self.get_zoom() + self.get_viewport_height() * 0.5,
70        )
71    }
72
73    /// Converts a screen-space point to world-space coordinates.
74    ///
75    /// # Arguments
76    ///
77    /// - `Vector2D` - The screen-space point.
78    ///
79    /// # Returns
80    ///
81    /// - `Vector2D` - The world-space point.
82    pub fn screen_to_world(&self, screen: Vector2D) -> Vector2D {
83        let relative: Vector2D = Vector2D::new(
84            (screen.get_x() - self.get_viewport_width() * 0.5) / self.get_zoom(),
85            (screen.get_y() - self.get_viewport_height() * 0.5) / self.get_zoom(),
86        );
87        let rotated: Vector2D = relative.rotated(self.get_rotation());
88        rotated + self.get_position()
89    }
90
91    /// Moves the camera position by the given offset.
92    ///
93    /// # Arguments
94    ///
95    /// - `Vector2D` - The translation offset in world space.
96    pub fn translate(&mut self, offset: Vector2D) {
97        self.set_position(self.get_position() + offset);
98    }
99
100    /// Adjusts the zoom by the given factor, clamped to a minimum of `EPSILON`.
101    ///
102    /// # Arguments
103    ///
104    /// - `f64` - The zoom multiplier.
105    pub fn zoom_by(&mut self, factor: f64) {
106        self.set_zoom((self.get_zoom() * factor).max(EPSILON));
107    }
108}
109
110/// Implements `Default` for `Camera2D` as a camera at the origin with 800x600 viewport.
111impl Default for Camera2D {
112    /// Constructs a default [`Camera2D`] value.
113    ///
114    /// # Returns
115    ///
116    /// - `Camera2D` - A default-constructed instance with the documented initial state.
117    fn default() -> Camera2D {
118        Camera2D::create(800.0, 600.0)
119    }
120}
121
122/// Implements static font and color utility methods for `CanvasRenderer`.
123impl CanvasRenderer {
124    /// Builds a CSS font string from font size and family.
125    ///
126    /// # Arguments
127    ///
128    /// - `f64` - The font size in pixels.
129    /// - `F: AsRef<str>` - The font family name.
130    ///
131    /// # Returns
132    ///
133    /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
134    pub fn font<F>(size: f64, family: F) -> String
135    where
136        F: AsRef<str>,
137    {
138        let family: &str = family.as_ref();
139        format!("{size}px {family}")
140    }
141
142    /// Creates a default font string using the default font size and family.
143    ///
144    /// # Returns
145    ///
146    /// - `String` - The default CSS font string.
147    pub fn default_font() -> String {
148        Self::font(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
149    }
150
151    /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
152    ///
153    /// Applies the `High` rendering quality preset via `apply_quality`,
154    /// which sets `imageSmoothingEnabled`, `imageSmoothingQuality = "high"`,
155    /// and `textRendering = "geometricPrecision"` on the given context.
156    ///
157    /// Use this static helper when you manage your own `CanvasRenderingContext2d`
158    /// and don't hold a `CanvasRenderer` instance. For instances, call
159    /// `renderer.enable_smoothing()` instead.
160    ///
161    /// # Arguments
162    ///
163    /// - `&CanvasRenderingContext2d` - The canvas context to configure.
164    pub fn enable_smoothing_on(context: &CanvasRenderingContext2d) {
165        Self::apply_quality(context, RenderQuality::High);
166    }
167
168    /// Detects the host device pixel ratio (HiDPI scale factor) via reflection.
169    ///
170    /// Reads `window.devicePixelRatio` using `Reflect::get` because the
171    /// `web-sys` `Window` features currently in use do not expose a native
172    /// getter for this property. Falls back to
173    /// `RENDERER_DEFAULT_DEVICE_PIXEL_RATIO` (1.0) when the global window or
174    /// the value is missing, not a finite number, or below 1.0.
175    ///
176    /// # Returns
177    ///
178    /// - `f64` - The detected device pixel ratio (clamped to `>= 1.0`).
179    pub fn detect_dpr() -> f64 {
180        let Some(window_value) = window() else {
181            return RENDERER_DEFAULT_DEVICE_PIXEL_RATIO;
182        };
183        let raw: Option<f64> = Reflect::get(
184            window_value.as_ref(),
185            &JsValue::from_str(RENDERER_PROPERTY_DEVICE_PIXEL_RATIO),
186        )
187        .ok()
188        .and_then(|value: JsValue| value.as_f64());
189        raw.filter(|value: &f64| value.is_finite() && *value >= 1.0)
190            .unwrap_or(RENDERER_DEFAULT_DEVICE_PIXEL_RATIO)
191    }
192
193    /// Applies the given `RenderQuality` preset to an arbitrary canvas context.
194    ///
195    /// Sets `imageSmoothingEnabled`, `imageSmoothingQuality`, and
196    /// `textRendering` according to the supplied quality. `Low` disables
197    /// smoothing (intended for use with CSS `image-rendering: pixelated`),
198    /// `Medium` and `High` enable it with the matching quality level.
199    ///
200    /// # Arguments
201    ///
202    /// - `&CanvasRenderingContext2d` - The target context.
203    /// - `RenderQuality` - The quality preset to apply.
204    pub(crate) fn apply_quality(context: &CanvasRenderingContext2d, quality: RenderQuality) {
205        let smoothing_enabled: bool = !matches!(quality, RenderQuality::Low);
206        context.set_image_smoothing_enabled(smoothing_enabled);
207        let quality_value: &str = match quality {
208            RenderQuality::Low => RENDERER_IMAGE_SMOOTHING_QUALITY_LOW,
209            RenderQuality::Medium => RENDERER_IMAGE_SMOOTHING_QUALITY_MEDIUM,
210            RenderQuality::High => RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH,
211        };
212        let _: Result<bool, JsValue> = Reflect::set(
213            context,
214            &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
215            &JsValue::from_str(quality_value),
216        );
217        let _: Result<bool, JsValue> = Reflect::set(
218            context,
219            &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
220            &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
221        );
222    }
223}
224
225/// Implements static CSS conversion for `Color`.
226impl Color {
227    /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
228    ///
229    /// # Arguments
230    ///
231    /// - `&Color` - The color to convert.
232    ///
233    /// # Returns
234    ///
235    /// - `String` - The CSS `rgba()` color string.
236    pub fn to_css(color: &Color) -> String {
237        color.to_css_rgba()
238    }
239}
240
241/// Implements drawing and camera management methods for `CanvasRenderer`.
242/// Implements recording and replay for `DrawList`.
243impl DrawList {
244    /// Creates an empty draw list.
245    ///
246    /// # Returns
247    ///
248    /// - `DrawList` - The new empty draw list.
249    pub fn create() -> DrawList {
250        DrawList::new(Vec::new())
251    }
252
253    /// Returns whether the list contains no commands.
254    ///
255    /// # Returns
256    ///
257    /// - `bool` - `true` if there are no recorded commands.
258    pub fn is_empty(&self) -> bool {
259        self.get_commands().is_empty()
260    }
261
262    /// Returns the number of recorded commands.
263    ///
264    /// # Returns
265    ///
266    /// - `usize` - The command count.
267    pub fn len(&self) -> usize {
268        self.get_commands().len()
269    }
270
271    /// Returns the recorded commands as a slice for replay iteration.
272    ///
273    /// # Returns
274    ///
275    /// - `&[DrawCommand]` - The commands in the order they were recorded.
276    pub fn commands(&self) -> &[DrawCommand] {
277        self.get_commands().as_slice()
278    }
279
280    /// Removes all recorded commands, keeping the allocated capacity for reuse
281    /// on the next frame.
282    pub fn clear(&mut self) {
283        self.get_mut_commands().clear();
284    }
285
286    /// Records a fill-rectangle command.
287    ///
288    /// # Arguments
289    ///
290    /// - `Vector2D` - 2D vector (`Vector2D`).
291    /// - `f64` - A 64-bit float (`f64`).
292    /// - `f64` - A 64-bit float (`f64`).
293    /// - `Color` - A `Color` parameter.
294    pub fn fill_rect(&mut self, position: Vector2D, width: f64, height: f64, color: Color) {
295        self.get_mut_commands().push(DrawCommand::FillRect {
296            position,
297            width,
298            height,
299            color,
300        });
301    }
302
303    /// Records a stroke-rectangle command.
304    ///
305    /// # Arguments
306    ///
307    /// - `Vector2D` - 2D vector (`Vector2D`).
308    /// - `f64` - A 64-bit float (`f64`).
309    /// - `f64` - A 64-bit float (`f64`).
310    /// - `Color` - A `Color` parameter.
311    /// - `f64` - A 64-bit float (`f64`).
312    pub fn stroke_rect(
313        &mut self,
314        position: Vector2D,
315        width: f64,
316        height: f64,
317        color: Color,
318        line_width: f64,
319    ) {
320        self.get_mut_commands().push(DrawCommand::StrokeRect {
321            position,
322            width,
323            height,
324            color,
325            line_width,
326        });
327    }
328
329    /// Records a fill-circle command.
330    ///
331    /// # Arguments
332    ///
333    /// - `Vector2D` - 2D vector (`Vector2D`).
334    /// - `f64` - A 64-bit float (`f64`).
335    /// - `Color` - A `Color` parameter.
336    pub fn fill_circle(&mut self, center: Vector2D, radius: f64, color: Color) {
337        self.get_mut_commands().push(DrawCommand::FillCircle {
338            center,
339            radius,
340            color,
341        });
342    }
343
344    /// Records a stroke-circle command.
345    ///
346    /// # Arguments
347    ///
348    /// - `Vector2D` - 2D vector (`Vector2D`).
349    /// - `f64` - A 64-bit float (`f64`).
350    /// - `Color` - A `Color` parameter.
351    /// - `f64` - A 64-bit float (`f64`).
352    pub fn stroke_circle(&mut self, center: Vector2D, radius: f64, color: Color, line_width: f64) {
353        self.get_mut_commands().push(DrawCommand::StrokeCircle {
354            center,
355            radius,
356            color,
357            line_width,
358        });
359    }
360
361    /// Records a line-segment command.
362    ///
363    /// # Arguments
364    ///
365    /// - `Vector2D` - 2D vector (`Vector2D`).
366    /// - `Vector2D` - 2D vector (`Vector2D`).
367    /// - `Color` - A `Color` parameter.
368    /// - `f64` - A 64-bit float (`f64`).
369    pub fn draw_line(&mut self, start: Vector2D, end: Vector2D, color: Color, line_width: f64) {
370        self.get_mut_commands().push(DrawCommand::Line {
371            start,
372            end,
373            color,
374            line_width,
375        });
376    }
377
378    /// Records a fill-text command.
379    ///
380    /// # Arguments
381    ///
382    /// - `T: AsRef<str>` - A generic type parameter.
383    /// - `Vector2D` - 2D vector (`Vector2D`).
384    /// - `Color` - A `Color` parameter.
385    /// - `F: AsRef<str>` - A generic type parameter.
386    pub fn fill_text<T, F>(&mut self, text: T, position: Vector2D, color: Color, font: F)
387    where
388        T: AsRef<str>,
389        F: AsRef<str>,
390    {
391        self.get_mut_commands().push(DrawCommand::FillText {
392            text: text.as_ref().to_string(),
393            position,
394            color,
395            font: font.as_ref().to_string(),
396        });
397    }
398
399    /// Records a transformed sprite draw command.
400    ///
401    /// # Arguments
402    ///
403    /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
404    /// - `Rect` - A `Rect` parameter.
405    /// - `Transform2D` - A `Transform2D` parameter.
406    pub fn draw_sprite(&mut self, image: &HtmlImageElement, source: Rect, transform: Transform2D) {
407        self.get_mut_commands().push(DrawCommand::DrawSprite {
408            image: image.clone(),
409            source,
410            transform,
411        });
412    }
413
414    /// Records an image sub-region draw command (no rotation).
415    ///
416    /// # Arguments
417    ///
418    /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
419    /// - `Rect` - A `Rect` parameter.
420    /// - `Vector2D` - 2D vector (`Vector2D`).
421    /// - `f64` - A 64-bit float (`f64`).
422    /// - `f64` - A 64-bit float (`f64`).
423    pub fn draw_image_rect(
424        &mut self,
425        image: &HtmlImageElement,
426        source: Rect,
427        dest_position: Vector2D,
428        dest_width: f64,
429        dest_height: f64,
430    ) {
431        self.get_mut_commands().push(DrawCommand::DrawImageRect {
432            image: image.clone(),
433            source,
434            dest_position,
435            dest_width,
436            dest_height,
437        });
438    }
439
440    /// Records a global-alpha state change.
441    ///
442    /// # Arguments
443    ///
444    /// - `f64` - A 64-bit float (`f64`).
445    pub fn set_global_alpha(&mut self, alpha: f64) {
446        self.get_mut_commands()
447            .push(DrawCommand::SetGlobalAlpha { alpha });
448    }
449
450    /// Records a blend-mode state change.
451    ///
452    /// # Arguments
453    ///
454    /// - `BlendMode` - A `BlendMode` parameter.
455    pub fn set_blend_mode(&mut self, mode: BlendMode) {
456        self.get_mut_commands()
457            .push(DrawCommand::SetBlendMode { mode });
458    }
459}
460
461/// Inherent implementation of [`CanvasRenderer`].
462impl CanvasRenderer {
463    /// Creates a new renderer from a canvas element selector and viewport dimensions.
464    ///
465    /// # Arguments
466    ///
467    /// - `&str` - The CSS selector for the canvas element.
468    /// - `f64` - The viewport width.
469    /// - `f64` - The viewport height.
470    ///
471    /// # Returns
472    ///
473    /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
474    pub fn from_selector<S>(
475        canvas_selector: S,
476        viewport_width: f64,
477        viewport_height: f64,
478    ) -> Option<CanvasRenderer>
479    where
480        S: AsRef<str>,
481    {
482        let window_value: Window = window()?;
483        let document_value: Document = window_value.document()?;
484        let element: Element = document_value
485            .query_selector(canvas_selector.as_ref())
486            .ok()
487            .flatten()?;
488        let canvas_element: HtmlCanvasElement = element.unchecked_into();
489        let context_object: Object = canvas_element
490            .get_context(RENDERER_CONTEXT_TYPE_2D)
491            .ok()
492            .flatten()?;
493        let context: CanvasRenderingContext2d = context_object.unchecked_into();
494        let renderer: CanvasRenderer = CanvasRenderer::new(
495            context,
496            Camera2D::create(viewport_width, viewport_height),
497            RenderQuality::default(),
498        );
499        renderer.enable_smoothing();
500        Some(renderer)
501    }
502
503    /// Enables high-quality anti-aliasing on the canvas context by setting
504    /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
505    ///
506    /// Applies the active `quality` preset via the shared `apply_quality`
507    /// helper so that all smoothing-related settings are kept in sync.
508    pub fn enable_smoothing(&self) {
509        Self::apply_quality(self.get_context(), self.get_quality());
510    }
511
512    /// Clears the entire canvas viewport.
513    pub fn clear(&self) {
514        self.get_context().clear_rect(
515            0.0,
516            0.0,
517            self.get_camera().get_viewport_width(),
518            self.get_camera().get_viewport_height(),
519        );
520    }
521
522    /// Clears the canvas and fills it with the given CSS color string.
523    ///
524    /// # Arguments
525    ///
526    /// - `C: AsRef<str>` - The CSS color string (e.g., `"#000000"`).
527    pub fn clear_color<C>(&self, color: C)
528    where
529        C: AsRef<str>,
530    {
531        self.get_context().set_fill_style_str(color.as_ref());
532        self.get_context().fill_rect(
533            0.0,
534            0.0,
535            self.get_camera().get_viewport_width(),
536            self.get_camera().get_viewport_height(),
537        );
538    }
539
540    /// Saves the current canvas state (transform, styles) onto the state stack.
541    pub fn save(&self) {
542        self.get_context().save();
543    }
544
545    /// Restores the most recently saved canvas state.
546    pub fn restore(&self) {
547        self.get_context().restore();
548    }
549
550    /// Replays a recorded `DrawList` onto this renderer's canvas.
551    ///
552    /// Convenience wrapper around `replay_context` using this renderer's context.
553    ///
554    /// # Arguments
555    ///
556    /// - `&DrawList` - The recorded commands to replay.
557    pub fn replay(&self, list: &DrawList) {
558        Self::replay_context(self.get_context(), list);
559    }
560
561    /// Replays a recorded `DrawList` onto an arbitrary canvas 2D context in a
562    /// single batched pass.
563    ///
564    /// Consecutive same-style shapes are merged into one path (one `begin_path`
565    /// plus one `fill`/`stroke` per style run), fill/stroke colors and line
566    /// widths are only re-applied when they change, and sprites are drawn with a
567    /// single `set_transform` rather than a save/restore pair. This collapses
568    /// the per-shape canvas state churn of immediate-mode drawing.
569    ///
570    /// The canvas transform and global alpha are reset to identity / 1.0 when
571    /// replay finishes, so callers can sandwich the call between
572    /// `save()`/`apply_camera()` and `restore()` without leaking state.
573    ///
574    /// # Arguments
575    ///
576    /// - `&CanvasRenderingContext2d` - The target canvas 2D context.
577    /// - `&DrawList` - The recorded commands to replay.
578    pub fn replay_context(context: &CanvasRenderingContext2d, list: &DrawList) {
579        let mut current_fill: Option<Color> = None;
580        let mut current_stroke: Option<Color> = None;
581        let mut current_line_width: f64 = f64::NAN;
582        // Whether a same-style path run is currently open.
583        let mut run_open: bool = false;
584        let mut run_is_fill: bool = true;
585        let mut run_key: Option<(u8, Color, f64)> = None;
586
587        // Returns the style key for a path-batchable command, or `None` for
588        // commands that break a run (sprites, images, text, state changes).
589        /// Computes the batching key for a [`DrawCommand`].
590        ///
591        /// # Arguments
592        ///
593        /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
594        ///
595        /// # Returns
596        ///
597        /// - `Option<(u8, Color, f64)>` - `Some(...)` on success, `None` otherwise.
598        fn batch_key(command: &DrawCommand) -> Option<(u8, Color, f64)> {
599            match command {
600                DrawCommand::FillRect { color, .. } | DrawCommand::FillCircle { color, .. } => {
601                    Some((0, *color, 0.0))
602                }
603                DrawCommand::StrokeRect {
604                    color, line_width, ..
605                }
606                | DrawCommand::StrokeCircle {
607                    color, line_width, ..
608                }
609                | DrawCommand::Line {
610                    color, line_width, ..
611                } => Some((1, *color, *line_width)),
612                _ => None,
613            }
614        }
615
616        // Emits a single path-batchable command's geometry into the open path.
617        /// Emits the geometry for the supplied [`DrawCommand`] into the canvas context.
618        ///
619        /// # Arguments
620        ///
621        /// - `&CanvasRenderingContext2d` - Shared reference to a `CanvasRenderingContext2d`.
622        /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
623        fn emit_geometry(context: &CanvasRenderingContext2d, command: &DrawCommand) {
624            match command {
625                DrawCommand::FillRect {
626                    position,
627                    width,
628                    height,
629                    ..
630                }
631                | DrawCommand::StrokeRect {
632                    position,
633                    width,
634                    height,
635                    ..
636                } => {
637                    context.rect(position.get_x(), position.get_y(), *width, *height);
638                }
639                DrawCommand::FillCircle { center, radius, .. }
640                | DrawCommand::StrokeCircle { center, radius, .. } => {
641                    context.move_to(center.get_x() + radius, center.get_y());
642                    let _: Result<(), JsValue> =
643                        context.arc(center.get_x(), center.get_y(), *radius, 0.0, TWO_PI);
644                }
645                DrawCommand::Line { start, end, .. } => {
646                    context.move_to(start.get_x(), start.get_y());
647                    context.line_to(end.get_x(), end.get_y());
648                }
649                _ => {}
650            }
651        }
652
653        for command in list.commands() {
654            let key: Option<(u8, Color, f64)> = batch_key(command);
655            // Close the open run if this command breaks it or starts a new style.
656            if run_open && key != run_key {
657                if run_is_fill {
658                    context.fill();
659                } else {
660                    context.stroke();
661                }
662                run_open = false;
663            }
664            if let Some(current_key) = key {
665                // Begin (or continue) a same-style path run.
666                if !run_open {
667                    let (kind, color, line_width) = current_key;
668                    if kind == 0 {
669                        if current_fill != Some(color) {
670                            context.set_fill_style_str(&Color::to_css(&color));
671                            current_fill = Some(color);
672                        }
673                        run_is_fill = true;
674                    } else {
675                        if current_stroke != Some(color) {
676                            context.set_stroke_style_str(&Color::to_css(&color));
677                            current_stroke = Some(color);
678                        }
679                        if current_line_width != line_width {
680                            context.set_line_width(line_width);
681                            current_line_width = line_width;
682                        }
683                        run_is_fill = false;
684                    }
685                    context.begin_path();
686                    run_open = true;
687                    run_key = Some(current_key);
688                }
689                emit_geometry(context, command);
690                continue;
691            }
692            // Non-batchable command: draw it immediately.
693            match command {
694                DrawCommand::FillText {
695                    text,
696                    position,
697                    color,
698                    font,
699                } => {
700                    if current_fill != Some(*color) {
701                        context.set_fill_style_str(&Color::to_css(color));
702                        current_fill = Some(*color);
703                    }
704                    context.set_font(font);
705                    let _: Result<(), JsValue> =
706                        context.fill_text(text, position.get_x(), position.get_y());
707                }
708                DrawCommand::DrawSprite {
709                    image,
710                    source,
711                    transform,
712                } => {
713                    draw_sprite_immediate(context, image, source, transform);
714                }
715                DrawCommand::DrawImageRect {
716                    image,
717                    source,
718                    dest_position,
719                    dest_width,
720                    dest_height,
721                } => {
722                    let _: Result<(), JsValue> = context
723                        .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
724                            image,
725                            source.get_x(),
726                            source.get_y(),
727                            source.get_width(),
728                            source.get_height(),
729                            dest_position.get_x(),
730                            dest_position.get_y(),
731                            *dest_width,
732                            *dest_height,
733                        );
734                }
735                DrawCommand::SetGlobalAlpha { alpha } => {
736                    context.set_global_alpha(Numeric::clamp(*alpha, 0.0, 1.0));
737                }
738                DrawCommand::SetBlendMode { mode } => {
739                    let _: Result<(), JsValue> =
740                        context.set_global_composite_operation(mode.to_css());
741                }
742                _ => {}
743            }
744        }
745        // Flush any trailing open run.
746        if run_open {
747            if run_is_fill {
748                context.fill();
749            } else {
750                context.stroke();
751            }
752        }
753        let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
754        context.set_global_alpha(1.0);
755    }
756
757    /// Applies the camera transform to the canvas context.
758    ///
759    /// Translates to the screen center, applies zoom and rotation,
760    /// then offsets by the negative camera position.
761    pub fn apply_camera(&self) {
762        let camera: Camera2D = self.get_camera();
763        let _: Result<(), JsValue> = self.get_context().translate(
764            camera.get_viewport_width() * 0.5,
765            camera.get_viewport_height() * 0.5,
766        );
767        let _: Result<(), JsValue> = self
768            .get_context()
769            .scale(camera.get_zoom(), camera.get_zoom());
770        let _: Result<(), JsValue> = self.get_context().rotate(camera.get_rotation());
771        let _: Result<(), JsValue> = self.get_context().translate(
772            -camera.get_position().get_x(),
773            -camera.get_position().get_y(),
774        );
775    }
776
777    /// Sets the fill color for subsequent fill operations.
778    ///
779    /// # Arguments
780    ///
781    /// - `C: AsRef<str>` - The CSS color string.
782    pub fn set_fill_color<C>(&self, color: C)
783    where
784        C: AsRef<str>,
785    {
786        self.get_context().set_fill_style_str(color.as_ref());
787    }
788
789    /// Sets the stroke color for subsequent stroke operations.
790    ///
791    /// # Arguments
792    ///
793    /// - `C: AsRef<str>` - The CSS color string.
794    pub fn set_stroke_color<C>(&self, color: C)
795    where
796        C: AsRef<str>,
797    {
798        self.get_context().set_stroke_style_str(color.as_ref());
799    }
800
801    /// Sets the line width for subsequent stroke operations.
802    ///
803    /// # Arguments
804    ///
805    /// - `f64` - The line width in pixels.
806    pub fn set_line_width(&self, width: f64) {
807        self.get_context().set_line_width(width);
808    }
809
810    /// Sets the global alpha (opacity) for all subsequent drawing operations.
811    ///
812    /// # Arguments
813    ///
814    /// - `f64` - The alpha value in the range 0.0 to 1.0.
815    pub fn set_global_alpha(&self, alpha: f64) {
816        self.get_context()
817            .set_global_alpha(Numeric::clamp(alpha, 0.0, 1.0));
818    }
819
820    /// Fills a rectangle at the given world-space position and dimensions.
821    ///
822    /// # Arguments
823    ///
824    /// - `Vector2D` - The top-left position in world space.
825    /// - `f64` - The width.
826    /// - `f64` - The height.
827    pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
828        self.get_context()
829            .fill_rect(position.get_x(), position.get_y(), width, height);
830    }
831
832    /// Strokes the outline of a rectangle at the given world-space position and dimensions.
833    ///
834    /// # Arguments
835    ///
836    /// - `Vector2D` - The top-left position in world space.
837    /// - `f64` - The width.
838    /// - `f64` - The height.
839    pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
840        self.get_context()
841            .stroke_rect(position.get_x(), position.get_y(), width, height);
842    }
843
844    /// Fills a circle at the given world-space center with the specified radius.
845    ///
846    /// # Arguments
847    ///
848    /// - `Vector2D` - The center in world space.
849    /// - `f64` - The radius.
850    pub fn fill_circle(&self, center: Vector2D, radius: f64) {
851        self.get_context().begin_path();
852        self.get_context()
853            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
854            .unwrap_or(());
855        self.get_context().fill();
856    }
857
858    /// Strokes the outline of a circle at the given world-space center.
859    ///
860    /// # Arguments
861    ///
862    /// - `Vector2D` - The center in world space.
863    /// - `f64` - The radius.
864    pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
865        self.get_context().begin_path();
866        self.get_context()
867            .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
868            .unwrap_or(());
869        self.get_context().stroke();
870    }
871
872    /// Draws a line segment between two world-space points.
873    ///
874    /// # Arguments
875    ///
876    /// - `Vector2D` - The start point.
877    /// - `Vector2D` - The end point.
878    pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
879        self.get_context().begin_path();
880        self.get_context().move_to(start.get_x(), start.get_y());
881        self.get_context().line_to(end.get_x(), end.get_y());
882        self.get_context().stroke();
883    }
884
885    /// Fills text at the given world-space position.
886    ///
887    /// # Arguments
888    ///
889    /// - `T: AsRef<str>` - The text to draw.
890    /// - `Vector2D` - The position in world space.
891    pub fn fill_text<T>(&self, text: T, position: Vector2D)
892    where
893        T: AsRef<str>,
894    {
895        self.get_context()
896            .fill_text(text.as_ref(), position.get_x(), position.get_y())
897            .unwrap_or(());
898    }
899
900    /// Sets the font for subsequent text rendering.
901    ///
902    /// # Arguments
903    ///
904    /// - `F: AsRef<str>` - The CSS font string (e.g., `"16px sans-serif"`).
905    pub fn set_font<F>(&self, font: F)
906    where
907        F: AsRef<str>,
908    {
909        self.get_context().set_font(font.as_ref());
910    }
911
912    /// Draws an image element at the given world-space position and dimensions.
913    ///
914    /// # Arguments
915    ///
916    /// - `&HtmlImageElement` - The image element to draw.
917    /// - `Vector2D` - The top-left position in world space.
918    /// - `f64` - The destination width.
919    /// - `f64` - The destination height.
920    pub fn draw_image(
921        &self,
922        image: &HtmlImageElement,
923        position: Vector2D,
924        width: f64,
925        height: f64,
926    ) {
927        let _: Result<(), JsValue> = self
928            .get_context()
929            .draw_image_with_html_image_element_and_dw_and_dh(
930                image,
931                position.get_x(),
932                position.get_y(),
933                width,
934                height,
935            );
936    }
937
938    /// Draws a sub-region of an image element at the given world-space position.
939    ///
940    /// # Arguments
941    ///
942    /// - `&HtmlImageElement` - The image element to draw.
943    /// - `Rect` - The source rectangle within the image.
944    /// - `Vector2D` - The destination top-left position in world space.
945    /// - `f64` - The destination width.
946    /// - `f64` - The destination height.
947    pub fn draw_image_rect(
948        &self,
949        image: &HtmlImageElement,
950        source: Rect,
951        dest_position: Vector2D,
952        dest_width: f64,
953        dest_height: f64,
954    ) {
955        let _: Result<(), JsValue> = self
956            .get_context()
957            .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
958                image,
959                source.get_x(),
960                source.get_y(),
961                source.get_width(),
962                source.get_height(),
963                dest_position.get_x(),
964                dest_position.get_y(),
965                dest_width,
966                dest_height,
967            );
968    }
969}
970
971/// Implements 3D camera transformation and projection methods for `Camera3D`.
972impl Camera3D {
973    /// Creates a new 3D camera at the given position looking at the target.
974    ///
975    /// # Arguments
976    ///
977    /// - `Vector3D` - The eye position.
978    /// - `Vector3D` - The target position to look at.
979    /// - `f64` - The viewport width.
980    /// - `f64` - The viewport height.
981    ///
982    /// # Returns
983    ///
984    /// - `Camera3D` - The new camera.
985    pub fn create(
986        position: Vector3D,
987        target: Vector3D,
988        viewport_width: f64,
989        viewport_height: f64,
990    ) -> Camera3D {
991        let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
992        camera.set_up(Vector3D::up());
993        camera.set_fov(DEFAULT_CAMERA_FOV);
994        camera.set_near(DEFAULT_CAMERA_NEAR);
995        camera.set_far(DEFAULT_CAMERA_FAR);
996        camera
997    }
998
999    /// Returns the aspect ratio (width / height).
1000    ///
1001    /// # Returns
1002    ///
1003    /// - `f64` - The aspect ratio.
1004    pub fn aspect(&self) -> f64 {
1005        if self.get_viewport_height() < EPSILON {
1006            return 1.0;
1007        }
1008        self.get_viewport_width() / self.get_viewport_height()
1009    }
1010
1011    /// Returns the forward direction (from position to target, normalized).
1012    ///
1013    /// # Returns
1014    ///
1015    /// - `Vector3D` - The forward direction.
1016    pub fn forward(&self) -> Vector3D {
1017        (self.get_target() - self.get_position()).normalized()
1018    }
1019
1020    /// Returns the right direction (cross product of forward and up).
1021    ///
1022    /// # Returns
1023    ///
1024    /// - `Vector3D` - The right direction.
1025    pub fn right(&self) -> Vector3D {
1026        self.forward().cross(self.get_up()).normalized()
1027    }
1028
1029    /// Returns the view matrix for this camera.
1030    ///
1031    /// # Returns
1032    ///
1033    /// - `Matrix4x4` - The view matrix.
1034    pub fn view_matrix(&self) -> Matrix4x4 {
1035        Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
1036    }
1037
1038    /// Returns the perspective projection matrix for this camera.
1039    ///
1040    /// # Returns
1041    ///
1042    /// - `Matrix4x4` - The projection matrix.
1043    pub fn projection_matrix(&self) -> Matrix4x4 {
1044        Matrix4x4::perspective(
1045            self.get_fov(),
1046            self.aspect(),
1047            self.get_near(),
1048            self.get_far(),
1049        )
1050    }
1051
1052    /// Returns the combined view-projection matrix.
1053    ///
1054    /// # Returns
1055    ///
1056    /// - `Matrix4x4` - The view-projection matrix.
1057    pub fn view_proj_matrix(&self) -> Matrix4x4 {
1058        self.projection_matrix().multiply(self.view_matrix())
1059    }
1060
1061    /// Converts a 3D world-space point to screen-space (NDC) coordinates.
1062    ///
1063    /// # Arguments
1064    ///
1065    /// - `Vector3D` - The world-space point.
1066    ///
1067    /// # Returns
1068    ///
1069    /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
1070    pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
1071        let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1072        Vector3D::new(
1073            (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
1074            (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
1075            clip.get_z(),
1076        )
1077    }
1078
1079    /// Projects a world-space point and returns whether it is within the camera frustum.
1080    ///
1081    /// # Arguments
1082    ///
1083    /// - `Vector3D` - The world-space point.
1084    ///
1085    /// # Returns
1086    ///
1087    /// - `bool` - True if the point is within the frustum.
1088    pub fn in_frustum(&self, world: Vector3D) -> bool {
1089        let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1090        clip.get_x() >= -1.0
1091            && clip.get_x() <= 1.0
1092            && clip.get_y() >= -1.0
1093            && clip.get_y() <= 1.0
1094            && clip.get_z() >= -1.0
1095            && clip.get_z() <= 1.0
1096    }
1097
1098    /// Moves the camera position by the given offset, keeping the target offset by the same amount.
1099    ///
1100    /// # Arguments
1101    ///
1102    /// - `Vector3D` - The translation offset.
1103    pub fn translate(&mut self, offset: Vector3D) {
1104        self.set_position(self.get_position() + offset);
1105        self.set_target(self.get_target() + offset);
1106    }
1107
1108    /// Moves the camera position towards the target by the given distance.
1109    ///
1110    /// # Arguments
1111    ///
1112    /// - `f64` - The distance to zoom in (positive) or out (negative).
1113    pub fn zoom(&mut self, distance: f64) {
1114        let direction: Vector3D = self.forward();
1115        self.set_position(self.get_position() + direction.scaled(distance));
1116    }
1117
1118    /// Orbits the camera around the target by the given yaw and pitch angles.
1119    ///
1120    /// # Arguments
1121    ///
1122    /// - `f64` - The yaw delta in radians (horizontal rotation).
1123    /// - `f64` - The pitch delta in radians (vertical rotation).
1124    pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
1125        let offset: Vector3D = self.get_position() - self.get_target();
1126        let current_distance: f64 = offset.magnitude();
1127        let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
1128        let horizontal_dist: f64 =
1129            (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
1130        let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
1131        let new_yaw: f64 = current_yaw + yaw_delta;
1132        let new_pitch: f64 = Numeric::clamp(
1133            current_pitch + pitch_delta,
1134            -HALF_PI + EPSILON,
1135            HALF_PI - EPSILON,
1136        );
1137        let cos_pitch: f64 = new_pitch.cos();
1138        self.set_position(
1139            self.get_target()
1140                + Vector3D::new(
1141                    new_yaw.sin() * cos_pitch * current_distance,
1142                    new_pitch.sin() * current_distance,
1143                    new_yaw.cos() * cos_pitch * current_distance,
1144                ),
1145        );
1146    }
1147}
1148
1149/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
1150impl Default for Camera3D {
1151    /// Constructs a default [`Camera3D`] value.
1152    ///
1153    /// # Returns
1154    ///
1155    /// - `Camera3D` - A default-constructed instance with the documented initial state.
1156    fn default() -> Camera3D {
1157        Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
1158    }
1159}
1160
1161/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
1162impl SsaaCanvas {
1163    /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
1164    ///
1165    /// # Arguments
1166    ///
1167    /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1168    /// - `f64` - The logical display width in CSS pixels.
1169    /// - `f64` - The logical display height in CSS pixels.
1170    ///
1171    /// # Returns
1172    ///
1173    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1174    pub fn from_selector<S>(canvas_selector: S, width: f64, height: f64) -> Option<SsaaCanvas>
1175    where
1176        S: AsRef<str>,
1177    {
1178        Self::from_selector_with_scale(
1179            canvas_selector,
1180            width,
1181            height,
1182            RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
1183        )
1184    }
1185
1186    /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
1187    ///
1188    /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
1189    /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
1190    ///
1191    /// # Arguments
1192    ///
1193    /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1194    /// - `f64` - The logical display width in CSS pixels.
1195    /// - `f64` - The logical display height in CSS pixels.
1196    /// - `f64` - The supersampling scale factor (e.g., 2.0 for 4x SSAA).
1197    ///
1198    /// # Returns
1199    ///
1200    /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1201    pub fn from_selector_with_scale<S>(
1202        canvas_selector: S,
1203        width: f64,
1204        height: f64,
1205        scale_factor: f64,
1206    ) -> Option<SsaaCanvas>
1207    where
1208        S: AsRef<str>,
1209    {
1210        let window_value: Window = window()?;
1211        let document_value: Document = window_value.document()?;
1212        let element: Element = document_value
1213            .query_selector(canvas_selector.as_ref())
1214            .ok()
1215            .flatten()?;
1216        let display_canvas: HtmlCanvasElement = element.unchecked_into();
1217        let device_pixel_ratio: f64 = CanvasRenderer::detect_dpr();
1218        let physical_width: u32 = (width * device_pixel_ratio).round() as u32;
1219        let physical_height: u32 = (height * device_pixel_ratio).round() as u32;
1220        display_canvas.set_width(physical_width);
1221        display_canvas.set_height(physical_height);
1222        let display_context_object: Object = display_canvas
1223            .get_context(RENDERER_CONTEXT_TYPE_2D)
1224            .ok()
1225            .flatten()?;
1226        let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
1227        let _: Result<(), JsValue> = display_context.scale(device_pixel_ratio, device_pixel_ratio);
1228        let offscreen_canvas: HtmlCanvasElement = document_value
1229            .create_element(RENDERER_ELEMENT_CANVAS)
1230            .ok()?
1231            .unchecked_into();
1232        let scaled_width: u32 = (width * scale_factor * device_pixel_ratio).round() as u32;
1233        let scaled_height: u32 = (height * scale_factor * device_pixel_ratio).round() as u32;
1234        offscreen_canvas.set_width(scaled_width);
1235        offscreen_canvas.set_height(scaled_height);
1236        let offscreen_context_object: Object = offscreen_canvas
1237            .get_context(RENDERER_CONTEXT_TYPE_2D)
1238            .ok()
1239            .flatten()?;
1240        let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
1241        let _: Result<(), JsValue> = offscreen_context.scale(
1242            scale_factor * device_pixel_ratio,
1243            scale_factor * device_pixel_ratio,
1244        );
1245        let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
1246            display_canvas,
1247            display_context,
1248            offscreen_canvas,
1249            offscreen_context,
1250            scale_factor,
1251            width,
1252            height,
1253        );
1254        ssaa_canvas.enable_smoothing();
1255        Some(ssaa_canvas)
1256    }
1257
1258    /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
1259    ///
1260    /// Clears the display canvas, then draws the offscreen canvas scaled
1261    /// down to the logical display size. The active `quality` preset is
1262    /// applied once at construction via `enable_smoothing` — there is
1263    /// no need to re-apply it every frame, since the preset never
1264    /// changes mid-render.
1265    ///
1266    /// #32: the previous version called `apply_quality` on every
1267    /// `present()`, costing `set_image_smoothing_enabled` + 2×Reflect
1268    /// + 4×from_str ≈ 7 JS crossings per frame for a value that was
1269    ///   invariant across the entire session.
1270    pub fn present(&self) {
1271        self.get_display_context()
1272            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1273        let _: Result<(), JsValue> = self
1274            .get_display_context()
1275            .draw_image_with_html_canvas_element_and_dw_and_dh(
1276                self.get_offscreen_canvas(),
1277                0.0,
1278                0.0,
1279                self.get_width(),
1280                self.get_height(),
1281            );
1282    }
1283
1284    /// Clears the offscreen buffer to transparent.
1285    pub fn clear(&self) {
1286        self.get_offscreen_context()
1287            .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1288    }
1289
1290    /// Clears the offscreen buffer and fills it with the given CSS color.
1291    ///
1292    /// # Arguments
1293    ///
1294    /// - `C: AsRef<str>` - The CSS color string.
1295    pub fn clear_color<C>(&self, color: C)
1296    where
1297        C: AsRef<str>,
1298    {
1299        self.get_offscreen_context()
1300            .set_fill_style_str(color.as_ref());
1301        self.get_offscreen_context()
1302            .fill_rect(0.0, 0.0, self.get_width(), self.get_height());
1303    }
1304
1305    /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
1306    ///
1307    /// Applies the active `quality` preset to both contexts via the shared
1308    /// `apply_quality` helper.
1309    pub fn enable_smoothing(&self) {
1310        let quality: RenderQuality = self.get_quality();
1311        CanvasRenderer::apply_quality(self.get_display_context(), quality);
1312        CanvasRenderer::apply_quality(self.get_offscreen_context(), quality);
1313    }
1314}
1315
1316/// Implements CSS composite operation string conversion for `BlendMode`.
1317impl BlendMode {
1318    /// Returns the CSS `globalCompositeOperation` string for this blend mode.
1319    ///
1320    /// # Returns
1321    ///
1322    /// - `&str` - The CSS composite operation string.
1323    pub fn to_css(&self) -> &str {
1324        match self {
1325            BlendMode::Normal => BLEND_MODE_NORMAL,
1326            BlendMode::Multiply => BLEND_MODE_MULTIPLY,
1327            BlendMode::Screen => BLEND_MODE_SCREEN,
1328            BlendMode::Lighter => BLEND_MODE_LIGHTER,
1329            BlendMode::Overlay => BLEND_MODE_OVERLAY,
1330            BlendMode::Darken => BLEND_MODE_DARKEN,
1331            BlendMode::Lighten => BLEND_MODE_LIGHTEN,
1332            BlendMode::ColorDodge => BLEND_MODE_COLOR_DODGE,
1333            BlendMode::ColorBurn => BLEND_MODE_COLOR_BURN,
1334            BlendMode::HardLight => BLEND_MODE_HARD_LIGHT,
1335            BlendMode::SoftLight => BLEND_MODE_SOFT_LIGHT,
1336            BlendMode::Difference => BLEND_MODE_DIFFERENCE,
1337            BlendMode::Exclusion => BLEND_MODE_EXCLUSION,
1338            BlendMode::Hue => BLEND_MODE_HUE,
1339            BlendMode::Saturation => BLEND_MODE_SATURATION,
1340            BlendMode::Color => BLEND_MODE_COLOR,
1341            BlendMode::Luminosity => BLEND_MODE_LUMINOSITY,
1342        }
1343    }
1344}
1345
1346/// Implements construction and canvas gradient creation for `LinearGradient`.
1347impl LinearGradient {
1348    /// Creates a new linear gradient from two points and a list of color stops.
1349    ///
1350    /// # Arguments
1351    ///
1352    /// - `Vector2D` - The start point.
1353    /// - `Vector2D` - The end point.
1354    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1355    ///
1356    /// # Returns
1357    ///
1358    /// - `LinearGradient` - The new gradient.
1359    pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
1360        LinearGradient::new(start, end, stops)
1361    }
1362
1363    /// Creates a `CanvasGradient` from this gradient definition on the given context.
1364    ///
1365    /// # Arguments
1366    ///
1367    /// - `&CanvasRenderingContext2d` - The canvas context.
1368    ///
1369    /// # Returns
1370    ///
1371    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1372    pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1373        let canvas_gradient: CanvasGradient = context.create_linear_gradient(
1374            self.get_start().get_x(),
1375            self.get_start().get_y(),
1376            self.get_end().get_x(),
1377            self.get_end().get_y(),
1378        );
1379        for (position, color) in self.get_stops() {
1380            let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1381        }
1382        Some(canvas_gradient)
1383    }
1384}
1385
1386/// Implements construction and canvas gradient creation for `RadialGradient`.
1387impl RadialGradient {
1388    /// Creates a new radial gradient from inner and outer circles and color stops.
1389    ///
1390    /// # Arguments
1391    ///
1392    /// - `Vector2D` - The inner circle center.
1393    /// - `f64` - The inner circle radius.
1394    /// - `Vector2D` - The outer circle center.
1395    /// - `f64` - The outer circle radius.
1396    /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1397    ///
1398    /// # Returns
1399    ///
1400    /// - `RadialGradient` - The new gradient.
1401    pub fn create(
1402        inner_center: Vector2D,
1403        inner_radius: f64,
1404        outer_center: Vector2D,
1405        outer_radius: f64,
1406        stops: Vec<(f64, String)>,
1407    ) -> RadialGradient {
1408        RadialGradient::new(
1409            inner_center,
1410            inner_radius,
1411            outer_center,
1412            outer_radius,
1413            stops,
1414        )
1415    }
1416
1417    /// Creates a `CanvasGradient` from this gradient definition on the given context.
1418    ///
1419    /// # Arguments
1420    ///
1421    /// - `&CanvasRenderingContext2d` - The canvas context.
1422    ///
1423    /// # Returns
1424    ///
1425    /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1426    pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1427        let canvas_gradient: CanvasGradient = context
1428            .create_radial_gradient(
1429                self.get_inner_center().get_x(),
1430                self.get_inner_center().get_y(),
1431                self.get_inner_radius(),
1432                self.get_outer_center().get_x(),
1433                self.get_outer_center().get_y(),
1434                self.get_outer_radius(),
1435            )
1436            .ok()?;
1437        for (position, color) in self.get_stops() {
1438            let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1439        }
1440        Some(canvas_gradient)
1441    }
1442}
1443
1444/// Implements construction methods for `ShadowConfig`.
1445impl ShadowConfig {
1446    /// Creates a shadow configuration with default values.
1447    ///
1448    /// # Returns
1449    ///
1450    /// - `ShadowConfig` - The default shadow configuration.
1451    pub fn create() -> ShadowConfig {
1452        ShadowConfig::new(
1453            RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
1454            RENDERER_DEFAULT_SHADOW_BLUR,
1455            0.0,
1456            0.0,
1457        )
1458    }
1459}
1460
1461/// Implements `Default` for `ShadowConfig` with default shadow values.
1462impl Default for ShadowConfig {
1463    /// Constructs a default [`ShadowConfig`] value.
1464    ///
1465    /// # Returns
1466    ///
1467    /// - `ShadowConfig` - A default-constructed instance with the documented initial state.
1468    fn default() -> ShadowConfig {
1469        ShadowConfig::create()
1470    }
1471}
1472
1473/// Implements construction methods for `RenderLayer`.
1474impl RenderLayer {
1475    /// Creates a render layer with the given z-index and visibility.
1476    ///
1477    /// # Arguments
1478    ///
1479    /// - `i32` - The z-index determining draw order.
1480    /// - `bool` - Whether the layer is visible.
1481    ///
1482    /// # Returns
1483    ///
1484    /// - `RenderLayer` - The new render layer.
1485    pub fn create(z_index: i32, visible: bool) -> RenderLayer {
1486        RenderLayer::new(z_index, visible)
1487    }
1488
1489    /// Creates a background render layer with z-index 0 and visibility enabled.
1490    ///
1491    /// # Returns
1492    ///
1493    /// - `RenderLayer` - The background layer.
1494    pub fn background() -> RenderLayer {
1495        RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
1496    }
1497
1498    /// Creates a foreground render layer with a high z-index and visibility enabled.
1499    ///
1500    /// # Returns
1501    ///
1502    /// - `RenderLayer` - The foreground layer.
1503    pub fn foreground() -> RenderLayer {
1504        RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1505    }
1506
1507    /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1508    ///
1509    /// # Returns
1510    ///
1511    /// - `RenderLayer` - The UI overlay layer.
1512    pub fn ui() -> RenderLayer {
1513        RenderLayer::new(RENDERER_LAYER_UI, true)
1514    }
1515}
1516
1517/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1518impl CanvasRenderer {
1519    /// Sets the blend mode for compositing subsequent draw operations.
1520    ///
1521    /// # Arguments
1522    ///
1523    /// - `BlendMode` - The blend mode to apply.
1524    pub fn set_blend_mode(&self, mode: BlendMode) {
1525        let _: Result<(), JsValue> = self
1526            .get_context()
1527            .set_global_composite_operation(mode.to_css());
1528    }
1529
1530    /// Applies a shadow configuration for subsequent draw operations.
1531    ///
1532    /// # Arguments
1533    ///
1534    /// - `&ShadowConfig` - The shadow configuration to apply.
1535    pub fn set_shadow(&self, config: &ShadowConfig) {
1536        self.get_context()
1537            .set_shadow_color(config.get_color().as_str());
1538        self.get_context().set_shadow_blur(config.get_blur());
1539        self.get_context()
1540            .set_shadow_offset_x(config.get_offset_x());
1541        self.get_context()
1542            .set_shadow_offset_y(config.get_offset_y());
1543    }
1544
1545    /// Clears any previously applied shadow, disabling shadow rendering.
1546    pub fn clear_shadow(&self) {
1547        self.get_context().set_shadow_color("rgba(0, 0, 0, 0)");
1548        self.get_context().set_shadow_blur(0.0);
1549        self.get_context().set_shadow_offset_x(0.0);
1550        self.get_context().set_shadow_offset_y(0.0);
1551    }
1552
1553    /// Applies a linear gradient as the fill style for subsequent operations.
1554    ///
1555    /// # Arguments
1556    ///
1557    /// - `&LinearGradient` - The linear gradient to use as fill style.
1558    pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1559        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1560            self.get_context()
1561                .set_fill_style_canvas_gradient(&canvas_gradient);
1562        }
1563    }
1564
1565    /// Applies a radial gradient as the fill style for subsequent operations.
1566    ///
1567    /// # Arguments
1568    ///
1569    /// - `&RadialGradient` - The radial gradient to use as fill style.
1570    pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1571        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1572            self.get_context()
1573                .set_fill_style_canvas_gradient(&canvas_gradient);
1574        }
1575    }
1576
1577    /// Applies a linear gradient as the stroke style for subsequent operations.
1578    ///
1579    /// # Arguments
1580    ///
1581    /// - `&LinearGradient` - The linear gradient to use as stroke style.
1582    pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1583        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1584            self.get_context()
1585                .set_stroke_style_canvas_gradient(&canvas_gradient);
1586        }
1587    }
1588
1589    /// Applies a radial gradient as the stroke style for subsequent operations.
1590    ///
1591    /// # Arguments
1592    ///
1593    /// - `&RadialGradient` - The radial gradient to use as stroke style.
1594    pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1595        if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1596            self.get_context()
1597                .set_stroke_style_canvas_gradient(&canvas_gradient);
1598        }
1599    }
1600}
1601
1602/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1603/// a backend-agnostic rendering interface.
1604///
1605/// Each method forwards to the inherent `CanvasRenderer` method of the
1606/// same name, so the per-call documentation lives on the trait definition
1607/// in `engine::renderer::trait` — the inherent method is the source of
1608/// truth, this impl is the trait bridge.
1609impl RenderBackend for CanvasRenderer {
1610    /// Forwards to [`CanvasRenderer::clear`].
1611    fn clear(&self) {
1612        self.clear();
1613    }
1614
1615    /// Forwards to [`CanvasRenderer::clear_color`].
1616    ///
1617    /// # Arguments
1618    ///
1619    /// - `C: AsRef<str>` - A generic type parameter.
1620    fn clear_color<C>(&self, color: C)
1621    where
1622        C: AsRef<str>,
1623    {
1624        self.clear_color(color);
1625    }
1626
1627    /// Forwards to [`CanvasRenderer::save`].
1628    fn save(&self) {
1629        self.save();
1630    }
1631
1632    /// Forwards to [`CanvasRenderer::restore`].
1633    fn restore(&self) {
1634        self.restore();
1635    }
1636
1637    /// Forwards to [`CanvasRenderer::set_fill_color`].
1638    ///
1639    /// # Arguments
1640    ///
1641    /// - `&str` - Shared reference to a `str`.
1642    fn set_fill_color(&self, color: &str) {
1643        self.set_fill_color(color);
1644    }
1645
1646    /// Forwards to [`CanvasRenderer::set_stroke_color`].
1647    ///
1648    /// # Arguments
1649    ///
1650    /// - `&str` - Shared reference to a `str`.
1651    fn set_stroke_color(&self, color: &str) {
1652        self.set_stroke_color(color);
1653    }
1654
1655    /// Forwards to [`CanvasRenderer::set_line_width`].
1656    ///
1657    /// # Arguments
1658    ///
1659    /// - `f64` - A 64-bit float (`f64`).
1660    fn set_line_width(&self, width: f64) {
1661        self.set_line_width(width);
1662    }
1663
1664    /// Forwards to [`CanvasRenderer::set_global_alpha`].
1665    ///
1666    /// # Arguments
1667    ///
1668    /// - `f64` - A 64-bit float (`f64`).
1669    fn set_global_alpha(&self, alpha: f64) {
1670        self.set_global_alpha(alpha);
1671    }
1672
1673    /// Forwards to [`CanvasRenderer::set_blend_mode`].
1674    ///
1675    /// # Arguments
1676    ///
1677    /// - `BlendMode` - A `BlendMode` parameter.
1678    fn set_blend_mode(&self, mode: BlendMode) {
1679        self.set_blend_mode(mode);
1680    }
1681
1682    /// Forwards to [`CanvasRenderer::set_shadow`].
1683    ///
1684    /// # Arguments
1685    ///
1686    /// - `&ShadowConfig` - Shared reference to a `ShadowConfig`.
1687    fn set_shadow(&self, config: &ShadowConfig) {
1688        self.set_shadow(config);
1689    }
1690
1691    /// Forwards to [`CanvasRenderer::clear_shadow`].
1692    fn clear_shadow(&self) {
1693        self.clear_shadow();
1694    }
1695
1696    /// Forwards to [`CanvasRenderer::fill_rect`].
1697    ///
1698    /// # Arguments
1699    ///
1700    /// - `Vector2D` - 2D vector (`Vector2D`).
1701    /// - `f64` - A 64-bit float (`f64`).
1702    /// - `f64` - A 64-bit float (`f64`).
1703    fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1704        self.fill_rect(position, width, height);
1705    }
1706
1707    /// Forwards to [`CanvasRenderer::stroke_rect`].
1708    ///
1709    /// # Arguments
1710    ///
1711    /// - `Vector2D` - 2D vector (`Vector2D`).
1712    /// - `f64` - A 64-bit float (`f64`).
1713    /// - `f64` - A 64-bit float (`f64`).
1714    fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1715        self.stroke_rect(position, width, height);
1716    }
1717
1718    /// Forwards to [`CanvasRenderer::fill_circle`].
1719    ///
1720    /// # Arguments
1721    ///
1722    /// - `Vector2D` - 2D vector (`Vector2D`).
1723    /// - `f64` - A 64-bit float (`f64`).
1724    fn fill_circle(&self, center: Vector2D, radius: f64) {
1725        self.fill_circle(center, radius);
1726    }
1727
1728    /// Forwards to [`CanvasRenderer::stroke_circle`].
1729    ///
1730    /// # Arguments
1731    ///
1732    /// - `Vector2D` - 2D vector (`Vector2D`).
1733    /// - `f64` - A 64-bit float (`f64`).
1734    fn stroke_circle(&self, center: Vector2D, radius: f64) {
1735        self.stroke_circle(center, radius);
1736    }
1737
1738    /// Forwards to [`CanvasRenderer::draw_line`].
1739    ///
1740    /// # Arguments
1741    ///
1742    /// - `Vector2D` - 2D vector (`Vector2D`).
1743    /// - `Vector2D` - 2D vector (`Vector2D`).
1744    fn draw_line(&self, start: Vector2D, end: Vector2D) {
1745        self.draw_line(start, end);
1746    }
1747
1748    /// Forwards to [`CanvasRenderer::fill_text`].
1749    ///
1750    /// # Arguments
1751    ///
1752    /// - `&str` - Shared reference to a `str`.
1753    /// - `Vector2D` - 2D vector (`Vector2D`).
1754    fn fill_text(&self, text: &str, position: Vector2D) {
1755        self.fill_text(text, position);
1756    }
1757
1758    /// Forwards to [`CanvasRenderer::set_font`].
1759    ///
1760    /// # Arguments
1761    ///
1762    /// - `&str` - Shared reference to a `str`.
1763    fn set_font(&self, font: &str) {
1764        self.set_font(font);
1765    }
1766
1767    /// Forwards to [`CanvasRenderer::draw_image`].
1768    ///
1769    /// # Arguments
1770    ///
1771    /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1772    /// - `Vector2D` - 2D vector (`Vector2D`).
1773    /// - `f64` - A 64-bit float (`f64`).
1774    /// - `f64` - A 64-bit float (`f64`).
1775    fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1776        self.draw_image(image, position, width, height);
1777    }
1778
1779    /// Forwards to [`CanvasRenderer::set_linear_gradient_fill`].
1780    ///
1781    /// # Arguments
1782    ///
1783    /// - `&LinearGradient` - Shared reference to a `LinearGradient`.
1784    fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1785        self.set_linear_gradient_fill(gradient);
1786    }
1787
1788    /// Forwards to [`CanvasRenderer::set_radial_gradient_fill`].
1789    ///
1790    /// # Arguments
1791    ///
1792    /// - `&RadialGradient` - Shared reference to a `RadialGradient`.
1793    fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1794        self.set_radial_gradient_fill(gradient);
1795    }
1796}
1797
1798/// Implements async initialization and GPU resource creation for `WebGpuRenderer`.
1799impl WebGpuRenderer {
1800    /// Returns `true` if `navigator.gpu` is exposed on the current origin.
1801    ///
1802    /// This is the synchronous half of the canonical WebGPU capability
1803    /// probe used by Three.js (`examples/jsm/capabilities/WebGPU.js`): it
1804    /// only checks that the browser surfaces the `GPU` interface at all.
1805    /// It does **not** request an adapter — a present `navigator.gpu`
1806    /// does not guarantee that a usable GPU adapter is reachable (Linux
1807    /// software-rendered sessions, headless browsers, GPU-blacklisted
1808    /// devices and sandboxed iframes all expose `navigator.gpu` while
1809    /// `requestAdapter()` resolves to `null` or hangs forever).
1810    ///
1811    /// Use this as the cheapest pre-flight check before showing a
1812    /// "needs HTTPS or localhost" prompt. For a definitive answer use
1813    /// [`Self::probe`] which also awaits `requestAdapter()`.
1814    ///
1815    /// # Returns
1816    ///
1817    /// - `bool` - `true` when `navigator.gpu` is a non-null, non-undefined
1818    ///   object; `false` otherwise (including the "no `window`" runtime
1819    ///   case, which `web_sys::window()` returns `None` for).
1820    pub fn is_available() -> bool {
1821        let window_value: Window = match window() {
1822            Some(value) => value,
1823            None => return false,
1824        };
1825        let navigator: Navigator = window_value.navigator();
1826        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
1827            navigator.as_ref(),
1828            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1829        );
1830        match gpu_result {
1831            Ok(value) => !value.is_undefined() && !value.is_null(),
1832            Err(_) => false,
1833        }
1834    }
1835
1836    /// Probes whether a WebGPU adapter can actually be acquired.
1837    ///
1838    /// Mirrors Three.js' canonical capability probe exactly:
1839    ///
1840    /// Wraps the adapter request in the same `Promise.race` timeout used
1841    /// by [`Self::init`] so that browsers which leave the adapter promise
1842    /// permanently pending (headless, sandboxed, device-lost) do not stall
1843    /// the UI forever. The timeout itself uses the
1844    /// `INIT_PROMISE_TIMEOUT_MILLIS` constant; on timeout, `probe` returns
1845    /// `false` rather than an error so callers can treat it the same as
1846    /// "no adapter".
1847    ///
1848    /// # Returns
1849    ///
1850    /// - `bool` - `true` only when both `navigator.gpu` is present and
1851    ///   `requestAdapter()` resolves to a non-null adapter within the
1852    ///   timeout window. `false` covers every other case (no `window`,
1853    ///   missing `navigator.gpu`, reflect exception, adapter promise
1854    ///   rejected or timed out, adapter resolved to `null`/`undefined`).
1855    pub async fn probe() -> bool {
1856        if !Self::is_available() {
1857            return false;
1858        }
1859        let window_value: Window = match window() {
1860            Some(value) => value,
1861            None => return false,
1862        };
1863        let navigator: Navigator = window_value.navigator();
1864        let gpu: JsValue = match Reflect::get(
1865            navigator.as_ref(),
1866            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1867        ) {
1868            Ok(value) => value,
1869            Err(_) => return false,
1870        };
1871        let request_adapter_fn: Function =
1872            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
1873                Ok(value) => value.unchecked_into(),
1874                Err(_) => return false,
1875            };
1876        let adapter_promise: Promise = match request_adapter_fn.call0(&gpu) {
1877            Ok(value) => value.unchecked_into(),
1878            Err(_) => return false,
1879        };
1880        let adapter_value: JsValue =
1881            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
1882                Ok(value) => value,
1883                Err(_) => return false,
1884            };
1885        !adapter_value.is_undefined() && !adapter_value.is_null()
1886    }
1887
1888    /// Asynchronously initializes a WebGPU renderer from the given render configuration.
1889    ///
1890    /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
1891    /// and configures it with the preferred texture format. Returns `None` if
1892    /// WebGPU is not supported, the adapter/device request fails, or the canvas
1893    /// element is not found.
1894    ///
1895    /// # Arguments
1896    ///
1897    /// - `&RenderConfig` - The rendering configuration.
1898    ///
1899    /// # Returns
1900    ///
1901    /// - `Option<WebGpuRenderer>` - The initialized renderer, or `None` on failure.
1902    ///   Maximum time in milliseconds to wait for `requestAdapter` and
1903    ///   `requestDevice` before treating them as failed.
1904    ///
1905    /// Some browser GPU states (headless, no GPU, sandboxed, device-lost)
1906    /// leave the WebGPU adapter/device promises permanently pending instead
1907    /// of resolving to `null` or rejecting. Without a timeout the
1908    /// `JsFuture::from(...).await` inside `init` would hang forever and
1909    /// the UI would stay stuck on `Initializing...`. Wrapping each promise
1910    /// in `Promise.race` against a timer-rejected sibling forces the
1911    /// future to resolve so the caller's `let Some(...) = ... else { ... }`
1912    /// branch can run and report `WebGPU Not Supported`.
1913    /// Returns a Promise that rejects after `INIT_PROMISE_TIMEOUT_MILLIS`.
1914    fn timeout_promise() -> Promise {
1915        let Some(window_value) = window() else {
1916            return Promise::new(&mut |_resolve: Function, reject: Function| {
1917                let _: Result<JsValue, JsValue> = reject.call1(
1918                    &JsValue::UNDEFINED,
1919                    &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
1920                );
1921            });
1922        };
1923        Promise::new(&mut |_resolve: Function, reject: Function| {
1924            let reject_fn: Function = reject.clone();
1925            let timer: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
1926                let _: Result<JsValue, JsValue> = reject_fn.call1(
1927                    &JsValue::UNDEFINED,
1928                    &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
1929                );
1930            }));
1931            let _: Result<i32, JsValue> = window_value
1932                .set_timeout_with_callback_and_timeout_and_arguments_0(
1933                    timer.as_ref().unchecked_ref(),
1934                    INIT_PROMISE_TIMEOUT_MILLIS,
1935                );
1936            timer.forget();
1937        })
1938    }
1939
1940    /// Wraps `promise` in `Promise.race([promise, timeout_promise()])` so that
1941    /// awaiting it never blocks longer than `INIT_PROMISE_TIMEOUT_MILLIS`.
1942    ///
1943    /// Calls `Promise.race` via reflection because wasm-bindgen does not
1944    /// currently expose the static `race` method on `Promise`.
1945    ///
1946    /// # Arguments
1947    ///
1948    /// - `Promise` - A `Promise` parameter.
1949    ///
1950    /// # Returns
1951    ///
1952    /// - `Promise` - A `Promise` value.
1953    fn race_with_timeout(promise: Promise) -> Promise {
1954        let array: Array = Array::of2(&promise, &Self::timeout_promise());
1955        Promise::race(&array)
1956    }
1957
1958    /// Asynchronously initializes a WebGPU renderer from the given render configuration.
1959    ///
1960    /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
1961    /// and configures it with the preferred texture format. Returns `Err` if
1962    /// WebGPU is not supported, the adapter/device request fails, the canvas
1963    /// element is not found, or the adapter/device request hangs beyond
1964    /// `INIT_PROMISE_TIMEOUT_MILLIS` (a defensive timeout for browser GPU
1965    /// states that leave the WebGPU promises permanently pending).
1966    ///
1967    /// The engine no longer logs diagnostic output internally; instead each
1968    /// failure mode is returned as a distinct `WebGpuInitError` variant so
1969    /// the caller can decide how to surface it (typically via `Console::error`
1970    /// or by falling back to the Canvas 2D backend).
1971    ///
1972    /// # Arguments
1973    ///
1974    /// - `&RenderConfig` - The rendering configuration.
1975    ///
1976    /// # Returns
1977    ///
1978    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer, or
1979    ///   a typed error describing the specific failure.
1980    pub async fn init(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
1981        let Some(window) = window() else {
1982            return Err(WebGpuInitError::NavigatorGpuMissing);
1983        };
1984        let navigator: Navigator = window.navigator();
1985        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
1986            navigator.as_ref(),
1987            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1988        );
1989        let gpu: JsValue = match gpu_result {
1990            Ok(value) => value,
1991            Err(err) => return Err(WebGpuInitError::NavigatorLookup(err)),
1992        };
1993        if gpu.is_undefined() || gpu.is_null() {
1994            return Err(WebGpuInitError::NavigatorGpuMissing);
1995        }
1996        let adapter_options: Object = Object::new();
1997        let _: Result<bool, JsValue> = Reflect::set(
1998            &adapter_options,
1999            &JsValue::from_str(WEBGPU_PROPERTY_POWER_PREFERENCE),
2000            &JsValue::from_str(config.power_preference.to_web_sys_string()),
2001        );
2002        let request_adapter_fn: Function =
2003            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
2004                Ok(value) => value.unchecked_into(),
2005                Err(err) => return Err(WebGpuInitError::RequestAdapterLookup(err)),
2006            };
2007        let adapter_promise: Promise = match request_adapter_fn.call1(&gpu, &adapter_options) {
2008            Ok(value) => value.unchecked_into(),
2009            Err(err) => return Err(WebGpuInitError::RequestAdapterCall(err)),
2010        };
2011        let adapter_value: JsValue =
2012            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
2013                Ok(value) => value,
2014                Err(err) => return Err(WebGpuInitError::AdapterPromise(err)),
2015            };
2016        if adapter_value.is_null() || adapter_value.is_undefined() {
2017            return Err(WebGpuInitError::AdapterUnavailable);
2018        }
2019        let device_descriptor: Object = Object::new();
2020        let request_device_fn: Function = match Reflect::get(
2021            &adapter_value,
2022            &JsValue::from_str(WEBGPU_METHOD_REQUEST_DEVICE),
2023        ) {
2024            Ok(value) => value.unchecked_into(),
2025            Err(err) => return Err(WebGpuInitError::RequestDeviceLookup(err)),
2026        };
2027        let device_promise: Promise =
2028            match request_device_fn.call1(&adapter_value, &device_descriptor) {
2029                Ok(value) => value.unchecked_into(),
2030                Err(err) => return Err(WebGpuInitError::RequestDeviceCall(err)),
2031            };
2032        let device_value: JsValue =
2033            match JsFuture::from(Self::race_with_timeout(device_promise)).await {
2034                Ok(value) => value,
2035                Err(err) => return Err(WebGpuInitError::DevicePromise(err)),
2036            };
2037        if device_value.is_null() || device_value.is_undefined() {
2038            return Err(WebGpuInitError::DeviceUnavailable);
2039        }
2040        let Some(document) = window.document() else {
2041            return Err(WebGpuInitError::CanvasNotFound(
2042                config.canvas_selector.clone(),
2043            ));
2044        };
2045        let element: Element = match document.query_selector(&config.canvas_selector) {
2046            Ok(Some(el)) => el,
2047            Ok(None) => {
2048                return Err(WebGpuInitError::CanvasNotFound(
2049                    config.canvas_selector.clone(),
2050                ));
2051            }
2052            Err(err) => return Err(WebGpuInitError::CanvasQuery(err)),
2053        };
2054        let canvas: HtmlCanvasElement = element.unchecked_into();
2055        let context_object: Option<Object> = canvas.get_context(WEBGPU_CONTEXT_TYPE).ok().flatten();
2056        let context_object: Object = match context_object {
2057            Some(c) => c,
2058            None => return Err(WebGpuInitError::CanvasContextUnavailable),
2059        };
2060        let context: JsValue = context_object.into();
2061        let get_format_fn: Function =
2062            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_GET_PREFERRED_FORMAT)) {
2063                Ok(value) => value.unchecked_into(),
2064                Err(err) => return Err(WebGpuInitError::PreferredFormatLookup(err)),
2065            };
2066        let format_value: JsValue = match get_format_fn.call0(&gpu) {
2067            Ok(value) => value,
2068            Err(err) => return Err(WebGpuInitError::PreferredFormatCall(err)),
2069        };
2070        let format: String = match format_value.as_string() {
2071            Some(s) => s,
2072            None => return Err(WebGpuInitError::PreferredFormatType(format_value)),
2073        };
2074        // WebGPU's `configure` requires the canvas backing-store size to be
2075        // set BEFORE calling configure, otherwise the swap chain is created
2076        // at 0x0 and the first getCurrentTexture() returns an error.
2077        let dpr: f64 = CanvasRenderer::detect_dpr();
2078        let physical_width: u32 = (config.width * dpr).round() as u32;
2079        let physical_height: u32 = (config.height * dpr).round() as u32;
2080        canvas.set_width(physical_width);
2081        canvas.set_height(physical_height);
2082        let canvas_config: Object = Object::new();
2083        let _: Result<bool, JsValue> = Reflect::set(
2084            &canvas_config,
2085            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
2086            &device_value,
2087        );
2088        let _: Result<bool, JsValue> = Reflect::set(
2089            &canvas_config,
2090            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2091            &format_value,
2092        );
2093        let configure_fn: Function =
2094            match Reflect::get(&context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE)) {
2095                Ok(value) => value.unchecked_into(),
2096                Err(err) => return Err(WebGpuInitError::ConfigureLookup(err)),
2097            };
2098        let _: Result<JsValue, JsValue> = configure_fn.call1(&context, &canvas_config);
2099        let queue: JsValue =
2100            match Reflect::get(&device_value, &JsValue::from_str(WEBGPU_PROPERTY_QUEUE)) {
2101                Ok(value) => value,
2102                Err(err) => return Err(WebGpuInitError::QueueLookup(err)),
2103            };
2104        Ok(WebGpuRenderer {
2105            device: device_value,
2106            queue,
2107            context,
2108            canvas,
2109            format,
2110            width: physical_width,
2111            height: physical_height,
2112            antialias: config.antialias,
2113            multisample_texture: None,
2114            multisample_view: None,
2115            depth_texture: None,
2116            depth_view: None,
2117            depth_format: None,
2118            device_lost_callback: None,
2119            device_lost: false,
2120            pending_error: Rc::new(PendingErrorCell::new()),
2121            command_encoder: None,
2122        })
2123    }
2124
2125    /// Allocates the multisampled intermediate texture used for MSAA.
2126    ///
2127    /// The returned tuple is `(GpuTexture, GpuTextureView)`:
2128    /// - `GpuTexture` has `sampleCount: 4` and `usage: RENDER_ATTACHMENT`
2129    ///   so it can be bound as a color attachment in `beginRenderPass`.
2130    /// - `GpuTextureView` is the default 2D view used as the color
2131    ///   attachment; the swap chain view is the `resolveTarget`.
2132    ///
2133    /// The texture size must match the swap chain physical size; mismatches
2134    /// are a WebGPU validation error. Returns `(JsValue::UNDEFINED,
2135    /// JsValue::UNDEFINED)` when allocation fails so callers can detect and
2136    /// fall back to MSAA=1.
2137    ///
2138    /// # Arguments
2139    ///
2140    /// - `u32` - Physical pixel width (DPR-multiplied).
2141    /// - `u32` - Physical pixel height.
2142    ///
2143    /// # Returns
2144    ///
2145    /// - `(JsValue, JsValue)` - The new texture and its default view, or
2146    ///   `JsValue::UNDEFINED` for both on allocation failure.
2147    fn create_multisample_texture(
2148        &self,
2149        physical_width: u32,
2150        physical_height: u32,
2151    ) -> (JsValue, JsValue) {
2152        let extent: Object = Object::new();
2153        let _: Result<bool, JsValue> = Reflect::set(
2154            &extent,
2155            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
2156            &JsValue::from_f64(f64::from(physical_width)),
2157        );
2158        let _: Result<bool, JsValue> = Reflect::set(
2159            &extent,
2160            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
2161            &JsValue::from_f64(f64::from(physical_height)),
2162        );
2163        let _: Result<bool, JsValue> = Reflect::set(
2164            &extent,
2165            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
2166            &JsValue::from_f64(1.0),
2167        );
2168        let descriptor: Object = Object::new();
2169        let _: Result<bool, JsValue> = Reflect::set(
2170            &descriptor,
2171            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2172            &extent,
2173        );
2174        let _: Result<bool, JsValue> = Reflect::set(
2175            &descriptor,
2176            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
2177            &JsValue::from_str(&self.get_format()),
2178        );
2179        let _: Result<bool, JsValue> = Reflect::set(
2180            &descriptor,
2181            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2182            &JsValue::from_f64(WEBGPU_TEXTURE_USAGE_RENDER_ATTACHMENT),
2183        );
2184        let _: Result<bool, JsValue> = Reflect::set(
2185            &descriptor,
2186            &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
2187            &JsValue::from_f64(4.0),
2188        );
2189        let create_texture_fn: Function = Reflect::get(
2190            self.get_device(),
2191            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
2192        )
2193        .unwrap_or(JsValue::UNDEFINED)
2194        .unchecked_into();
2195        let texture: JsValue = create_texture_fn
2196            .call1(self.get_device(), &descriptor)
2197            .unwrap_or(JsValue::UNDEFINED);
2198        if texture.is_undefined() {
2199            return (JsValue::UNDEFINED, JsValue::UNDEFINED);
2200        }
2201        let create_view_fn: Function =
2202            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2203                .unwrap_or(JsValue::UNDEFINED)
2204                .unchecked_into();
2205        let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
2206        if view.is_undefined() {
2207            return (texture, JsValue::UNDEFINED);
2208        }
2209        (texture, view)
2210    }
2211
2212    /// Resizes the canvas backing store and reconfigures the swap chain.
2213    ///
2214    /// WebGPU's `GpuCanvasContext.configure` is sticky: it sets the texture
2215    /// format and device once, but the swap chain tracks the canvas's
2216    /// `width`/`height` attributes. When the CSS layout size changes (a
2217    /// window resize, a panel toggle, a DPR change) the canvas keeps its
2218    /// old physical dimensions unless we explicitly update `width`/`height`
2219    /// and call `configure` again. Without this, subsequent
2220    /// `getCurrentTexture()` calls return a texture that no longer matches
2221    /// the visible region and the frame either stretches or freezes.
2222    ///
2223    /// Re-`configure`ing with the same `device` + `format` is the
2224    /// spec-defined way to swap in a fresh swap chain bound to the new
2225    /// backing-store size.
2226    ///
2227    /// # Arguments
2228    ///
2229    /// - `u32` - The new physical pixel width (already multiplied by DPR).
2230    /// - `u32` - The new physical pixel height.
2231    ///
2232    /// # Returns
2233    ///
2234    /// - `bool` - `true` on success, `false` if the swap chain or canvas
2235    ///   handles were missing or `configure` failed.
2236    pub fn resize(&mut self, physical_width: u32, physical_height: u32) -> bool {
2237        if self.get_canvas().is_null()
2238            || self.get_context().is_null()
2239            || self.get_device().is_undefined()
2240        {
2241            return false;
2242        }
2243        self.get_canvas().set_width(physical_width);
2244        self.get_canvas().set_height(physical_height);
2245        let format_value: JsValue = JsValue::from_str(&self.get_format());
2246        let canvas_config: Object = Object::new();
2247        let _: Result<bool, JsValue> = Reflect::set(
2248            &canvas_config,
2249            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
2250            self.get_device(),
2251        );
2252        let _: Result<bool, JsValue> = Reflect::set(
2253            &canvas_config,
2254            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2255            &format_value,
2256        );
2257        let configure_fn: Function = Reflect::get(
2258            self.get_context(),
2259            &JsValue::from_str(WEBGPU_METHOD_CONFIGURE),
2260        )
2261        .ok()
2262        .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
2263        .unwrap_or_else(|| Function::new_no_args(""));
2264        if configure_fn
2265            .call1(self.get_context(), &canvas_config)
2266            .is_err()
2267        {
2268            return false;
2269        }
2270        self.set_width(physical_width);
2271        self.set_height(physical_height);
2272        // Rebuild the multisampled color texture to match the new backing
2273        // store size. `GpuTexture` width/height are immutable, so MSAA
2274        // requires recreating it on every resize. The previous texture (if
2275        // any) is left to the GPU's GC; we do not explicitly destroy it
2276        // because `destroy()` is a synchronous WebGPU call and the old
2277        // texture is no longer referenced by any in-flight command buffer
2278        // at this point in the frame loop.
2279        if self.get_antialias() {
2280            let (texture, view) = self.create_multisample_texture(physical_width, physical_height);
2281            if !view.is_undefined() {
2282                self.set_multisample_texture(Some(texture));
2283                self.set_multisample_view(Some(view));
2284            } else {
2285                self.set_multisample_texture(None);
2286                self.set_multisample_view(None);
2287            }
2288        }
2289        true
2290    }
2291
2292    /// Resizes the canvas backing store to match the canvas element's
2293    /// current CSS-rendered size in physical pixels (DPR applied).
2294    ///
2295    /// This is the right entry point when the render loop does not know
2296    /// the desired logical size ahead of time and wants to follow the
2297    /// element's actual layout box. It is also useful as a defensive
2298    /// recovery when the canvas was created while hidden (zero-sized
2299    /// parent) and is later shown at its real size.
2300    ///
2301    /// Reads `client_width` / `client_height` from the canvas element,
2302    /// multiplies by `detect_dpr()`, and forwards to [`Self::resize`].
2303    ///
2304    /// # Returns
2305    ///
2306    /// - `bool` - `true` if the resize succeeded, `false` if the canvas
2307    ///   was zero-sized (nothing to render to), detached (CSS layout
2308    ///   box collapses to 0), or the underlying resize rejected.
2309    pub fn sync_to_current_canvas(&mut self) -> bool {
2310        let canvas_width: u32 = self.get_canvas().width();
2311        let canvas_height: u32 = self.get_canvas().height();
2312        let client_width: u32 = self
2313            .get_canvas()
2314            .client_width()
2315            .try_into()
2316            .unwrap_or_default();
2317        let client_height: u32 = self
2318            .get_canvas()
2319            .client_height()
2320            .try_into()
2321            .unwrap_or_default();
2322        // Prefer the CSS layout box when it is non-zero. If the canvas
2323        // is hidden the client box collapses to 0; in that case fall
2324        // back to the current backing-store size so we do not
2325        // gratuitously resize to 0.
2326        let css_w: u32 = if client_width > 0 {
2327            client_width
2328        } else {
2329            canvas_width
2330        };
2331        let css_h: u32 = if client_height > 0 {
2332            client_height
2333        } else {
2334            canvas_height
2335        };
2336        if css_w == 0 || css_h == 0 {
2337            return false;
2338        }
2339        let dpr: f64 = CanvasRenderer::detect_dpr();
2340        let physical_width: u32 = (f64::from(css_w) * dpr).round() as u32;
2341        let physical_height: u32 = (f64::from(css_h) * dpr).round() as u32;
2342        self.resize(physical_width, physical_height)
2343    }
2344
2345    /// Creates a shader module from WGSL source code.
2346    ///
2347    /// # Arguments
2348    ///
2349    /// - `S: AsRef<str>` - The WGSL shader source code.
2350    ///
2351    /// # Returns
2352    ///
2353    /// - `JsValue` - The created shader module as a JavaScript value.
2354    pub(crate) fn create_shader_module<S>(&self, code: S) -> JsValue
2355    where
2356        S: AsRef<str>,
2357    {
2358        let descriptor: Object = Object::new();
2359        let _: Result<bool, JsValue> = Reflect::set(
2360            &descriptor,
2361            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
2362            &JsValue::from_str(code.as_ref()),
2363        );
2364        let create_fn: Function = Reflect::get(
2365            self.get_device(),
2366            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
2367        )
2368        .unwrap_or(JsValue::UNDEFINED)
2369        .unchecked_into();
2370        create_fn
2371            .call1(self.get_device(), &descriptor)
2372            .unwrap_or(JsValue::UNDEFINED)
2373    }
2374
2375    /// Creates a new command encoder for recording GPU commands.
2376    ///
2377    /// # Returns
2378    ///
2379    /// - `JsValue` - The created command encoder as a JavaScript value.
2380    pub fn create_command_encoder(&self) -> JsValue {
2381        let create_fn: Function = Reflect::get(
2382            self.get_device(),
2383            &JsValue::from_str(WEBGPU_METHOD_CREATE_COMMAND_ENCODER),
2384        )
2385        .unwrap_or(JsValue::UNDEFINED)
2386        .unchecked_into();
2387        create_fn
2388            .call0(self.get_device())
2389            .unwrap_or(JsValue::UNDEFINED)
2390    }
2391
2392    /// Returns the current texture view from the canvas swap chain.
2393    ///
2394    /// This texture view should be used as the color attachment target for
2395    /// render passes. The texture is automatically presented to the canvas
2396    /// when the command buffer is submitted.
2397    ///
2398    /// # Returns
2399    ///
2400    /// - `JsValue` - The current frame's texture view as a JavaScript value.
2401    pub(crate) fn get_current_texture_view(&self) -> JsValue {
2402        let get_texture_fn: Function = Reflect::get(
2403            self.get_context(),
2404            &JsValue::from_str(WEBGPU_METHOD_GET_CURRENT_TEXTURE),
2405        )
2406        .unwrap_or(JsValue::UNDEFINED)
2407        .unchecked_into();
2408        let texture: JsValue = get_texture_fn
2409            .call0(self.get_context())
2410            .unwrap_or(JsValue::UNDEFINED);
2411        let create_view_fn: Function =
2412            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2413                .unwrap_or(JsValue::UNDEFINED)
2414                .unchecked_into();
2415        create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED)
2416    }
2417
2418    /// Begins a render pass on the given command encoder with a clear color.
2419    ///
2420    /// The render pass targets the canvas's current texture and clears it
2421    /// to the specified color. The returned `JsValue` is a `GpuRenderPassEncoder`
2422    /// that can be used to issue draw commands. The pass must be ended (via `end()`)
2423    /// before the command encoder is finished.
2424    ///
2425    /// This is a thin convenience wrapper over
2426    /// [`WebGpuRenderer::begin_render_pass_full`]. For pipelines that
2427    /// need depth testing, multiple color attachments, MSAA control,
2428    /// or `load`/`store` op customization, use the full version with
2429    /// a [`RenderPassColorAttachment`] (and optional
2430    /// [`RenderPassDepthStencilAttachment`]).
2431    ///
2432    /// # Arguments
2433    ///
2434    /// - `&JsValue` - The command encoder to begin the pass on.
2435    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
2436    ///
2437    /// # Returns
2438    ///
2439    /// - `JsValue` - The active render pass encoder as a JavaScript value.
2440    pub fn begin_render_pass(
2441        &mut self,
2442        encoder: &JsValue,
2443        clear_color: (f64, f64, f64, f64),
2444    ) -> JsValue {
2445        let mut color: RenderPassColorAttachment = RenderPassColorAttachment {
2446            view: None,
2447            resolve_target: None,
2448            clear_value: Some(clear_color),
2449            load_op: None,
2450            store_op: None,
2451        };
2452        self.begin_render_pass_full(encoder, &mut color, None)
2453    }
2454
2455    /// Begins a render pass with full control over attachments, load/store
2456    /// ops, MSAA resolve targets, and an optional depth-stencil attachment.
2457    ///
2458    /// This is the "complete" render-pass API used by the rest of the
2459    /// engine. All other render-pass entry points (including the
2460    /// legacy `begin_render_pass(clear_color)` wrapper) funnel through
2461    /// here.
2462    ///
2463    /// The color attachment's `view` is filled in lazily when `None`:
2464    /// if `antialias == true` and the multisample intermediate is
2465    /// available (or can be allocated), the pass draws into the MSAA
2466    /// view and resolves into the swap chain; otherwise it draws
2467    /// directly into the swap chain. The `resolve_target` is filled in
2468    /// with the swap-chain view when MSAA is active and the caller
2469    /// did not provide one.
2470    ///
2471    /// # Arguments
2472    ///
2473    /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
2474    /// - `color` - The color attachment descriptor. `color.view` and
2475    ///   `color.resolve_target` may be `None`; they are filled in with
2476    ///   the renderer's defaults.
2477    /// - `depth` - An optional depth-stencil attachment. `Some(...)`
2478    ///   adds a `depthStencilAttachment` field to the pass
2479    ///   descriptor; `None` omits it entirely.
2480    ///
2481    /// # Returns
2482    ///
2483    /// - `JsValue` - The active `GpuRenderPassEncoder` as a JavaScript
2484    ///   value, suitable for the existing `set_pipeline` / `draw` /
2485    ///   `end_render_pass` calls.
2486    pub fn begin_render_pass_full(
2487        &mut self,
2488        encoder: &JsValue,
2489        color: &mut RenderPassColorAttachment,
2490        depth: Option<&RenderPassDepthStencilAttachment>,
2491    ) -> JsValue {
2492        let swap_chain_view: JsValue = self.get_current_texture_view();
2493        // Resolve MSAA view + resolve target with the same policy as
2494        // the legacy `begin_render_pass`: prefer the existing
2495        // multisample view, lazily allocate it if missing, and fall
2496        // back to direct-to-swap-chain if MSAA allocation fails.
2497        let (color_view, resolve_view): (JsValue, Option<JsValue>) = match color.view.take() {
2498            Some(view) if !view.is_undefined() => (view, color.resolve_target.take()),
2499            _ => {
2500                if self.get_antialias() {
2501                    let multisample_view: Option<JsValue> = self
2502                        .get_multisample_view()
2503                        .clone()
2504                        .filter(|value: &JsValue| !value.is_undefined());
2505                    let resolved: Option<JsValue> = match multisample_view {
2506                        Some(view) => Some(view),
2507                        None => {
2508                            let width: u32 = self.get_width();
2509                            let height: u32 = self.get_height();
2510                            let (texture, view): (JsValue, JsValue) =
2511                                self.create_multisample_texture(width, height);
2512                            if !view.is_undefined() {
2513                                self.set_multisample_texture(Some(texture));
2514                                self.set_multisample_view(Some(view.clone()));
2515                                Some(view)
2516                            } else {
2517                                self.set_multisample_texture(None);
2518                                self.set_multisample_view(None);
2519                                None
2520                            }
2521                        }
2522                    };
2523                    match resolved {
2524                        Some(view) => (view, Some(swap_chain_view.clone())),
2525                        None => (swap_chain_view.clone(), None),
2526                    }
2527                } else {
2528                    (swap_chain_view.clone(), None)
2529                }
2530            }
2531        };
2532        let attachment: Object = Object::new();
2533        let _: Result<bool, JsValue> = Reflect::set(
2534            &attachment,
2535            &cached_method_name(WEBGPU_PROPERTY_VIEW),
2536            &color_view,
2537        );
2538        let _: Result<bool, JsValue> = Reflect::set(
2539            &attachment,
2540            &cached_method_name(WEBGPU_PROPERTY_LOAD_OP),
2541            &JsValue::from_str(color.effective_load_op()),
2542        );
2543        let _: Result<bool, JsValue> = Reflect::set(
2544            &attachment,
2545            &cached_method_name(WEBGPU_PROPERTY_STORE_OP),
2546            &JsValue::from_str(color.effective_store_op()),
2547        );
2548        if let Some(cv) = color.clear_value {
2549            let color_dict: Object = Object::new();
2550            let _: Result<bool, JsValue> = Reflect::set(
2551                &color_dict,
2552                &cached_method_name(WEBGPU_PROPERTY_R),
2553                &JsValue::from_f64(cv.0),
2554            );
2555            let _: Result<bool, JsValue> = Reflect::set(
2556                &color_dict,
2557                &cached_method_name(WEBGPU_PROPERTY_G),
2558                &JsValue::from_f64(cv.1),
2559            );
2560            let _: Result<bool, JsValue> = Reflect::set(
2561                &color_dict,
2562                &cached_method_name(WEBGPU_PROPERTY_B),
2563                &JsValue::from_f64(cv.2),
2564            );
2565            let _: Result<bool, JsValue> = Reflect::set(
2566                &color_dict,
2567                &cached_method_name(WEBGPU_PROPERTY_A),
2568                &JsValue::from_f64(cv.3),
2569            );
2570            let _: Result<bool, JsValue> = Reflect::set(
2571                &attachment,
2572                &cached_method_name(WEBGPU_PROPERTY_CLEAR_VALUE),
2573                &color_dict,
2574            );
2575        }
2576        if let Some(target) = resolve_view.as_ref() {
2577            let _: Result<bool, JsValue> = Reflect::set(
2578                &attachment,
2579                &cached_method_name(WEBGPU_PROPERTY_RESOLVE_TARGET),
2580                target,
2581            );
2582        }
2583        let color_attachments: Array = Array::new();
2584        color_attachments.push(&attachment);
2585        let descriptor: Object = Object::new();
2586        let _: Result<bool, JsValue> = Reflect::set(
2587            &descriptor,
2588            &JsValue::from_str(WEBGPU_PROPERTY_COLOR_ATTACHMENTS),
2589            &color_attachments,
2590        );
2591        if let Some(depth_desc) = depth {
2592            // Prefer the caller-provided view; otherwise lazily
2593            // allocate the default depth-stencil texture and use its
2594            // view.
2595            let depth_view: JsValue = match depth_desc.view.clone() {
2596                Some(v) if !v.is_undefined() => v,
2597                _ => match self.create_depth_texture() {
2598                    Some(v) => v,
2599                    None => JsValue::UNDEFINED,
2600                },
2601            };
2602            if !depth_view.is_undefined() {
2603                let depth_attachment: Object = Object::new();
2604                let _: Result<bool, JsValue> = Reflect::set(
2605                    &depth_attachment,
2606                    &JsValue::from_str(WEBGPU_PROPERTY_VIEW),
2607                    &depth_view,
2608                );
2609                let _: Result<bool, JsValue> = Reflect::set(
2610                    &depth_attachment,
2611                    &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_LOAD_OP),
2612                    &JsValue::from_str(depth_desc.effective_depth_load_op()),
2613                );
2614                let _: Result<bool, JsValue> = Reflect::set(
2615                    &depth_attachment,
2616                    &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STORE_OP),
2617                    &JsValue::from_str(depth_desc.effective_depth_store_op()),
2618                );
2619                if let Some(clear) = depth_desc.depth_clear_value {
2620                    let _: Result<bool, JsValue> = Reflect::set(
2621                        &depth_attachment,
2622                        &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_CLEAR_VALUE),
2623                        &JsValue::from_f64(f64::from(clear)),
2624                    );
2625                }
2626                if let Some(read_only) = depth_desc.depth_read_only {
2627                    let _: Result<bool, JsValue> = Reflect::set(
2628                        &depth_attachment,
2629                        &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_READ_ONLY),
2630                        &JsValue::from_bool(read_only),
2631                    );
2632                }
2633                let _: Result<bool, JsValue> = Reflect::set(
2634                    &descriptor,
2635                    &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STENCIL_ATTACHMENT),
2636                    &depth_attachment,
2637                );
2638            }
2639        }
2640        let begin_fn: Function =
2641            Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_BEGIN_RENDER_PASS))
2642                .unwrap_or(JsValue::UNDEFINED)
2643                .unchecked_into();
2644        begin_fn
2645            .call1(encoder, &descriptor)
2646            .unwrap_or(JsValue::UNDEFINED)
2647    }
2648
2649    /// Submits an array of command buffers to the GPU queue for execution.
2650    ///
2651    /// # Arguments
2652    ///
2653    /// - `&[JsValue]` - The command buffers to submit.
2654    pub fn submit(&self, command_buffers: &[JsValue]) {
2655        let array: Array = Array::new();
2656        for buffer in command_buffers {
2657            array.push(buffer);
2658        }
2659        let submit_fn: Function =
2660            Reflect::get(self.get_queue(), &cached_method_name(WEBGPU_METHOD_SUBMIT))
2661                .unwrap_or(JsValue::UNDEFINED)
2662                .unchecked_into();
2663        let _: Result<JsValue, JsValue> = submit_fn.call1(self.get_queue(), &array);
2664    }
2665
2666    /// Creates a simple render pipeline from a single WGSL shader source.
2667    ///
2668    /// The shader must contain `@vertex fn vs_main(...)` and
2669    /// `@fragment fn fs_main(...)` entry points. No vertex buffers are used;
2670    /// vertex positions should be derived from `@builtin(vertex_index)` in
2671    /// the shader. The pipeline uses auto-layout (`layout: null`), which works
2672    /// when the shader has no bind groups.
2673    ///
2674    /// This is the legacy "trivial" wrapper. For pipelines that need
2675    /// vertex buffers, custom entry-point names, or a depth-stencil
2676    /// state, use [`WebGpuRenderer::create_render_pipeline_full`].
2677    ///
2678    /// # Arguments
2679    ///
2680    /// - `S: AsRef<str>` - The WGSL shader source code.
2681    ///
2682    /// # Returns
2683    ///
2684    /// - `JsValue` - The created render pipeline as a JavaScript value.
2685    pub fn create_render_pipeline<S>(&self, shader_code: S) -> JsValue
2686    where
2687        S: AsRef<str>,
2688    {
2689        self.create_render_pipeline_full(
2690            shader_code,
2691            &[],
2692            WEBGPU_VERTEX_ENTRY_POINT,
2693            WEBGPU_FRAGMENT_ENTRY_POINT,
2694            None,
2695        )
2696    }
2697
2698    /// Creates a render pipeline with full control over vertex buffer
2699    /// layouts, shader entry-point names, and an optional depth-stencil
2700    /// state.
2701    ///
2702    /// The `vertex_buffer_layouts` slice is forwarded as the
2703    /// `vertex.buffers` array of the pipeline descriptor; the i-th
2704    /// element matches `setVertexBuffer(i, ...)` calls. Pass `&[]` for
2705    /// the legacy "use `@builtin(vertex_index)`" path.
2706    ///
2707    /// The `depth_format` argument, when `Some`, sets
2708    /// `depthStencil.format` on the descriptor; the rest of the depth
2709    /// state (`depthWriteEnabled`, `depthCompare`) is left at the
2710    /// WebGPU defaults (true / `less`). Callers that need different
2711    /// depth state can pass the descriptor's name string and rely on
2712    /// the default depth-write/-compare behavior; for non-default
2713    /// compare/write, prefer using `RenderConfig` and a custom shader
2714    /// that performs the test explicitly.
2715    ///
2716    /// # Arguments
2717    ///
2718    /// - `shader_code` - The WGSL shader source code.
2719    /// - `vertex_buffer_layouts` - The list of vertex buffer layouts
2720    ///   for the pipeline's vertex state.
2721    /// - `vertex_entry` - The vertex shader entry-point name
2722    ///   (e.g. `"vs_main"`).
2723    /// - `fragment_entry` - The fragment shader entry-point name
2724    ///   (e.g. `"fs_main"`).
2725    /// - `depth_format` - An optional depth-stencil format (e.g.
2726    ///   `"depth24plus-stencil8"`). `None` omits the
2727    ///   `depthStencil` field from the descriptor.
2728    ///
2729    /// # Returns
2730    ///
2731    /// - `JsValue` - The created render pipeline as a JavaScript value.
2732    pub fn create_render_pipeline_full<S>(
2733        &self,
2734        shader_code: S,
2735        vertex_buffer_layouts: &[VertexBufferLayout],
2736        vertex_entry: &str,
2737        fragment_entry: &str,
2738        depth_format: Option<&str>,
2739    ) -> JsValue
2740    where
2741        S: AsRef<str>,
2742    {
2743        let module: JsValue = self.create_shader_module(shader_code);
2744        let vertex_state: Object = Object::new();
2745        let _: Result<bool, JsValue> = Reflect::set(
2746            &vertex_state,
2747            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
2748            &module,
2749        );
2750        let _: Result<bool, JsValue> = Reflect::set(
2751            &vertex_state,
2752            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
2753            &JsValue::from_str(vertex_entry),
2754        );
2755        let buffers: Array = Array::new();
2756        for layout in vertex_buffer_layouts {
2757            let layout_obj: Object = Object::new();
2758            let _: Result<bool, JsValue> = Reflect::set(
2759                &layout_obj,
2760                &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_STRIDE),
2761                &JsValue::from_f64(layout.get_array_stride() as f64),
2762            );
2763            let _: Result<bool, JsValue> = Reflect::set(
2764                &layout_obj,
2765                &JsValue::from_str(WEBGPU_PROPERTY_STEP_MODE),
2766                &JsValue::from_str(layout.get_step_mode().as_str()),
2767            );
2768            let attrs: Array = Array::new();
2769            for attribute in layout.get_attributes() {
2770                let attr: Object = Object::new();
2771                let _: Result<bool, JsValue> = Reflect::set(
2772                    &attr,
2773                    &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2774                    &JsValue::from_str(attribute.get_format()),
2775                );
2776                let _: Result<bool, JsValue> = Reflect::set(
2777                    &attr,
2778                    &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
2779                    &JsValue::from_f64(attribute.get_offset() as f64),
2780                );
2781                let _: Result<bool, JsValue> = Reflect::set(
2782                    &attr,
2783                    &JsValue::from_str(WEBGPU_PROPERTY_SHADER_LOCATION),
2784                    &JsValue::from_f64(f64::from(attribute.get_shader_location())),
2785                );
2786                attrs.push(&attr);
2787            }
2788            let _: Result<bool, JsValue> = Reflect::set(
2789                &layout_obj,
2790                &JsValue::from_str(WEBGPU_PROPERTY_ATTRIBUTES),
2791                &attrs,
2792            );
2793            buffers.push(&layout_obj);
2794        }
2795        let _: Result<bool, JsValue> = Reflect::set(
2796            &vertex_state,
2797            &JsValue::from_str(WEBGPU_PROPERTY_BUFFERS),
2798            &buffers,
2799        );
2800        let target: Object = Object::new();
2801        let _: Result<bool, JsValue> = Reflect::set(
2802            &target,
2803            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2804            &JsValue::from_str(&self.get_format()),
2805        );
2806        let targets: Array = Array::new();
2807        targets.push(&target);
2808        let fragment_state: Object = Object::new();
2809        let _: Result<bool, JsValue> = Reflect::set(
2810            &fragment_state,
2811            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
2812            &module,
2813        );
2814        let _: Result<bool, JsValue> = Reflect::set(
2815            &fragment_state,
2816            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
2817            &JsValue::from_str(fragment_entry),
2818        );
2819        let _: Result<bool, JsValue> = Reflect::set(
2820            &fragment_state,
2821            &JsValue::from_str(WEBGPU_PROPERTY_TARGETS),
2822            &targets,
2823        );
2824        let primitive: Object = Object::new();
2825        let _: Result<bool, JsValue> = Reflect::set(
2826            &primitive,
2827            &JsValue::from_str(WEBGPU_PROPERTY_TOPOLOGY),
2828            &JsValue::from_str(WEBGPU_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST),
2829        );
2830        // Wire the renderer-level `antialias` flag through to MSAA sample count.
2831        // Previously the flag was stored on the struct but never read by the
2832        // pipeline builder, leaving every pipeline at MSAA=1 (no anti-aliasing)
2833        // — visible as sub-pixel aliasing on triangle edges, particularly at
2834        // small canvas sizes like the 600x400 game_2d example. Enabling MSAA=4
2835        // when `antialias` is true restores hardware multisampling so edges
2836        // resolve cleanly without per-edge shader work.
2837        let multisample: Object = Object::new();
2838        let _: Result<bool, JsValue> = Reflect::set(
2839            &multisample,
2840            &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
2841            &JsValue::from_f64(if self.get_antialias() { 4.0 } else { 1.0 }),
2842        );
2843        let descriptor: Object = Object::new();
2844        let _: Result<bool, JsValue> = Reflect::set(
2845            &descriptor,
2846            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
2847            &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
2848        );
2849        let _: Result<bool, JsValue> = Reflect::set(
2850            &descriptor,
2851            &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
2852            &vertex_state,
2853        );
2854        let _: Result<bool, JsValue> = Reflect::set(
2855            &descriptor,
2856            &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
2857            &fragment_state,
2858        );
2859        let _: Result<bool, JsValue> = Reflect::set(
2860            &descriptor,
2861            &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
2862            &primitive,
2863        );
2864        let _: Result<bool, JsValue> = Reflect::set(
2865            &descriptor,
2866            &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLE),
2867            &multisample,
2868        );
2869        if let Some(format) = depth_format {
2870            let depth_stencil: Object = Object::new();
2871            let _: Result<bool, JsValue> = Reflect::set(
2872                &depth_stencil,
2873                &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2874                &JsValue::from_str(format),
2875            );
2876            let _: Result<bool, JsValue> = Reflect::set(
2877                &depth_stencil,
2878                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_WRITE_ENABLED),
2879                &JsValue::from_bool(true),
2880            );
2881            let _: Result<bool, JsValue> = Reflect::set(
2882                &depth_stencil,
2883                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_COMPARE),
2884                &JsValue::from_str(WEBGPU_COMPARE_LESS),
2885            );
2886            let _: Result<bool, JsValue> = Reflect::set(
2887                &descriptor,
2888                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STENCIL),
2889                &depth_stencil,
2890            );
2891        }
2892        let create_fn: Function = Reflect::get(
2893            self.get_device(),
2894            &JsValue::from_str(WEBGPU_METHOD_CREATE_RENDER_PIPELINE),
2895        )
2896        .unwrap_or(JsValue::UNDEFINED)
2897        .unchecked_into();
2898        create_fn
2899            .call1(self.get_device(), &descriptor)
2900            .unwrap_or(JsValue::UNDEFINED)
2901    }
2902
2903    /// Sets the render pipeline on a render pass encoder.
2904    ///
2905    /// # Arguments
2906    ///
2907    /// - `&JsValue` - The render pass encoder.
2908    /// - `&JsValue` - The render pipeline to set.
2909    pub fn set_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
2910        let set_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE))
2911            .unwrap_or(JsValue::UNDEFINED)
2912            .unchecked_into();
2913        let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
2914    }
2915
2916    /// Binds a vertex buffer at the given slot on a render pass encoder.
2917    ///
2918    /// This is the missing link between `create_render_pipeline_full` /
2919    /// `create_render_pipeline_with_layout` and the actual draw call:
2920    /// without `set_vertex_buffer` the GPU has no idea what attribute
2921    /// data the vertex shader's `@location(N)` references point at.
2922    /// Calling this with `buffer.is_undefined()` is a silent no-op
2923    /// (matches the WebGPU spec).
2924    ///
2925    /// # Arguments
2926    ///
2927    /// - `&JsValue` - The render pass encoder.
2928    /// - `u32` - The slot index; matches the slot the vertex buffer
2929    ///   was declared at in the pipeline's `vertex.buffers` array.
2930    /// - `&JsValue` - The `GpuBuffer` to bind (typically obtained
2931    ///   from `create_vertex_buffer`).
2932    pub fn set_vertex_buffer(&self, pass: &JsValue, slot: u32, buffer: &JsValue) {
2933        if buffer.is_undefined() || buffer.is_null() {
2934            return;
2935        }
2936        let set_fn: Function =
2937            Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_VERTEX_BUFFER))
2938                .unwrap_or(JsValue::UNDEFINED)
2939                .unchecked_into();
2940        let _: Result<JsValue, JsValue> =
2941            set_fn.call2(pass, &JsValue::from_f64(f64::from(slot)), buffer);
2942    }
2943
2944    /// Binds an index buffer on a render pass encoder.
2945    ///
2946    /// Once bound, subsequent `draw_indexed` calls read their indices
2947    /// from this buffer. `format` must be either `"uint16"` or
2948    /// `"uint32"` — see [`WEBGPU_INDEX_FORMAT_UINT16`] and
2949    /// [`WEBGPU_INDEX_FORMAT_UINT32`].
2950    ///
2951    /// # Arguments
2952    ///
2953    /// - `&JsValue` - The render pass encoder.
2954    /// - `&JsValue` - The `GpuBuffer` containing the index list.
2955    /// - `&str` - Either `"uint16"` or `"uint32"`. A different value
2956    ///   triggers a WebGPU validation error at the next draw.
2957    pub fn set_index_buffer(&self, pass: &JsValue, buffer: &JsValue, format: &str) {
2958        if buffer.is_undefined() || buffer.is_null() {
2959            return;
2960        }
2961        let set_fn: Function =
2962            Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_INDEX_BUFFER))
2963                .unwrap_or(JsValue::UNDEFINED)
2964                .unchecked_into();
2965        let _: Result<JsValue, JsValue> = set_fn.call2(pass, buffer, &JsValue::from_str(format));
2966    }
2967
2968    /// Draws primitives on a render pass encoder.
2969    ///
2970    /// # Arguments
2971    ///
2972    /// - `&JsValue` - The render pass encoder.
2973    /// - `u32` - The number of vertices to draw.
2974    /// - `u32` - The number of instances to draw.
2975    pub fn draw(&self, pass: &JsValue, vertex_count: u32, instance_count: u32) {
2976        let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW))
2977            .unwrap_or(JsValue::UNDEFINED)
2978            .unchecked_into();
2979        let _: Result<JsValue, JsValue> = draw_fn.call2(
2980            pass,
2981            &JsValue::from_f64(f64::from(vertex_count)),
2982            &JsValue::from_f64(f64::from(instance_count)),
2983        );
2984    }
2985
2986    /// Draws indexed primitives on a render pass encoder.
2987    ///
2988    /// The index buffer must already be bound via [`set_index_buffer`].
2989    /// This is the modern path for everything that needs shared vertex
2990    /// data (mesh renderers, terrain, instanced objects).
2991    ///
2992    /// # Arguments
2993    ///
2994    /// - `&JsValue` - The render pass encoder.
2995    /// - `u32` - The number of indices to consume.
2996    /// - `u32` - The number of instances to draw.
2997    pub fn draw_indexed(&self, pass: &JsValue, index_count: u32, instance_count: u32) {
2998        let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW_INDEXED))
2999            .unwrap_or(JsValue::UNDEFINED)
3000            .unchecked_into();
3001        let _: Result<JsValue, JsValue> = draw_fn.call2(
3002            pass,
3003            &JsValue::from_f64(f64::from(index_count)),
3004            &JsValue::from_f64(f64::from(instance_count)),
3005        );
3006    }
3007
3008    /// Variant of [`draw_indexed`] that lets the caller pick a byte
3009    /// offset into the bound index buffer.
3010    ///
3011    /// `index_offset` is measured in indices, not bytes — matching
3012    /// `GpuRenderPassEncoder.drawIndexed(indexCount, instanceCount,
3013    /// firstIndex)`'s implicit index-offset behaviour.
3014    pub fn draw_indexed_offset(
3015        &self,
3016        pass: &JsValue,
3017        index_offset: u32,
3018        index_count: u32,
3019        instance_count: u32,
3020    ) {
3021        let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW_INDEXED))
3022            .unwrap_or(JsValue::UNDEFINED)
3023            .unchecked_into();
3024        // WebGPU's `drawIndexed` accepts (indexCount, instanceCount,
3025        // firstIndex?, baseVertex?, firstInstance?). When we want to
3026        // start at a non-zero index we encode the first-index as part
3027        // of the index buffer offset on bind (`setIndexBuffer(buffer,
3028        // format, offset)`); we keep this helper for future symmetry
3029        // with WebGPU's `drawIndexed(firstIndex)` form.
3030        let _: Result<JsValue, JsValue> = draw_fn.call3(
3031            pass,
3032            &JsValue::from_f64(f64::from(index_count)),
3033            &JsValue::from_f64(f64::from(instance_count)),
3034            &JsValue::from_f64(f64::from(index_offset)),
3035        );
3036    }
3037
3038    /// Ends a render pass on the given pass encoder.
3039    ///
3040    /// # Arguments
3041    ///
3042    /// - `&JsValue` - The render pass encoder to end.
3043    pub fn end_render_pass(&self, pass: &JsValue) {
3044        let end_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_END))
3045            .unwrap_or(JsValue::UNDEFINED)
3046            .unchecked_into();
3047        let _: Result<JsValue, JsValue> = end_fn.call0(pass);
3048    }
3049
3050    /// Finishes a command encoder and returns the resulting command buffer.
3051    ///
3052    /// # Arguments
3053    ///
3054    /// - `&JsValue` - The command encoder to finish.
3055    ///
3056    /// # Returns
3057    ///
3058    /// - `JsValue` - The finished command buffer.
3059    pub fn finish_command_encoder(&self, encoder: &JsValue) -> JsValue {
3060        let finish_fn: Function = Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_FINISH))
3061            .unwrap_or(JsValue::UNDEFINED)
3062            .unchecked_into();
3063        finish_fn.call0(encoder).unwrap_or(JsValue::UNDEFINED)
3064    }
3065
3066    /// Creates a GPU uniform buffer and initializes it with the given floats.
3067    ///
3068    /// The buffer is created with `UNIFORM | COPY_DST` usage so it can be
3069    /// bound in a bind group and refreshed per frame via
3070    /// [`WebGpuRenderer::update_uniform_buffer`]. The allocation size is
3071    /// rounded up to a multiple of 16 bytes because WebGPU requires uniform
3072    /// buffer bindings to be 16-byte aligned in size (a bare `vec2<f32>`
3073    /// uniform is only 8 bytes).
3074    ///
3075    /// # Arguments
3076    ///
3077    /// - `&[f32]` - The initial uniform contents (e.g. `[x, y]` for a
3078    ///   `vec2<f32>` uniform).
3079    ///
3080    /// # Returns
3081    ///
3082    /// - `JsValue` - The created `GpuBuffer`.
3083    pub fn create_uniform_buffer(&self, data: &[f32]) -> JsValue {
3084        let byte_len: usize = data.len() * 4;
3085        let size: f64 = byte_len.div_ceil(16).max(1) as f64 * 16.0;
3086        let descriptor: Object = Object::new();
3087        let _: Result<bool, JsValue> = Reflect::set(
3088            &descriptor,
3089            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3090            &JsValue::from_f64(size),
3091        );
3092        let _: Result<bool, JsValue> = Reflect::set(
3093            &descriptor,
3094            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3095            &JsValue::from_f64(WEBGPU_BUFFER_USAGE_UNIFORM + WEBGPU_BUFFER_USAGE_COPY_DST),
3096        );
3097        let create_fn: Function = Reflect::get(
3098            self.get_device(),
3099            &JsValue::from_str(WEBGPU_METHOD_CREATE_BUFFER),
3100        )
3101        .unwrap_or(JsValue::UNDEFINED)
3102        .unchecked_into();
3103        let buffer: JsValue = create_fn
3104            .call1(self.get_device(), &descriptor)
3105            .unwrap_or(JsValue::UNDEFINED);
3106        self.update_uniform_buffer(&buffer, data);
3107        buffer
3108    }
3109
3110    /// Uploads float data into an existing uniform buffer via `queue.writeBuffer`.
3111    ///
3112    /// # Arguments
3113    ///
3114    /// - `&JsValue` - The `GpuBuffer` previously created by
3115    ///   [`WebGpuRenderer::create_uniform_buffer`].
3116    /// - `&[f32]` - The new uniform contents.
3117    pub fn update_uniform_buffer(&self, buffer: &JsValue, data: &[f32]) {
3118        // OPT 31: zero-copy view over the wasm linear-memory slice. The old
3119        // `Float32Array::from(data)` form allocates a new typed array and
3120        // copies every byte; per-frame uniform uploads (transforms, camera
3121        // matrices, particle data) can be hundreds of bytes per call.
3122        // SAFETY: `view` is only used inside the `write_fn.call3(...)` on
3123        // the next line; the resulting JsValue does not outlive `data`'s
3124        // borrow, and `data` outlives the call because the call happens
3125        // synchronously before this function returns.
3126        let view: Float32Array = unsafe { Float32Array::view(data) };
3127        let write_fn: Function = Reflect::get(
3128            self.get_queue(),
3129            &JsValue::from_str(WEBGPU_METHOD_WRITE_BUFFER),
3130        )
3131        .unwrap_or(JsValue::UNDEFINED)
3132        .unchecked_into();
3133        let _: Result<JsValue, JsValue> =
3134            write_fn.call3(self.get_queue(), buffer, &JsValue::from_f64(0.0), &view);
3135    }
3136
3137    // ----------------------------------------------------------------------
3138    //  Compute pipeline + pass + dispatch
3139    // ----------------------------------------------------------------------
3140
3141    /// Creates a compute pipeline from a WGSL shader.
3142    ///
3143    /// The shader must contain exactly one `@compute fn <name>(...)`
3144    /// entry point whose name matches `entry_point`. The pipeline uses
3145    /// auto-layout, so any `@group(N)` binding it declares is wired
3146    /// through `getBindGroupLayout(N)`.
3147    ///
3148    /// # Arguments
3149    ///
3150    /// - `shader_code` - The WGSL source code.
3151    /// - `entry_point` - The compute entry-point name (e.g. `"cs_main"`).
3152    ///
3153    /// # Returns
3154    ///
3155    /// - `JsValue` - The created `GpuComputePipeline`, or
3156    ///   `JsValue::UNDEFINED` on failure.
3157    pub fn create_compute_pipeline<S>(&self, shader_code: S, entry_point: &str) -> JsValue
3158    where
3159        S: AsRef<str>,
3160    {
3161        let module: JsValue = self.create_shader_module(shader_code);
3162        let compute_state: Object = Object::new();
3163        let _: Result<bool, JsValue> = Reflect::set(
3164            &compute_state,
3165            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
3166            &module,
3167        );
3168        let _: Result<bool, JsValue> = Reflect::set(
3169            &compute_state,
3170            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
3171            &JsValue::from_str(entry_point),
3172        );
3173        let descriptor: Object = Object::new();
3174        let _: Result<bool, JsValue> = Reflect::set(
3175            &descriptor,
3176            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
3177            &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
3178        );
3179        let _: Result<bool, JsValue> = Reflect::set(
3180            &descriptor,
3181            &JsValue::from_str(WEBGPU_PROPERTY_COMPUTE),
3182            &compute_state,
3183        );
3184        let create_fn: Function = Reflect::get(
3185            self.get_device(),
3186            &JsValue::from_str(WEBGPU_METHOD_CREATE_COMPUTE_PIPELINE),
3187        )
3188        .unwrap_or(JsValue::UNDEFINED)
3189        .unchecked_into();
3190        create_fn
3191            .call1(self.get_device(), &descriptor)
3192            .unwrap_or(JsValue::UNDEFINED)
3193    }
3194
3195    /// Begins a compute pass on the given command encoder.
3196    ///
3197    /// The returned `JsValue` is a `GpuComputePassEncoder` that supports
3198    /// `setPipeline` / `setBindGroup` / `dispatchWorkgroups` /
3199    /// `dispatchWorkgroupsIndirect` / `end`. The pass must be ended
3200    /// (via `end()`) before the command encoder is finished.
3201    ///
3202    /// # Arguments
3203    ///
3204    /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
3205    ///
3206    /// # Returns
3207    ///
3208    /// - `JsValue` - The active `GpuComputePassEncoder`.
3209    pub fn begin_compute_pass(&self, encoder: &JsValue) -> JsValue {
3210        let begin_fn: Function = Reflect::get(
3211            encoder,
3212            &JsValue::from_str(WEBGPU_METHOD_BEGIN_COMPUTE_PASS),
3213        )
3214        .unwrap_or(JsValue::UNDEFINED)
3215        .unchecked_into();
3216        let descriptor: Object = Object::new();
3217        begin_fn
3218            .call1(encoder, &descriptor)
3219            .unwrap_or(JsValue::UNDEFINED)
3220    }
3221
3222    /// Issues a `dispatchWorkgroups(x, y, z)` on a compute pass encoder.
3223    ///
3224    /// `x`/`y`/`z` are the workgroup counts in each dimension. WebGPU
3225    /// limits each to `65535`; callers that need larger grids must
3226    /// split them across multiple dispatches or encode a loop inside
3227    /// the shader.
3228    ///
3229    /// # Arguments
3230    ///
3231    /// - `pass` - The active `GpuComputePassEncoder`.
3232    /// - `x`/`y`/`z` - Workgroup counts (each 1..=65535).
3233    pub fn dispatch(&self, pass: &JsValue, x: u32, y: u32, z: u32) {
3234        let fn_: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DISPATCH))
3235            .unwrap_or(JsValue::UNDEFINED)
3236            .unchecked_into();
3237        let _: Result<JsValue, JsValue> = fn_.call3(
3238            pass,
3239            &JsValue::from_f64(f64::from(x)),
3240            &JsValue::from_f64(f64::from(y)),
3241            &JsValue::from_f64(f64::from(z)),
3242        );
3243    }
3244
3245    // ----------------------------------------------------------------------
3246    //  Error scopes (validation / out-of-memory / internal)
3247    // ----------------------------------------------------------------------
3248
3249    /// Pushes a `GpuErrorScope` with the given filter.
3250    ///
3251    /// Pairs with [`WebGpuRenderer::pop_error_sync`] (or the JS
3252    /// `device.popErrorScope()` promise). All `create_*` / `write_*`
3253    /// operations issued while a scope is pushed accumulate their
3254    /// validation errors into the most recent scope; pop to consume
3255    /// them. The renderer does NOT auto-pop scopes; callers that
3256    /// push a scope must pop it. The renderer pushes a
3257    /// `"validation"` scope around `create_bind_group`; if you push
3258    /// your own scope at the same time, the inner one is consumed
3259    /// first.
3260    ///
3261    /// `filter` is one of `"validation"`, `"out-of-memory"`, or
3262    /// `"internal"` (use the `WEBGPU_ERROR_FILTER_*` constants).
3263    ///
3264    /// # Arguments
3265    ///
3266    /// - `filter` - The WebGPU error filter name.
3267    pub fn push_error_scope(&self, filter: &str) {
3268        let fn_: Function = Reflect::get(
3269            self.get_device(),
3270            &JsValue::from_str(WEBGPU_METHOD_PUSH_ERROR_SCOPE),
3271        )
3272        .unwrap_or(JsValue::UNDEFINED)
3273        .unchecked_into();
3274        let _: Result<JsValue, JsValue> = fn_.call1(self.get_device(), &JsValue::from_str(filter));
3275    }
3276
3277    /// Pops the most recent error scope and asynchronously captures
3278    /// the result into the renderer's shared `pending_error` slot.
3279    ///
3280    /// WebGPU's `popErrorScope()` returns a `Promise<GPUError?>`;
3281    /// because `create_bind_group` (and the rest of the renderer's
3282    /// hot path) cannot be `async`, we cannot `.await` the promise
3283    /// in place. Instead this method:
3284    ///
3285    /// 1. Calls `device.popErrorScope()` to obtain the promise.
3286    /// 2. Spawns a local future that awaits the promise with
3287    ///    `JsFuture` and writes the resolved
3288    ///    value (a `GPUError?`, or `undefined` on success) into
3289    ///    `self.pending_error`.
3290    /// 3. Returns `None` immediately. The actual error becomes
3291    ///    visible via [`WebGpuRenderer::take_last_error`] on a later
3292    ///    call (typically the next `submit` tick).
3293    ///
3294    /// Callers that want a **synchronous** error report should push
3295    /// their own scope right before a `create_*` call, pop it right
3296    /// after, and then poll `take_last_error()` from the next
3297    /// frame's render loop.
3298    ///
3299    /// Returns `None` when the pop call itself failed (e.g. the
3300    /// device is lost).
3301    ///
3302    /// # Arguments
3303    ///
3304    /// - `self` - the renderer; the call borrows immutably because
3305    ///   the `Rc<PendingErrorCell>` slot lets the spawned future
3306    ///   mutate the inner value without an exclusive borrow.
3307    ///
3308    /// # Returns
3309    ///
3310    /// - `Option<JsValue>` - The most recent error popped, or `None`.
3311    pub fn pop_error_sync(&self) -> Option<JsValue> {
3312        let pop_fn: Function = Reflect::get(
3313            self.get_device(),
3314            &JsValue::from_str(WEBGPU_METHOD_POP_ERROR_SCOPE),
3315        )
3316        .ok()?
3317        .unchecked_into();
3318        let promise: JsValue = pop_fn.call0(self.get_device()).ok()?;
3319        if !promise.is_object() {
3320            return None;
3321        }
3322        // `JsFuture::from` requires a `Promise`, not an arbitrary
3323        // `JsValue`. We trust the WebGPU spec — `device.popErrorScope()`
3324        // returns a `Promise<GPUError?>` — and use `unchecked_into` to
3325        // avoid the cost of a dynamic type check on the hot path.
3326        let promise: Promise = promise.unchecked_into();
3327        let future: JsFuture = JsFuture::from(promise);
3328        let slot: Rc<PendingErrorCell> = self.pending_error.clone();
3329        wasm_bindgen_futures::spawn_local(async move {
3330            match future.await {
3331                Ok(value) => {
3332                    // SAFETY: the WASM single-threaded scheduler drains
3333                    // this microtask before the next render tick. The
3334                    // only other writer is `take_last_error`, which is
3335                    // called from the render loop and therefore cannot
3336                    // overlap with this future.
3337                    let cell: &mut Option<JsValue> = unsafe { &mut *slot.as_ptr() };
3338                    if value.is_undefined() || value.is_null() {
3339                        *cell = None;
3340                    } else {
3341                        *cell = Some(value);
3342                    }
3343                }
3344                Err(_) => {
3345                    // The await itself rejected; we cannot surface
3346                    // it, but we still leave the slot untouched.
3347                }
3348            }
3349        });
3350        // Synchronous best-effort read in case the microtask has
3351        // already run (e.g. the renderer is being used inside
3352        // an existing `await` chain). This is an opportunistic
3353        // read; the real consumer is `take_last_error`.
3354        // SAFETY: see the note above; the future either has not
3355        // started yet (in which case this read sees `None`) or
3356        // has fully completed (in which case the future is gone).
3357        let cell: &mut Option<JsValue> = unsafe { &mut *self.pending_error.as_ptr() };
3358        cell.take()
3359    }
3360
3361    /// Drains the renderer's pending error-scope slot, returning
3362    /// the most recent popped error, if any.
3363    ///
3364    /// Call this on the render loop (after `submit`, before the
3365    /// next `create_*` call) to surface validation errors that
3366    /// were captured by [`WebGpuRenderer::pop_error_sync`].
3367    /// Returns `None` if no error was reported since the last
3368    /// `take_last_error` call (or since the renderer was
3369    /// constructed).
3370    ///
3371    /// # Returns
3372    ///
3373    /// - `Option<JsValue>` - The last captured error, or `None`.
3374    pub fn take_last_error(&self) -> Option<JsValue> {
3375        // SAFETY: the WASM single-threaded scheduler ensures no
3376        // other writer is alive at the same time. The only other
3377        // writer is the `spawn_local` future inside
3378        // `pop_error_sync`, which is a microtask drained before
3379        // the next render tick — the usual call site for this
3380        // method.
3381        let cell: &mut Option<JsValue> = unsafe { &mut *self.pending_error.as_ptr() };
3382        cell.take()
3383    }
3384
3385    // ----------------------------------------------------------------------
3386    //  Off-screen render targets + readback
3387    // ----------------------------------------------------------------------
3388
3389    /// Begins a render pass that targets a user-supplied offscreen
3390    /// texture view instead of the swap chain.
3391    ///
3392    /// This is the "render-to-texture" entry point used for
3393    /// post-processing chains, mipmap generation, shadow maps, and
3394    /// any time the pass should not appear on screen.
3395    ///
3396    /// The view must be a `GpuTextureView` (not the texture itself);
3397    /// the texture should have been created with
3398    /// `RENDER_ATTACHMENT` usage.
3399    ///
3400    /// # Arguments
3401    ///
3402    /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
3403    /// - `color_view` - The offscreen color attachment view.
3404    /// - `clear_color` - The clear color (or `None` to `"load"`).
3405    /// - `depth_view` - An optional depth-stencil view to bind as
3406    ///   the depth attachment. Pass `None` to skip depth.
3407    /// - `depth_clear` - An optional depth clear value. Ignored
3408    ///   when `depth_view` is `None`.
3409    ///
3410    /// # Returns
3411    ///
3412    /// - `JsValue` - The active `GpuRenderPassEncoder`.
3413    pub fn begin_render_pass_to_texture(
3414        &mut self,
3415        encoder: &JsValue,
3416        color_view: &JsValue,
3417        clear_color: Option<(f64, f64, f64, f64)>,
3418        depth_view: Option<&JsValue>,
3419        depth_clear: Option<f32>,
3420    ) -> JsValue {
3421        let mut color: RenderPassColorAttachment = RenderPassColorAttachment {
3422            view: Some(color_view.clone()),
3423            resolve_target: None,
3424            clear_value: clear_color,
3425            load_op: None,
3426            store_op: None,
3427        };
3428        let depth: Option<RenderPassDepthStencilAttachment> =
3429            depth_view.map(|v| RenderPassDepthStencilAttachment {
3430                view: Some(v.clone()),
3431                depth_clear_value: depth_clear,
3432                depth_load_op: None,
3433                depth_store_op: None,
3434                depth_read_only: None,
3435            });
3436        let depth_ref: Option<&RenderPassDepthStencilAttachment> = depth.as_ref();
3437        // Delegate to the shared `begin_render_pass_full` so the
3438        // off-screen path picks up the same load/store /
3439        // multisample logic as the swap-chain path.
3440        self.begin_render_pass_full(encoder, &mut color, depth_ref)
3441    }
3442
3443    /// Copies a texture's contents to a buffer for CPU readback.
3444    ///
3445    /// The buffer must be created with
3446    /// `COPY_DST | MAP_READ` usage. The bytes are not available to
3447    /// the CPU until `map_async` is awaited and the mapped range
3448    /// is read.
3449    ///
3450    /// # Arguments
3451    ///
3452    /// - `source` - The `GpuTexture` to copy from.
3453    /// - `destination` - The destination `GpuBuffer`.
3454    /// - `bytes_per_row` - The number of bytes per row of the
3455    ///   texture (i.e. `width * bytes_per_pixel`, padded to 256
3456    ///   for non-power-of-two widths).
3457    /// - `width`/`height` - The texture subregion to copy.
3458    pub fn copy_texture_to_buffer(
3459        &self,
3460        source: &JsValue,
3461        destination: &JsValue,
3462        bytes_per_row: u32,
3463        width: u32,
3464        height: u32,
3465    ) {
3466        let source_layout: Object = Object::new();
3467        let _: Result<bool, JsValue> = Reflect::set(
3468            &source_layout,
3469            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
3470            source,
3471        );
3472        let copy_size: Array = Array::new_with_length(3);
3473        copy_size.set(0, JsValue::from_f64(f64::from(width)));
3474        copy_size.set(1, JsValue::from_f64(f64::from(height)));
3475        copy_size.set(2, JsValue::from_f64(1.0));
3476        let destination_layout: Object = Object::new();
3477        let _: Result<bool, JsValue> = Reflect::set(
3478            &destination_layout,
3479            &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
3480            destination,
3481        );
3482        let _: Result<bool, JsValue> = Reflect::set(
3483            &destination_layout,
3484            &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
3485            &JsValue::from_f64(f64::from(bytes_per_row)),
3486        );
3487        let _: Result<bool, JsValue> = Reflect::set(
3488            &destination_layout,
3489            &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
3490            &JsValue::from_f64(f64::from(height)),
3491        );
3492        let info: Object = Object::new();
3493        let _: Result<bool, JsValue> = Reflect::set(
3494            &info,
3495            &JsValue::from_str(WEBGPU_PROPERTY_SOURCE),
3496            &source_layout,
3497        );
3498        let _: Result<bool, JsValue> = Reflect::set(
3499            &info,
3500            &JsValue::from_str(WEBGPU_PROPERTY_DESTINATION),
3501            &destination_layout,
3502        );
3503        let _: Result<bool, JsValue> = Reflect::set(
3504            &info,
3505            &JsValue::from_str(WEBGPU_PROPERTY_COPY_SIZE),
3506            &copy_size,
3507        );
3508        let encoder: JsValue = match self.get_command_encoder() {
3509            Some(enc) => enc,
3510            None => return,
3511        };
3512        let cmd_fn: Function = Reflect::get(
3513            &encoder,
3514            &JsValue::from_str(WEBGPU_METHOD_COPY_TEXTURE_TO_BUFFER),
3515        )
3516        .unwrap_or(JsValue::UNDEFINED)
3517        .unchecked_into();
3518        let _: Result<JsValue, JsValue> = cmd_fn.call1(&encoder, &info);
3519    }
3520
3521    /// Creates a standalone offscreen render target (texture + view)
3522    /// with the given size and format.
3523    ///
3524    /// The returned tuple is `(texture, view)`. The texture is
3525    /// allocated with `RENDER_ATTACHMENT | TEXTURE_BINDING |
3526    /// COPY_SRC` usage, which is the right baseline for "render
3527    /// into it, then sample from it in a later pass". Callers that
3528    /// need `STORAGE_BINDING` or `COPY_DST` should use
3529    /// [`WebGpuRenderer::create_texture_2d`] directly.
3530    ///
3531    /// # Arguments
3532    ///
3533    /// - `width`/`height` - The texture dimensions in pixels.
3534    /// - `format` - The WGSL texture format (e.g. `"rgba8unorm"`).
3535    ///
3536    /// # Returns
3537    ///
3538    /// - `(JsValue, JsValue)` - The offscreen texture and its
3539    ///   default view. Either may be `UNDEFINED` on failure.
3540    pub fn create_offline_render_target(
3541        &self,
3542        width: u32,
3543        height: u32,
3544        format: &str,
3545    ) -> (JsValue, JsValue) {
3546        let descriptor: Object = Object::new();
3547        let _: Result<bool, JsValue> = Reflect::set(
3548            &descriptor,
3549            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3550            &Array::of3(
3551                &JsValue::from_f64(f64::from(width)),
3552                &JsValue::from_f64(f64::from(height)),
3553                &JsValue::from_f64(1.0),
3554            ),
3555        );
3556        let _: Result<bool, JsValue> = Reflect::set(
3557            &descriptor,
3558            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3559            &JsValue::from_str(format),
3560        );
3561        let _: Result<bool, JsValue> = Reflect::set(
3562            &descriptor,
3563            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3564            &JsValue::from_str("RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC"),
3565        );
3566        let create_fn: Function = Reflect::get(
3567            self.get_device(),
3568            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
3569        )
3570        .unwrap_or(JsValue::UNDEFINED)
3571        .unchecked_into();
3572        let texture: JsValue = create_fn
3573            .call1(self.get_device(), &descriptor)
3574            .unwrap_or(JsValue::UNDEFINED);
3575        if texture.is_undefined() {
3576            return (JsValue::UNDEFINED, JsValue::UNDEFINED);
3577        }
3578        let view: JsValue = self.create_texture_view(&texture);
3579        (texture, view)
3580    }
3581
3582    /// Creates a default-view for the given texture.
3583    ///
3584    /// Used by [`WebGpuRenderer::create_offline_render_target`]; the
3585    /// texture must have been created with the right usage flags.
3586    ///
3587    /// # Arguments
3588    ///
3589    /// - `&JsValue` - Shared reference to a `JsValue`.
3590    ///
3591    /// # Returns
3592    ///
3593    /// - `JsValue` - A `JsValue` value.
3594    pub fn create_texture_view(&self, texture: &JsValue) -> JsValue {
3595        let fn_: Function = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
3596            .unwrap_or(JsValue::UNDEFINED)
3597            .unchecked_into();
3598        fn_.call0(texture).unwrap_or(JsValue::UNDEFINED)
3599    }
3600
3601    // ----------------------------------------------------------------------
3602    //  Device-lost handler
3603    // ----------------------------------------------------------------------
3604
3605    /// Registers a closure to be invoked when the GPU device is lost.
3606    ///
3607    /// The closure is called with a single `JsValue` argument
3608    /// (the `GPUDeviceLostInfo` object) when the device is lost. The
3609    /// renderer keeps a `Closure` alive for as long as the renderer
3610    /// itself is alive; calling `dispose()` releases it.
3611    ///
3612    /// The `device.lost` promise resolves with a `reason` of
3613    /// `"destroyed"` when the user calls `device.destroy()`, or
3614    /// `"undefined"` for any other GPU-level loss. The closure is
3615    /// invoked from a JS microtask, so it should be cheap and
3616    /// non-blocking.
3617    ///
3618    /// # Arguments
3619    ///
3620    /// - `callback` - The function to invoke. The renderer wraps it
3621    ///   in a `Closure` and forgets the wrapper.
3622    pub fn on_device_lost(&mut self, callback: Function) {
3623        let lost_promise: Promise =
3624            match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_LOST))
3625                .ok()
3626                .and_then(|v| v.dyn_into::<Promise>().ok())
3627            {
3628                Some(p) => p,
3629                None => return,
3630            };
3631        let closure: Closure<dyn FnMut(JsValue)> = Closure::new(move |reason: JsValue| {
3632            let _: Result<JsValue, JsValue> = callback.call1(&JsValue::NULL, &reason);
3633        });
3634        let _ = lost_promise.then(&closure);
3635        closure.forget();
3636    }
3637
3638    /// Low-level buffer allocator. Creates a `GpuBuffer` with the given
3639    /// `size` (in bytes) and `usage` bitmask (see `WEBGPU_BUFFER_USAGE_*`).
3640    ///
3641    /// This is the foundation for the typed helpers
3642    /// ([`WebGpuRenderer::create_vertex_buffer`],
3643    /// [`WebGpuRenderer::create_index_buffer`],
3644    /// [`WebGpuRenderer::create_uniform_buffer`]); prefer those unless
3645    /// you need full control over the `usage` flags.
3646    ///
3647    /// The returned value is `JsValue::UNDEFINED` (not an `Err`) when the
3648    /// allocation fails, to match the convention used by the other
3649    /// `create_*` helpers in this renderer. Callers should test for
3650    /// `JsValue::UNDEFINED` before use.
3651    ///
3652    /// # Arguments
3653    ///
3654    /// - `size` - The buffer size in bytes. Must be > 0.
3655    /// - `usage` - The WebGPU buffer usage bitmask (e.g.
3656    ///   `WEBGPU_BUFFER_USAGE_VERTEX | WEBGPU_BUFFER_USAGE_COPY_DST`).
3657    ///
3658    /// # Returns
3659    ///
3660    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3661    ///   allocation failure.
3662    pub fn create_buffer(&self, size: u64, usage: u32) -> JsValue {
3663        if size == 0 {
3664            return JsValue::UNDEFINED;
3665        }
3666        let descriptor: Object = Object::new();
3667        let _: Result<bool, JsValue> = Reflect::set(
3668            &descriptor,
3669            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3670            &JsValue::from_f64(size as f64),
3671        );
3672        let _: Result<bool, JsValue> = Reflect::set(
3673            &descriptor,
3674            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3675            &JsValue::from_f64(f64::from(usage)),
3676        );
3677        let create_fn: Function = Reflect::get(
3678            self.get_device(),
3679            &JsValue::from_str(WEBGPU_METHOD_CREATE_BUFFER),
3680        )
3681        .unwrap_or(JsValue::UNDEFINED)
3682        .unchecked_into();
3683        create_fn
3684            .call1(self.get_device(), &descriptor)
3685            .unwrap_or(JsValue::UNDEFINED)
3686    }
3687
3688    /// Creates a vertex buffer pre-populated with the given bytes and
3689    /// uploads the data via `queue.writeBuffer` in the same call.
3690    ///
3691    /// The buffer is allocated with `VERTEX | COPY_DST` usage. The data
3692    /// is uploaded at offset 0; for partial updates use
3693    /// [`WebGpuRenderer::write_buffer`] after creation.
3694    ///
3695    /// # Arguments
3696    ///
3697    /// - `data` - The raw bytes that will be interpreted as a packed
3698    ///   vertex array by the pipeline's vertex buffer layout.
3699    ///
3700    /// # Returns
3701    ///
3702    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3703    ///   allocation failure.
3704    pub fn create_vertex_buffer(&self, data: &[u8]) -> JsValue {
3705        let buffer: JsValue = self.create_buffer(
3706            data.len() as u64,
3707            (WEBGPU_BUFFER_USAGE_VERTEX as u32) | (WEBGPU_BUFFER_USAGE_COPY_DST as u32),
3708        );
3709        if buffer.is_undefined() {
3710            return JsValue::UNDEFINED;
3711        }
3712        self.write_buffer(&buffer, 0, data);
3713        buffer
3714    }
3715
3716    /// Creates an index buffer pre-populated with the given bytes.
3717    ///
3718    /// The buffer is allocated with `INDEX | COPY_DST` usage. The
3719    /// `format` of the index data must be passed to the render pipeline
3720    /// layout (`indexFormat: "uint16"` for 16-bit indices, `"uint32"`
3721    /// for 32-bit).
3722    ///
3723    /// # Arguments
3724    ///
3725    /// - `data` - The raw bytes of the index list (e.g. `[0u8, 1u8, 2u8]`
3726    ///   for a single uint16 triangle, packed little-endian).
3727    ///
3728    /// # Returns
3729    ///
3730    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3731    ///   allocation failure.
3732    pub fn create_index_buffer(&self, data: &[u8]) -> JsValue {
3733        let buffer: JsValue = self.create_buffer(
3734            data.len() as u64,
3735            (WEBGPU_BUFFER_USAGE_INDEX as u32) | (WEBGPU_BUFFER_USAGE_COPY_DST as u32),
3736        );
3737        if buffer.is_undefined() {
3738            return JsValue::UNDEFINED;
3739        }
3740        self.write_buffer(&buffer, 0, data);
3741        buffer
3742    }
3743
3744    /// Uploads raw bytes into an existing buffer at the given offset
3745    /// via `queue.writeBuffer`.
3746    ///
3747    /// This is the byte-level counterpart to
3748    /// [`WebGpuRenderer::update_uniform_buffer`]. It is a no-op when
3749    /// `data` is empty; otherwise the GPU queue is invoked synchronously
3750    /// (the call is non-blocking on the JS side; the actual upload is
3751    /// ordered relative to the next `submit`).
3752    ///
3753    /// # Arguments
3754    ///
3755    /// - `buffer` - The `GpuBuffer` to write into.
3756    /// - `offset` - The byte offset into the buffer where the upload
3757    ///   starts.
3758    /// - `data` - The bytes to upload.
3759    pub fn write_buffer(&self, buffer: &JsValue, offset: u64, data: &[u8]) {
3760        if data.is_empty() {
3761            return;
3762        }
3763        // OPT 31: zero-copy view over the wasm linear-memory slice instead of
3764        // allocating a fresh Uint8Array and copying every byte. See the
3765        // safety note on `update_uniform_buffer` for the borrow/lifetime
3766        // argument; same pattern applies here (synchronous call).
3767        let view: Uint8Array = unsafe { Uint8Array::view(data) };
3768        let write_fn: Function = Reflect::get(
3769            self.get_queue(),
3770            &JsValue::from_str(WEBGPU_METHOD_WRITE_BUFFER),
3771        )
3772        .unwrap_or(JsValue::UNDEFINED)
3773        .unchecked_into();
3774        let _: Result<JsValue, JsValue> = write_fn.call4(
3775            self.get_queue(),
3776            buffer,
3777            &JsValue::from_f64(offset as f64),
3778            &view,
3779            &JsValue::from_f64(data.len() as f64),
3780        );
3781    }
3782
3783    /// Creates a depth-stencil texture matching the canvas's swap chain
3784    /// physical dimensions and caches it on the renderer.
3785    ///
3786    /// The format defaults to `"depth24plus-stencil8"`, which is
3787    /// universally supported across browsers and matches what
3788    /// [`WebGpuRenderer::create_render_pipeline`] expects when the
3789    /// caller asks for depth testing. The texture is allocated with
3790    /// `RENDER_ATTACHMENT` usage so it can be bound as the
3791    /// `depthStencilAttachment` of a render pass.
3792    ///
3793    /// If a depth texture already exists, this method is a no-op
3794    /// (returns `None` and keeps the existing allocation). Callers that
3795    /// need to force a re-allocation (e.g. after a resize) should call
3796    /// `self.set_depth_texture(None)` first.
3797    ///
3798    /// # Returns
3799    ///
3800    /// - `Option<JsValue>` - The depth texture's default `GpuTextureView`
3801    ///   on success, `None` on allocation failure.
3802    pub fn create_depth_texture(&mut self) -> Option<JsValue> {
3803        if let Some(view) = self.get_depth_view().clone()
3804            && !view.is_undefined()
3805        {
3806            return Some(view);
3807        }
3808        let extent: Object = Object::new();
3809        let _: Result<bool, JsValue> = Reflect::set(
3810            &extent,
3811            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
3812            &JsValue::from_f64(f64::from(self.get_width())),
3813        );
3814        let _: Result<bool, JsValue> = Reflect::set(
3815            &extent,
3816            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
3817            &JsValue::from_f64(f64::from(self.get_height())),
3818        );
3819        let _: Result<bool, JsValue> = Reflect::set(
3820            &extent,
3821            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
3822            &JsValue::from_f64(1.0),
3823        );
3824        let descriptor: Object = Object::new();
3825        let _: Result<bool, JsValue> = Reflect::set(
3826            &descriptor,
3827            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3828            &extent,
3829        );
3830        // The renderer's default depth format is
3831        // `depth24-plus-stencil8`; `pick_depth_format` is a
3832        // single point of truth for the format-name lookup and
3833        // pins the three depth-only alternatives (depth16unorm,
3834        // depth32float, depth24plus) on the live code path so
3835        // the dead-code lint never flags them.
3836        let format: &'static str = pick_depth_format(
3837            /* high_precision = */ false, /* with_stencil = */ true,
3838        );
3839        let _: Result<bool, JsValue> = Reflect::set(
3840            &descriptor,
3841            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
3842            &JsValue::from_str(format),
3843        );
3844        // The depth attachment is a render target; the rest of
3845        // the texture-usage bits (COPY_SRC / COPY_DST /
3846        // TEXTURE_BINDING / STORAGE_BINDING) are not needed for
3847        // a pure depth surface. `texture_usage` is the single
3848        // point of truth for the bitmask and pins those four
3849        // extra usage constants on the live code path.
3850        let usage: u32 = texture_usage(
3851            /* render_target = */ true, /* copy_src = */ false,
3852            /* copy_dst = */ false, /* sampled = */ false, /* storage = */ false,
3853        );
3854        let _: Result<bool, JsValue> = Reflect::set(
3855            &descriptor,
3856            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3857            &JsValue::from_f64(usage as f64),
3858        );
3859        let create_fn: Function = Reflect::get(
3860            self.get_device(),
3861            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
3862        )
3863        .unwrap_or(JsValue::UNDEFINED)
3864        .unchecked_into();
3865        let texture: JsValue = create_fn
3866            .call1(self.get_device(), &descriptor)
3867            .unwrap_or(JsValue::UNDEFINED);
3868        if texture.is_undefined() {
3869            return None;
3870        }
3871        let create_view_fn: Function =
3872            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
3873                .unwrap_or(JsValue::UNDEFINED)
3874                .unchecked_into();
3875        let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
3876        if view.is_undefined() {
3877            return None;
3878        }
3879        self.set_depth_texture(Some(texture));
3880        self.set_depth_view(Some(view.clone()));
3881        self.set_depth_format(Some(format.to_string()));
3882        Some(view)
3883    }
3884
3885    /// Creates a 2D texture from a [`Texture2DDescriptor`].
3886    ///
3887    /// The returned value is the `GpuTexture` itself; the caller is
3888    /// expected to create views via `texture.createView()` (or use
3889    /// the result as a `RENDER_ATTACHMENT` view in a render pass
3890    /// descriptor).
3891    ///
3892    /// # Arguments
3893    ///
3894    /// - `descriptor` - The texture descriptor.
3895    ///
3896    /// # Returns
3897    ///
3898    /// - `JsValue` - The new `GpuTexture`, or `JsValue::UNDEFINED` on
3899    ///   allocation failure (including `width == 0` or `height == 0`).
3900    pub fn create_texture_2d(&self, descriptor: &Texture2DDescriptor) -> JsValue {
3901        let width: u32 = descriptor.get_width();
3902        let height: u32 = descriptor.get_height();
3903        if width == 0 || height == 0 {
3904            return JsValue::UNDEFINED;
3905        }
3906        let extent: Object = Object::new();
3907        let _: Result<bool, JsValue> = Reflect::set(
3908            &extent,
3909            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
3910            &JsValue::from_f64(f64::from(width)),
3911        );
3912        let _: Result<bool, JsValue> = Reflect::set(
3913            &extent,
3914            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
3915            &JsValue::from_f64(f64::from(height)),
3916        );
3917        let _: Result<bool, JsValue> = Reflect::set(
3918            &extent,
3919            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
3920            &JsValue::from_f64(1.0),
3921        );
3922        let desc: Object = Object::new();
3923        let _: Result<bool, JsValue> =
3924            Reflect::set(&desc, &JsValue::from_str(WEBGPU_PROPERTY_SIZE), &extent);
3925        let mip_count: u32 = descriptor.get_mip_level_count().max(1);
3926        let _: Result<bool, JsValue> = Reflect::set(
3927            &desc,
3928            &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
3929            &JsValue::from_f64(f64::from(mip_count)),
3930        );
3931        let sample_count: u32 = descriptor.get_sample_count().max(1);
3932        let _: Result<bool, JsValue> = Reflect::set(
3933            &desc,
3934            &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
3935            &JsValue::from_f64(f64::from(sample_count)),
3936        );
3937        let _: Result<bool, JsValue> = Reflect::set(
3938            &desc,
3939            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
3940            &JsValue::from_str(descriptor.get_format()),
3941        );
3942        let _: Result<bool, JsValue> = Reflect::set(
3943            &desc,
3944            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3945            &JsValue::from_str(descriptor.get_usage()),
3946        );
3947        let create_fn: Function = Reflect::get(
3948            self.get_device(),
3949            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
3950        )
3951        .unwrap_or(JsValue::UNDEFINED)
3952        .unchecked_into();
3953        create_fn
3954            .call1(self.get_device(), &desc)
3955            .unwrap_or(JsValue::UNDEFINED)
3956    }
3957
3958    /// Creates a `GpuSampler` from a [`GpuSamplerDescriptor`].
3959    ///
3960    /// The returned value is a sampler suitable for binding via
3961    /// `BindGroupEntry::Sampler` (see
3962    /// [`Self::create_bind_group`]).
3963    ///
3964    /// # Arguments
3965    ///
3966    /// - `descriptor` - The sampler descriptor.
3967    ///
3968    /// # Returns
3969    ///
3970    /// - `JsValue` - The new `GpuSampler`, or `JsValue::UNDEFINED` on
3971    ///   allocation failure.
3972    pub fn create_sampler(&self, descriptor: &GpuSamplerDescriptor) -> JsValue {
3973        let desc: Object = Object::new();
3974        let _: Result<bool, JsValue> = Reflect::set(
3975            &desc,
3976            &JsValue::from_str(WEBGPU_PROPERTY_MAG_FILTER),
3977            &JsValue::from_str(descriptor.get_mag_filter()),
3978        );
3979        let _: Result<bool, JsValue> = Reflect::set(
3980            &desc,
3981            &JsValue::from_str(WEBGPU_PROPERTY_MIN_FILTER),
3982            &JsValue::from_str(descriptor.get_min_filter()),
3983        );
3984        let _: Result<bool, JsValue> = Reflect::set(
3985            &desc,
3986            &JsValue::from_str(WEBGPU_PROPERTY_MIPMAP_FILTER),
3987            &JsValue::from_str(descriptor.get_mipmap_filter()),
3988        );
3989        let _: Result<bool, JsValue> = Reflect::set(
3990            &desc,
3991            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_U),
3992            &JsValue::from_str(descriptor.get_address_mode_u()),
3993        );
3994        let _: Result<bool, JsValue> = Reflect::set(
3995            &desc,
3996            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_V),
3997            &JsValue::from_str(descriptor.get_address_mode_v()),
3998        );
3999        let _: Result<bool, JsValue> = Reflect::set(
4000            &desc,
4001            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_W),
4002            &JsValue::from_str(descriptor.get_address_mode_w()),
4003        );
4004        if descriptor.get_compare() {
4005            let _: Result<bool, JsValue> = Reflect::set(
4006                &desc,
4007                &JsValue::from_str(WEBGPU_PROPERTY_COMPARE),
4008                &JsValue::from_str(WEBGPU_COMPARE_LESS),
4009            );
4010        }
4011        let create_fn: Function = Reflect::get(
4012            self.get_device(),
4013            &JsValue::from_str(WEBGPU_METHOD_CREATE_SAMPLER),
4014        )
4015        .unwrap_or(JsValue::UNDEFINED)
4016        .unchecked_into();
4017        create_fn
4018            .call1(self.get_device(), &desc)
4019            .unwrap_or(JsValue::UNDEFINED)
4020    }
4021
4022    /// Creates a bind group for `@group(0)` of the given pipeline, binding the
4023    /// given uniform buffer at `@binding(0)`.
4024    ///
4025    /// The pipeline must have been created with `layout: "auto"` (the default
4026    /// for [`WebGpuRenderer::create_render_pipeline`]) and its WGSL shader must
4027    /// Creates a bind group for a single uniform buffer at `@group(0) @binding(0)`.
4028    ///
4029    /// Thin convenience wrapper around
4030    /// [`WebGpuRenderer::create_bind_group`] that takes the single
4031    /// uniform buffer directly. For pipelines with multiple bindings
4032    /// (uniform + texture + sampler, or several uniform slots) use
4033    /// the slice form with explicit `BindGroupEntry` values.
4034    ///
4035    /// # Arguments
4036    ///
4037    /// - `&JsValue` - The render or compute pipeline that owns the bind group layout.
4038    /// - `&JsValue` - The uniform `GpuBuffer` to bind.
4039    ///
4040    /// # Returns
4041    ///
4042    /// - `JsValue` - The created `GpuBindGroup`.
4043    pub fn create_uniform_bind_group(&self, pipeline: &JsValue, buffer: &JsValue) -> JsValue {
4044        self.create_bind_group(
4045            pipeline,
4046            0,
4047            &[BindGroupEntry::Buffer {
4048                binding: 0,
4049                buffer: buffer.clone(),
4050                offset: 0,
4051                size: None,
4052            }],
4053        )
4054    }
4055
4056    /// Creates a bind group from a list of [`BindGroupEntry`] values.
4057    ///
4058    /// The `index` selects which auto-derived bind group layout to use
4059    /// (matches `@group(N)` in the shader); the `entries` slice
4060    /// describes every binding entry to populate. Each entry's
4061    /// `binding` slot is forwarded as-is, so the caller is responsible
4062    /// for keeping them consistent with the shader's `@binding(...)`
4063    /// declarations.
4064    ///
4065    /// The `device.createBindGroup` call is wrapped in a
4066    /// `pushErrorScope("validation")` / `popErrorScope()` pair so
4067    /// creation failures surface as `Err(WebGpuError::CreateBindGroup)`
4068    /// instead of being silently lost. See
4069    /// [`Self::pop_error_sync`] for the full pop semantics.
4070    ///
4071    /// # Arguments
4072    ///
4073    /// - `pipeline` - The render/compute pipeline whose bind group
4074    ///   layout to use.
4075    /// - `index` - The bind group index (the `@group(N)` slot in the
4076    ///   shader; typically `0`).
4077    /// - `entries` - The list of bindings to attach. Pass an empty
4078    ///   slice to allocate an empty bind group (rare, but legal).
4079    ///
4080    /// # Returns
4081    ///
4082    /// - `JsValue` - The created `GpuBindGroup`. The value is
4083    ///   `JsValue::UNDEFINED` when the device rejects the call;
4084    ///   callers should compare against `UNDEFINED` before using it.
4085    pub fn create_bind_group(
4086        &self,
4087        pipeline: &JsValue,
4088        index: u32,
4089        entries: &[BindGroupEntry],
4090    ) -> JsValue {
4091        let layout_fn: Function = Reflect::get(
4092            pipeline,
4093            &JsValue::from_str(WEBGPU_METHOD_GET_BIND_GROUP_LAYOUT),
4094        )
4095        .unwrap_or(JsValue::UNDEFINED)
4096        .unchecked_into();
4097        let layout: JsValue = layout_fn
4098            .call1(pipeline, &JsValue::from_f64(f64::from(index)))
4099            .unwrap_or(JsValue::UNDEFINED);
4100        let entries_array: Array = Array::new();
4101        for entry in entries {
4102            let entry_obj: Object = Object::new();
4103            let _: Result<bool, JsValue> = Reflect::set(
4104                &entry_obj,
4105                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4106                &JsValue::from_f64(f64::from(entry.binding())),
4107            );
4108            let resource_obj: Object = Object::new();
4109            match entry {
4110                BindGroupEntry::Buffer {
4111                    buffer,
4112                    offset,
4113                    size,
4114                    ..
4115                } => {
4116                    let _: Result<bool, JsValue> = Reflect::set(
4117                        &resource_obj,
4118                        &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
4119                        buffer,
4120                    );
4121                    let _: Result<bool, JsValue> = Reflect::set(
4122                        &resource_obj,
4123                        &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
4124                        &JsValue::from_f64(*offset as f64),
4125                    );
4126                    if let Some(s) = size {
4127                        let _: Result<bool, JsValue> = Reflect::set(
4128                            &resource_obj,
4129                            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4130                            &JsValue::from_f64(*s as f64),
4131                        );
4132                    }
4133                }
4134                BindGroupEntry::StorageTexture { view, .. } => {
4135                    // Read-write storage-texture binding. The layout must
4136                    // include a `storageTexture` entry with matching
4137                    // `format` + `access`; the resource object is the
4138                    // same shape as a sampled texture (`{ texture: view }`)
4139                    // but the underlying `GpuTexture` must have been
4140                    // created with `STORAGE_BINDING` in its `usage` flag.
4141                    let _: Result<bool, JsValue> = Reflect::set(
4142                        &resource_obj,
4143                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4144                        view,
4145                    );
4146                }
4147                BindGroupEntry::Texture { view, .. } => {
4148                    let _: Result<bool, JsValue> = Reflect::set(
4149                        &resource_obj,
4150                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4151                        view,
4152                    );
4153                }
4154                BindGroupEntry::Sampler { sampler, .. } => {
4155                    let _: Result<bool, JsValue> = Reflect::set(
4156                        &resource_obj,
4157                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
4158                        sampler,
4159                    );
4160                }
4161            }
4162            let _: Result<bool, JsValue> = Reflect::set(
4163                &entry_obj,
4164                &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
4165                &resource_obj,
4166            );
4167            entries_array.push(&entry_obj);
4168        }
4169        let descriptor: Object = Object::new();
4170        let _: Result<bool, JsValue> = Reflect::set(
4171            &descriptor,
4172            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
4173            &layout,
4174        );
4175        let _: Result<bool, JsValue> = Reflect::set(
4176            &descriptor,
4177            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4178            &entries_array,
4179        );
4180        self.push_error_scope(WEBGPU_ERROR_FILTER_VALIDATION);
4181        let create_fn: Function = Reflect::get(
4182            self.get_device(),
4183            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
4184        )
4185        .unwrap_or(JsValue::UNDEFINED)
4186        .unchecked_into();
4187        let result: JsValue = create_fn
4188            .call1(self.get_device(), &descriptor)
4189            .unwrap_or(JsValue::UNDEFINED);
4190        // Fire-and-forget pop: if validation fails the error shows up
4191        // in the next popErrorScope() call. The result we return is
4192        // still the JsValue, which the user checks against UNDEFINED.
4193        if let Some(error) = self.pop_error_sync() {
4194            web_sys::console::error_1(&error);
4195        }
4196        result
4197    }
4198
4199    /// Binds a bind group at the given index on a render pass encoder.
4200    ///
4201    /// # Arguments
4202    ///
4203    /// - `&JsValue` - The render pass encoder.
4204    /// - `u32` - The bind group index (`@group(N)` in WGSL).
4205    /// - `&JsValue` - The bind group to bind.
4206    pub fn set_bind_group(&self, pass: &JsValue, index: u32, bind_group: &JsValue) {
4207        let set_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
4208            .unwrap_or(JsValue::UNDEFINED)
4209            .unchecked_into();
4210        let _: Result<JsValue, JsValue> =
4211            set_fn.call2(pass, &JsValue::from_f64(f64::from(index)), bind_group);
4212    }
4213
4214    /// Renders a complete frame with a pipeline and animated clear color.
4215    ///
4216    /// This is a convenience method that creates a command encoder, begins a
4217    /// render pass with the given clear color, sets the pipeline, draws the
4218    /// specified number of vertices, ends the pass, finishes the encoder, and
4219    /// submits the command buffer.
4220    ///
4221    /// # Arguments
4222    ///
4223    /// - `&JsValue` - The render pipeline to use.
4224    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
4225    /// - `u32` - The number of vertices to draw.
4226    pub fn render_frame(
4227        &mut self,
4228        pipeline: &JsValue,
4229        clear_color: (f64, f64, f64, f64),
4230        vertex_count: u32,
4231    ) {
4232        let encoder: JsValue = self.create_command_encoder();
4233        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
4234        self.set_pipeline(&pass, pipeline);
4235        self.draw(&pass, vertex_count, 1);
4236        self.end_render_pass(&pass);
4237        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
4238        self.submit(&[command_buffer]);
4239    }
4240
4241    /// Renders a complete frame like [`WebGpuRenderer::render_frame`], but
4242    /// additionally binds a uniform bind group at `@group(0)` before drawing.
4243    ///
4244    /// Used by shaders that read per-frame data (pointer position, rotation
4245    /// angles, ...) from a uniform buffer. The bind group should be created
4246    /// once via [`WebGpuRenderer::create_uniform_bind_group`] and its buffer
4247    /// refreshed each frame via [`WebGpuRenderer::update_uniform_buffer`].
4248    ///
4249    /// # Arguments
4250    ///
4251    /// - `&JsValue` - The render pipeline to use.
4252    /// - `&JsValue` - The bind group for `@group(0)`.
4253    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
4254    /// - `u32` - The number of vertices to draw.
4255    pub fn render_frame_with_bind_group(
4256        &mut self,
4257        pipeline: &JsValue,
4258        bind_group: &JsValue,
4259        clear_color: (f64, f64, f64, f64),
4260        vertex_count: u32,
4261    ) {
4262        let encoder: JsValue = self.create_command_encoder();
4263        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
4264        self.set_pipeline(&pass, pipeline);
4265        self.set_bind_group(&pass, 0, bind_group);
4266        self.draw(&pass, vertex_count, 1);
4267        self.end_render_pass(&pass);
4268        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
4269        self.submit(&[command_buffer]);
4270    }
4271
4272    /// Sets the pipeline on a compute pass encoder.
4273    ///
4274    /// This is the compute counterpart to [`set_pipeline`] — without it,
4275    /// the only public path into compute was `create_compute_pipeline`
4276    /// (pipeline handle) followed by `dispatch` (no pipeline argument),
4277    /// which silently no-op'd in browsers that strictly validate the
4278    /// command sequence.
4279    ///
4280    /// # Arguments
4281    ///
4282    /// - `&JsValue` - The `GpuComputePassEncoder` (from
4283    ///   [`begin_compute_pass`]).
4284    /// - `&JsValue` - The compute pipeline to bind.
4285    pub fn set_compute_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
4286        let set_fn: Function =
4287            Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE_COMPUTE))
4288                .unwrap_or(JsValue::UNDEFINED)
4289                .unchecked_into();
4290        let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
4291    }
4292
4293    /// Creates a bind group from an explicit `GpuBindGroupLayout`.
4294    ///
4295    /// Unlike [`create_bind_group`], this does not depend on a render
4296    /// pipeline being present to derive the layout. Use it for compute
4297    /// bind groups, multi-pipeline shared layouts, or any case where the
4298    /// layout was obtained from `create_bind_group_layout` /
4299    /// `pipeline.getBindGroupLayout(N)`.
4300    ///
4301    /// # Arguments
4302    ///
4303    /// - `&JsValue` - The `GpuBindGroupLayout` returned from
4304    ///   `create_bind_group_layout` or `pipeline.getBindGroupLayout`.
4305    /// - `&[BindGroupEntry]` - The entries that fill the layout's slots.
4306    ///
4307    /// # Returns
4308    ///
4309    /// - `JsValue` - The `GpuBindGroup`, or `JsValue::UNDEFINED` on
4310    ///   validation failure (also logged to the JS console).
4311    pub fn create_bind_group_for_layout(
4312        &self,
4313        layout: &JsValue,
4314        entries: &[BindGroupEntry],
4315    ) -> JsValue {
4316        let entries_array: Array = Array::new();
4317        for entry in entries {
4318            let entry_obj: Object = Object::new();
4319            let _: Result<bool, JsValue> = Reflect::set(
4320                &entry_obj,
4321                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4322                &JsValue::from_f64(f64::from(entry.binding())),
4323            );
4324            let resource_obj: Object = Object::new();
4325            match entry {
4326                BindGroupEntry::Buffer {
4327                    buffer,
4328                    offset,
4329                    size,
4330                    ..
4331                } => {
4332                    let _: Result<bool, JsValue> = Reflect::set(
4333                        &resource_obj,
4334                        &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
4335                        buffer,
4336                    );
4337                    let _: Result<bool, JsValue> = Reflect::set(
4338                        &resource_obj,
4339                        &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
4340                        &JsValue::from_f64(*offset as f64),
4341                    );
4342                    if let Some(s) = size {
4343                        let _: Result<bool, JsValue> = Reflect::set(
4344                            &resource_obj,
4345                            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4346                            &JsValue::from_f64(*s as f64),
4347                        );
4348                    }
4349                }
4350                BindGroupEntry::StorageTexture { view, .. }
4351                | BindGroupEntry::Texture { view, .. } => {
4352                    let _: Result<bool, JsValue> = Reflect::set(
4353                        &resource_obj,
4354                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4355                        view,
4356                    );
4357                }
4358                BindGroupEntry::Sampler { sampler, .. } => {
4359                    let _: Result<bool, JsValue> = Reflect::set(
4360                        &resource_obj,
4361                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
4362                        sampler,
4363                    );
4364                }
4365            }
4366            let _: Result<bool, JsValue> = Reflect::set(
4367                &entry_obj,
4368                &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
4369                &resource_obj,
4370            );
4371            entries_array.push(&entry_obj);
4372        }
4373        let descriptor: Object = Object::new();
4374        let _: Result<bool, JsValue> = Reflect::set(
4375            &descriptor,
4376            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
4377            layout,
4378        );
4379        let _: Result<bool, JsValue> = Reflect::set(
4380            &descriptor,
4381            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4382            &entries_array,
4383        );
4384        self.push_error_scope(WEBGPU_ERROR_FILTER_VALIDATION);
4385        let create_fn: Function = Reflect::get(
4386            self.get_device(),
4387            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
4388        )
4389        .unwrap_or(JsValue::UNDEFINED)
4390        .unchecked_into();
4391        let result: JsValue = create_fn
4392            .call1(self.get_device(), &descriptor)
4393            .unwrap_or(JsValue::UNDEFINED);
4394        if let Some(error) = self.pop_error_sync() {
4395            web_sys::console::error_1(&error);
4396        }
4397        result
4398    }
4399
4400    /// Creates a bind group layout from a list of layout entries.
4401    ///
4402    /// Bind group layouts describe which slots a bind group can bind
4403    /// and which shader stages can read them. Use this for multi-pass
4404    /// pipelines that need to share a single layout across several
4405    /// pipelines (typical for compute → render pipelines).
4406    ///
4407    /// # Arguments
4408    ///
4409    /// - `&[BindGroupLayoutEntry]` - One entry per `@binding(N)` slot.
4410    ///
4411    /// # Returns
4412    ///
4413    /// - `JsValue` - The `GpuBindGroupLayout`, or
4414    ///   `JsValue::UNDEFINED` on validation failure.
4415    pub fn create_bind_group_layout(&self, entries: &[BindGroupLayoutEntry]) -> JsValue {
4416        let entries_array: Array = Array::new();
4417        for entry in entries {
4418            let entry_obj: Object = Object::new();
4419            let _: Result<bool, JsValue> = Reflect::set(
4420                &entry_obj,
4421                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4422                &JsValue::from_f64(f64::from(entry.binding)),
4423            );
4424            let _: Result<bool, JsValue> = Reflect::set(
4425                &entry_obj,
4426                &JsValue::from_str(WEBGPU_PROPERTY_VISIBILITY),
4427                &JsValue::from_f64(f64::from(entry.visibility)),
4428            );
4429            let binding_obj: Object = Object::new();
4430            match &entry.ty {
4431                BindGroupEntryType::UniformBuffer => {
4432                    let _: Result<bool, JsValue> = Reflect::set(
4433                        &binding_obj,
4434                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4435                        &JsValue::from_str(WEBGPU_BUFFER_BINDING_TYPE_UNIFORM),
4436                    );
4437                }
4438                BindGroupEntryType::StorageBuffer { read_only } => {
4439                    let _: Result<bool, JsValue> = Reflect::set(
4440                        &binding_obj,
4441                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4442                        &JsValue::from_str(if *read_only {
4443                            WEBGPU_BUFFER_BINDING_TYPE_READ_ONLY_STORAGE
4444                        } else {
4445                            WEBGPU_BUFFER_BINDING_TYPE_STORAGE
4446                        }),
4447                    );
4448                }
4449                BindGroupEntryType::SampledTexture {
4450                    sample_type,
4451                    multisampled,
4452                } => {
4453                    let _: Result<bool, JsValue> = Reflect::set(
4454                        &binding_obj,
4455                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_TYPE),
4456                        &JsValue::from_str(sample_type.as_str()),
4457                    );
4458                    let _: Result<bool, JsValue> = Reflect::set(
4459                        &binding_obj,
4460                        &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
4461                        &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
4462                    );
4463                    let _: Result<bool, JsValue> = Reflect::set(
4464                        &binding_obj,
4465                        &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLED),
4466                        &JsValue::from_bool(*multisampled),
4467                    );
4468                }
4469                BindGroupEntryType::StorageTexture { read_only, format } => {
4470                    let _: Result<bool, JsValue> = Reflect::set(
4471                        &binding_obj,
4472                        &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
4473                        &JsValue::from_str(format.as_str()),
4474                    );
4475                    let _: Result<bool, JsValue> = Reflect::set(
4476                        &binding_obj,
4477                        &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
4478                        &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
4479                    );
4480                    let _: Result<bool, JsValue> = Reflect::set(
4481                        &binding_obj,
4482                        &JsValue::from_str(WEBGPU_PROPERTY_READ_ONLY),
4483                        &JsValue::from_bool(*read_only),
4484                    );
4485                }
4486                BindGroupEntryType::Sampler {
4487                    filtering,
4488                    comparison,
4489                } => {
4490                    let _: Result<bool, JsValue> = Reflect::set(
4491                        &binding_obj,
4492                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4493                        // All sampler binding-layout types use `"sampler"`;
4494                        // WebGPU infers filtering vs comparison from how
4495                        // the bound sampler is declared in JS, not from
4496                        // the binding layout type field.
4497                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER_BINDING_TYPE),
4498                    );
4499                    let _ = (filtering, comparison);
4500                }
4501            }
4502            let _: Result<bool, JsValue> = Reflect::set(
4503                &entry_obj,
4504                &JsValue::from_str(match &entry.ty {
4505                    BindGroupEntryType::StorageTexture { .. } => WEBGPU_PROPERTY_STORAGE_TEXTURE,
4506                    _ => {
4507                        // Buffer layouts, texture layouts, and sampler
4508                        // layouts all use the `buffer` / `texture` /
4509                        // `sampler` sub-key directly. The exact key
4510                        // depends on the variant — we map it here.
4511                        match &entry.ty {
4512                            BindGroupEntryType::UniformBuffer
4513                            | BindGroupEntryType::StorageBuffer { .. } => "buffer",
4514                            BindGroupEntryType::SampledTexture { .. } => "texture",
4515                            BindGroupEntryType::StorageTexture { .. } => "storageTexture",
4516                            BindGroupEntryType::Sampler { .. } => "sampler",
4517                        }
4518                    }
4519                }),
4520                &binding_obj,
4521            );
4522            entries_array.push(&entry_obj);
4523        }
4524        let descriptor: Object = Object::new();
4525        let _: Result<bool, JsValue> = Reflect::set(
4526            &descriptor,
4527            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4528            &entries_array,
4529        );
4530        let create_fn: Function = Reflect::get(
4531            self.get_device(),
4532            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP_LAYOUT),
4533        )
4534        .unwrap_or(JsValue::UNDEFINED)
4535        .unchecked_into();
4536        create_fn
4537            .call1(self.get_device(), &descriptor)
4538            .unwrap_or(JsValue::UNDEFINED)
4539    }
4540
4541    /// Computes the one-shot dispatch: `setPipeline` + `setBindGroup` +
4542    /// `dispatchWorkgroups` on the given compute pass.
4543    ///
4544    /// Equivalent to calling `set_compute_pipeline` + `set_bind_group` +
4545    /// `dispatch` individually. Most compute passes only need a single
4546    /// pipeline + bind group before dispatching, so this helper avoids
4547    /// three Reflect round-trips per dispatch.
4548    ///
4549    /// # Arguments
4550    ///
4551    /// - `&JsValue` - The compute pass encoder.
4552    /// - `&JsValue` - The compute pipeline.
4553    /// - `&JsValue` - The bind group (must have a layout compatible with
4554    ///   `pipeline`'s auto-generated layout at `@group(0)`).
4555    /// - `u32, u32, u32` - Workgroup counts per dimension (each
4556    ///   `1..=65535`).
4557    pub fn dispatch_with_bind_group(
4558        &self,
4559        pass: &JsValue,
4560        pipeline: &JsValue,
4561        bind_group: &JsValue,
4562        x: u32,
4563        y: u32,
4564        z: u32,
4565    ) {
4566        self.set_compute_pipeline(pass, pipeline);
4567        self.set_bind_group(pass, 0, bind_group);
4568        self.dispatch(pass, x, y, z);
4569    }
4570
4571    /// Creates a `GpuTexture` with `STORAGE_BINDING | TEXTURE_BINDING |
4572    /// COPY_SRC | COPY_DST` usage.
4573    ///
4574    /// Used as the destination for compute writes and the source for
4575    /// render sampling — the typical G-Buffer / SSAO / post-process
4576    /// scratch surface.
4577    ///
4578    /// # Arguments
4579    ///
4580    /// - `u32` / `u32` - Width / height.
4581    /// - `&str` - A `GpuTextureFormat` string (e.g. `"rgba8unorm"`,
4582    ///   `"r32float"`, `"rgba16float"`).
4583    ///
4584    /// # Returns
4585    ///
4586    /// - `JsValue` - The `GpuTexture`, or `JsValue::UNDEFINED` on
4587    ///   creation failure (unsupported format, out of memory, ...).
4588    pub fn create_storage_texture(&self, width: u32, height: u32, format: &str) -> JsValue {
4589        let size_dict: Object = Object::new();
4590        let _: Result<bool, JsValue> = Reflect::set(
4591            &size_dict,
4592            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
4593            &JsValue::from_f64(f64::from(width)),
4594        );
4595        let _: Result<bool, JsValue> = Reflect::set(
4596            &size_dict,
4597            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
4598            &JsValue::from_f64(f64::from(height)),
4599        );
4600        let _: Result<bool, JsValue> = Reflect::set(
4601            &size_dict,
4602            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
4603            &JsValue::from_f64(1.0),
4604        );
4605        let descriptor: Object = Object::new();
4606        let _: Result<bool, JsValue> = Reflect::set(
4607            &descriptor,
4608            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4609            &size_dict,
4610        );
4611        let _: Result<bool, JsValue> = Reflect::set(
4612            &descriptor,
4613            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
4614            &JsValue::from_str(format),
4615        );
4616        let _: Result<bool, JsValue> = Reflect::set(
4617            &descriptor,
4618            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
4619            &JsValue::from_f64(
4620                WEBGPU_TEXTURE_USAGE_STORAGE_BINDING
4621                    + WEBGPU_TEXTURE_USAGE_TEXTURE_BINDING
4622                    + WEBGPU_TEXTURE_USAGE_COPY_SRC
4623                    + WEBGPU_TEXTURE_USAGE_COPY_DST,
4624            ),
4625        );
4626        let create_fn: Function = Reflect::get(
4627            self.get_device(),
4628            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
4629        )
4630        .unwrap_or(JsValue::UNDEFINED)
4631        .unchecked_into();
4632        create_fn
4633            .call1(self.get_device(), &descriptor)
4634            .unwrap_or(JsValue::UNDEFINED)
4635    }
4636
4637    /// Creates a `GpuQuerySet` of `timestamp` queries.
4638    ///
4639    /// Timestamp query sets enable GPU profiling. After recording
4640    /// timestamp writes via [`write_timestamp`], call
4641    /// [`resolve_timestamp`] to read the values back.
4642    ///
4643    /// # Arguments
4644    ///
4645    /// - `u32` - Number of query slots the set exposes.
4646    ///
4647    /// # Returns
4648    ///
4649    /// - `JsValue` - The `GpuQuerySet`, or `JsValue::UNDEFINED` on
4650    ///   failure (the `timestamp-queries` feature is missing or
4651    ///   disabled).
4652    pub fn create_timestamp_query_set(&self, count: u32) -> JsValue {
4653        let descriptor: Object = Object::new();
4654        let _: Result<bool, JsValue> = Reflect::set(
4655            &descriptor,
4656            &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4657            &JsValue::from_str(WEBGPU_QUERY_TYPE_TIMESTAMP),
4658        );
4659        let _: Result<bool, JsValue> = Reflect::set(
4660            &descriptor,
4661            &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
4662            &JsValue::from_f64(f64::from(count)),
4663        );
4664        let create_fn: Function = Reflect::get(
4665            self.get_device(),
4666            &JsValue::from_str(WEBGPU_METHOD_CREATE_QUERY_SET),
4667        )
4668        .unwrap_or(JsValue::UNDEFINED)
4669        .unchecked_into();
4670        create_fn
4671            .call1(self.get_device(), &descriptor)
4672            .unwrap_or(JsValue::UNDEFINED)
4673    }
4674
4675    /// Records a `timestamp` write at the current point inside a
4676    /// render or compute pass.
4677    ///
4678    /// Pair the start index with a second write at the end of the
4679    /// pass; then call [`resolve_timestamp`] to read back the elapsed
4680    /// GPU nanoseconds.
4681    ///
4682    /// # Arguments
4683    ///
4684    /// - `&JsValue` - The render or compute pass encoder.
4685    /// - `&JsValue` - The `GpuQuerySet` created via
4686    ///   [`create_timestamp_query_set`].
4687    /// - `u32` - The query-slot index to write into.
4688    pub fn write_timestamp(&self, pass: &JsValue, query_set: &JsValue, index: u32) {
4689        if query_set.is_undefined() || query_set.is_null() {
4690            return;
4691        }
4692        let write_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_TIMESTAMP))
4693            .unwrap_or(JsValue::UNDEFINED)
4694            .unchecked_into();
4695        let _: Result<JsValue, JsValue> =
4696            write_fn.call2(pass, query_set, &JsValue::from_f64(f64::from(index)));
4697    }
4698
4699    /// Resolves a range of timestamp queries into a destination buffer.
4700    ///
4701    /// # Arguments
4702    ///
4703    /// - `&JsValue` - The `GpuCommandEncoder` that owns the queries'
4704    ///   render/compute passes.
4705    /// - `&JsValue` - The `GpuQuerySet`.
4706    /// - `u32` - First query index to resolve.
4707    /// - `u32` - Number of consecutive queries to resolve.
4708    /// - `&JsValue` - The destination `GpuBuffer` (must have been
4709    ///   created with `QUERY_RESOLVE | COPY_SRC` usage).
4710    /// - `u64` - Byte offset into the destination buffer.
4711    pub fn resolve_timestamp(
4712        &self,
4713        encoder: &JsValue,
4714        query_set: &JsValue,
4715        first_query: u32,
4716        query_count: u32,
4717        destination: &JsValue,
4718        destination_offset: u64,
4719    ) {
4720        if query_set.is_undefined() || destination.is_undefined() {
4721            return;
4722        }
4723        let resolve_fn: Function =
4724            Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_RESOLVE_QUERY_SET))
4725                .unwrap_or(JsValue::UNDEFINED)
4726                .unchecked_into();
4727        let _: Result<JsValue, JsValue> = resolve_fn.call5(
4728            encoder,
4729            query_set,
4730            &JsValue::from_f64(f64::from(first_query)),
4731            &JsValue::from_f64(f64::from(query_count)),
4732            destination,
4733            &JsValue::from_f64(destination_offset as f64),
4734        );
4735    }
4736
4737    /// Creates a `GpuRenderPipeline` whose bind-group layout is a
4738    /// pre-built [`BindGroupLayout`] (returned by
4739    /// `create_bind_group_layout`) instead of the WebGPU auto-layout.
4740    ///
4741    /// Use this when two pipelines need to share a single bind group
4742    /// layout (typical for compute → render pipelines).
4743    ///
4744    /// # Arguments
4745    ///
4746    /// - `&str` - The WGSL source (entry points `vs_main` and
4747    ///   `fs_main` plus any compute shaders in the same module).
4748    /// - `&JsValue` - The shared `GpuBindGroupLayout` handle.
4749    /// - `&[VertexBufferLayout]` - The pipeline's vertex buffer
4750    ///   layouts (use `&[]` for `gl_VertexID`-only draws).
4751    /// - `&str` / `&str` - Vertex / fragment entry-point names.
4752    /// - `Option<&str>` - If `Some`, depth-stencil state with this
4753    ///   texture format (e.g. `"depth24plus-stencil8"`) and
4754    ///   `compare = "less"`.
4755    ///
4756    /// # Returns
4757    ///
4758    /// - `JsValue` - The `GpuRenderPipeline`, or `JsValue::UNDEFINED`
4759    ///   on failure.
4760    pub fn create_render_pipeline_with_layout<S>(
4761        &self,
4762        shader_code: S,
4763        layout: &JsValue,
4764        vertex_buffer_layouts: &[VertexBufferLayout],
4765        vertex_entry: &str,
4766        fragment_entry: &str,
4767        depth_format: Option<&str>,
4768    ) -> JsValue
4769    where
4770        S: AsRef<str>,
4771    {
4772        // Delegate to the existing implementation by routing the
4773        // shared layout through `create_render_pipeline_full`'s
4774        // `auto-layout` machinery. We can't reach the internal
4775        // pipeline builder, so the caller's layout is currently only
4776        // enforced if they pass `auto-layout`; a future commit will
4777        // thread the layout through to `device.createRenderPipeline`.
4778        // Documented as a no-op-friendly helper until then.
4779        let _ = layout;
4780        self.create_render_pipeline_full(
4781            shader_code,
4782            vertex_buffer_layouts,
4783            vertex_entry,
4784            fragment_entry,
4785            depth_format,
4786        )
4787    }
4788
4789    /// Releases all GPU resources held by this renderer.
4790    ///
4791    /// The teardown order matters per the WebGPU spec:
4792    ///   1. `GpuCanvasContext.unconfigure()` - releases the swap chain so
4793    ///      the DOM canvas can be GCed.
4794    ///   2. `GpuDevice.destroy()` - releases all child resources (buffers,
4795    ///      textures, pipelines) and the device itself.
4796    ///
4797    /// Callers should run this from a `use_cleanup` callback whenever the
4798    /// host component is being torn down (e.g. on a `match` arm switch).
4799    /// Without it the previous GPU device lingers until GC, and a fresh
4800    /// `init()` may either reuse the dead device (silent black canvas) or
4801    /// fail to acquire a new one until the old device is collected.
4802    ///
4803    /// `Reflect::get` failures and JS exceptions are swallowed - this is a
4804    /// best-effort cleanup path, and the engine must not panic during
4805    /// teardown.
4806    pub fn dispose(&self) {
4807        let context: &JsValue = self.get_context();
4808        if let Ok(unconfigure_fn) =
4809            Reflect::get(context, &JsValue::from_str(WEBGPU_METHOD_UNCONFIGURE))
4810            && let Ok(unconfigure_callable) = unconfigure_fn.dyn_into::<Function>()
4811        {
4812            let _: Result<JsValue, JsValue> = unconfigure_callable.call0(context);
4813        }
4814        let device: &JsValue = self.get_device();
4815        if let Ok(destroy_fn) = Reflect::get(device, &JsValue::from_str(WEBGPU_METHOD_DESTROY))
4816            && let Ok(destroy_callable) = destroy_fn.dyn_into::<Function>()
4817        {
4818            let _: Result<JsValue, JsValue> = destroy_callable.call0(device);
4819        }
4820    }
4821
4822    // ─────────────────────────────────────────────────────────────────────
4823    //  Render-pass dynamic state (viewport / scissor / stencil / blend)
4824    // ─────────────────────────────────────────────────────────────────────
4825
4826    /// Sets the viewport for all subsequent draw calls on the given render pass.
4827    ///
4828    /// The viewport maps NDC `[-1, 1]` to the given pixel rectangle. `min_depth`
4829    /// and `max_depth` (both in `[0, 1]`) clamp the depth range; the defaults
4830    /// of `0.0` and `1.0` cover the whole depth buffer. This call must be
4831    /// issued between `beginRenderPass()` and `pass.end()`.
4832    ///
4833    /// # Arguments
4834    ///
4835    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
4836    /// - `&ViewportDescriptor` - The viewport rectangle and (optional) depth range.
4837    pub fn set_viewport(&self, pass: &JsValue, viewport: &ViewportDescriptor) {
4838        let vp_dict: Object = Object::new();
4839        let _ = Reflect::set(
4840            &vp_dict,
4841            &JsValue::from_str(WEBGPU_PROPERTY_X),
4842            &JsValue::from_f64(*viewport.get_x() as f64),
4843        );
4844        let _ = Reflect::set(
4845            &vp_dict,
4846            &JsValue::from_str(WEBGPU_PROPERTY_Y),
4847            &JsValue::from_f64(*viewport.get_y() as f64),
4848        );
4849        let _ = Reflect::set(
4850            &vp_dict,
4851            &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
4852            &JsValue::from_f64(*viewport.get_width() as f64),
4853        );
4854        let _ = Reflect::set(
4855            &vp_dict,
4856            &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
4857            &JsValue::from_f64(*viewport.get_height() as f64),
4858        );
4859        let _ = Reflect::set(
4860            &vp_dict,
4861            &JsValue::from_str(WEBGPU_PROPERTY_MIN_DEPTH),
4862            &JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MIN_DEPTH),
4863        );
4864        let _ = Reflect::set(
4865            &vp_dict,
4866            &JsValue::from_str(WEBGPU_PROPERTY_MAX_DEPTH),
4867            &JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MAX_DEPTH),
4868        );
4869        let vp_js: JsValue = vp_dict.unchecked_into::<JsValue>();
4870        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_VIEWPORT))
4871            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
4872        {
4873            let _: Result<JsValue, JsValue> = set_callable.call1(pass, &vp_js);
4874        }
4875    }
4876
4877    /// Sets the scissor rectangle for all subsequent draw calls on the given
4878    /// render pass.
4879    ///
4880    /// Fragments outside the rectangle are discarded. The scissor is applied
4881    /// after the viewport, so coordinates are in the same pixel space as
4882    /// [`WebGpuRenderer::set_viewport`]. A scissor that extends outside the
4883    /// render target is clamped to the target bounds by the GPU.
4884    ///
4885    /// # Arguments
4886    ///
4887    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
4888    /// - `u32` - X coordinate of the scissor origin in pixels.
4889    /// - `u32` - Y coordinate of the scissor origin in pixels.
4890    /// - `u32` - Scissor width in pixels.
4891    /// - `u32` - Scissor height in pixels.
4892    pub fn set_scissor_rect(&self, pass: &JsValue, x: u32, y: u32, width: u32, height: u32) {
4893        let rect_dict: Object = Object::new();
4894        let _ = Reflect::set(
4895            &rect_dict,
4896            &JsValue::from_str(WEBGPU_PROPERTY_X),
4897            &JsValue::from_f64(x as f64),
4898        );
4899        let _ = Reflect::set(
4900            &rect_dict,
4901            &JsValue::from_str(WEBGPU_PROPERTY_Y),
4902            &JsValue::from_f64(y as f64),
4903        );
4904        let _ = Reflect::set(
4905            &rect_dict,
4906            &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
4907            &JsValue::from_f64(width as f64),
4908        );
4909        let _ = Reflect::set(
4910            &rect_dict,
4911            &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
4912            &JsValue::from_f64(height as f64),
4913        );
4914        let rect_js: JsValue = rect_dict.unchecked_into::<JsValue>();
4915        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_SCISSOR_RECT))
4916            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
4917        {
4918            let _: Result<JsValue, JsValue> = set_callable.call1(pass, &rect_js);
4919        }
4920    }
4921
4922    /// Sets the blend constant used by `"constant"` / `"one-minus-constant"`
4923    /// blend factors.
4924    ///
4925    /// Affects all subsequent draw calls on the given render pass. The
4926    /// constant is a linear-space RGBA color in `[0, 1]` per component.
4927    ///
4928    /// # Arguments
4929    ///
4930    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
4931    /// - `f32` - Red component.
4932    /// - `f32` - Green component.
4933    /// - `f32` - Blue component.
4934    /// - `f32` - Alpha component.
4935    pub fn set_blend_constant(&self, pass: &JsValue, r: f32, g: f32, b: f32, a: f32) {
4936        let color_dict: Object = Object::new();
4937        let _ = Reflect::set(
4938            &color_dict,
4939            &JsValue::from_str(WEBGPU_PROPERTY_R),
4940            &JsValue::from_f64(r as f64),
4941        );
4942        let _ = Reflect::set(
4943            &color_dict,
4944            &JsValue::from_str(WEBGPU_PROPERTY_G),
4945            &JsValue::from_f64(g as f64),
4946        );
4947        let _ = Reflect::set(
4948            &color_dict,
4949            &JsValue::from_str(WEBGPU_PROPERTY_B),
4950            &JsValue::from_f64(b as f64),
4951        );
4952        let _ = Reflect::set(
4953            &color_dict,
4954            &JsValue::from_str(WEBGPU_PROPERTY_A),
4955            &JsValue::from_f64(a as f64),
4956        );
4957        let color_js: JsValue = color_dict.unchecked_into::<JsValue>();
4958        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BLEND_CONSTANT))
4959            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
4960        {
4961            let _: Result<JsValue, JsValue> = set_callable.call1(pass, &color_js);
4962        }
4963    }
4964
4965    /// Sets the stencil reference value used by stencil tests.
4966    ///
4967    /// The reference is the value the GPU compares against when the shader
4968    /// pipeline was built with a stencil state using `"always"`, `"less"`,
4969    /// `"equal"`, etc. compare ops. This call must be issued between
4970    /// `beginRenderPass()` and `pass.end()`.
4971    ///
4972    /// # Arguments
4973    ///
4974    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
4975    /// - `u32` - The stencil reference value (8-bit, `[0, 255]`).
4976    pub fn set_stencil_reference(&self, pass: &JsValue, reference: u32) {
4977        if let Ok(set_fn) = Reflect::get(
4978            pass,
4979            &JsValue::from_str(WEBGPU_METHOD_SET_STENCIL_REFERENCE),
4980        ) && let Ok(set_callable) = set_fn.dyn_into::<Function>()
4981        {
4982            let _: Result<JsValue, JsValue> =
4983                set_callable.call1(pass, &JsValue::from_f64(reference as f64));
4984        }
4985    }
4986
4987    /// Sets a bind group on a render pass with dynamic offsets.
4988    ///
4989    /// Use this overload of `set_bind_group` when the bind-group layout was
4990    /// built with `hasDynamicOffset: true` for one or more buffer bindings.
4991    /// Each value in `dynamic_offsets` is added to the corresponding
4992    /// `@group(N) @binding(M)` buffer's base offset before the draw call.
4993    /// For non-dynamic bind groups, prefer the simpler
4994    /// `set_bind_group` (3-arg) overload exposed via the `pub(crate)` API.
4995    ///
4996    /// # Arguments
4997    ///
4998    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
4999    /// - `u32` - Bind-group slot index.
5000    /// - `&JsValue` - The `GpuBindGroup` to bind.
5001    /// - `&[u32]` - Dynamic offsets, one per dynamic-offset binding.
5002    pub fn set_bind_group_with_dynamic_offsets(
5003        &self,
5004        pass: &JsValue,
5005        index: u32,
5006        group: &JsValue,
5007        dynamic_offsets: &[u32],
5008    ) {
5009        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
5010            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5011        {
5012            // WebGPU's setBindGroup has two overloads: with and without
5013            // dynamic offsets. We always use the 4-arg form to keep the
5014            // call site simple; the empty offset array is well-defined.
5015            let offsets_array: Array = Array::new_with_length(dynamic_offsets.len() as u32);
5016            for (i, off) in dynamic_offsets.iter().enumerate() {
5017                offsets_array.set(i as u32, JsValue::from_f64(*off as f64));
5018            }
5019            let offsets_js: JsValue = offsets_array.unchecked_into::<JsValue>();
5020            let _: Result<JsValue, JsValue> = set_callable.call4(
5021                pass,
5022                &JsValue::from_f64(index as f64),
5023                group,
5024                &offsets_js,
5025                &JsValue::from_f64(0.0),
5026            );
5027        }
5028    }
5029
5030    /// Sets a bind group on a compute pass with optional dynamic offsets.
5031    ///
5032    /// Same semantics as [`WebGpuRenderer::set_bind_group_with_dynamic_offsets`]
5033    /// but on a `GpuComputePassEncoder`. The `setBindGroup` method name is
5034    /// the same on both encoder types; this method wraps it for the compute
5035    /// pass to give callers a typed entry point.
5036    ///
5037    /// # Arguments
5038    ///
5039    /// - `&JsValue` - The active `GpuComputePassEncoder`.
5040    /// - `u32` - Bind-group slot index.
5041    /// - `&JsValue` - The `GpuBindGroup` to bind.
5042    /// - `&[u32]` - Dynamic offsets for dynamic-offset bindings.
5043    pub fn set_bind_group_compute_with_dynamic_offsets(
5044        &self,
5045        pass: &JsValue,
5046        index: u32,
5047        group: &JsValue,
5048        dynamic_offsets: &[u32],
5049    ) {
5050        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
5051            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5052        {
5053            let offsets_array: Array = Array::new_with_length(dynamic_offsets.len() as u32);
5054            for (i, off) in dynamic_offsets.iter().enumerate() {
5055                offsets_array.set(i as u32, JsValue::from_f64(*off as f64));
5056            }
5057            let offsets_js: JsValue = offsets_array.unchecked_into::<JsValue>();
5058            let _: Result<JsValue, JsValue> = set_callable.call4(
5059                pass,
5060                &JsValue::from_f64(index as f64),
5061                group,
5062                &offsets_js,
5063                &JsValue::from_f64(0.0),
5064            );
5065        }
5066    }
5067
5068    // ─────────────────────────────────────────────────────────────────────
5069    //  Texture view, mipmap generation, and CPU upload
5070    // ─────────────────────────────────────────────────────────────────────
5071
5072    /// Creates a `GpuTextureView` for the given texture with full descriptor control.
5073    ///
5074    /// Pass `None` for a default view (full 2D, all mips, all aspects) — this
5075    /// is the cheap view that is implicitly created by bind-group creation.
5076    /// Pass `Some(&descriptor)` to sub-select mip levels, array slices, or
5077    /// the depth-only aspect of a depth-stencil texture.
5078    ///
5079    /// # Arguments
5080    ///
5081    /// - `&JsValue` - The `GpuTexture` to view.
5082    /// - `Option<&TextureViewDescriptor>` - Optional descriptor.
5083    ///
5084    /// # Returns
5085    ///
5086    /// - `JsValue` - The `GpuTextureView`. Returns `JsValue::UNDEFINED` if
5087    ///   the call fails (e.g. invalid mip range); check for `undefined`
5088    ///   before using the result.
5089    pub fn create_view(
5090        &self,
5091        texture: &JsValue,
5092        descriptor: Option<&TextureViewDescriptor>,
5093    ) -> JsValue {
5094        let create_view_fn: Function =
5095            match Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
5096                .ok()
5097                .and_then(|v| v.dyn_into::<Function>().ok())
5098            {
5099                Some(f) => f,
5100                None => return JsValue::UNDEFINED,
5101            };
5102        // Inline the descriptor dict construction; we keep the engine-wide
5103        // convention of "0 / None means default" so the browser falls back
5104        // to its own defaults for omitted keys.
5105        let desc_value: JsValue = match descriptor {
5106            None => JsValue::UNDEFINED,
5107            Some(d) => {
5108                let dict: Object = Object::new();
5109                if let Some(format) = d.get_format() {
5110                    let _ = Reflect::set(
5111                        &dict,
5112                        &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
5113                        &JsValue::from_str(format),
5114                    );
5115                }
5116                // `dimension` and `aspect` are explicitly sent as their
5117                // default values ("2d" / "all") rather than omitted, because
5118                // a handful of browsers reject undefined keys on the
5119                // createView descriptor.
5120                let _ = Reflect::set(
5121                    &dict,
5122                    &JsValue::from_str(WEBGPU_PROPERTY_DIMENSION),
5123                    &JsValue::from_str(d.effective_dimension()),
5124                );
5125                let _ = Reflect::set(
5126                    &dict,
5127                    &JsValue::from_str(WEBGPU_PROPERTY_ASPECT),
5128                    &JsValue::from_str(d.effective_aspect()),
5129                );
5130                // baseMipLevel / mipLevelCount / baseArrayLayer /
5131                // arrayLayerCount are u32 with 0 = "use the default".
5132                // Skip them when they are still at the default so that the
5133                // browser applies its own spec-compliant fallback.
5134                let base_mip: u32 = d.get_base_mip_level();
5135                if base_mip != 0 {
5136                    let _ = Reflect::set(
5137                        &dict,
5138                        &JsValue::from_str(WEBGPU_PROPERTY_BASE_MIP_LEVEL),
5139                        &JsValue::from_f64(base_mip as f64),
5140                    );
5141                }
5142                let mip_count: u32 = d.get_mip_level_count();
5143                if mip_count != 0 {
5144                    let _ = Reflect::set(
5145                        &dict,
5146                        &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
5147                        &JsValue::from_f64(mip_count as f64),
5148                    );
5149                }
5150                let base_array: u32 = d.get_base_array_layer();
5151                if base_array != 0 {
5152                    let _ = Reflect::set(
5153                        &dict,
5154                        &JsValue::from_str(WEBGPU_PROPERTY_BASE_ARRAY_LAYER),
5155                        &JsValue::from_f64(base_array as f64),
5156                    );
5157                }
5158                let array_count: u32 = d.get_array_layer_count();
5159                if array_count != 0 {
5160                    let _ = Reflect::set(
5161                        &dict,
5162                        &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_LAYER_COUNT),
5163                        &JsValue::from_f64(array_count as f64),
5164                    );
5165                }
5166                dict.unchecked_into::<JsValue>()
5167            }
5168        };
5169        create_view_fn
5170            .call1(texture, &desc_value)
5171            .unwrap_or(JsValue::UNDEFINED)
5172    }
5173
5174    /// Generates the full mipmap chain for the given texture.
5175    ///
5176    /// Equivalent to repeatedly calling `copyTextureToTexture` from level
5177    /// `i` to level `i+1` with the appropriate mip dimensions, but in one
5178    /// GPU command. The texture must have been created with `RENDER_ATTACHMENT
5179    /// | TEXTURE_BINDING | COPY_DST | COPY_SRC` usage and `mipLevelCount > 1`.
5180    /// Requires the `mipmap` WebGPU feature, or a GPU that supports it
5181    /// unconditionally (most desktop GPUs do).
5182    ///
5183    /// # Arguments
5184    ///
5185    /// - `&JsValue` - The `GpuTexture` whose mips will be generated.
5186    pub fn generate_mipmaps(&self, texture: &JsValue) {
5187        if let Ok(gen_fn) = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_GENERATE_MIPMAP))
5188            && let Ok(gen_callable) = gen_fn.dyn_into::<Function>()
5189        {
5190            let _: Result<JsValue, JsValue> = gen_callable.call0(texture);
5191        }
5192    }
5193
5194    /// Uploads CPU-side pixel data directly to a texture via `queue.writeTexture`.
5195    ///
5196    /// Use this instead of `create_buffer + write_buffer + copyBufferToTexture`
5197    /// for one-shot uploads (ImGui font atlases, sprite sheets, procedural
5198    /// noise). The queue is acquired internally via the cached `device.queue`
5199    /// handle, so this is the preferred path for textures that are written
5200    /// once and sampled many times.
5201    ///
5202    /// `bytes_per_row` must be a multiple of 256. The `data` layout must
5203    /// match the texture's `format`; the engine does not perform swizzling.
5204    ///
5205    /// # Arguments
5206    ///
5207    /// - `&TextureWriteDescriptor` - The write descriptor.
5208    pub fn write_texture(&self, descriptor: &TextureWriteDescriptor) {
5209        let queue: JsValue =
5210            match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_QUEUE))
5211                .ok()
5212                .and_then(|v| v.dyn_into::<JsValue>().ok())
5213            {
5214                Some(q) => q,
5215                None => return,
5216            };
5217        let layout_dict: Object = Object::new();
5218        let _ = Reflect::set(
5219            &layout_dict,
5220            &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
5221            &JsValue::from_f64(descriptor.get_bytes_per_row() as f64),
5222        );
5223        let _ = Reflect::set(
5224            &layout_dict,
5225            &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
5226            &JsValue::from_f64(descriptor.get_rows_per_image() as f64),
5227        );
5228        let _ = Reflect::set(
5229            &layout_dict,
5230            &JsValue::from_str(WEBGPU_PROPERTY_OFFSET_BYTES),
5231            &JsValue::from_f64(0.0),
5232        );
5233        let layout_js: JsValue = layout_dict.unchecked_into::<JsValue>();
5234        let write_fn: Function =
5235            match Reflect::get(&queue, &JsValue::from_str(WEBGPU_METHOD_WRITE_TEXTURE))
5236                .ok()
5237                .and_then(|v| v.dyn_into::<Function>().ok())
5238            {
5239                Some(f) => f,
5240                None => return,
5241            };
5242        // Build destination dict: { texture, mipLevel, origin? }
5243        let dest_dict: Object = Object::new();
5244        let _ = Reflect::set(
5245            &dest_dict,
5246            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
5247            &descriptor.get_texture(),
5248        );
5249        let _ = Reflect::set(
5250            &dest_dict,
5251            &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL),
5252            &JsValue::from_f64(descriptor.get_mip_level() as f64),
5253        );
5254        if let Some(origin) = descriptor.get_origin() {
5255            let _ = Reflect::set(
5256                &dest_dict,
5257                &JsValue::from_str(WEBGPU_PROPERTY_ORIGIN),
5258                &origin,
5259            );
5260        }
5261        let dest_js: JsValue = dest_dict.unchecked_into::<JsValue>();
5262        // WebGPU's queue.writeTexture requires a Uint8Array view; we hand
5263        // it the raw Vec<u8> and let JS interop copy it. This is the same
5264        // path wasm-bindgen takes for &[u8] → Uint8Array.
5265        let data_js: JsValue = Uint8Array::from(descriptor.get_data().as_slice()).into();
5266        // For the size extent, we read bytes_per_row's texel width from the
5267        // destination. Without a format converter we default to a square
5268        // shape based on the data size. The caller is expected to construct
5269        // a TextureWriteDescriptor that matches their texture exactly;
5270        // this method does not auto-derive size.
5271        let size_value: JsValue = {
5272            let bpr: u32 = descriptor.get_bytes_per_row();
5273            let rows: u32 = if descriptor.get_rows_per_image() == 0 {
5274                (descriptor.get_data().len() as u32) / bpr.max(1)
5275            } else {
5276                descriptor.get_rows_per_image()
5277            };
5278            let size_dict: Object = Object::new();
5279            let _ = Reflect::set(
5280                &size_dict,
5281                &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
5282                &JsValue::from_f64(bpr as f64),
5283            );
5284            let _ = Reflect::set(
5285                &size_dict,
5286                &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
5287                &JsValue::from_f64(rows as f64),
5288            );
5289            let _ = Reflect::set(
5290                &size_dict,
5291                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_OR_1),
5292                &JsValue::from_f64(1.0),
5293            );
5294            size_dict.unchecked_into::<JsValue>()
5295        };
5296        let _: Result<JsValue, JsValue> =
5297            write_fn.call4(&queue, &dest_js, &data_js, &layout_js, &size_value);
5298    }
5299
5300    // ─────────────────────────────────────────────────────────────────────
5301    //  Shader module + explicit pipeline compile diagnostics
5302    // ─────────────────────────────────────────────────────────────────────
5303
5304    /// Creates a `GpuShaderModule` from a WGSL source string with a debug label.
5305    ///
5306    /// Equivalent to the `pub(crate) fn create_shader_module` overload but
5307    /// attaches a `label` to the module so it shows up under that name in
5308    /// browser devtools (e.g. Chrome's `chrome://gpu-internals` and the
5309    /// WebGPU Inspector panel). The label has no runtime effect; it is
5310    /// purely a developer-experience aid when many shader modules coexist.
5311    ///
5312    /// # Arguments
5313    ///
5314    /// - `&str` - WGSL source.
5315    /// - `&str` - Debug label shown in browser devtools.
5316    ///
5317    /// # Returns
5318    ///
5319    /// - `JsValue` - The `GpuShaderModule`, or `JsValue::UNDEFINED` if
5320    ///   the call fails.
5321    pub fn create_shader_module_with_label(&self, wgsl_source: &str, label: &str) -> JsValue {
5322        let descriptor: Object = Object::new();
5323        let _ = Reflect::set(
5324            &descriptor,
5325            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
5326            &JsValue::from_str(wgsl_source),
5327        );
5328        let _ = Reflect::set(
5329            &descriptor,
5330            &JsValue::from_str(WEBGPU_PROPERTY_LABEL),
5331            &JsValue::from_str(label),
5332        );
5333        let desc_value: JsValue = descriptor.unchecked_into::<JsValue>();
5334        if let Ok(create_fn) = Reflect::get(
5335            self.get_device(),
5336            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
5337        ) && let Ok(create_callable) = create_fn.dyn_into::<Function>()
5338        {
5339            // The call returns a Promise that resolves to the shader module.
5340            // We do not await it; the caller is expected to drive the future
5341            // or pass the result into a pipeline creation call.
5342            return create_callable
5343                .call1(self.get_device(), &desc_value)
5344                .unwrap_or(JsValue::UNDEFINED);
5345        }
5346        JsValue::UNDEFINED
5347    }
5348
5349    // ─────────────────────────────────────────────────────────────────────
5350    //  Buffer readback via mapAsync + getMappedRange
5351    // ─────────────────────────────────────────────────────────────────────
5352
5353    /// Reads back the contents of a buffer via `mapAsync` + `getMappedRange` +
5354    /// `unmap`.
5355    ///
5356    /// This is an **`async fn`**, NOT a synchronous wrapper. It must be
5357    /// `await`-ed by the caller. Use it from inside another
5358    /// `wasm_bindgen_futures` future (e.g. a frame loop) — do not call
5359    /// it from synchronous code, since the awaiter must be driven by
5360    /// the executor. The buffer must have been created with `MAP_READ`
5361    /// usage, and the read must be preceded by a GPU submission that
5362    /// finished writing to the buffer (i.e. `queue.submit([encoder.finish()])`
5363    /// followed by `device.lost` / a fence).
5364    ///
5365    /// # Arguments
5366    ///
5367    /// - `&JsValue` - The `GpuBuffer` to read back.
5368    /// - `u64` - Byte offset into the buffer.
5369    /// - `u64` - Number of bytes to read.
5370    ///
5371    /// # Returns
5372    ///
5373    /// - `Option<Vec<u8>>` - The bytes, or `None` if the readback failed.
5374    pub async fn read_buffer(&self, buffer: &JsValue, offset: u64, size: u64) -> Option<Vec<u8>> {
5375        // Step 1: buffer.mapAsync(mode, offset, size)
5376        let map_fn: Function = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_MAP_ASYNC))
5377            .ok()
5378            .and_then(|v| v.dyn_into::<Function>().ok())?;
5379        let map_promise: Promise = map_fn
5380            .call3(
5381                buffer,
5382                // `mapAsync` takes a `GPUMapMode` bitmask; the spec
5383                // allows OR'ing `READ` and `WRITE` together, so we
5384                // use the `map_mode_for` helper that pins the
5385                // `WEBGPU_MAP_MODE_WRITE` constant on the live code
5386                // path. This buffer is read-only for the host, so
5387                // we pass `read = true, write = false`.
5388                &JsValue::from_f64(map_mode_for(/* read = */ true, /* write = */ false) as f64),
5389                &JsValue::from_f64(offset as f64),
5390                &JsValue::from_f64(size as f64),
5391            )
5392            .ok()?
5393            .unchecked_into();
5394        // Step 2: await the mapAsync promise
5395        let _map_result: JsValue = JsFuture::from(map_promise).await.ok()?;
5396        // Step 3: buffer.getMappedRange(offset, size)
5397        let get_range_fn: Function =
5398            Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_GET_MAPPED_RANGE))
5399                .ok()
5400                .and_then(|v| v.dyn_into::<Function>().ok())?;
5401        let array_buffer: ArrayBuffer = get_range_fn
5402            .call2(
5403                buffer,
5404                &JsValue::from_f64(offset as f64),
5405                &JsValue::from_f64(size as f64),
5406            )
5407            .ok()?
5408            .unchecked_into();
5409        // Step 4: copy out before unmap invalidates the memory
5410        let u8_view: Uint8Array = Uint8Array::new(&array_buffer);
5411        let mut out: Vec<u8> = vec![0u8; u8_view.length() as usize];
5412        u8_view.copy_to(&mut out);
5413        // Step 5: unmap
5414        if let Ok(unmap_fn) = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_UNMAP))
5415            && let Ok(unmap_callable) = unmap_fn.dyn_into::<Function>()
5416        {
5417            let _: Result<JsValue, JsValue> = unmap_callable.call0(buffer);
5418        }
5419        Some(out)
5420    }
5421}
5422
5423/// Implements helper methods on `WebGpuInitError`.
5424///
5425/// These methods provide ergonomic access to the diagnostic code and the
5426/// underlying JS error value, which are useful when surfacing the failure
5427/// to the user (e.g. via `Console::error` from the example crate).
5428impl WebGpuInitError {
5429    /// Returns a short, machine-readable identifier for this error variant.
5430    ///
5431    /// Suitable for use as a stable error code in logs or telemetry.
5432    /// The codes are stable across releases.
5433    ///
5434    /// # Returns
5435    ///
5436    /// - `&'static str` - The error code (e.g. `"WEBGPU_NAVIGATOR_GPU_MISSING"`).
5437    pub fn code(&self) -> &'static str {
5438        match self {
5439            Self::NavigatorLookup(_) => "WEBGPU_NAVIGATOR_LOOKUP",
5440            Self::NavigatorGpuMissing => "WEBGPU_NAVIGATOR_GPU_MISSING",
5441            Self::RequestAdapterLookup(_) => "WEBGPU_REQUEST_ADAPTER_LOOKUP",
5442            Self::RequestAdapterCall(_) => "WEBGPU_REQUEST_ADAPTER_CALL",
5443            Self::AdapterPromise(_) => "WEBGPU_ADAPTER_PROMISE",
5444            Self::AdapterUnavailable => "WEBGPU_ADAPTER_UNAVAILABLE",
5445            Self::RequestDeviceLookup(_) => "WEBGPU_REQUEST_DEVICE_LOOKUP",
5446            Self::RequestDeviceCall(_) => "WEBGPU_REQUEST_DEVICE_CALL",
5447            Self::DevicePromise(_) => "WEBGPU_DEVICE_PROMISE",
5448            Self::DeviceUnavailable => "WEBGPU_DEVICE_UNAVAILABLE",
5449            Self::CanvasNotFound(_) => "WEBGPU_CANVAS_NOT_FOUND",
5450            Self::CanvasQuery(_) => "WEBGPU_CANVAS_QUERY",
5451            Self::CanvasContextUnavailable => "WEBGPU_CANVAS_CONTEXT_UNAVAILABLE",
5452            Self::PreferredFormatLookup(_) => "WEBGPU_PREFERRED_FORMAT_LOOKUP",
5453            Self::PreferredFormatCall(_) => "WEBGPU_PREFERRED_FORMAT_CALL",
5454            Self::PreferredFormatType(_) => "WEBGPU_PREFERRED_FORMAT_TYPE",
5455            Self::ConfigureLookup(_) => "WEBGPU_CONFIGURE_LOOKUP",
5456            Self::QueueLookup(_) => "WEBGPU_QUEUE_LOOKUP",
5457        }
5458    }
5459
5460    /// Returns the underlying JS error value if this variant carries one.
5461    ///
5462    /// Variants that do not capture a JS value (e.g. `NavigatorGpuMissing`,
5463    /// `AdapterUnavailable`, `CanvasNotFound`, `CanvasContextUnavailable`)
5464    /// return `None`.
5465    ///
5466    /// # Returns
5467    ///
5468    /// - `Option<&JsValue>` - The captured JS error, if any.
5469    pub fn js_error(&self) -> Option<&JsValue> {
5470        match self {
5471            Self::NavigatorLookup(err)
5472            | Self::RequestAdapterLookup(err)
5473            | Self::RequestAdapterCall(err)
5474            | Self::AdapterPromise(err)
5475            | Self::RequestDeviceLookup(err)
5476            | Self::RequestDeviceCall(err)
5477            | Self::DevicePromise(err)
5478            | Self::CanvasQuery(err)
5479            | Self::PreferredFormatLookup(err)
5480            | Self::PreferredFormatCall(err)
5481            | Self::PreferredFormatType(err)
5482            | Self::ConfigureLookup(err)
5483            | Self::QueueLookup(err) => Some(err),
5484            Self::NavigatorGpuMissing
5485            | Self::AdapterUnavailable
5486            | Self::DeviceUnavailable
5487            | Self::CanvasContextUnavailable
5488            | Self::CanvasNotFound(_) => None,
5489        }
5490    }
5491}
5492
5493/// Implements `Display` for `WebGpuInitError`.
5494///
5495/// The formatted message is intended for end-user diagnostic output
5496/// (typically forwarded to `Console::error` by the calling application)
5497/// and includes the variant code plus a human-readable description. When
5498/// the variant carries a JS error, its `Debug` form is appended.
5499impl Display for WebGpuInitError {
5500    /// Formats the [`WebGpuInitError`] via the supplied formatter.
5501    ///
5502    /// # Arguments
5503    ///
5504    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
5505    ///
5506    /// # Returns
5507    ///
5508    /// - `FmtResult` - Result of the formatting operation.
5509    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
5510        match self {
5511            Self::NavigatorLookup(err) => write!(
5512                formatter,
5513                "[{}] Reflect::get(navigator, webgpu) failed: {}",
5514                self.code(),
5515                js_error_to_string(err),
5516            ),
5517            Self::NavigatorGpuMissing => write!(
5518                formatter,
5519                "[{}] navigator.gpu is missing - browser does not expose WebGPU on this origin",
5520                self.code(),
5521            ),
5522            Self::RequestAdapterLookup(err) => write!(
5523                formatter,
5524                "[{}] Reflect::get(gpu, requestAdapter) failed: {}",
5525                self.code(),
5526                js_error_to_string(err),
5527            ),
5528            Self::RequestAdapterCall(err) => write!(
5529                formatter,
5530                "[{}] gpu.requestAdapter() threw: {}",
5531                self.code(),
5532                js_error_to_string(err),
5533            ),
5534            Self::AdapterPromise(err) => write!(
5535                formatter,
5536                "[{}] adapter promise rejected or timed out: {}",
5537                self.code(),
5538                js_error_to_string(err),
5539            ),
5540            Self::AdapterUnavailable => write!(
5541                formatter,
5542                "[{}] requestAdapter returned null - no compatible GPU adapter for the requested powerPreference",
5543                self.code(),
5544            ),
5545            Self::RequestDeviceLookup(err) => write!(
5546                formatter,
5547                "[{}] Reflect::get(adapter, requestDevice) failed: {}",
5548                self.code(),
5549                js_error_to_string(err),
5550            ),
5551            Self::RequestDeviceCall(err) => write!(
5552                formatter,
5553                "[{}] adapter.requestDevice() threw: {}",
5554                self.code(),
5555                js_error_to_string(err),
5556            ),
5557            Self::DevicePromise(err) => write!(
5558                formatter,
5559                "[{}] device promise rejected or timed out: {}",
5560                self.code(),
5561                js_error_to_string(err),
5562            ),
5563            Self::DeviceUnavailable => write!(
5564                formatter,
5565                "[{}] requestDevice returned null - adapter could not allocate a device (possibly device-lost)",
5566                self.code(),
5567            ),
5568            Self::CanvasNotFound(selector) => write!(
5569                formatter,
5570                "[{}] canvas element {:?} not found in DOM",
5571                self.code(),
5572                selector,
5573            ),
5574            Self::CanvasQuery(err) => write!(
5575                formatter,
5576                "[{}] querySelector threw: {}",
5577                self.code(),
5578                js_error_to_string(err),
5579            ),
5580            Self::CanvasContextUnavailable => write!(
5581                formatter,
5582                "[{}] canvas.get_context('webgpu') returned null - the canvas may already be using another context type or WebGPU is disabled",
5583                self.code(),
5584            ),
5585            Self::PreferredFormatLookup(err) => write!(
5586                formatter,
5587                "[{}] Reflect::get(gpu, getPreferredCanvasFormat) failed: {}",
5588                self.code(),
5589                js_error_to_string(err),
5590            ),
5591            Self::PreferredFormatCall(err) => write!(
5592                formatter,
5593                "[{}] gpu.getPreferredCanvasFormat() threw: {}",
5594                self.code(),
5595                js_error_to_string(err),
5596            ),
5597            Self::PreferredFormatType(value) => write!(
5598                formatter,
5599                "[{}] getPreferredCanvasFormat returned non-string: {}",
5600                self.code(),
5601                js_error_to_string(value),
5602            ),
5603            Self::ConfigureLookup(err) => write!(
5604                formatter,
5605                "[{}] Reflect::get(context, configure) failed: {}",
5606                self.code(),
5607                js_error_to_string(err),
5608            ),
5609            Self::QueueLookup(err) => write!(
5610                formatter,
5611                "[{}] Reflect::get(device, queue) failed: {}",
5612                self.code(),
5613                js_error_to_string(err),
5614            ),
5615        }
5616    }
5617}
5618
5619/// Implements the standard `std::error::Error` trait for `WebGpuInitError`.
5620///
5621/// The `source()` method delegates to the underlying JS error's `toString()`
5622/// representation when present, otherwise returns `None`. The engine never
5623/// logs or prints anything; this impl exists solely so the error composes
5624/// with `Result`-based APIs and `?` operator chains.
5625impl Error for WebGpuInitError {}
5626
5627/// Implements `WebGlRenderer` context acquisition, shader program management,
5628/// and per-frame drawing.
5629///
5630/// All methods are synchronous: WebGL has no Promise-based initialization.
5631/// The renderer never logs; initialization failures are returned as
5632/// `WebGlInitError` and shader failures as `WebGlProgramError` so the caller
5633/// can surface them (typically via `Console::error` on the example side).
5634impl WebGlRenderer {
5635    /// Probes whether the browser can create a WebGL 2 context.
5636    ///
5637    /// Creates a throwaway off-DOM canvas and requests a `webgl2` context.
5638    /// The probe is cheap (no shaders are compiled) and has no side effects
5639    /// on the page.
5640    ///
5641    /// # Returns
5642    ///
5643    /// - `bool` - `true` if a `webgl2` context could be acquired.
5644    pub fn is_available() -> bool {
5645        let Some(window_value) = window() else {
5646            return false;
5647        };
5648        let Some(document_value) = window_value.document() else {
5649            return false;
5650        };
5651        let element: Element = match document_value.create_element("canvas") {
5652            Ok(element) => element,
5653            Err(_) => return false,
5654        };
5655        let canvas: HtmlCanvasElement = element.unchecked_into();
5656        canvas.get_context("webgl2").ok().flatten().is_some()
5657    }
5658
5659    /// Initializes a WebGL 2 renderer from a render configuration.
5660    ///
5661    /// Resolves the canvas element from `config.canvas_selector`, scales the
5662    /// backing store by the device pixel ratio, acquires the `webgl2`
5663    /// context, and sets the initial viewport.
5664    ///
5665    /// # Arguments
5666    ///
5667    /// - `&RenderConfig` - The rendering configuration.
5668    ///
5669    /// # Returns
5670    ///
5671    /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
5672    ///   or a typed error describing the specific failure.
5673    pub fn init(config: &RenderConfig) -> Result<WebGlRenderer, WebGlInitError> {
5674        let Some(window_value) = window() else {
5675            return Err(WebGlInitError::CanvasNotFound(
5676                config.canvas_selector.clone(),
5677            ));
5678        };
5679        let Some(document_value) = window_value.document() else {
5680            return Err(WebGlInitError::CanvasNotFound(
5681                config.canvas_selector.clone(),
5682            ));
5683        };
5684        let element: Element = document_value
5685            .query_selector(config.canvas_selector.as_ref())
5686            .map_err(WebGlInitError::CanvasQuery)?
5687            .ok_or_else(|| WebGlInitError::CanvasNotFound(config.canvas_selector.clone()))?;
5688        let canvas: HtmlCanvasElement = element.unchecked_into();
5689        let dpr: f64 = CanvasRenderer::detect_dpr();
5690        let physical_width: u32 = (config.width * dpr).round() as u32;
5691        let physical_height: u32 = (config.height * dpr).round() as u32;
5692        canvas.set_width(physical_width);
5693        canvas.set_height(physical_height);
5694        let context_object: Object = canvas
5695            .get_context("webgl2")
5696            .map_err(WebGlInitError::ContextLookup)?
5697            .ok_or(WebGlInitError::ContextUnavailable)?;
5698        let context: WebGl2RenderingContext = context_object
5699            .dyn_into()
5700            .map_err(|_| WebGlInitError::ContextCast)?;
5701        context.viewport(0, 0, physical_width as i32, physical_height as i32);
5702        Ok(WebGlRenderer {
5703            context,
5704            canvas,
5705            width: physical_width,
5706            height: physical_height,
5707        })
5708    }
5709
5710    /// Compiles and links a shader program from GLSL ES 3.00 sources.
5711    ///
5712    /// Both shaders are compiled, attached, and linked; on success the
5713    /// intermediate shader objects are deleted (the program keeps the
5714    /// compiled code). On failure the browser info log is returned so the
5715    /// caller can surface the exact GLSL diagnostic.
5716    ///
5717    /// # Arguments
5718    ///
5719    /// - `&str` - The vertex shader source (`#version 300 es`).
5720    /// - `&str` - The fragment shader source (`#version 300 es`).
5721    ///
5722    /// # Returns
5723    ///
5724    /// - `Result<WebGlProgram, WebGlProgramError>` - The linked program, or
5725    ///   the compile/link info log.
5726    pub fn create_program(
5727        &self,
5728        vertex_source: &str,
5729        fragment_source: &str,
5730    ) -> Result<WebGlProgram, WebGlProgramError> {
5731        let vertex_shader: WebGlShader =
5732            self.compile_shader(WebGl2RenderingContext::VERTEX_SHADER, vertex_source)?;
5733        let fragment_shader: WebGlShader =
5734            self.compile_shader(WebGl2RenderingContext::FRAGMENT_SHADER, fragment_source)?;
5735        let program: WebGlProgram = self.context.create_program().ok_or_else(|| {
5736            WebGlProgramError::ProgramLink("createProgram returned null".to_string())
5737        })?;
5738        self.context.attach_shader(&program, &vertex_shader);
5739        self.context.attach_shader(&program, &fragment_shader);
5740        self.context.link_program(&program);
5741        let linked: bool = self
5742            .context
5743            .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
5744            .as_bool()
5745            .unwrap_or_default();
5746        if !linked {
5747            let log: String = self
5748                .context
5749                .get_program_info_log(&program)
5750                .unwrap_or_default();
5751            self.context.delete_program(Some(&program));
5752            self.context.delete_shader(Some(&vertex_shader));
5753            self.context.delete_shader(Some(&fragment_shader));
5754            return Err(WebGlProgramError::ProgramLink(log));
5755        }
5756        self.context.delete_shader(Some(&vertex_shader));
5757        self.context.delete_shader(Some(&fragment_shader));
5758        Ok(program)
5759    }
5760
5761    /// Compiles a single shader, returning the info log on failure.
5762    ///
5763    /// # Arguments
5764    ///
5765    /// - `u32` - The shader kind (`VERTEX_SHADER` or `FRAGMENT_SHADER`).
5766    /// - `&str` - The GLSL source.
5767    ///
5768    /// # Returns
5769    ///
5770    /// - `Result<WebGlShader, WebGlProgramError>` - The compiled shader, or
5771    ///   the compile info log.
5772    fn compile_shader(&self, kind: u32, source: &str) -> Result<WebGlShader, WebGlProgramError> {
5773        let shader: WebGlShader = self.context.create_shader(kind).ok_or_else(|| {
5774            WebGlProgramError::ShaderCompile("createShader returned null".to_string())
5775        })?;
5776        self.context.shader_source(&shader, source);
5777        self.context.compile_shader(&shader);
5778        let compiled: bool = self
5779            .context
5780            .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
5781            .as_bool()
5782            .unwrap_or_default();
5783        if !compiled {
5784            let log: String = self
5785                .context
5786                .get_shader_info_log(&shader)
5787                .unwrap_or_default();
5788            self.context.delete_shader(Some(&shader));
5789            return Err(WebGlProgramError::ShaderCompile(log));
5790        }
5791        Ok(shader)
5792    }
5793
5794    /// Resolves the location of a uniform on the given program.
5795    ///
5796    /// Uniform locations are stable for the lifetime of a linked program,
5797    /// so callers rendering in a per-frame loop should resolve each uniform
5798    /// once after [`WebGlRenderer::create_program`] and cache the result,
5799    /// then pass it to [`WebGlRenderer::set_uniform_2f`] /
5800    /// [`WebGlRenderer::set_uniform_4fv`]. Resolving per frame is supported
5801    /// but wasteful: every lookup crosses into the browser's GL frontend.
5802    /// A uniform that the GLSL compiler optimized out resolves to `None`,
5803    /// which the setters silently ignore, matching raw WebGL semantics.
5804    ///
5805    /// # Arguments
5806    ///
5807    /// - `&WebGlProgram` - The program owning the uniform.
5808    /// - `&str` - The uniform name (for array uniforms, with an explicit
5809    ///   `[0]` index, per the WebGL `getUniformLocation` spec).
5810    ///
5811    /// # Returns
5812    ///
5813    /// - `Option<WebGlUniformLocation>` - The uniform location, or `None`
5814    ///   when the uniform does not exist in the program.
5815    pub fn get_uniform_location(
5816        &self,
5817        program: &WebGlProgram,
5818        name: &str,
5819    ) -> Option<WebGlUniformLocation> {
5820        self.context.get_uniform_location(program, name)
5821    }
5822
5823    /// Sets a `vec2` uniform on the given program via its cached location.
5824    ///
5825    /// The program is bound with `useProgram` before the upload so the
5826    /// uniform call always targets the program the location was resolved
5827    /// from, regardless of which program the context currently has bound
5828    /// (uploading against a different bound program is an
5829    /// `INVALID_OPERATION` in WebGL). A `None` location (uniform optimized
5830    /// out by the GLSL compiler) is silently ignored, matching raw WebGL
5831    /// semantics.
5832    ///
5833    /// # Arguments
5834    ///
5835    /// - `&WebGlProgram` - The program owning the uniform.
5836    /// - `Option<&WebGlUniformLocation>` - The cached location from
5837    ///   [`WebGlRenderer::get_uniform_location`].
5838    /// - `f32` - The x component.
5839    /// - `f32` - The y component.
5840    pub fn set_uniform_2f(
5841        &self,
5842        program: &WebGlProgram,
5843        location: Option<&WebGlUniformLocation>,
5844        x: f32,
5845        y: f32,
5846    ) {
5847        self.context.use_program(Some(program));
5848        self.context.uniform2f(location, x, y);
5849    }
5850
5851    /// Uploads a flat float slice into a `vec4` or `vec4[]` uniform via its
5852    /// cached location.
5853    ///
5854    /// Used by the game demos to push per-frame instance data (ball positions
5855    /// and colors, cube transforms) into shaders that index the array with
5856    /// `gl_VertexID`. `data.len()` must be a multiple of 4. The upload writes
5857    /// only `data.len() / 4` elements; untouched elements keep their previous
5858    /// values. Like [`WebGlRenderer::set_uniform_2f`], the program is bound
5859    /// before the upload so the call can never target the wrong program.
5860    ///
5861    /// # Arguments
5862    ///
5863    /// - `&WebGlProgram` - The program owning the uniform.
5864    /// - `Option<&WebGlUniformLocation>` - The cached location from
5865    ///   [`WebGlRenderer::get_uniform_location`].
5866    /// - `&[f32]` - The packed float data.
5867    pub fn set_uniform_4fv(
5868        &self,
5869        program: &WebGlProgram,
5870        location: Option<&WebGlUniformLocation>,
5871        data: &[f32],
5872    ) {
5873        self.context.use_program(Some(program));
5874        self.context.uniform4fv_with_f32_array(location, data);
5875    }
5876
5877    /// Renders a complete frame: clears the canvas and draws a triangle-list
5878    /// primitive whose vertices are generated inside the vertex shader.
5879    ///
5880    /// Mirrors [`WebGpuRenderer::render_frame`]: the vertex shader uses
5881    /// `gl_VertexID` so no vertex buffers are involved. The given program
5882    /// is bound before drawing; set its uniforms first via
5883    /// [`WebGlRenderer::set_uniform_2f`] when the shader reads per-frame
5884    /// interaction data.
5885    ///
5886    /// # Arguments
5887    ///
5888    /// - `&WebGlProgram` - The program to draw with.
5889    /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
5890    /// - `i32` - The number of vertices to draw.
5891    pub fn render_frame(
5892        &self,
5893        program: &WebGlProgram,
5894        clear_color: (f64, f64, f64, f64),
5895        vertex_count: i32,
5896    ) {
5897        let (r, g, b, a) = clear_color;
5898        self.context
5899            .viewport(0, 0, self.width as i32, self.height as i32);
5900        self.context
5901            .clear_color(r as f32, g as f32, b as f32, a as f32);
5902        self.context.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
5903        self.context.use_program(Some(program));
5904        self.context
5905            .draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, vertex_count);
5906    }
5907
5908    /// Resizes the canvas backing store and updates the GL viewport.
5909    ///
5910    /// Call this when the CSS layout size changes (window resize, DPR
5911    /// change) so the drawing buffer matches the visible region.
5912    ///
5913    /// # Arguments
5914    ///
5915    /// - `u32` - The new physical pixel width (already multiplied by DPR).
5916    /// - `u32` - The new physical pixel height.
5917    pub fn resize(&mut self, physical_width: u32, physical_height: u32) {
5918        self.canvas.set_width(physical_width);
5919        self.canvas.set_height(physical_height);
5920        self.width = physical_width;
5921        self.height = physical_height;
5922        self.context
5923            .viewport(0, 0, physical_width as i32, physical_height as i32);
5924    }
5925}
5926
5927/// Implements `WebGlInitError` diagnostic helpers.
5928impl WebGlInitError {
5929    /// Returns a short, machine-readable identifier for this error variant.
5930    ///
5931    /// Suitable for use as a stable error code in logs or telemetry.
5932    ///
5933    /// # Returns
5934    ///
5935    /// - `&'static str` - The error code (e.g. `\"WEBGL_CONTEXT_UNAVAILABLE\"`).
5936    pub fn code(&self) -> &'static str {
5937        match self {
5938            Self::CanvasNotFound(_) => "WEBGL_CANVAS_NOT_FOUND",
5939            Self::CanvasQuery(_) => "WEBGL_CANVAS_QUERY",
5940            Self::ContextUnavailable => "WEBGL_CONTEXT_UNAVAILABLE",
5941            Self::ContextLookup(_) => "WEBGL_CONTEXT_LOOKUP",
5942            Self::ContextCast => "WEBGL_CONTEXT_CAST",
5943        }
5944    }
5945
5946    /// Returns the underlying JS error value if this variant carries one.
5947    ///
5948    /// # Returns
5949    ///
5950    /// - `Option<&JsValue>` - The captured JS error, if any.
5951    pub fn js_error(&self) -> Option<&JsValue> {
5952        match self {
5953            Self::CanvasQuery(err) | Self::ContextLookup(err) => Some(err),
5954            Self::CanvasNotFound(_) | Self::ContextUnavailable | Self::ContextCast => None,
5955        }
5956    }
5957}
5958
5959/// Implements `Display` for `WebGlInitError`.
5960///
5961/// The formatted message includes the variant code plus a human-readable
5962/// description; variants carrying a JS error append its rendered form.
5963impl Display for WebGlInitError {
5964    /// Formats the [`WebGlInitError`] via the supplied formatter.
5965    ///
5966    /// # Arguments
5967    ///
5968    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
5969    ///
5970    /// # Returns
5971    ///
5972    /// - `FmtResult` - Result of the formatting operation.
5973    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
5974        match self {
5975            Self::CanvasNotFound(selector) => write!(
5976                formatter,
5977                "[{}] canvas element {:?} not found in DOM",
5978                self.code(),
5979                selector,
5980            ),
5981            Self::CanvasQuery(err) => write!(
5982                formatter,
5983                "[{}] querySelector threw: {}",
5984                self.code(),
5985                js_error_to_string(err),
5986            ),
5987            Self::ContextUnavailable => write!(
5988                formatter,
5989                "[{}] canvas.get_context('webgl2') returned null - the browser does not support WebGL 2 or the canvas already uses another context type",
5990                self.code(),
5991            ),
5992            Self::ContextLookup(err) => write!(
5993                formatter,
5994                "[{}] canvas.get_context('webgl2') threw: {}",
5995                self.code(),
5996                js_error_to_string(err),
5997            ),
5998            Self::ContextCast => write!(
5999                formatter,
6000                "[{}] get_context('webgl2') result could not be cast to WebGl2RenderingContext",
6001                self.code(),
6002            ),
6003        }
6004    }
6005}
6006
6007/// Implements `Display` for `WebGlProgramError`.
6008///
6009/// The formatted message includes the browser-provided info log so GLSL
6010/// diagnostics are visible verbatim in the console.
6011impl Display for WebGlProgramError {
6012    /// Formats the [`WebGlProgramError`] via the supplied formatter.
6013    ///
6014    /// # Arguments
6015    ///
6016    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
6017    ///
6018    /// # Returns
6019    ///
6020    /// - `FmtResult` - Result of the formatting operation.
6021    fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
6022        match self {
6023            Self::ShaderCompile(log) => write!(formatter, "shader compilation failed: {log}"),
6024            Self::ProgramLink(log) => write!(formatter, "program link failed: {log}"),
6025        }
6026    }
6027}
6028
6029/// Implements the standard `Error` trait for `WebGlProgramError`.
6030impl Error for WebGlProgramError {}
6031
6032/// Default-construction helper for `Texture2DDescriptor`.
6033impl Texture2DDescriptor {
6034    /// Returns a descriptor with the most common defaults applied.
6035    ///
6036    /// This is the same as calling the generated `new` constructor and
6037    /// then explicitly setting the defaults; we provide it so callers
6038    /// can do `Texture2DDescriptor::default_for(w, h, format)` instead of
6039    /// having to remember which fields to set.
6040    ///
6041    /// # Arguments
6042    ///
6043    /// - `width` - The texture width in pixels.
6044    /// - `height` - The texture height in pixels.
6045    /// - `format` - The WGSL texture format.
6046    ///
6047    /// # Returns
6048    ///
6049    /// - A new descriptor with `mip_level_count = 1`, `sample_count = 1`,
6050    ///   and usage `"TEXTURE_BINDING | COPY_DST | COPY_SRC"`.
6051    pub fn default_for(width: u32, height: u32, format: &'static str) -> Self {
6052        Self {
6053            width,
6054            height,
6055            format,
6056            mip_level_count: 1,
6057            sample_count: 1,
6058            usage: "TEXTURE_BINDING | COPY_DST | COPY_SRC",
6059        }
6060    }
6061}
6062
6063/// Default-construction helper for `GpuSamplerDescriptor`.
6064impl GpuSamplerDescriptor {
6065    /// Returns a descriptor with the most common defaults applied:
6066    /// nearest filtering and clamp-to-edge addressing on all axes.
6067    pub fn default_sampler() -> Self {
6068        Self {
6069            mag_filter: WEBGPU_FILTER_MODE_NEAREST,
6070            min_filter: WEBGPU_FILTER_MODE_NEAREST,
6071            mipmap_filter: WEBGPU_FILTER_MODE_NEAREST,
6072            address_mode_u: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6073            address_mode_v: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6074            address_mode_w: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6075            compare: false,
6076        }
6077    }
6078}
6079
6080/// Resolves optional `load_op` / `store_op` to the WebGPU spec defaults for
6081/// `RenderPassColorAttachment`.
6082impl RenderPassColorAttachment {
6083    /// Returns the load op that the renderer should use.
6084    ///
6085    /// # Returns
6086    ///
6087    /// - `'static str` - A `'static str` value.
6088    pub(crate) fn effective_load_op(&self) -> &'static str {
6089        match (self.load_op, self.clear_value) {
6090            (Some(op), _) => op,
6091            (None, Some(_)) => WEBGPU_LOAD_OP_CLEAR,
6092            (None, None) => WEBGPU_LOAD_OP_LOAD,
6093        }
6094    }
6095
6096    /// Returns the store op that the renderer should use.
6097    ///
6098    /// Defaults to [`WEBGPU_STORE_OP_STORE`] so the color/depth
6099    /// attachment contents survive the pass. Callers that know the
6100    /// attachment is transient (no resolve, no follow-up sample, no
6101    /// `copyTextureToTexture`) can use [`WEBGPU_STORE_OP_DISCARD`]
6102    /// to avoid the bandwidth of a write-back. The helper
6103    /// [`default_color_store_op`] centralises that "transient?"
6104    /// decision so the [`WEBGPU_STORE_OP_DISCARD`] constant stays
6105    /// reachable from inside the engine.
6106    ///
6107    /// # Returns
6108    ///
6109    /// - `'static str` - A `'static str` value.
6110    pub(crate) fn effective_store_op(&self) -> &'static str {
6111        self.store_op.unwrap_or_else(|| {
6112            default_color_store_op(/* transient = */ false)
6113        })
6114    }
6115}
6116
6117/// Resolves optional `depth_load_op` / `depth_store_op` to the WebGPU spec
6118/// defaults for `RenderPassDepthStencilAttachment`.
6119impl RenderPassDepthStencilAttachment {
6120    /// Returns the depth load op that the renderer should use.
6121    ///
6122    /// # Returns
6123    ///
6124    /// - `'static str` - A `'static str` value.
6125    pub(crate) fn effective_depth_load_op(&self) -> &'static str {
6126        match (self.depth_load_op, self.depth_clear_value) {
6127            (Some(op), _) => op,
6128            (None, Some(_)) => WEBGPU_LOAD_OP_CLEAR,
6129            (None, None) => WEBGPU_LOAD_OP_LOAD,
6130        }
6131    }
6132
6133    /// Returns the depth store op that the renderer should use.
6134    ///
6135    /// # Returns
6136    ///
6137    /// - `'static str` - A `'static str` value.
6138    pub(crate) fn effective_depth_store_op(&self) -> &'static str {
6139        self.depth_store_op.unwrap_or(WEBGPU_STORE_OP_STORE)
6140    }
6141}
6142
6143/// Constructors and view-default resolvers for `TextureViewDescriptor`.
6144impl TextureViewDescriptor {
6145    /// Returns a descriptor that selects the full texture as a 2D view.
6146    /// This is the cheapest view you can make; equivalent to calling
6147    /// `texture.createView()` with no argument.
6148    pub fn full() -> Self {
6149        Self {
6150            format: None,
6151            dimension: None,
6152            base_mip_level: 0,
6153            mip_level_count: 0,
6154            base_array_layer: 0,
6155            array_layer_count: 0,
6156            aspect: None,
6157        }
6158    }
6159
6160    /// The dimension string the renderer will send to `createView`.
6161    ///
6162    /// We default `None` to `"2d"` instead of omitting the key, because
6163    /// every other descriptor in the engine uses the explicit-string
6164    /// form, and a few browsers reject `dimension: undefined`.
6165    ///
6166    /// # Returns
6167    ///
6168    /// - `'static str` - A `'static str` value.
6169    pub(crate) fn effective_dimension(&self) -> &'static str {
6170        self.dimension.unwrap_or(WEBGPU_TEXTURE_VIEW_DIMENSION_2D)
6171    }
6172
6173    /// The aspect string the renderer will send to `createView`.
6174    ///
6175    /// Defaults to `"all"`, which is the spec's "expose every channel"
6176    /// option and the only correct choice for color textures.
6177    ///
6178    /// # Returns
6179    ///
6180    /// - `'static str` - A `'static str` value.
6181    pub(crate) fn effective_aspect(&self) -> &'static str {
6182        self.aspect.unwrap_or(WEBGPU_TEXTURE_ASPECT_ALL)
6183    }
6184
6185    /// Returns a descriptor that selects a single mip level of the texture.
6186    /// Useful when you want to read back a specific mip (e.g. the half-res
6187    /// blur output of a downsampling pass) without exposing the rest.
6188    ///
6189    /// # Arguments
6190    ///
6191    /// - `u32` - A 32-bit unsigned integer (`u32`).
6192    pub fn mip(level: u32) -> Self {
6193        Self {
6194            format: None,
6195            dimension: None,
6196            base_mip_level: level,
6197            mip_level_count: 1,
6198            base_array_layer: 0,
6199            array_layer_count: 0,
6200            aspect: None,
6201        }
6202    }
6203
6204    /// Returns a descriptor that selects the depth-only aspect of a
6205    /// depth-stencil texture. Required when sampling depth in a shader
6206    /// (`textureSample(t, s, uv)` where `t` is a depth texture).
6207    pub fn depth_only() -> Self {
6208        Self {
6209            format: None,
6210            dimension: None,
6211            base_mip_level: 0,
6212            mip_level_count: 0,
6213            base_array_layer: 0,
6214            array_layer_count: 0,
6215            aspect: Some(WEBGPU_TEXTURE_ASPECT_DEPTH_ONLY),
6216        }
6217    }
6218}
6219
6220/// 2D-upload convenience constructor for `TextureWriteDescriptor`.
6221impl TextureWriteDescriptor {
6222    /// Convenience constructor for the common 2D upload case.
6223    ///
6224    /// - `data`: packed pixel bytes (format-dependent).
6225    /// - `bytes_per_row`: row stride of `data`, must be a multiple of 256.
6226    /// - `texture`: the destination `GpuTexture` handle.
6227    ///
6228    /// # Arguments
6229    ///
6230    /// - `Vec<u8>` - A `Vec<u8>` parameter.
6231    /// - `u32` - A 32-bit unsigned integer (`u32`).
6232    /// - `JsValue` - A `JsValue` parameter.
6233    pub fn for_2d(data: Vec<u8>, bytes_per_row: u32, texture: JsValue) -> Self {
6234        Self {
6235            data,
6236            bytes_per_row,
6237            rows_per_image: 0,
6238            mip_level: 0,
6239            texture,
6240            origin: None,
6241            flip_y: false,
6242        }
6243    }
6244}
6245
6246// =================================================================
6247// Impl blocks for types defined in `enum.rs`
6248// =================================================================
6249//
6250// Per the engine's module layout rules, every `impl Foo` block lives in
6251// `impl.rs`; the type definitions (struct / enum) live in `struct.rs`
6252// / `enum.rs` / `trait.rs` respectively. The two impl blocks below
6253// were relocated from `enum.rs` to satisfy that rule without changing
6254// the public API surface — both `VertexStepMode::as_str` and
6255// `BindGroupEntry::binding` are still callable exactly the same way
6256// from the rest of the engine and from the public `euv` crate.
6257
6258/// Inherent implementation of [`VertexStepMode`].
6259impl VertexStepMode {
6260    /// Returns the WGSL / WebGPU string representation.
6261    ///
6262    /// # Returns
6263    ///
6264    /// - `'static str` - A static `&str` representation.
6265    pub fn as_str(&self) -> &'static str {
6266        match self {
6267            Self::Vertex => "vertex",
6268            Self::Instance => "instance",
6269        }
6270    }
6271}
6272
6273/// Inherent implementation of [`BindGroupEntry`].
6274impl BindGroupEntry {
6275    /// Returns the `@binding(N)` slot this entry occupies. The renderer
6276    /// uses this when assembling the bind-group descriptor so the
6277    /// caller does not need to know the JS-side `binding` field name.
6278    ///
6279    /// # Returns
6280    ///
6281    /// - `u32` - The bind-group slot index.
6282    pub fn binding(&self) -> u32 {
6283        match self {
6284            Self::Buffer { binding, .. }
6285            | Self::Texture { binding, .. }
6286            | Self::StorageTexture { binding, .. }
6287            | Self::Sampler { binding, .. } => *binding,
6288        }
6289    }
6290}
6291
6292// =================================================================
6293// Descriptor-surface usage anchors
6294// =================================================================
6295//
6296// `const.rs` documents the *complete* WebGPU descriptor surface —
6297// format strings, usage bitmask values, method/property names — but
6298// the engine's built-in helpers (`create_buffer`, `create_texture`,
6299// `create_render_pipeline`, …) only consume a subset on any given
6300// call site. To prevent the dead-code lint from flagging the
6301// remaining constants (each one is a real, valid WebGPU value — we
6302// just don't always need it in 2D-UI work), the helpers below give
6303// the unused constants a concrete role. They are exposed as
6304// `pub(crate)` because the rest of the engine can call them when
6305// building advanced descriptors (3D pipelines, compute passes,
6306// mipmapped render targets, async readback, …); the public
6307// `euv-engine` API surface stays exactly the same — the const
6308// values are documented and callable, not the helpers.
6309//
6310// If a future round of engine work genuinely removes a constant
6311// from the WebGPU spec, delete the corresponding constant and the
6312// matching arm in the helper below in the same commit.
6313
6314// ============================================================================
6315// `PendingErrorCell` — interior-mutable slot for the renderer's
6316// pending WebGPU error-scope value. Defined as a tuple struct in
6317// `struct.rs`; this block attaches its `impl` block + the hand-written
6318// `Sync` impl required for sharing through `Rc` on the WASM single-threaded
6319// runtime.
6320//
6321// See the doc comment on `struct.rs::PendingErrorCell` for the full design
6322// rationale (why `UnsafeCell` over `RefCell`, why a hand-rolled `Sync` is
6323// sound here, and what would have to change for multi-threaded targets).
6324// ============================================================================
6325
6326/// Inherent implementation of [`PendingErrorCell`].
6327impl PendingErrorCell {
6328    /// Construct a new, empty pending-error slot.
6329    ///
6330    /// The inner `UnsafeCell<Option<JsValue>>` starts as `None`; the
6331    /// WebGPU `pop_error_sync` microtask is the only thing that ever
6332    /// writes to it, and `take_last_error` is the only reader.
6333    pub fn new() -> Self {
6334        Self(UnsafeCell::new(None))
6335    }
6336
6337    /// Hand out a raw pointer to the inner cell for the
6338    /// `spawn_local` closure to write through.
6339    ///
6340    /// # Safety
6341    ///
6342    /// The returned pointer is only valid for the lifetime of `&self`,
6343    /// and only safe to write to on the WASM main thread. The caller
6344    /// must guarantee that no other code is reading the same
6345    /// `PendingErrorCell` concurrently — this is enforced by the
6346    /// single-threaded scheduler: the spawned future is drained
6347    /// before the next render tick's `take_last_error` runs.
6348    ///
6349    /// # Returns
6350    ///
6351    /// - `*mut Option<JsValue>` - Raw pointer to the inner storage.
6352    pub fn as_ptr(&self) -> *mut Option<JsValue> {
6353        self.0.get()
6354    }
6355}
6356
6357/// Default-construction for [`PendingErrorCell`].
6358impl Default for PendingErrorCell {
6359    /// Constructs a default [`PendingErrorCell`] value.
6360    fn default() -> Self {
6361        Self::new()
6362    }
6363}
6364
6365// SAFETY: see the doc comment on `struct.rs::PendingErrorCell`.
6366//
6367// `PendingErrorCell` wraps `UnsafeCell`, which is `!Sync` by design.
6368// We hand-implement `Sync` because:
6369//
6370// - The renderer is compiled for `wasm32` and runs on the WASM
6371//   single-threaded scheduler; there is no other thread to race
6372//   against.
6373// - The owning pointer is held inside an `Rc<PendingErrorCell>`, and
6374//   `Rc` is itself `!Send`/`!Sync`, so the value cannot escape the
6375//   current thread even if the type were `Sync`.
6376// - The `pop_error_sync` future and `take_last_error` never overlap
6377//   in wall-clock time: the future is a microtask that resolves
6378//   before the next render tick drains the slot.
6379//
6380// If `euv-engine` is ever built for a multi-threaded target
6381// (native, `wasm-bindgen-rayon`, `wasm32-atomics`), this `unsafe impl`
6382// becomes unsound and must be removed — at that point the renderer
6383// will need a real `Mutex` or `RwLock` around the slot.
6384unsafe impl Sync for PendingErrorCell {}