Skip to main content

euv_engine/renderer/
impl.rs

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