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