Skip to main content

euv_engine/renderer/
impl.rs

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