Skip to main content

euv_engine/renderer/
impl.rs

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