Skip to main content

euv_engine/renderer/
impl.rs

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