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